use similar::TextDiff;
const MAX_DIFF_INPUT_BYTES: usize = 128 * 1024;
const LARGE_DIFF_MESSAGE: &str = "Diff omitted: file is too large.";
pub(super) const UNREADABLE_FILE_DIFF_MESSAGE: &str =
"Diff omitted: existing file could not be read.";
pub(super) fn unified_diff(old: &str, new: &str, display_path: &str, created: bool) -> String {
if old == new {
return "No changes.".into();
}
if old.len().saturating_add(new.len()) > MAX_DIFF_INPUT_BYTES {
return LARGE_DIFF_MESSAGE.into();
}
let old_header = if created {
"/dev/null".to_string()
} else {
format!("a/{display_path}")
};
let new_header = format!("b/{display_path}");
TextDiff::from_lines(old, new)
.unified_diff()
.context_radius(1)
.header(&old_header, &new_header)
.to_string()
}
pub(super) fn compact_diff_for_display(diff: &str) -> Option<String> {
let mut in_hunk = false;
let mut lines = Vec::new();
for line in diff.lines() {
if line.starts_with("@@") {
in_hunk = true;
continue;
}
if !in_hunk || line.starts_with('\\') {
continue;
}
let Some(content) = line.get(1..) else {
continue;
};
match &line[..1] {
"+" | "-" => lines.push(line.to_string()),
" " => lines.push(content.to_string()),
_ => {}
}
}
(!lines.is_empty()).then(|| lines.join("\n"))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn compacts_diff_to_one_context_line_without_metadata() {
let diff = unified_diff(
"before\nkeep\nold\nafter\nfar away\n",
"before\nkeep\nnew\nafter\nfar away\n",
"hello.txt",
false,
);
assert_eq!(
compact_diff_for_display(&diff).unwrap(),
"keep\n-old\n+new\nafter"
);
}
#[test]
fn compact_diff_omits_metadata_for_created_files() {
let diff = unified_diff("", "hello\n", "hello.txt", true);
assert_eq!(compact_diff_for_display(&diff).unwrap(), "+hello");
}
#[test]
fn omits_diff_for_large_files() {
let large = "x".repeat(MAX_DIFF_INPUT_BYTES);
assert_eq!(
unified_diff(&large, "updated", "large.txt", false),
LARGE_DIFF_MESSAGE
);
}
#[test]
fn compact_diff_returns_none_for_empty_diff() {
assert_eq!(compact_diff_for_display(""), None);
}
#[test]
fn formats_created_file() {
let diff = unified_diff("", "hello\n", "nested/hello.txt", true);
assert!(diff.starts_with("--- /dev/null\n+++ b/nested/hello.txt\n"));
assert!(diff.contains("@@ -0,0 +1 @@\n+hello\n"));
}
#[test]
fn formats_changed_file_with_context() {
let diff = unified_diff(
"before\nhello\nold\nafter\n",
"before\nhello\nnew\nafter\n",
"hello.txt",
false,
);
assert!(diff.starts_with("--- a/hello.txt\n+++ b/hello.txt\n"));
assert!(diff.contains(" hello\n-old\n+new\n after\n"));
}
#[test]
fn reports_unchanged_content() {
assert_eq!(
unified_diff("same\n", "same\n", "same.txt", false),
"No changes."
);
}
}