use super::{
Operation, SaveFailure, SavedCopy, SideDecision, clean_paths_note, conflicted_paths, decide_sides, literal_path, operation_in_progress,
save_copies, saved_notice, stamp,
};
use crate::repo_read_index::IndexEntry;
use crate::test_support::{fixture, remove};
use std::collections::BTreeSet;
use std::ffi::OsString;
use std::os::unix::fs::PermissionsExt;
use std::path::{Path, PathBuf};
fn entry(path: &str, stage: u8) -> IndexEntry {
return IndexEntry { path: path.as_bytes().to_vec(), mode: 0o100_644, id: vec![0; 20], stage, extended: 0 };
}
fn set(paths: &[&str]) -> BTreeSet<Vec<u8>> {
return paths.iter().map(|each| { return each.as_bytes().to_vec(); }).collect();
}
fn tokens(values: &[&str]) -> Vec<OsString> {
return values.iter().map(OsString::from).collect();
}
#[test]
fn conflicted_paths_are_the_unmerged_entries() {
let entries = vec![entry("a.rs", 0), entry("b.rs", 1), entry("b.rs", 2), entry("b.rs", 3), entry("c.rs", 2), entry("d.rs", 0)];
assert_eq!(conflicted_paths(entries.as_slice()), set(&["b.rs", "c.rs"]));
assert_eq!(conflicted_paths(&[]), set(&[]));
}
#[test]
fn the_operation_in_progress_is_read_from_the_state_files() {
let root: PathBuf = fixture("sides-operation");
assert_eq!(operation_in_progress(root.as_path()), None);
for (file, expected) in [("MERGE_HEAD", Operation::Merge), ("CHERRY_PICK_HEAD", Operation::CherryPick), ("REVERT_HEAD", Operation::Revert)] {
std::fs::write(root.join(file), b"0\n").expect("state file");
assert_eq!(operation_in_progress(root.as_path()), Some(expected), "{file}");
std::fs::remove_file(root.join(file)).expect("remove");
}
std::fs::create_dir(root.join("rebase-merge")).expect("merge backend");
assert_eq!(operation_in_progress(root.as_path()), Some(Operation::Rebase));
std::fs::remove_dir(root.join("rebase-merge")).expect("remove");
std::fs::create_dir(root.join("rebase-apply")).expect("apply backend");
assert_eq!(operation_in_progress(root.as_path()), None, "an am keeps rebase-apply without rebasing");
std::fs::write(root.join("rebase-apply/applying"), b"").expect("am");
assert_eq!(operation_in_progress(root.as_path()), None);
std::fs::write(root.join("rebase-apply/rebasing"), b"").expect("rebase");
assert_eq!(operation_in_progress(root.as_path()), Some(Operation::Rebase));
remove(root.as_path());
}
#[test]
fn only_a_literal_file_spelling_resolves() {
let root: &Path = Path::new("/work/tree");
for (prefix, token, expected) in [
("", "a.rs", Some("a.rs")),
("sub/", "a.rs", Some("sub/a.rs")),
("sub/", "../a.rs", Some("a.rs")),
("", "./dir/./a.rs", Some("dir/a.rs")),
("", "dir//a.rs", Some("dir/a.rs")),
("", "/work/tree/dir/a.rs", Some("dir/a.rs")),
("", "/elsewhere/a.rs", None),
("", "../a.rs", None),
("", ".", None),
("", "", None),
("", "dir/", None),
("", "*.rs", None),
("", "a?.rs", None),
("", "[ab].rs", None),
("", ":(glob)a.rs", None),
("", ":/a.rs", None),
("", "a\\b", None),
] {
assert_eq!(
literal_path(prefix.as_bytes(), root, OsString::from(token).as_os_str()),
expected.map(|each| { return each.as_bytes().to_vec(); }),
"{prefix:?} {token:?}"
);
}
}
#[test]
fn a_line_is_allowed_only_when_every_path_is_conflicted() {
let root: &Path = Path::new("/work/tree");
let conflicted: BTreeSet<Vec<u8>> = set(&["a.rs", "sub/b.rs"]);
assert_eq!(
decide_sides(tokens(&["a.rs", "sub/b.rs", "./a.rs"]).as_slice(), b"", root, &conflicted),
SideDecision::Allow(vec![b"a.rs".to_vec(), b"sub/b.rs".to_vec()])
);
assert_eq!(decide_sides(tokens(&["b.rs"]).as_slice(), b"sub/", root, &conflicted), SideDecision::Allow(vec![b"sub/b.rs".to_vec()]));
assert_eq!(
decide_sides(tokens(&["a.rs", "clean.rs", "sub", "*.rs"]).as_slice(), b"", root, &conflicted),
SideDecision::Refuse(tokens(&["clean.rs", "sub", "*.rs"]))
);
assert_eq!(decide_sides(tokens(&["clean.rs"]).as_slice(), b"", root, &conflicted), SideDecision::Refuse(tokens(&["clean.rs"])));
assert_eq!(decide_sides(tokens(&["a.rs"]).as_slice(), b"", root, &set(&[])), SideDecision::Refuse(tokens(&["a.rs"])));
}
fn tree(name: &str) -> (PathBuf, PathBuf, PathBuf) {
let base: PathBuf = fixture(name);
let root: PathBuf = base.join("tree");
let common: PathBuf = base.join("common");
std::fs::create_dir_all(root.join("sub")).expect("worktree");
std::fs::create_dir_all(&common).expect("common directory");
return (base, root, common);
}
#[test]
fn a_copy_keeps_content_mode_and_link_target() {
let (base, root, common) = tree("sides-copy");
std::fs::write(root.join("a.rs"), b"half \xff resolved\n").expect("a");
std::fs::set_permissions(root.join("a.rs"), std::fs::Permissions::from_mode(0o754)).expect("mode");
std::fs::write(root.join("sub/b.rs"), b"").expect("b");
std::os::unix::fs::symlink("target/elsewhere", root.join("link")).expect("link");
let paths: Vec<Vec<u8>> = vec![b"a.rs".to_vec(), b"sub/b.rs".to_vec(), b"link".to_vec(), b"gone.rs".to_vec()];
let milliseconds: i64 = 1_793_628_067_000;
let copies: Vec<SavedCopy> = save_copies(root.as_path(), common.as_path(), paths.as_slice(), milliseconds).expect("saved");
let directory: PathBuf = common.join("cli-git/discarded").join(stamp(milliseconds));
assert_eq!(stamp(milliseconds), "2026-11-02T14-01-07");
assert_eq!(copies[0].saved_to, Some(directory.join("a.rs")));
assert_eq!(std::fs::read(directory.join("a.rs")).expect("copy"), b"half \xff resolved\n");
assert_eq!(std::fs::metadata(directory.join("a.rs")).expect("mode").permissions().mode() & 0o777, 0o754);
assert_eq!(std::fs::read(directory.join("sub/b.rs")).expect("empty copy"), b"");
assert_eq!(std::fs::read_link(directory.join("link")).expect("link copy"), PathBuf::from("target/elsewhere"));
assert_eq!(copies[3], SavedCopy { path: String::from("gone.rs"), saved_to: None });
let notice: String = saved_notice(copies.as_slice());
assert!(notice.contains(format!("saved your copy of a.rs to\n {}", directory.join("a.rs").display()).as_str()), "{notice}");
assert!(notice.contains("gone.rs was conflicted and is deleted in your worktree, so there is no copy to save"), "{notice}");
remove(base.as_path());
}
#[test]
fn a_second_save_in_the_same_second_keeps_the_first() {
let (base, root, common) = tree("sides-twice");
std::fs::write(root.join("a.rs"), b"first\n").expect("a");
let milliseconds: i64 = 1_793_628_067_000;
let first = save_copies(root.as_path(), common.as_path(), &[b"a.rs".to_vec()], milliseconds).expect("first");
std::fs::write(root.join("a.rs"), b"second\n").expect("a");
let second = save_copies(root.as_path(), common.as_path(), &[b"a.rs".to_vec()], milliseconds).expect("second");
assert_ne!(first[0].saved_to, second[0].saved_to);
assert_eq!(std::fs::read(first[0].saved_to.as_ref().expect("first place")).expect("first copy"), b"first\n");
assert_eq!(std::fs::read(second[0].saved_to.as_ref().expect("second place")).expect("second copy"), b"second\n");
remove(base.as_path());
}
#[test]
fn a_copy_that_cannot_be_written_is_a_failure() {
let (base, root, common) = tree("sides-failure");
std::fs::write(root.join("a.rs"), b"mine\n").expect("a");
std::fs::write(common.join("cli-git"), b"in the way").expect("blocker");
let failure: SaveFailure = save_copies(root.as_path(), common.as_path(), &[b"a.rs".to_vec()], 0).expect_err("no place");
assert!(failure.subject.contains("discarded"), "{failure:?}");
assert!(!failure.reason.is_empty(), "{failure:?}");
std::fs::remove_file(common.join("cli-git")).expect("remove blocker");
std::fs::create_dir(root.join("dir.rs")).expect("directory");
let directory_failure: SaveFailure = save_copies(root.as_path(), common.as_path(), &[b"dir.rs".to_vec()], 0).expect_err("a directory");
assert_eq!(directory_failure.subject, "dir.rs");
assert!(directory_failure.reason.contains("neither a file nor a symbolic link"), "{directory_failure:?}");
remove(base.as_path());
}
#[test]
fn the_rejection_names_the_paths_that_are_not_conflicted() {
let note: String = clean_paths_note(tokens(&["b.rs", "dir"]).as_slice(), Operation::CherryPick);
assert!(note.contains("This cherry-pick is in progress"), "{note}");
assert!(note.contains("`b.rs`, `dir`"), "{note}");
}