#[path = "support/pack_git.rs"]
#[allow(dead_code)]
mod pack_git;
use std::sync::atomic::AtomicBool;
use girt::content_diff::BinaryMode;
use girt::patch::{Limits, Options};
use girt::rewrites::{self, Copies};
use girt::tree::TreeEditor;
use girt::tree_diff::Entry;
use girt::{EntryMode, InitKind, ObjectFormat, ObjectId, Repository, TreeValue};
use pack_git::git;
use rstest::rstest;
type Spec<'a> = &'a [(&'a [u8], EntryMode, &'a [u8])];
const fn blob(
path: &'static [u8],
bytes: &'static [u8],
) -> (&'static [u8], EntryMode, &'static [u8]) {
(path, EntryMode::Blob, bytes)
}
const fn exe(
path: &'static [u8],
bytes: &'static [u8],
) -> (&'static [u8], EntryMode, &'static [u8]) {
(path, EntryMode::Executable, bytes)
}
const fn link(
path: &'static [u8],
target: &'static [u8],
) -> (&'static [u8], EntryMode, &'static [u8]) {
(path, EntryMode::Symlink, target)
}
const fn sub(
path: &'static [u8],
seed: &'static [u8],
) -> (&'static [u8], EntryMode, &'static [u8]) {
(path, EntryMode::Gitlink, seed)
}
#[derive(Clone, Copy, Debug)]
enum Pairing {
Exact,
Renames,
Copies,
}
const NUMBERED: &[u8] =
b"fn one() {\n 1\n}\n\nfn two() {\n 2\n}\n\nfn three() {\n 3\n}\n\n\
fn four() {\n 4\n}\n\nfn five() {\n 5\n}\n\nfn six() {\n 6\n}\n";
const NUMBERED_EDITED: &[u8] =
b"fn one() {\n 1\n}\n\nfn two() {\n 22\n}\n\nfn three() {\n 3\n}\n\n\
fn four() {\n 4\n}\n\nfn five() {\n 5\n}\n\nfn six() {\n 66\n}\n";
const ONE_FUNCTION: &[u8] = b"fn main() {\n a\n b\n c\n d\n e\n f\n g\n h\n i\n j\n k\n l\n m\n}\n";
const ONE_FUNCTION_EDITED: &[u8] = b"fn main() {\n a\n B\n c\n d\n e\n f\n g\n h\n i\n j\n k\n L\n m\n}\n";
const PROSE: &[u8] = b"alpha\nbravo\ncharlie\ndelta\necho\nfoxtrot\ngolf\nhotel\nindia\njuliet\n";
const PROSE_EDITED: &[u8] =
b"alpha\nbravo\ncharlie\ndelta\necho\nfoxtrot\ngolf\nhotel\nindia\njuliet\nkilo\n";
fn tree(repo: &Repository, spec: Spec<'_>) -> ObjectId {
let loose = repo.loose_objects();
let mut editor = TreeEditor::new(&loose);
for (path, mode, bytes) in spec {
let id = match mode {
EntryMode::Gitlink => ObjectId::for_blob(repo.object_format(), bytes),
_ => loose.write_blob(bytes).unwrap(),
};
editor
.set_entry(path, TreeValue { mode: *mode, id })
.unwrap();
}
editor.write().unwrap()
}
fn patches(
format: ObjectFormat,
old: Spec<'_>,
new: Spec<'_>,
pairing: Pairing,
options: Options,
git_flags: &[&str],
) -> (Vec<u8>, Vec<u8>) {
let root = tempfile::tempdir().unwrap();
let repo = Repository::init(format, root.path().join("r"), InitKind::Bare).unwrap();
let (old, new) = (tree(&repo, old), tree(&repo, new));
let objects = repo.objects().unwrap();
let copies = match pairing {
Pairing::Copies => Copies::Modified,
_ => Copies::Disabled,
};
let entries: Vec<Entry> = match pairing {
Pairing::Exact => objects
.compare_trees(Some(old), Some(new))
.unwrap()
.into_iter()
.map(Entry::Change)
.collect(),
Pairing::Renames | Pairing::Copies => {
let options = rewrites::Options::default()
.with_copies(copies)
.with_track_empty(true);
objects
.diff_trees_with(
Some(old),
Some(new),
options,
Default::default(),
&AtomicBool::new(false),
)
.unwrap()
.entries
}
};
let mut ours = Vec::new();
objects
.write_patch_with(
&mut ours,
&entries,
options,
Limits::default(),
&AtomicBool::new(false),
)
.unwrap();
let pairing = match pairing {
Pairing::Exact => "--no-renames",
Pairing::Renames => "-M",
Pairing::Copies => "-C",
};
let (old, new) = (old.to_string(), new.to_string());
let mut args = vec![
"diff",
"--no-ext-diff",
"--no-textconv",
"--no-color",
if options.indent_heuristic {
"--indent-heuristic"
} else {
"--no-indent-heuristic"
},
pairing,
];
args.extend(git_flags);
args.extend([old.as_str(), new.as_str()]);
let theirs = git(repo.git_dir(), &args, b"");
(ours, theirs)
}
fn assert_same(ours: &[u8], theirs: &[u8]) {
assert!(
ours == theirs,
"girt:\n{}\ngit:\n{}",
String::from_utf8_lossy(ours),
String::from_utf8_lossy(theirs)
);
}
#[rstest]
#[case::text_hunks_and_function_context(
&[blob(b"src/lib.rs", NUMBERED)],
&[blob(b"src/lib.rs", NUMBERED_EDITED)],
)]
#[case::function_context_shared_by_hunks(
&[blob(b"main.rs", ONE_FUNCTION)],
&[blob(b"main.rs", ONE_FUNCTION_EDITED)],
)]
#[case::addition_and_deletion(
&[blob(b"gone", b"old\nlines\n")],
&[blob(b"added", b"new\nlines\n")],
)]
#[case::empty_files(
&[blob(b"empty-old", b"")],
&[blob(b"empty-new", b"")],
)]
#[case::mode_only(
&[blob(b"tool", b"#!/bin/sh\n")],
&[exe(b"tool", b"#!/bin/sh\n")],
)]
#[case::mode_and_content(
&[blob(b"tool", b"#!/bin/sh\n")],
&[exe(b"tool", b"#!/bin/sh\nexit 0\n")],
)]
#[case::missing_final_newlines(
&[blob(b"a", b"one\ntwo"), blob(b"b", b"one\ntwo\n"), blob(b"c", b"x\ntail")],
&[blob(b"a", b"one\ntwo\n"), blob(b"b", b"one\ntwo"), blob(b"c", b"y\ntail")],
)]
#[case::crlf_and_invalid_utf8(
&[blob(b"dos.txt", b"one\r\ntwo\r\n\xff\xfe\r\n")],
&[blob(b"dos.txt", b"one\r\nTWO\r\n\xff\xfe\r\n")],
)]
#[case::binary(
&[blob(b"bin", b"\0old"), blob(b"bin-gone", b"\0x")],
&[blob(b"bin", b"\0new"), blob(b"bin-new", b"\0y")],
)]
#[case::symlinks_and_type_changes(
&[link(b"link", b"target"), blob(b"was-file", b"file\n"), link(b"was-link", b"t")],
&[link(b"link", b"other"), link(b"was-file", b"file"), blob(b"was-link", b"t\n")],
)]
#[case::gitlinks(
&[sub(b"sub", b"1"), sub(b"sub-gone", b"2"), sub(b"sub-to-file", b"3")],
&[sub(b"sub", b"4"), sub(b"sub-new", b"5"), blob(b"sub-to-file", b"file\n")],
)]
#[case::spaces_in_quoted_and_binary_names(
&[blob(b"x y\tz", b"1\n"), blob(b"caf\xc3\xa9 y", b"1\n"), blob(b"bin sp", b"\0a"), blob(b"gone sp", b"x\n")],
&[blob(b"x y\tz", b"2\n"), blob(b"caf\xc3\xa9 y", b"2\n"), blob(b"bin sp", b"\0b"), blob(b"new sp", b"y\n")],
)]
#[case::quoted_paths(
&[blob(b"sp ace", b"1\n"), blob(b"tab\there", b"1\n"), blob(b"caf\xc3\xa9", b"1\n"), blob(b"q\"uote", b"1\n"), blob(b"back\\slash", b"1\n")],
&[blob(b"sp ace", b"2\n"), blob(b"tab\there", b"2\n"), blob(b"caf\xc3\xa9", b"2\n"), blob(b"q\"uote", b"2\n"), blob(b"back\\slash", b"2\n")],
)]
fn exact_paths_match_git(#[case] old: Spec<'_>, #[case] new: Spec<'_>) {
let (ours, theirs) = patches(
ObjectFormat::Sha1,
old,
new,
Pairing::Exact,
Options::default(),
&[],
);
assert_same(&ours, &theirs);
}
#[rstest]
#[case::identical_rename(
&[blob(b"old-name", PROSE)],
&[blob(b"new-name", PROSE)],
Pairing::Renames,
)]
#[case::empty_rename(
&[blob(b"empty", b""), blob(b"keep", PROSE)],
&[blob(b"moved", b""), blob(b"keep", PROSE)],
Pairing::Renames,
)]
#[case::edited_rename_with_mode(
&[blob(b"old-name", PROSE)],
&[exe(b"dir/new name", PROSE_EDITED)],
Pairing::Renames,
)]
#[case::identical_rename_with_mode(
&[blob(b"old-name", PROSE)],
&[exe(b"caf\xc3\xa9", PROSE)],
Pairing::Renames,
)]
#[case::copy_from_modified(
&[blob(b"source", PROSE)],
&[blob(b"source", PROSE_EDITED), blob(b"copy", PROSE)],
Pairing::Copies,
)]
fn rewrites_match_git(#[case] old: Spec<'_>, #[case] new: Spec<'_>, #[case] pairing: Pairing) {
let (ours, theirs) = patches(
ObjectFormat::Sha1,
old,
new,
pairing,
Options::default(),
&[],
);
assert_same(&ours, &theirs);
}
#[rstest]
#[case::no_context(Options::default().with_context_lines(0), &["-U0"])]
#[case::one_line(Options::default().with_context_lines(1), &["-U1"])]
#[case::merged_hunks(Options::default().with_context_lines(8), &["-U8"])]
#[case::whole_file(Options::default().with_context_lines(100), &["-U100"])]
#[case::abbrev(Options::default().with_abbrev_len(Some(12)), &["--abbrev=12"])]
#[case::text(Options::default().with_binary(BinaryMode::Text), &["--text"])]
fn options_match_git(#[case] options: Options, #[case] flags: &[&str]) {
let old: Spec<'_> = &[
blob(b"src/lib.rs", NUMBERED),
blob(b"bin", b"keep\n\0old\n"),
blob(b"gone", b"x\n"),
];
let new: Spec<'_> = &[
blob(b"src/lib.rs", NUMBERED_EDITED),
blob(b"bin", b"keep\n\0new\n"),
];
let (ours, theirs) = patches(ObjectFormat::Sha1, old, new, Pairing::Exact, options, flags);
assert_same(&ours, &theirs);
}
#[test]
fn sha256_matches_git() {
let old: Spec<'_> = &[blob(b"file", b"one\n")];
let new: Spec<'_> = &[blob(b"file", b"two\n"), blob(b"added", b"three\n")];
let (ours, theirs) = patches(
ObjectFormat::Sha256,
old,
new,
Pairing::Exact,
Options::default(),
&[],
);
assert_same(&ours, &theirs);
}
#[test]
fn handle_matches_plain_git_diff() {
let root = tempfile::tempdir().unwrap();
let repo = Repository::init(ObjectFormat::Sha1, root.path().join("r"), InitKind::Bare).unwrap();
let old = tree(
&repo,
&[
blob(b"empty", b""),
blob(b"prose", PROSE),
blob(b"lib.rs", NUMBERED),
],
);
let new = tree(
&repo,
&[
blob(b"moved", b""),
blob(b"renamed", PROSE_EDITED),
blob(b"lib.rs", NUMBERED_EDITED),
],
);
let git_dir = repo.git_dir().to_path_buf();
let mut ours = Vec::new();
girt::Repo::new(repo)
.write_patch(&mut ours, old, new)
.unwrap();
let theirs = git(&git_dir, &["diff", &old.to_string(), &new.to_string()], b"");
assert_same(&ours, &theirs);
}
#[rstest]
fn indent_placement_matches_git(
#[values(ObjectFormat::Sha1, ObjectFormat::Sha256)] format: ObjectFormat,
#[values(false, true)] indent: bool,
) {
let (ours, theirs) = patches(
format,
&[blob(b"code", b"head\n\nhead\n body\n}\n}\n\n\n}\n")],
&[blob(
b"code",
b"head\n\nhead\n body\nhead\n body\n}\n}\n\n\n}\n",
)],
Pairing::Exact,
Options::default().with_indent_heuristic(indent),
&[],
);
assert_same(&ours, &theirs);
}