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", .. }]));
}
}