use super::super::support::{Fixture, Observed, direct, fixture, git, observe, remove, run_wrapped, stderr_of, wrapped};
use super::{HAND_EDITED, Stop, audit_lines, conflicted, rejected_by_policy, saved_root};
use std::path::PathBuf;
const LOOK_ALIKE_EDIT: &[u8] = b"my unsaved edit of the look-alike\n";
fn with_look_alike(fixture: &Fixture) -> PathBuf {
let repo: PathBuf = conflicted(fixture, Stop::Merge);
std::fs::write(repo.join("A.rs"), "staged\n").expect("look-alike");
git(fixture, repo.as_path(), &["add", "--", "A.rs"]);
std::fs::write(repo.join("A.rs"), LOOK_ALIKE_EDIT).expect("unsaved edit");
return repo;
}
fn assert_untouched(repo: &std::path::Path, said: &str) {
assert_eq!(std::fs::read(repo.join("a.rs")).expect("a.rs"), HAND_EDITED, "a.rs is as the person had it: {said}");
assert_eq!(std::fs::read(repo.join("A.rs")).expect("A.rs"), LOOK_ALIKE_EDIT, "A.rs is as the person had it: {said}");
assert!(!saved_root(repo).exists(), "no copy was made: {said}");
assert!(audit_lines(repo).iter().all(|line| { return !line.contains("saved-conflict"); }), "no saved-conflict record: {said}");
}
#[test]
fn real_git_with_case_insensitive_pathspecs_overwrites_the_look_alike() {
let fixture: Fixture = fixture("icase-oracle");
let repo: PathBuf = with_look_alike(&fixture);
let mut command = direct(&fixture);
command.current_dir(&repo).env("GIT_ICASE_PATHSPECS", "1").args(["checkout", "--theirs", "--", "a.rs"]);
let observed: Observed = observe(&mut command, b"");
assert_eq!(observed.code, Some(0), "{}", stderr_of(&observed));
assert_eq!(std::fs::read(repo.join("A.rs")).expect("A.rs"), b"staged\n", "Git replaced the look-alike with the index copy");
remove(&fixture);
}
#[test]
fn a_global_pathspec_mode_keeps_the_rejection() {
let fixture: Fixture = fixture("icase-modes");
let repo: PathBuf = with_look_alike(&fixture);
for variable in ["GIT_ICASE_PATHSPECS", "GIT_GLOB_PATHSPECS", "GIT_NOGLOB_PATHSPECS", "GIT_LITERAL_PATHSPECS"] {
let mut command = wrapped(&fixture);
command.current_dir(&repo).env(variable, "1").args(["checkout", "--theirs", "--", "a.rs"]);
let observed: Observed = observe(&mut command, b"");
let said: String = stderr_of(&observed);
assert!(rejected_by_policy(&observed), "{variable}: {said}");
assert_untouched(repo.as_path(), said.as_str());
}
for option in ["--icase-pathspecs", "--glob-pathspecs", "--noglob-pathspecs", "--literal-pathspecs"] {
let observed: Observed = run_wrapped(&fixture, repo.as_path(), &[option, "checkout", "--theirs", "--", "a.rs"]);
let said: String = stderr_of(&observed);
assert!(rejected_by_policy(&observed), "{option}: {said}");
assert_untouched(repo.as_path(), said.as_str());
}
for token in [":(icase)a.rs", ":(glob)a.rs", ":(exclude)A.rs", ":!A.rs", ":/a.rs", ":(literal)a.rs"] {
let observed: Observed = run_wrapped(&fixture, repo.as_path(), &["checkout", "--theirs", "--", token]);
let said: String = stderr_of(&observed);
assert!(rejected_by_policy(&observed), "{token}: {said}");
assert_untouched(repo.as_path(), said.as_str());
}
let plain: Observed = run_wrapped(&fixture, repo.as_path(), &["checkout", "--theirs", "--", "a.rs"]);
assert_eq!(plain.code, Some(0), "{}", stderr_of(&plain));
assert_eq!(std::fs::read(repo.join("A.rs")).expect("A.rs"), LOOK_ALIKE_EDIT, "the look-alike is not named, so it stays");
remove(&fixture);
}
#[derive(Clone, Copy, Debug)]
enum InTheWay {
LinkToDirectory,
DanglingLink,
RegularFile,
}
fn conflicted_below_a_replaced_directory(fixture: &Fixture, in_the_way: InTheWay) -> PathBuf {
let repo: PathBuf = super::super::support::repository(fixture, std::ffi::OsStr::new("repo"));
std::fs::create_dir(repo.join("dir")).expect("dir");
std::fs::write(repo.join("dir/file"), "base\n").expect("base");
git(fixture, repo.as_path(), &["add", "--", "dir/file"]);
git(fixture, repo.as_path(), &["commit", "--quiet", "--message=base"]);
git(fixture, repo.as_path(), &["switch", "--quiet", "--create", "topic"]);
std::fs::write(repo.join("dir/file"), "topic\n").expect("topic");
git(fixture, repo.as_path(), &["commit", "--quiet", "--all", "--message=topic side"]);
git(fixture, repo.as_path(), &["switch", "--quiet", "main"]);
std::fs::write(repo.join("dir/file"), "main\n").expect("main");
git(fixture, repo.as_path(), &["commit", "--quiet", "--all", "--message=main side"]);
let started: Observed = super::super::support::run_direct(fixture, repo.as_path(), &["merge", "--no-edit", "topic"]);
assert_eq!(started.code, Some(1), "the merge stops with a conflict: {}", stderr_of(&started));
std::fs::remove_dir_all(repo.join("dir")).expect("remove the directory");
match in_the_way {
InTheWay::LinkToDirectory => {
std::fs::create_dir(repo.join("elsewhere")).expect("elsewhere");
std::fs::write(repo.join("elsewhere/file"), "the person's file reached through the link\n").expect("file");
std::os::unix::fs::symlink("elsewhere", repo.join("dir")).expect("link");
}
InTheWay::DanglingLink => std::os::unix::fs::symlink("nowhere", repo.join("dir")).expect("link"),
InTheWay::RegularFile => std::fs::write(repo.join("dir"), "the person's file named dir\n").expect("file"),
}
return repo;
}
fn assert_in_the_way_kept(repo: &std::path::Path, in_the_way: InTheWay, said: &str) {
match in_the_way {
InTheWay::LinkToDirectory => {
assert_eq!(std::fs::read_link(repo.join("dir")).expect("dir is still a link"), PathBuf::from("elsewhere"), "{said}");
assert_eq!(std::fs::read(repo.join("elsewhere/file")).expect("file"), b"the person's file reached through the link\n", "{said}");
}
InTheWay::DanglingLink => assert_eq!(std::fs::read_link(repo.join("dir")).expect("dir is still a link"), PathBuf::from("nowhere"), "{said}"),
InTheWay::RegularFile => assert_eq!(std::fs::read(repo.join("dir")).expect("dir is still a file"), b"the person's file named dir\n", "{said}"),
}
}
#[test]
fn something_where_a_directory_was_is_never_replaced_unsaved() {
for (in_the_way, kind) in [(InTheWay::LinkToDirectory, "symbolic link"), (InTheWay::DanglingLink, "symbolic link"), (InTheWay::RegularFile, "file")] {
let fixture: Fixture = fixture(format!("side-ancestor-{in_the_way:?}").to_lowercase().as_str());
let repo: PathBuf = conflicted_below_a_replaced_directory(&fixture, in_the_way);
for arguments in [["checkout", "--theirs", "--", "dir/file"], ["restore", "--ours", "--", "dir/file"]] {
let observed: Observed = run_wrapped(&fixture, repo.as_path(), &arguments);
let said: String = stderr_of(&observed);
assert!(rejected_by_policy(&observed), "{in_the_way:?} {arguments:?}: {said}");
assert!(said.contains("`dir`") && said.contains(kind), "{in_the_way:?} the refusal names the ancestor and what it is: {said}");
assert!(said.contains("--no-enforce-worktree") && said.contains("Move"), "{in_the_way:?} the refusal says what to do: {said}");
assert_in_the_way_kept(repo.as_path(), in_the_way, said.as_str());
}
assert!(audit_lines(repo.as_path()).iter().all(|line| { return !line.contains("saved-conflict"); }), "{in_the_way:?}");
remove(&fixture);
}
}
#[test]
fn an_option_value_named_like_the_worktree_option_is_not_that_option() {
let fixture: Fixture = fixture("side-option-value");
let repo: PathBuf = conflicted(&fixture, Stop::Merge);
git(&fixture, repo.as_path(), &["update-ref", "refs/tags/-W", "topic"]);
let observed: Observed = run_wrapped(&fixture, repo.as_path(), &["restore", "--source", "-W", "--staged", "--", "a.rs"]);
let said: String = stderr_of(&observed);
assert!(!said.contains("saved your copy"), "no worktree file is replaced, so none is saved: {said}");
assert_eq!(std::fs::read(repo.join("a.rs")).expect("a.rs"), HAND_EDITED, "{said}");
assert!(!saved_root(repo.as_path()).exists(), "no copy was made: {said}");
assert!(audit_lines(repo.as_path()).iter().all(|line| { return !line.contains("saved-conflict"); }), "{said}");
remove(&fixture);
}
#[test]
fn an_option_value_named_like_the_worktree_option_is_rejected_as_an_index_restore() {
let fixture: Fixture = fixture("side-option-value-form");
let repo: PathBuf = conflicted(&fixture, Stop::Merge);
git(&fixture, repo.as_path(), &["update-ref", "refs/tags/-W", "topic"]);
let observed: Observed = run_wrapped(&fixture, repo.as_path(), &["restore", "--source", "-W", "--staged", "--", "a.rs"]);
let said: String = stderr_of(&observed);
assert!(rejected_by_policy(&observed), "{said}");
assert!(said.contains("git restore --staged") && said.contains("removes staged entries"), "the index-only form is named: {said}");
assert_eq!(std::fs::read(repo.join("a.rs")).expect("a.rs"), HAND_EDITED, "{said}");
let worktree: Observed = run_wrapped(&fixture, repo.as_path(), &["restore", "--staged", "-W", "--theirs", "--source=topic", "--", "c.rs"]);
assert!(stderr_of(&worktree).contains("overwrites files"), "{}", stderr_of(&worktree));
remove(&fixture);
}
const ALLOWED_WITHOUT: &str = "saved first and then allowed";
fn modes() -> Vec<(&'static str, Option<&'static str>, Option<&'static str>)> {
return vec![
("GIT_ICASE_PATHSPECS", Some("GIT_ICASE_PATHSPECS"), None),
("GIT_GLOB_PATHSPECS", Some("GIT_GLOB_PATHSPECS"), None),
("GIT_NOGLOB_PATHSPECS", Some("GIT_NOGLOB_PATHSPECS"), None),
("GIT_LITERAL_PATHSPECS", Some("GIT_LITERAL_PATHSPECS"), None),
("--icase-pathspecs", None, Some("--icase-pathspecs")),
("--glob-pathspecs", None, Some("--glob-pathspecs")),
("--noglob-pathspecs", None, Some("--noglob-pathspecs")),
("--literal-pathspecs", None, Some("--literal-pathspecs")),
("--no-literal-pathspecs", None, Some("--no-literal-pathspecs")),
];
}
fn run_under_mode(fixture: &Fixture, repo: &std::path::Path, mode: (&str, Option<&str>, Option<&str>), arguments: &[&str]) -> Observed {
let mut command = wrapped(fixture);
command.current_dir(repo);
if let Some(variable) = mode.1 {
command.env(variable, "1");
}
if let Some(option) = mode.2 {
command.arg(option);
}
command.args(arguments);
return observe(&mut command, b"");
}
#[test]
fn a_refusal_for_a_pathspec_mode_names_the_mode_and_the_way_out() {
let fixture: Fixture = fixture("mode-note");
let repo: PathBuf = with_look_alike(&fixture);
for mode in modes() {
for arguments in [["checkout", "--theirs", "--", "a.rs"], ["restore", "--ours", "--", "a.rs"]] {
let observed: Observed = run_under_mode(&fixture, repo.as_path(), mode, &arguments);
let said: String = stderr_of(&observed);
assert!(rejected_by_policy(&observed), "{mode:?} {arguments:?}: {said}");
assert!(said.contains(format!("the same command without `{}`", mode.0).as_str()), "{mode:?} {arguments:?} names the mode: {said}");
assert!(said.contains(ALLOWED_WITHOUT), "{mode:?} {arguments:?} says the way out: {said}");
assert_untouched(repo.as_path(), said.as_str());
}
}
remove(&fixture);
}
#[test]
fn no_pathspec_mode_note_appears_when_the_carve_out_would_refuse_anyway() {
let fixture: Fixture = fixture("mode-note-absent");
let repo: PathBuf = with_look_alike(&fixture);
for mode in modes() {
for arguments in [
vec!["checkout", "--theirs", "--", "c.rs"],
vec!["checkout", "--theirs", "--", "a.rs", "c.rs"],
vec!["checkout", "--theirs", "--", "A.rs"],
vec!["checkout", "-f", "--", "a.rs"],
vec!["checkout", "--theirs", "a.rs"],
vec!["restore", "--staged", "--theirs", "--", "a.rs"],
vec!["checkout", "--theirs", "--", "*.rs"],
] {
let observed: Observed = run_under_mode(&fixture, repo.as_path(), mode, arguments.as_slice());
let said: String = stderr_of(&observed);
assert!(rejected_by_policy(&observed), "{mode:?} {arguments:?}: {said}");
assert!(!said.contains(ALLOWED_WITHOUT) && !said.contains(mode.0), "{mode:?} {arguments:?} has no note: {said}");
}
}
let ended: PathBuf = repo.join(".git/MERGE_HEAD");
let saved: Vec<u8> = std::fs::read(&ended).expect("MERGE_HEAD");
std::fs::remove_file(&ended).expect("end the operation's state");
for mode in modes() {
let observed: Observed = run_under_mode(&fixture, repo.as_path(), mode, &["checkout", "--theirs", "--", "a.rs"]);
let said: String = stderr_of(&observed);
assert!(rejected_by_policy(&observed), "{mode:?}: {said}");
assert!(!said.contains(ALLOWED_WITHOUT), "{mode:?} no operation, no note: {said}");
}
std::fs::write(&ended, saved).expect("restore MERGE_HEAD");
assert_untouched(repo.as_path(), "");
remove(&fixture);
}
#[test]
fn a_refusal_for_pathspec_magic_names_the_magic_and_the_file() {
let fixture: Fixture = fixture("magic-note");
let repo: PathBuf = with_look_alike(&fixture);
for (token, magic) in [(":(icase)a.rs", ":(icase)"), (":(glob)a.rs", ":(glob)"), (":(literal)a.rs", ":(literal)"), (":!a.rs", ":!"), (":/a.rs", ":/"), (":(top)a.rs", ":(top)")] {
let observed: Observed = run_wrapped(&fixture, repo.as_path(), &["checkout", "--theirs", "--", token]);
let said: String = stderr_of(&observed);
assert!(rejected_by_policy(&observed), "{token}: {said}");
assert!(said.contains(format!("`{magic}`").as_str()) && said.contains("`a.rs`") && said.contains(ALLOWED_WITHOUT), "{token}: {said}");
assert_untouched(repo.as_path(), said.as_str());
}
for arguments in [
vec!["checkout", "--theirs", "--", ":(icase)c.rs"],
vec!["checkout", "--theirs", "--", ":(icase)a.rs", "c.rs"],
vec!["checkout", "--theirs", "--", ":(icase)*.rs"],
vec!["checkout", "--theirs", "--", ":(attr:text)a.rs"],
vec!["checkout", "-f", "--", ":(icase)a.rs"],
] {
let observed: Observed = run_wrapped(&fixture, repo.as_path(), arguments.as_slice());
let said: String = stderr_of(&observed);
assert!(rejected_by_policy(&observed), "{arguments:?}: {said}");
assert!(!said.contains(ALLOWED_WITHOUT), "{arguments:?} has no note: {said}");
}
remove(&fixture);
}