use super::visit_files;
fn units(input: &[u8]) -> anyhow::Result<Vec<(u64, String)>> {
let mut units = Vec::new();
let total = visit_files(input, |start, unit| {
let start_index = usize::try_from(start)?;
assert_eq!(&input[start_index..start_index + unit.len()], unit);
units.push((start, String::from_utf8(unit.to_vec())?));
Ok(())
})?;
assert_eq!(total, input.len() as u64);
Ok(units)
}
fn error(input: &[u8]) -> String {
format!("{:#}", units(input).unwrap_err())
}
const TEXT: &str = "diff --git a/a.txt b/a.txt\nindex 1111111..2222222 100644\n--- a/a.txt\n+++ b/a.txt\n@@ -1,2 +1,2 @@\n a\n-b\n+c\n";
const RENAME: &str = "diff --git a/old.txt b/new.txt\nsimilarity index 100%\nrename from old.txt\nrename to new.txt\n";
const MODE: &str = "diff --git a/run.sh b/run.sh\nold mode 100644\nnew mode 100755\n";
const BINARY: &str = "diff --git a/b.bin b/b.bin\nnew file mode 100644\nindex 0000000..0f49c4a\nBinary files /dev/null and b/b.bin differ\n";
const GIT_BINARY: &str = "diff --git a/b.bin b/b.bin\nnew file mode 100644\nindex 0000000000000000000000000000000000000000..0f49c4a5a0ee2b0cbbb46a5b5ed0d2d2ab7fc0fb\nGIT binary patch\nliteral 9\nQcmZQzWJ=1+ODw7c00^)Gi2wiq\n\nliteral 0\nHcmV?d00001\n\n";
const EMPTY_FILE: &str =
"diff --git a/empty.txt b/empty.txt\nnew file mode 100644\nindex 0000000..e69de29\n";
fn commit(id: char, subject: &str) -> String {
format!(
"commit {}\nAuthor: Fixture <fixture@example.com>\nDate: Thu Jan 1 00:00:00 2026 +0000\n\n {subject}\n\n",
id.to_string().repeat(40)
)
}
#[test]
fn plain_git_diff_units_are_unchanged() {
let input = format!("{TEXT}{RENAME}{MODE}{TEXT}");
let found = units(input.as_bytes()).unwrap();
let expected = [TEXT, RENAME, MODE, TEXT];
assert_eq!(found.len(), expected.len());
let mut offset = 0;
for ((start, unit), expected) in found.iter().zip(expected) {
assert_eq!((*start, unit.as_str()), (offset, expected));
offset += expected.len() as u64;
}
}
#[test]
fn empty_and_whitespace_input_have_no_units() {
assert!(units(b"").unwrap().is_empty());
assert!(units(b"\n \n\t\n").unwrap().is_empty());
}
#[test]
fn git_show_header_and_message_are_skipped() {
let input = format!("{}{TEXT}", commit('a', "change a"));
let found = units(input.as_bytes()).unwrap();
assert_eq!(found, [(commit('a', "change a").len() as u64, TEXT.into())]);
}
#[test]
fn git_log_units_end_before_each_commit_header() {
let input = [
commit('a', "text"),
format!("{TEXT}\n"),
commit('b', "rename only"),
format!("{RENAME}\n"),
commit('c', "merge without a diff"),
commit('d', "mode only"),
format!("{MODE}\n"),
commit('e', "binary"),
format!("{BINARY}\n"),
commit('f', "empty file"),
format!("{EMPTY_FILE}\n"),
commit('g', "binary patch"),
format!("{GIT_BINARY}\n"),
commit('h', "last"),
TEXT.into(),
]
.concat();
let found: Vec<_> = units(input.as_bytes())
.unwrap()
.into_iter()
.map(|(_, unit)| unit)
.collect();
assert_eq!(
found,
[TEXT, RENAME, MODE, BINARY, EMPTY_FILE, GIT_BINARY, TEXT]
);
}
#[test]
fn header_only_unit_followed_directly_by_commit_header() {
for header_only in [RENAME, MODE, BINARY, EMPTY_FILE, GIT_BINARY] {
let input = format!("{header_only}{}{TEXT}", commit('b', "next"));
let found: Vec<_> = units(input.as_bytes())
.unwrap()
.into_iter()
.map(|(_, unit)| unit)
.collect();
assert_eq!(found, [header_only, TEXT]);
}
}
#[test]
fn forward_only_binary_patch_ends_before_next_text() {
let forward =
"diff --git a/b.bin b/b.bin\nGIT binary patch\nliteral 9\nQcmZQzWJ=1+ODw7c00^)Gi2wiq\n\n";
let input = format!("{forward}{}{TEXT}", commit('b', "next"));
let found: Vec<_> = units(input.as_bytes())
.unwrap()
.into_iter()
.map(|(_, unit)| unit)
.collect();
assert_eq!(found, [forward, TEXT]);
}
#[test]
fn format_patch_mail_headers_and_signatures_are_skipped() {
let mail = |n: u8, subject: &str| {
format!(
"From {} Mon Sep 17 00:00:00 2001\nFrom: Fixture <fixture@example.com>\nDate: Thu, 1 Jan 2026 00:00:00 +0000\nSubject: [PATCH {n}/3] {subject}\n\nUnindented body text.\n---\n a.txt | 2 +-\n 1 file changed, 1 insertion(+), 1 deletion(-)\n\n",
"0".repeat(40)
)
};
let signature = "-- \n2.50.1\n\n";
let no_newline =
"diff --git a/z b/z\n--- a/z\n+++ b/z\n@@ -1 +1 @@\n-x\n+y\n\\ No newline at end of file\n";
let input = [
mail(1, "text"),
TEXT.into(),
signature.into(),
mail(2, "rename"),
RENAME.into(),
signature.into(),
mail(3, "no newline"),
no_newline.into(),
"-- \r\n2.50.1\r\n".into(),
]
.concat();
let found: Vec<_> = units(input.as_bytes())
.unwrap()
.into_iter()
.map(|(_, unit)| unit)
.collect();
assert_eq!(found, [TEXT, RENAME, no_newline]);
}
#[test]
fn diffstat_before_patch_is_skipped() {
let input = format!(
" a.txt | 2 +-\n new.txt | 0\n 2 files changed, 1 insertion(+), 1 deletion(-)\n\n{TEXT}{RENAME}"
);
let found: Vec<_> = units(input.as_bytes())
.unwrap()
.into_iter()
.map(|(_, unit)| unit)
.collect();
assert_eq!(found, [TEXT, RENAME]);
}
#[test]
fn text_without_file_headers_is_an_error() {
assert!(error(b"just text\nno headers\n").contains("no `diff --git` file header"));
assert!(error(commit('a', "merge").as_bytes()).contains("no `diff --git` file header"));
}
#[test]
fn patches_without_git_headers_are_an_error() {
let traditional = "--- a.txt\t2026-01-01\n+++ b.txt\t2026-01-01\n@@ -1 +1 @@\n-a\n+b\n";
assert!(error(traditional.as_bytes()).contains("without a `diff --git` line"));
let after_git = format!("{TEXT}\n{traditional}");
assert!(error(after_git.as_bytes()).contains("without a `diff --git` line"));
}
#[test]
fn hunk_header_outside_a_file_patch_is_an_error() {
let input = "diff --git a/a b/a\nunknown header\n@@ -1 +1 @@\n-a\n+b\n";
assert!(error(input.as_bytes()).contains("hunk header outside a file patch"));
let input = format!("@@ not a hunk\n{TEXT}");
assert_eq!(units(input.as_bytes()).unwrap().len(), 1);
}
#[test]
fn incorrect_hunk_counts_remain_errors() {
let extra = format!("{TEXT}+d\n");
assert!(error(extra.as_bytes()).contains("incorrect hunk counts"));
let extra_context = format!("{TEXT} d\n");
assert!(error(extra_context.as_bytes()).contains("incorrect hunk counts"));
let short = TEXT.strip_suffix("+c\n").unwrap();
assert!(error(short.as_bytes()).contains("Hunk counts do not agree"));
let interrupted = format!("{short}{TEXT}");
assert!(error(interrupted.as_bytes()).contains("Hunk counts do not agree"));
let interrupted = format!("{short}{}", commit('b', "next"));
assert!(error(interrupted.as_bytes()).contains("incorrect hunk counts"));
}
#[test]
fn combined_diffs_remain_unsupported() {
let input = format!("{}diff --cc a.txt\nindex 1,2..3\n", commit('a', "merge"));
assert!(error(input.as_bytes()).contains("Combined merge diffs are not supported"));
}