use super::*;
use crate::policy_events::PolicyEvent;
use crate::service_messages::DENYLIST;
use crate::test_support::{fixture, git, git_context, git_text, remove, repository};
const FILE: &str = "f.txt";
fn lines_with(edits: &[(usize, &str)]) -> String {
return (1..=9)
.map(|number| {
let line: String = edits.iter().find(|(at, _)| { return *at == number; }).map_or_else(|| { return format!("line {number}"); }, |(_, edit)| { return (*edit).to_string(); });
return line + "\n";
})
.collect();
}
struct World {
root: PathBuf,
repo: PathBuf,
context: GitContext,
base: String,
}
fn world(name: &str) -> World {
let root: PathBuf = fixture(name);
let repo: PathBuf = repository(root.as_path(), "repo");
std::fs::write(repo.join(FILE), lines_with(&[])).expect("file");
git(repo.as_path(), &["add", FILE]);
git(repo.as_path(), &["commit", "--quiet", "--message=base"]);
let base: String = git_text(repo.as_path(), &["rev-parse", "HEAD"]);
std::fs::create_dir(root.join("scratch")).expect("scratch");
return World { root, repo, context: git_context(), base };
}
fn land(world: &World, edits: &[(usize, &str)], message: &str) {
std::fs::write(world.repo.join(FILE), lines_with(edits)).expect("edit");
git(world.repo.as_path(), &["commit", "--quiet", "--all", format!("--message={message}").as_str()]);
}
fn captured(world: &World, edits: &[(usize, &str)]) -> Captured {
git(world.repo.as_path(), &["checkout", "--quiet", "-b", "mine", world.base.as_str()]);
land(world, edits, "mine");
let tree: String = git_text(world.repo.as_path(), &["rev-parse", "HEAD^{tree}"]);
git(world.repo.as_path(), &["checkout", "--quiet", "main"]);
return Captured { base: world.base.clone(), tree, captured: 0, landed: Vec::new() };
}
fn merge(world: &World, captured: &Captured) -> Merged {
let tip: String = git_text(world.repo.as_path(), &["rev-parse", "HEAD"]);
let common: PathBuf = world.repo.join(".git");
let scratch: PathBuf = world.root.join("scratch");
let site: MergeSite<'_> =
MergeSite { context: &world.context, cwd: world.repo.as_path(), common_dir: common.as_path(), object_directory: &common.join("objects"), scratch: scratch.as_path() };
return merge_captured(&site, captured, tip.as_str(), 1).expect("the merge runs");
}
fn tip_captured(world: &World, captured: u64) -> LandedCapture {
return LandedCapture { commit: git_text(world.repo.as_path(), &["rev-parse", "HEAD"]), captured };
}
fn blob_of(world: &World, tree: &str) -> String {
return git_text(world.repo.as_path(), &["show", format!("{tree}:{FILE}").as_str()]) + "\n";
}
#[test]
fn edits_of_different_lines_merge_into_one_tree() {
let world: World = world("replay-clean");
let mine: Captured = captured(&world, &[(7, "mine")]);
land(&world, &[(1, "theirs")], "theirs");
let Merged::Tree(tree) = merge(&world, &mine) else { panic!("expected a clean merge") };
assert_eq!(blob_of(&world, tree.as_str()), lines_with(&[(1, "theirs"), (7, "mine")]));
remove(world.root.as_path());
}
#[test]
fn edits_of_the_same_line_conflict_and_the_first_commit_is_named() {
let world: World = world("replay-conflict");
let mine: Captured = captured(&world, &[(1, "mine")]);
land(&world, &[(1, "theirs")], "theirs");
land(&world, &[(1, "theirs"), (9, "later")], "later");
let Merged::Conflict(paths) = merge(&world, &mine) else { panic!("expected a conflict") };
assert_eq!(paths, vec![FILE.as_bytes().to_vec()]);
let tip: String = git_text(world.repo.as_path(), &["rev-parse", "HEAD"]);
let first = first_landed(&world.context, world.repo.as_path(), world.base.as_str(), tip.as_str(), paths.as_slice()).expect("the log runs").expect("a commit");
assert_eq!(first.subject, "theirs", "the commit that landed first, not the last");
assert_eq!(first.short, git_text(world.repo.as_path(), &["rev-parse", "--short", "HEAD~1"]));
remove(world.root.as_path());
}
#[test]
fn a_captured_file_that_holds_the_landed_edit_and_an_adjacent_one_lands_as_captured() {
let world: World = world("replay-adjacent");
let mine: Captured = captured(&world, &[(4, "theirs"), (5, "mine")]);
land(&world, &[(4, "theirs")], "theirs");
let Merged::Tree(tree) = merge(&world, &mine) else { panic!("expected the captured bytes to land") };
assert_eq!(blob_of(&world, tree.as_str()), lines_with(&[(4, "theirs"), (5, "mine")]));
remove(world.root.as_path());
}
#[test]
fn a_later_capture_lands_its_bytes_over_an_overlapping_earlier_one() {
let world: World = world("replay-later");
let mine: Captured = captured(&world, &[(1, "mine")]);
land(&world, &[(1, "theirs")], "theirs");
let with_order: Captured = Captured { captured: 200, landed: vec![tip_captured(&world, 100)], ..mine };
let Merged::Tree(tree) = merge(&world, &with_order) else { panic!("expected the later capture to land") };
assert_eq!(blob_of(&world, tree.as_str()), lines_with(&[(1, "mine")]));
remove(world.root.as_path());
}
#[test]
fn an_earlier_capture_keeps_the_bytes_of_an_overlapping_later_one() {
let world: World = world("replay-earlier");
let mine: Captured = captured(&world, &[(1, "mine")]);
land(&world, &[(1, "theirs")], "theirs");
let with_order: Captured = Captured { captured: 100, landed: vec![tip_captured(&world, 200)], ..mine };
let Merged::Tree(tree) = merge(&world, &with_order) else { panic!("expected the landed bytes to stay") };
assert_eq!(blob_of(&world, tree.as_str()), lines_with(&[(1, "theirs")]));
remove(world.root.as_path());
}
#[test]
fn an_unknown_commit_or_an_unknown_stamp_leaves_an_overlap_a_conflict() {
let world: World = world("replay-unknown");
let mine: Captured = captured(&world, &[(1, "mine")]);
land(&world, &[(1, "theirs")], "theirs");
land(&world, &[(1, "theirs"), (9, "later")], "later");
let only_one_known: Captured = Captured { captured: 200, landed: vec![tip_captured(&world, 100)], ..mine.clone() };
assert!(matches!(merge(&world, &only_one_known), Merged::Conflict(_)), "the commit before the tip is unknown");
let no_own_stamp: Captured = Captured { captured: 0, landed: vec![tip_captured(&world, 100)], ..mine };
assert!(matches!(merge(&world, &no_own_stamp), Merged::Conflict(_)));
remove(world.root.as_path());
}
#[test]
fn capture_stamps_do_not_go_back() {
let first: u64 = capture_stamp();
let second: u64 = capture_stamp();
assert!(first > 0 && second >= first);
}
#[test]
fn a_captured_file_that_rewrote_the_landed_lines_still_conflicts() {
let world: World = world("replay-rewrote");
let mine: Captured = captured(&world, &[(3, "theirs"), (4, "mine")]);
land(&world, &[(3, "theirs"), (4, "theirs again")], "theirs");
assert!(matches!(merge(&world, &mine), Merged::Conflict(_)));
remove(world.root.as_path());
}
#[test]
fn the_conflict_text_names_what_the_person_needs_and_leaks_no_internal_term() {
let conflict: ReplayConflict = ReplayConflict {
paths: vec![b"a/one.txt".to_vec(), b"two.txt".to_vec()],
landed: Some(LandedFirst { short: String::from("3f2a1c9"), subject: String::from("Add the thing") }),
git_dir: PathBuf::from("/work/repo/.git"),
};
let saved: PathBuf = PathBuf::from("/work/repo/.git/cli-git-saved-message-ab12");
let text: String = conflict_text(&conflict, Some(saved.as_path()));
for fact in ["a/one.txt", "two.txt", "3f2a1c9", "Add the thing", "/work/repo/.git/cli-git-saved-message-ab12", NOTHING_CHANGED] {
assert!(text.contains(fact), "{fact} missing: {text}");
}
assert!(text.contains("`git commit -F /work/repo/.git/cli-git-saved-message-ab12`") && text.contains("`git show 3f2a1c9`"), "{text}");
let unknown: String = conflict_text(&ReplayConflict { landed: None, ..conflict }, None);
for shown in [text, unknown] {
let plain: String = shown.split('`').enumerate().filter(|(at, _)| { return at % 2 == 0; }).map(|(_, part)| { return part; }).collect::<Vec<&str>>().join(" ").to_lowercase();
for term in DENYLIST {
assert!(!plain.split(|character: char| { return !character.is_alphanumeric(); }).any(|word| { return word == term; }), "{term} leaked: {shown}");
}
}
}
#[test]
fn both_endings_are_one_core_finding_with_status_one() {
let conflict: ReplayConflict = ReplayConflict { paths: vec![b"a.txt".to_vec()], landed: None, git_dir: PathBuf::from("/r/.git") };
for result in [conflict_result(&conflict, None), already_committed_result("main")] {
assert_eq!(result.exit_code, 1);
assert!(result.landed_oid.is_none());
assert!(matches!(result.events.as_slice(), [PolicyEvent::CoreFinding { core_id: "concurrent-commit", .. }]));
}
}
fn corpus(name: &str, extension: &str) -> Vec<u8> {
return std::fs::read(Path::new(env!("CARGO_MANIFEST_DIR")).join("src/native/merge/word_merge_corpus").join(format!("{name}.{extension}"))).expect("a corpus file");
}
fn corpus_world(name: &str, input: &str, path: &str) -> World {
let root: PathBuf = fixture(name);
let repo: PathBuf = repository(root.as_path(), "repo");
std::fs::create_dir_all(repo.join(path).parent().expect("a parent")).expect("directories");
std::fs::write(repo.join(path), corpus(input, "base")).expect("file");
git(repo.as_path(), &["add", "--all"]);
git(repo.as_path(), &["commit", "--quiet", "--message=base"]);
let base: String = git_text(repo.as_path(), &["rev-parse", "HEAD"]);
std::fs::create_dir(root.join("scratch")).expect("scratch");
return World { root, repo, context: git_context(), base };
}
fn corpus_sides(world: &World, input: &str, path: &str) -> Captured {
git(world.repo.as_path(), &["checkout", "--quiet", "-b", "mine", world.base.as_str()]);
std::fs::write(world.repo.join(path), corpus(input, "theirs")).expect("captured text");
git(world.repo.as_path(), &["commit", "--quiet", "--all", "--message=mine"]);
let tree: String = git_text(world.repo.as_path(), &["rev-parse", "HEAD^{tree}"]);
git(world.repo.as_path(), &["checkout", "--quiet", "main"]);
std::fs::write(world.repo.join(path), corpus(input, "ours")).expect("landed text");
git(world.repo.as_path(), &["commit", "--quiet", "--all", "--message=landed"]);
return Captured { base: world.base.clone(), tree, captured: 0, landed: Vec::new() };
}
#[test]
fn an_adjacent_edit_and_insertion_merge_by_word() {
let world: World = corpus_world("replay-word", "md1b", "notes.md");
let mine: Captured = corpus_sides(&world, "md1b", "notes.md");
let Merged::Combined { tree, files } = merge(&world, &mine) else { panic!("expected the word pass to merge the adjacent edits") };
assert_eq!(files.len(), 1, "the one merged file is reported");
assert_eq!(files[0].path, b"notes.md".to_vec());
let blob: String = git_text(world.repo.as_path(), &["show", format!("{tree}:notes.md").as_str()]) + "\n";
assert_eq!(blob.as_bytes(), corpus("md1b", "word.out").as_slice());
remove(world.root.as_path());
}
#[test]
fn two_sections_appended_at_one_point_stay_a_conflict() {
let world: World = corpus_world("replay-word-append", "md2", "notes.md");
let mine: Captured = corpus_sides(&world, "md2", "notes.md");
assert!(matches!(merge(&world, &mine), Merged::Conflict(paths) if paths == vec![b"notes.md".to_vec()]));
remove(world.root.as_path());
}
#[test]
fn the_union_attribute_of_the_landed_tip_applies_without_a_worktree_file() {
let world: World = corpus_world("replay-union", "md2", "doc/handover/log.md");
std::fs::write(world.repo.join(".gitattributes"), "doc/handover/*.md merge=union\n").expect("attributes");
git(world.repo.as_path(), &["add", "--all"]);
git(world.repo.as_path(), &["commit", "--quiet", "--message=attributes"]);
let attributed: String = git_text(world.repo.as_path(), &["rev-parse", "HEAD"]);
let attributed_world: World = World { base: attributed, ..world };
let mine: Captured = corpus_sides(&attributed_world, "md2", "doc/handover/log.md");
std::fs::remove_file(attributed_world.repo.join(".gitattributes")).expect("remove the attributes file");
git(attributed_world.repo.as_path(), &["rm", "--quiet", "--cached", ".gitattributes"]);
let Merged::Tree(tree) = merge(&attributed_world, &mine) else { panic!("expected the union driver of the landed tip to merge the appends") };
let blob: String = git_text(attributed_world.repo.as_path(), &["show", format!("{tree}:doc/handover/log.md").as_str()]);
assert!(blob.contains("Ours risk.") && blob.contains("Theirs question."), "{blob}");
remove(attributed_world.root.as_path());
}
#[test]
fn the_notice_lists_each_merged_part_and_is_absent_without_one() {
use crate::merge_second_pass::{Conflict, Merged as MergedFile};
use crate::word_merge::{LineSpan, MergedPart};
assert_eq!(word_merge_notice(&[]), None);
let span = LineSpan { first: 2, count: 1 };
let file = MergedFile {
path: b"notes.md".to_vec(),
mode: 0o100_644,
bytes: Vec::new(),
parts: vec![MergedPart { base: span, ours: span, theirs: span, merged: b"joined\n".to_vec() }],
stages: Conflict { path: b"notes.md".to_vec(), base: None, ours: None, theirs: None },
};
let Some(PolicyEvent::WordMerged { lines }) = word_merge_notice(&[file]) else { panic!("expected the event") };
assert_eq!(lines.len(), 1);
assert!(lines[0].contains("word-level merge") && lines[0].contains("notes.md") && lines[0].contains("joined"), "{lines:?}");
}
#[test]
fn a_driver_attribute_below_a_directory_is_honoured_from_a_subdirectory() {
let world: World = corpus_world("replay-subdirectory-driver", "md1b", "docs/notes.md");
std::fs::write(world.repo.join(".gitattributes"), "docs/*.md merge=binary\n").expect("attributes");
git(world.repo.as_path(), &["add", "--all"]);
git(world.repo.as_path(), &["commit", "--quiet", "--message=attributes"]);
let attributed: String = git_text(world.repo.as_path(), &["rev-parse", "HEAD"]);
let driven: World = World { base: attributed, ..world };
let mine: Captured = corpus_sides(&driven, "md1b", "docs/notes.md");
let tip: String = git_text(driven.repo.as_path(), &["rev-parse", "HEAD"]);
let common: PathBuf = driven.repo.join(".git");
let scratch: PathBuf = driven.root.join("scratch");
let subdirectory: PathBuf = driven.repo.join("docs");
let site: MergeSite<'_> =
MergeSite { context: &driven.context, cwd: subdirectory.as_path(), common_dir: common.as_path(), object_directory: &common.join("objects"), scratch: scratch.as_path() };
let merged: Merged = merge_captured(&site, &mine, tip.as_str(), 1).expect("the merge runs");
assert!(matches!(merged, Merged::Conflict(_)), "{merged:?}");
remove(driven.root.as_path());
}
#[test]
fn a_configured_attribute_tree_is_not_overridden() {
let world: World = corpus_world("replay-attr-tree", "md2", "doc/handover/log.md");
let without_attributes: String = git_text(world.repo.as_path(), &["rev-parse", "HEAD"]);
std::fs::write(world.repo.join(".gitattributes"), "doc/handover/*.md merge=union\n").expect("attributes");
git(world.repo.as_path(), &["add", "--all"]);
git(world.repo.as_path(), &["commit", "--quiet", "--message=attributes"]);
let attributed: String = git_text(world.repo.as_path(), &["rev-parse", "HEAD"]);
let attributed_world: World = World { base: attributed, ..world };
let mine: Captured = corpus_sides(&attributed_world, "md2", "doc/handover/log.md");
std::fs::remove_file(attributed_world.repo.join(".gitattributes")).expect("remove the attributes file");
git(attributed_world.repo.as_path(), &["rm", "--quiet", "--cached", ".gitattributes"]);
git(attributed_world.repo.as_path(), &["config", "attr.tree", without_attributes.as_str()]);
assert!(matches!(merge(&attributed_world, &mine), Merged::Conflict(_)), "the person's attribute tree has no union line");
git(attributed_world.repo.as_path(), &["config", "--unset", "attr.tree"]);
assert!(matches!(merge(&attributed_world, &mine), Merged::Tree(_)), "with none configured the tip's line applies");
remove(attributed_world.root.as_path());
}