use super::*;
use crate::direct_fix_install::InstallChange;
use crate::test_support::{REAL_GIT, fixture, git, remove, repository};
use std::path::PathBuf;
use std::rc::Rc;
fn context() -> GitContext {
return GitContext {
real_git: PathBuf::from(REAL_GIT),
overlay: vec![
(OsString::from("GIT_AUTHOR_NAME"), OsString::from("Fixture")),
(OsString::from("GIT_AUTHOR_EMAIL"), OsString::from("fixture@example.invalid")),
],
global_prefix: Vec::new(),
};
}
fn record(from_mode: &str, to_mode: &str, from: &str, to: &str, path: &str) -> String {
return format!(":{from_mode} {to_mode} {from} {to} M\0{path}\0");
}
#[test]
fn hook_changes_list_every_path_and_complete_only_ordinary_modifications() {
let text: String = [
record("100644", "100644", "aaaa", "bbbb", "a.txt"),
record("100755", "100755", "cccc", "dddd", "run.sh"),
record("100644", "100755", "eeee", "ffff", "mode.txt"),
record("120000", "120000", "1111", "2222", "link"),
]
.concat();
let changes: HookChanges = parse_hook_changes(text.as_str()).expect("parsed");
assert_eq!(changes.paths, vec!["a.txt", "run.sh", "mode.txt", "link"]);
assert_eq!(
changes.worktree_records,
vec![
AddedPath {
path: String::from("a.txt"),
git_mode: String::from("100644"),
original_oid: String::from("aaaa"),
intended_oid: String::from("bbbb"),
},
AddedPath {
path: String::from("run.sh"),
git_mode: String::from("100755"),
original_oid: String::from("cccc"),
intended_oid: String::from("dddd"),
},
]
);
assert!(parse_hook_changes(":100644 100644 a b\0x\0").is_err());
assert!(parse_hook_changes(":100644 100644 a b M").is_err());
assert_eq!(parse_hook_changes("").expect("empty"), HookChanges::default());
}
#[test]
fn combined_hook_changes_keep_paths_once_and_every_completion() {
let earlier: HookChanges = parse_hook_changes(record("100644", "100644", "a", "b", "x").as_str()).expect("earlier");
let later: HookChanges = parse_hook_changes(
[record("100644", "100644", "c", "d", "x"), record("100644", "100644", "e", "f", "y")]
.concat()
.as_str(),
)
.expect("later");
let combined: HookChanges = HookChanges::combine(&earlier, &later);
assert_eq!(combined.paths, vec!["x", "y"]);
assert_eq!(combined.worktree_records.len(), 3);
assert_eq!(combined.worktree_records[0].intended_oid, "b");
assert_eq!(combined.worktree_records[2].path, "y");
}
#[test]
fn equal_trees_have_no_hook_changes() {
let changes: HookChanges = describe_hook_changes(
&context(),
Path::new("/nonexistent"),
Path::new("/nonexistent/objects"),
"same",
"same",
)
.expect("no Git needed");
assert_eq!(changes, HookChanges::default());
}
#[test]
fn hook_changes_are_read_from_two_real_trees() {
let root: PathBuf = fixture("worktree-records-trees");
let repo: PathBuf = repository(root.as_path(), "repo");
std::fs::write(repo.join("a.txt"), b"one\n").expect("a");
git(repo.as_path(), &["add", "--", "a.txt"]);
let before: String = String::from_utf8_lossy(git(repo.as_path(), &["write-tree"]).stdout.as_slice()).trim().to_string();
std::fs::write(repo.join("a.txt"), b"two\n").expect("a");
git(repo.as_path(), &["add", "--", "a.txt"]);
let after: String = String::from_utf8_lossy(git(repo.as_path(), &["write-tree"]).stdout.as_slice()).trim().to_string();
let changes: HookChanges =
describe_hook_changes(&context(), repo.as_path(), repo.join(".git/objects").as_path(), before.as_str(), after.as_str())
.expect("changes");
assert_eq!(changes.paths, vec!["a.txt"]);
assert_eq!(changes.worktree_records.len(), 1);
assert_eq!(changes.worktree_records[0].git_mode, "100644");
remove(root.as_path());
}
#[test]
fn selected_corrections_are_recorded_only_while_the_worktree_holds_the_original() {
let root: PathBuf = fixture("worktree-records-selected");
let repo: PathBuf = repository(root.as_path(), "repo");
std::fs::write(repo.join("a.txt"), b"one").expect("a");
std::fs::write(repo.join("b.txt"), b"two").expect("b");
std::fs::write(repo.join("c.txt"), b"three").expect("c");
git(repo.as_path(), &["add", "--", "a.txt", "b.txt", "c.txt"]);
let index: PathBuf = repo.join(".git/index");
let corrected = |name: &str, bytes: &[u8]| {
let blob = {
let mut child = std::process::Command::new(REAL_GIT)
.current_dir(&repo)
.args(["hash-object", "-w", "--stdin"])
.stdin(std::process::Stdio::piped())
.stdout(std::process::Stdio::piped())
.spawn()
.expect("hash-object");
std::io::Write::write_all(&mut child.stdin.take().expect("stdin"), bytes).expect("write");
String::from_utf8_lossy(child.wait_with_output().expect("output").stdout.as_slice()).trim().to_string()
};
git(repo.as_path(), &["update-index", "--cacheinfo", "100644", blob.as_str(), name]);
};
corrected("a.txt", b"one\n");
corrected("b.txt", b"two\n");
corrected("c.txt", b"three\n");
std::fs::write(repo.join("b.txt"), b"two edited").expect("edited since");
std::fs::remove_file(repo.join("c.txt")).expect("removed since");
let change = |name: &str, original: &[u8], replacement: &[u8]| {
return InstallChange {
path: name.as_bytes().to_vec(),
mode: CandidateMode::Regular,
original: Rc::from(original),
replacement: Rc::from(replacement),
};
};
let records: Vec<AddedPath> = selected_worktree_records(
&context(),
repo.as_path(),
repo.as_path(),
index.as_path(),
&[
change("a.txt", b"one", b"one\n"),
change("b.txt", b"two", b"two\n"),
change("c.txt", b"three", b"three\n"),
],
)
.expect("records");
assert_eq!(records.len(), 1, "{records:?}");
assert_eq!(records[0].path, "a.txt");
assert_eq!(records[0].git_mode, "100644");
assert_ne!(records[0].original_oid, records[0].intended_oid);
remove(root.as_path());
}
#[test]
fn non_ordinary_modes_get_no_completion() {
assert_eq!(ordinary_git_mode(CandidateMode::Regular), Some("100644"));
assert_eq!(ordinary_git_mode(CandidateMode::Executable), Some("100755"));
assert_eq!(ordinary_git_mode(CandidateMode::Symlink), None);
assert_eq!(ordinary_git_mode(CandidateMode::Gitlink), None);
}
#[test]
fn a_changed_mode_or_an_unchanged_blob_gets_no_completion() {
let root: PathBuf = fixture("worktree-records-skips");
let repo: PathBuf = repository(root.as_path(), "repo");
std::fs::write(repo.join("same.txt"), b"same").expect("same");
std::fs::write(repo.join("moded.txt"), b"moded").expect("moded");
git(repo.as_path(), &["add", "--", "same.txt", "moded.txt"]);
let blob = |bytes: &[u8]| -> String {
let mut child = std::process::Command::new(REAL_GIT)
.current_dir(&repo)
.args(["hash-object", "-w", "--stdin"])
.stdin(std::process::Stdio::piped())
.stdout(std::process::Stdio::piped())
.spawn()
.expect("hash-object");
std::io::Write::write_all(&mut child.stdin.take().expect("stdin"), bytes).expect("write");
return String::from_utf8_lossy(child.wait_with_output().expect("output").stdout.as_slice()).trim().to_string();
};
git(repo.as_path(), &["update-index", "--cacheinfo", "100755", blob(b"moded\n").as_str(), "moded.txt"]);
let change = |name: &str, original: &[u8], replacement: &[u8]| {
return InstallChange {
path: name.as_bytes().to_vec(),
mode: CandidateMode::Regular,
original: Rc::from(original),
replacement: Rc::from(replacement),
};
};
let records: Vec<AddedPath> = selected_worktree_records(
&context(),
repo.as_path(),
repo.as_path(),
repo.join(".git/index").as_path(),
&[change("same.txt", b"same", b"same\n"), change("moded.txt", b"moded", b"moded\n")],
)
.expect("records");
assert_eq!(records, Vec::<AddedPath>::new());
remove(root.as_path());
}