use std::borrow::Cow;
use serde::{Deserialize, Serialize};
use similar::{ChangeTag, TextDiff};
use ts_rs_forge::TS;
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
#[serde(rename_all = "camelCase")]
pub struct FileDiffDetails {
pub file_name: Option<String>,
pub content: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
#[serde(rename_all = "camelCase")]
pub struct Diff {
pub change: DiffChangeKind,
pub old_path: Option<String>,
pub new_path: Option<String>,
pub old_content: Option<String>,
pub new_content: Option<String>,
pub content_omitted: bool,
pub additions: Option<usize>,
pub deletions: Option<usize>,
}
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
#[ts(export)]
#[serde(rename_all = "camelCase")]
pub enum DiffChangeKind {
Added,
Deleted,
Modified,
Renamed,
Copied,
PermissionChange,
}
pub fn create_unified_diff_hunk(old: &str, new: &str) -> String {
let mut old = old.to_string();
let mut new = new.to_string();
if !old.ends_with('\n') {
old.push('\n');
}
if !new.ends_with('\n') {
new.push('\n');
}
let diff = TextDiff::from_lines(&old, &new);
let mut out = String::new();
let old_count = diff.old_slices().len();
let new_count = diff.new_slices().len();
out.push_str(&format!("@@ -1,{old_count} +1,{new_count} @@\n"));
for change in diff.iter_all_changes() {
let sign = match change.tag() {
ChangeTag::Equal => ' ',
ChangeTag::Delete => '-',
ChangeTag::Insert => '+',
};
let val = change.value();
out.push(sign);
out.push_str(val);
}
out
}
pub fn create_unified_diff(file_path: &str, old: &str, new: &str) -> String {
let mut out = String::new();
out.push_str(format!("--- a/{file_path}\n+++ b/{file_path}\n").as_str());
out.push_str(&create_unified_diff_hunk(old, new));
out
}
pub fn compute_line_change_counts(old: &str, new: &str) -> (usize, usize) {
let old = ensure_newline(old);
let new = ensure_newline(new);
let diff = TextDiff::from_lines(&old, &new);
let mut additions = 0usize;
let mut deletions = 0usize;
for change in diff.iter_all_changes() {
match change.tag() {
ChangeTag::Insert => additions += 1,
ChangeTag::Delete => deletions += 1,
ChangeTag::Equal => {}
}
}
(additions, deletions)
}
fn ensure_newline(line: &str) -> Cow<'_, str> {
if line.ends_with('\n') {
Cow::Borrowed(line)
} else {
let mut owned = line.to_owned();
owned.push('\n');
Cow::Owned(owned)
}
}
pub fn extract_unified_diff_hunks(unified_diff: &str) -> Vec<String> {
let lines = unified_diff.split_inclusive('\n').collect::<Vec<_>>();
if !lines.iter().any(|l| l.starts_with("@@")) {
let hunk = lines
.iter()
.copied()
.filter(|line| line.starts_with([' ', '+', '-']))
.collect::<String>();
let old_count = lines
.iter()
.filter(|line| line.starts_with(['-', ' ']))
.count();
let new_count = lines
.iter()
.filter(|line| line.starts_with(['+', ' ']))
.count();
return if hunk.is_empty() {
vec![]
} else {
vec![format!("@@ -1,{old_count} +1,{new_count} @@\n{hunk}")]
};
}
let mut hunks = vec![];
let mut current_hunk: Option<String> = None;
for line in lines {
if line.starts_with("@@") {
if let Some(hunk) = current_hunk.take() {
if !hunk.is_empty() {
hunks.push(hunk);
}
}
current_hunk = Some(line.to_string());
} else if let Some(ref mut hunk) = current_hunk {
if line.starts_with([' ', '+', '-']) {
hunk.push_str(line);
} else {
if !hunk.is_empty() {
hunks.push(hunk.clone());
}
current_hunk = None;
}
}
}
if let Some(hunk) = current_hunk
&& !hunk.is_empty()
{
hunks.push(hunk);
}
hunks = fix_hunk_headers(hunks);
hunks
}
fn fix_hunk_headers(hunks: Vec<String>) -> Vec<String> {
if hunks.is_empty() {
return hunks;
}
let mut new_hunks = Vec::new();
for hunk in hunks {
let mut lines = hunk
.split_inclusive('\n')
.map(str::to_string)
.collect::<Vec<_>>();
if lines.len() < 2 {
continue;
}
let header = &lines[0];
if !header.starts_with("@@") {
continue;
}
if header.trim() == "@@" {
lines.remove(0);
let old_count = lines
.iter()
.filter(|line| line.starts_with(['-', ' ']))
.count();
let new_count = lines
.iter()
.filter(|line| line.starts_with(['+', ' ']))
.count();
let new_header = format!("@@ -1,{old_count} +1,{new_count} @@");
lines.insert(0, new_header);
new_hunks.push(lines.join(""));
} else {
new_hunks.push(hunk);
}
}
new_hunks
}
pub fn concatenate_diff_hunks(file_path: &str, hunks: &[String]) -> String {
let mut unified_diff = String::new();
let header = format!("--- a/{file_path}\n+++ b/{file_path}\n");
unified_diff.push_str(&header);
if !hunks.is_empty() {
let lines = hunks
.iter()
.flat_map(|hunk| hunk.lines())
.filter(|line| line.starts_with("@@ ") || line.starts_with([' ', '+', '-']))
.collect::<Vec<_>>();
unified_diff.push_str(lines.join("\n").as_str());
if !unified_diff.ends_with('\n') {
unified_diff.push('\n');
}
}
unified_diff
}