use super::*;
use std::ffi::OsStr;
use crate::subvolume_error::Interrupt;
use crate::subvolume_sync::{SyncOptions, sync_tree};
use crate::subvolume_test_support::{PlainPlatform, make_checkout};
use crate::test_support::{fixture, git, git_text, remove};
fn checkout_with_worktrees(name: &str) -> (PathBuf, PathBuf) {
let parent: PathBuf = fixture(name);
let root: PathBuf = make_checkout(parent.as_path(), "checkout");
git(root.as_path(), &["worktree", "add", "--quiet", "--detach", "inside-b"]);
git(root.as_path(), &["worktree", "add", "--quiet", "--detach", "inside-a"]);
git(root.as_path(), &["worktree", "add", "--quiet", "--detach", "../outside"]);
return (parent, root);
}
fn copy_of(source: &Path, destination: &Path) {
std::fs::create_dir(destination).expect("destination");
let options: SyncOptions<'_> = SyncOptions { delete: false, update_only: false, excluded: &[], created_since: None };
sync_tree(&PlainPlatform::new(), source, destination, options, &Interrupt::never()).expect("copy");
}
#[test]
fn only_existing_linked_worktrees_inside_the_checkout_are_found() {
let (parent, root) = checkout_with_worktrees("index-files-worktrees");
std::fs::remove_dir_all(parent.join("outside")).expect("outside is irrelevant either way");
git(root.as_path(), &["worktree", "add", "--quiet", "--detach", "gone"]);
std::fs::remove_dir_all(root.join("gone")).expect("the directory of a worktree Git would prune");
std::fs::create_dir(root.join(".git/worktrees/orphan")).expect("an administrative directory with no gitdir file");
let found: Vec<LinkedWorktree> = linked_worktrees_inside(root.as_path());
let worktrees: Vec<PathBuf> = found.iter().map(|linked: &LinkedWorktree| { return linked.worktree.clone(); }).collect();
assert_eq!(worktrees, vec![root.join("inside-a"), root.join("inside-b")]);
assert!(found.iter().all(|linked: &LinkedWorktree| { return linked.admin_directory.starts_with(root.join(".git/worktrees")); }));
assert_eq!(linked_worktrees_inside(parent.join("absent").as_path()), Vec::<LinkedWorktree>::new());
remove(parent.as_path());
}
#[test]
fn a_missing_and_a_declined_index_are_reported_and_left_alone() {
let parent: PathBuf = fixture("index-files-declined");
let index: PathBuf = parent.join("index");
assert_eq!(refresh_index_file(index.as_path(), parent.as_path()), "no index file");
std::fs::write(index.as_path(), b"not an index at all").expect("file");
let line: String = refresh_index_file(index.as_path(), parent.as_path());
assert!(line.starts_with("skipped (") && line.contains("no DIRC signature"), "{line}");
assert_eq!(std::fs::read(index.as_path()).expect("unchanged"), b"not an index at all");
assert!(!parent.join("index.lock").exists());
remove(parent.as_path());
}
#[test]
fn an_unreadable_index_is_reported() {
let parent: PathBuf = fixture("index-files-unreadable");
std::fs::create_dir(parent.join("index")).expect("a directory where the index would be");
let line: String = refresh_index_file(parent.join("index").as_path(), parent.as_path());
assert!(line.starts_with("skipped (cannot read it:"), "{line}");
remove(parent.as_path());
}
#[test]
fn a_copied_index_is_rewritten_through_its_lock() {
let parent: PathBuf = fixture("index-files-rewrite");
let root: PathBuf = make_checkout(parent.as_path(), "checkout");
let copy: PathBuf = parent.join("copy");
copy_of(root.as_path(), copy.as_path());
let before: String = git_text(copy.as_path(), &["ls-files", "--debug", "--", "README.md"]);
let line: String = refresh_index_file(copy.join(".git/index").as_path(), copy.as_path());
assert!(line.ends_with("entries refreshed") && !line.starts_with("0 of"), "{line}");
let after: String = git_text(copy.as_path(), &["ls-files", "--debug", "--", "README.md"]);
assert_ne!(before, after, "the recorded inode changed");
assert_eq!(git_text(copy.as_path(), &["status", "--porcelain"]), "");
assert!(!copy.join(".git/index.lock").exists());
remove(parent.as_path());
}
#[test]
fn a_held_lock_leaves_the_index_alone_and_says_so() {
let parent: PathBuf = fixture("index-files-locked");
let root: PathBuf = make_checkout(parent.as_path(), "checkout");
let copy: PathBuf = parent.join("copy");
copy_of(root.as_path(), copy.as_path());
let before: Vec<u8> = std::fs::read(copy.join(".git/index")).expect("index");
std::fs::write(copy.join(".git/index.lock"), b"").expect("lock");
let line: String = refresh_index_file(copy.join(".git/index").as_path(), copy.as_path());
assert!(line.contains("index.lock exists") && line.contains("slow"), "{line}");
assert_eq!(std::fs::read(copy.join(".git/index")).expect("index"), before);
remove(parent.as_path());
}
#[test]
fn the_lock_is_taken_before_the_index_is_read() {
let parent: PathBuf = fixture("index-files-lock-first");
std::fs::create_dir(parent.join("index")).expect("a directory where the index would be: reading it fails");
std::fs::write(parent.join("index.lock"), b"another command\n").expect("the other command's lock");
let line: String = refresh_index_file(parent.join("index").as_path(), parent.as_path());
assert!(line.contains("index.lock exists"), "the lock was checked first: {line}");
assert_eq!(std::fs::read(parent.join("index.lock")).expect("lock kept"), b"another command\n");
remove(parent.as_path());
}
#[test]
fn a_rewrite_under_the_held_lock_keeps_the_lock_and_a_foreign_lock_blocks_it() {
let parent: PathBuf = fixture("index-files-held");
let root: PathBuf = make_checkout(parent.as_path(), "checkout");
let copy: PathBuf = parent.join("copy");
copy_of(root.as_path(), copy.as_path());
let index: PathBuf = copy.join(".git/index");
let before: Vec<u8> = std::fs::read(index.as_path()).expect("index");
std::fs::write(copy.join(".git/index.lock"), b"git add is writing here\n").expect("foreign lock");
let blocked: String = refresh_index_file_locked(index.as_path(), copy.as_path(), IndexLock::Held("our token\n"));
assert!(blocked.contains("held by another Git command"), "{blocked}");
assert_eq!(std::fs::read(index.as_path()).expect("index"), before);
std::fs::write(copy.join(".git/index.lock"), b"our token\n").expect("our lock");
let line: String = refresh_index_file_locked(index.as_path(), copy.as_path(), IndexLock::Held("our token\n"));
assert!(line.ends_with("entries refreshed") && !line.starts_with("0 of"), "{line}");
assert_ne!(std::fs::read(index.as_path()).expect("index"), before, "the index was rewritten");
assert_eq!(std::fs::read(copy.join(".git/index.lock")).expect("lock kept"), b"our token\n");
assert!(!copy.join(".git/index.cli-git-new").exists(), "no sibling file is left");
remove(parent.as_path());
}
#[test]
fn a_directory_that_refuses_the_lock_is_reported() {
if unsafe_effective_user_is_root() {
return;
}
let parent: PathBuf = fixture("index-files-no-lock");
let root: PathBuf = make_checkout(parent.as_path(), "checkout");
let copy: PathBuf = parent.join("copy");
copy_of(root.as_path(), copy.as_path());
std::fs::set_permissions(copy.join(".git"), std::os::unix::fs::PermissionsExt::from_mode(0o500)).expect("read-only");
let line: String = refresh_index_file(copy.join(".git/index").as_path(), copy.as_path());
std::fs::set_permissions(copy.join(".git"), std::os::unix::fs::PermissionsExt::from_mode(0o755)).expect("restore");
assert!(line.starts_with("skipped (cannot create the lock:"), "{line}");
remove(parent.as_path());
}
fn unsafe_effective_user_is_root() -> bool {
return unsafe { libc::geteuid() } == 0;
}
#[test]
fn every_index_of_the_checkout_is_refreshed_and_reported() {
let parent: PathBuf = fixture("index-files-all");
let root: PathBuf = make_checkout(parent.as_path(), "checkout");
git(root.as_path(), &["worktree", "add", "--quiet", "--detach", "wt"]);
let copy: PathBuf = parent.join("copy");
copy_of(root.as_path(), copy.as_path());
std::fs::write(copy.join(".git/worktrees/wt/gitdir"), format!("{}\n", copy.join("wt/.git").display())).expect("gitdir");
std::fs::write(copy.join("wt/.git"), format!("gitdir: {}\n", copy.join(".git/worktrees/wt").display())).expect("gitfile");
let mut lines: Vec<String> = Vec::new();
let count: usize = refresh_all_indexes(copy.as_path(), IndexLock::Acquire, &[], &mut |text: &str| { lines.push(text.to_string()); });
assert_eq!(count, 2);
assert_eq!(lines.len(), 2);
assert!(lines[0].starts_with(format!("index {}:", copy.join(".git/index").display()).as_str()), "{lines:?}");
assert!(lines[1].contains(".git/worktrees/wt/index") && lines[1].ends_with("entries refreshed"), "{lines:?}");
assert_eq!(git_text(copy.join("wt").as_path(), &["status", "--porcelain"]), "");
remove(parent.as_path());
}
fn set_seconds(path: &Path, seconds: i64) {
let times: [libc::timespec; 2] = [libc::timespec { tv_sec: seconds, tv_nsec: 0 }, libc::timespec { tv_sec: seconds, tv_nsec: 0 }];
let name = std::ffi::CString::new(path.as_os_str().as_encoded_bytes()).expect("path text");
let result: libc::c_int = unsafe { libc::utimensat(libc::AT_FDCWD, name.as_ptr(), times.as_ptr(), libc::AT_SYMLINK_NOFOLLOW) };
assert_eq!(result, 0, "set the time of {}", path.display());
}
fn repository_with_silent_edits(name: &str) -> (PathBuf, PathBuf) {
let parent: PathBuf = fixture(name);
let root: PathBuf = crate::test_support::repository(parent.as_path(), "checkout");
let (racy, quiet): (i64, i64) = (1_600_000_000, 1_599_000_000);
std::fs::write(root.join("racy.txt"), b"aaaa").expect("racy");
std::fs::write(root.join("quiet.txt"), b"cccc").expect("quiet");
set_seconds(root.join("racy.txt").as_path(), racy);
set_seconds(root.join("quiet.txt").as_path(), quiet);
git(root.as_path(), &["add", "--all"]);
git(root.as_path(), &["commit", "--quiet", "--message=files"]);
std::fs::write(root.join("racy.txt"), b"bbbb").expect("racy edit");
std::fs::write(root.join("quiet.txt"), b"dddd").expect("quiet edit");
set_seconds(root.join("racy.txt").as_path(), racy);
set_seconds(root.join("quiet.txt").as_path(), quiet);
set_seconds(root.join(".git/index").as_path(), racy);
return (parent, root);
}
#[test]
fn a_racy_entry_is_left_for_git_to_hash() {
let (parent, root) = repository_with_silent_edits("index-files-racy");
let copy: PathBuf = parent.join("copy");
copy_of(root.as_path(), copy.as_path());
let line: String = refresh_index_file(copy.join(".git/index").as_path(), copy.as_path());
assert!(line.ends_with("entries refreshed"), "{line}");
let status: String = git_text(copy.as_path(), &["status", "--porcelain"]);
assert!(status.contains(" M racy.txt"), "the racy edit is seen: {status:?}");
remove(parent.as_path());
}
#[test]
fn a_file_that_changed_after_git_looked_is_not_refreshed_when_the_original_is_the_reference() {
let (parent, root) = repository_with_silent_edits("index-files-reference");
let copy: PathBuf = parent.join("copy");
copy_of(root.as_path(), copy.as_path());
let references: [PathBuf; 1] = [root.clone()];
let options: RefreshOptions<'_> = RefreshOptions { lock: IndexLock::Acquire, references: &references, hash_length: None };
let line: String = refresh_index_file_with(copy.join(".git/index").as_path(), copy.as_path(), &options);
assert!(line.ends_with("entries refreshed"), "{line}");
let status: String = git_text(copy.as_path(), &["status", "--porcelain"]);
assert!(status.contains(" M quiet.txt") && status.contains(" M racy.txt"), "both edits are seen: {status:?}");
remove(parent.as_path());
}
#[test]
fn a_linked_worktree_with_a_non_utf8_name_is_found() {
use std::os::unix::ffi::OsStrExt;
let parent: PathBuf = fixture("index-files-non-utf8");
let root: PathBuf = make_checkout(parent.as_path(), "checkout");
let worktree: PathBuf = root.join(OsStr::from_bytes(b"work-\xff"));
std::fs::create_dir(worktree.as_path()).expect("a directory whose name is not UTF-8");
let admin: PathBuf = root.join(".git/worktrees/odd");
std::fs::create_dir_all(admin.as_path()).expect("admin directory");
let mut recorded: Vec<u8> = worktree.join(".git").as_os_str().as_bytes().to_vec();
recorded.push(b'\n');
std::fs::write(admin.join("gitdir"), recorded).expect("gitdir");
let found: Vec<LinkedWorktree> = linked_worktrees_inside(root.as_path());
assert_eq!(found, vec![LinkedWorktree { admin_directory: admin, worktree }]);
remove(parent.as_path());
}
#[test]
fn the_hash_length_is_read_from_the_repository_configuration() {
let parent: PathBuf = fixture("index-files-hash-length");
assert_eq!(repository_hash_length(parent.as_path()), None);
for (text, expected) in [
("[core]\n\trepositoryformatversion = 0\n", 20),
("[core]\n\trepositoryformatversion = 1\n[extensions]\n\tobjectFormat = sha256\n", 32),
("[Extensions]\nOBJECTFORMAT=SHA256\n", 32),
("[extensions]\n\tobjectformat = sha1\n", 20),
("[core]\n\tobjectformat = sha256\n", 20),
("[extensions]\n\tworktreeConfig = true\n[core]\n\tobjectformat = sha256\n", 20),
] {
std::fs::write(parent.join("config"), text).expect("config");
assert_eq!(repository_hash_length(parent.as_path()), Some(expected), "{text:?}");
}
remove(parent.as_path());
}
fn recorded_and_present() -> (StatFields, StoredStat) {
let now: StatFields = StatFields { ctime_seconds: 10, ctime_nanos: 11, mtime_seconds: 20, mtime_nanos: 21, dev: 5, ino: 30, uid: 40, gid: 50, size: 60 };
let stored: StoredStat = StoredStat { ctime_seconds: 10, ctime_nanos: 11, mtime_seconds: 20, mtime_nanos: 21, ino: 30, uid: 40, gid: 50, size: 60 };
return (now, stored);
}
#[test]
fn an_entry_matches_a_file_when_every_recorded_field_agrees_and_not_when_one_differs() {
let (now, stored) = recorded_and_present();
assert!(matches_stored(&now, &stored));
assert!(matches_stored(&StatFields { dev: now.dev + 1, ..now }, &stored), "the device is not compared");
let changed: [StatFields; 8] = [
StatFields { size: now.size + 1, ..now },
StatFields { ino: now.ino + 1, ..now },
StatFields { uid: now.uid + 1, ..now },
StatFields { gid: now.gid + 1, ..now },
StatFields { ctime_seconds: now.ctime_seconds + 1, ..now },
StatFields { ctime_nanos: now.ctime_nanos + 1, ..now },
StatFields { mtime_seconds: now.mtime_seconds + 1, ..now },
StatFields { mtime_nanos: now.mtime_nanos + 1, ..now },
];
for (index, other) in changed.iter().enumerate() {
assert!(!matches_stored(other, &stored), "field {index} differs");
}
}
#[test]
fn a_recorded_zero_nanosecond_count_matches_any_and_a_non_zero_one_must_agree() {
let (now, stored) = recorded_and_present();
let other_nanos: u32 = now.mtime_nanos + 1;
let without_mtime_nanos: StoredStat = StoredStat { mtime_nanos: 0, ..stored };
assert!(matches_stored(&StatFields { mtime_nanos: other_nanos, ..now }, &without_mtime_nanos), "mtime recorded as zero matches any");
assert!(matches_stored(&now, &without_mtime_nanos));
assert!(!matches_stored(&StatFields { mtime_nanos: other_nanos, ..now }, &stored), "mtime recorded non-zero must agree");
let without_ctime_nanos: StoredStat = StoredStat { ctime_nanos: 0, ..stored };
assert!(matches_stored(&StatFields { ctime_nanos: now.ctime_nanos + 1, ..now }, &without_ctime_nanos), "ctime recorded as zero matches any");
assert!(!matches_stored(&StatFields { ctime_nanos: now.ctime_nanos + 1, ..now }, &stored), "ctime recorded non-zero must agree");
assert!(matches_stored(&now, &stored), "equal non-zero counts agree");
}
fn seed_and_admin(name: &str) -> (PathBuf, Vec<u8>, PathBuf, std::fs::File, StatFields) {
let root: PathBuf = fixture(name);
let repo: PathBuf = std::fs::canonicalize(crate::test_support::repository(root.as_path(), "repo")).expect("canonical");
std::fs::write(repo.join("a.txt"), b"alpha\n").expect("file");
crate::test_support::age(repo.join("a.txt").as_path());
git(repo.as_path(), &["add", "a.txt"]);
git(repo.as_path(), &["commit", "--quiet", "--message=first"]);
let seed: Vec<u8> = std::fs::read(repo.join(".git/index")).expect("index");
let admin: PathBuf = std::fs::canonicalize(root.as_path()).expect("canonical").join("admin");
std::fs::create_dir(admin.as_path()).expect("admin");
let held: std::fs::File = crate::fs_beneath::open_root(admin.as_path()).expect("handle");
let real: StatFields = fields_of(&std::fs::symlink_metadata(repo.join("a.txt")).expect("metadata"));
return (root, seed, admin, held, StatFields { ino: 6, ..real });
}
#[test]
fn an_index_is_installed_under_the_lock_with_the_stat_fields_it_was_given() {
let (root, seed, admin, held, fields) = seed_and_admin("install-index-ok");
let report: String = install_index_in(&held, seed.as_slice(), &mut |path: &[u8]| { return (path == b"a.txt").then_some(fields); }, 20).expect("installed");
assert_eq!(report, "1 of 1 entries refreshed");
assert!(admin.join("index").is_file() && !admin.join("index.lock").exists());
let entries = crate::worktree_clean::stored_stats(std::fs::read(admin.join("index")).expect("index").as_slice(), 20).expect("entries");
assert_eq!(entries[0].stored.ino, 6, "the fields of the provider");
remove(root.as_path());
}
#[test]
fn an_entry_the_provider_does_not_know_keeps_its_stored_fields() {
let (root, seed, admin, held, _fields) = seed_and_admin("install-index-unknown");
let report: String = install_index_in(&held, seed.as_slice(), &mut |_path: &[u8]| { return None; }, 20).expect("installed");
assert_eq!(report, "0 of 1 entries refreshed");
assert_eq!(std::fs::read(admin.join("index")).expect("index").len(), seed.len());
remove(root.as_path());
}
#[test]
fn an_existing_index_or_a_held_lock_ends_the_install_untouched() {
let (root, seed, admin, held, fields) = seed_and_admin("install-index-refused");
std::fs::write(admin.join("index.lock"), b"theirs").expect("their lock");
assert!(install_index_in(&held, seed.as_slice(), &mut |path: &[u8]| { return (path == b"a.txt").then_some(fields); }, 20).expect_err("locked").contains("index.lock exists"));
assert_eq!(std::fs::read(admin.join("index.lock")).expect("lock"), b"theirs");
assert!(!admin.join("index").exists());
std::fs::remove_file(admin.join("index.lock")).expect("their lock goes");
std::fs::write(admin.join("index"), b"theirs").expect("their index");
assert!(install_index_in(&held, seed.as_slice(), &mut |path: &[u8]| { return (path == b"a.txt").then_some(fields); }, 20).expect_err("an index stands").contains("appeared"));
assert_eq!(std::fs::read(admin.join("index")).expect("index"), b"theirs");
assert!(!admin.join("index.lock").exists(), "our lock went");
remove(root.as_path());
}
#[test]
fn an_index_written_before_the_publication_is_not_replaced() {
let (root, seed, admin, held, fields) = seed_and_admin("install-index-raced");
let mut write_theirs = || { std::fs::write(admin.join("index"), b"theirs").expect("their index"); };
let error: String = install_index_in_with(&held, seed.as_slice(), &mut |path: &[u8]| { return (path == b"a.txt").then_some(fields); }, 20, &mut InstallSeams { before_publish: &mut write_theirs, after_publish: &mut || {} }).expect_err("refused");
assert!(error.contains("cannot be put in place"), "{error}");
assert_eq!(std::fs::read(admin.join("index")).expect("index"), b"theirs");
assert!(!admin.join("index.lock").exists(), "our lock went");
remove(root.as_path());
}
#[test]
fn a_lock_replaced_before_the_publication_does_not_change_the_file_published() {
let (root, seed, admin, held, fields) = seed_and_admin("install-index-lock-replaced");
let mut replace = || {
std::fs::rename(admin.join("index.lock"), admin.join("index.lock.moved")).expect("our lock moved away");
std::fs::write(admin.join("index.lock"), b"theirs").expect("their lock");
};
install_index_in_with(&held, seed.as_slice(), &mut |path: &[u8]| { return (path == b"a.txt").then_some(fields); }, 20, &mut InstallSeams { before_publish: &mut replace, after_publish: &mut || {} }).expect("installed");
let entries = crate::worktree_clean::stored_stats(std::fs::read(admin.join("index")).expect("index").as_slice(), 20).expect("what was written is a valid index");
assert_eq!(entries[0].stored.ino, 6, "the index is the file written here");
assert_eq!(std::fs::read(admin.join("index.lock")).expect("lock"), b"theirs", "their lock stays");
remove(root.as_path());
}
#[test]
fn a_lock_taken_right_after_the_publication_stays() {
let (root, seed, admin, held, fields) = seed_and_admin("install-index-after");
let mut take_lock = || {
std::fs::rename(admin.join("index.lock"), admin.join("index.lock.moved")).expect("our lock moves");
std::fs::write(admin.join("index.lock"), b"the next writer").expect("their lock");
};
install_index_in_with(&held, seed.as_slice(), &mut |path: &[u8]| { return (path == b"a.txt").then_some(fields); }, 20, &mut InstallSeams { before_publish: &mut || {}, after_publish: &mut take_lock }).expect("installed");
assert_eq!(std::fs::read(admin.join("index.lock")).expect("lock"), b"the next writer", "released only while the name still holds our file");
assert!(admin.join("index").is_file());
remove(root.as_path());
}
#[test]
fn a_refresh_leaves_the_lock_of_the_next_command_alone() {
let parent: PathBuf = fixture("index-files-next-lock");
let root: PathBuf = make_checkout(parent.as_path(), "checkout");
let copy: PathBuf = parent.join("copy");
copy_of(root.as_path(), copy.as_path());
let index: PathBuf = copy.join(".git/index");
let lock: PathBuf = copy.join(".git/index.lock");
let mut next_command = || { std::fs::write(lock.as_path(), b"the next command's lock").expect("their lock"); };
let line: String = refresh_index_file_seamed(index.as_path(), copy.as_path(), &RefreshOptions { lock: IndexLock::Acquire, references: &[], hash_length: None }, &mut next_command);
assert!(line.ends_with("entries refreshed"), "{line}");
assert_eq!(std::fs::read(lock.as_path()).expect("lock"), b"the next command's lock");
remove(parent.as_path());
}
#[test]
fn a_refresh_that_published_nothing_removes_its_own_lock_only() {
let parent: PathBuf = fixture("index-files-own-lock");
std::fs::create_dir(parent.join("tree")).expect("tree");
let missing: PathBuf = parent.join("tree/index");
let line: String = refresh_index_file(missing.as_path(), parent.join("tree").as_path());
assert_eq!(line, "no index file");
assert!(!parent.join("tree/index.lock").exists(), "its own lock is removed");
let lock: PathBuf = parent.join("tree/index.lock");
let mut taken_over = || {
std::fs::remove_file(lock.as_path()).expect("our lock goes");
std::fs::write(lock.as_path(), b"theirs").expect("their lock");
};
let again: String = refresh_index_file_seamed(missing.as_path(), parent.join("tree").as_path(), &RefreshOptions { lock: IndexLock::Acquire, references: &[], hash_length: None }, &mut taken_over);
assert_eq!(again, "no index file");
assert_eq!(std::fs::read(lock.as_path()).expect("lock"), b"theirs", "a lock that is not ours stays");
remove(parent.as_path());
}
#[test]
fn an_existing_staging_name_is_never_followed_or_overwritten() {
let parent: PathBuf = fixture("index-files-staging");
let root: PathBuf = make_checkout(parent.as_path(), "checkout");
let copy: PathBuf = parent.join("copy");
copy_of(root.as_path(), copy.as_path());
let index: PathBuf = copy.join(".git/index");
let before: Vec<u8> = std::fs::read(index.as_path()).expect("index");
std::fs::write(copy.join(".git/index.lock"), b"our token\n").expect("our lock");
let victim: PathBuf = parent.join("victim.txt");
std::fs::write(victim.as_path(), b"a user's file").expect("victim");
std::os::unix::fs::symlink(victim.as_path(), copy.join(".git/index.cli-git-new")).expect("a link at the staging name");
let line: String = refresh_index_file_locked(index.as_path(), copy.as_path(), IndexLock::Held("our token\n"));
assert!(line.starts_with("skipped"), "{line}");
assert_eq!(std::fs::read(victim.as_path()).expect("victim"), b"a user's file", "the link was not followed");
assert_eq!(std::fs::read(index.as_path()).expect("index"), before);
assert!(std::fs::symlink_metadata(copy.join(".git/index.cli-git-new")).expect("link").file_type().is_symlink(), "the staging name was left alone");
std::fs::remove_file(copy.join(".git/index.cli-git-new")).expect("link removed");
std::fs::write(copy.join(".git/index.cli-git-new"), b"somebody's file").expect("a file at the staging name");
let again: String = refresh_index_file_locked(index.as_path(), copy.as_path(), IndexLock::Held("our token\n"));
assert!(again.starts_with("skipped"), "{again}");
assert_eq!(std::fs::read(copy.join(".git/index.cli-git-new")).expect("kept"), b"somebody's file");
remove(parent.as_path());
}
#[test]
fn an_unlinked_lock_fails_the_install_closed() {
let (root, seed, admin, held, fields) = seed_and_admin("install-index-lock-unlinked");
let mut unlink = || {
std::fs::remove_file(admin.join("index.lock")).expect("our lock is unlinked");
std::fs::write(admin.join("index.lock"), b"theirs").expect("their lock");
};
let error: String = install_index_in_with(&held, seed.as_slice(), &mut |path: &[u8]| { return (path == b"a.txt").then_some(fields); }, 20, &mut InstallSeams { before_publish: &mut unlink, after_publish: &mut || {} }).expect_err("refused");
assert!(error.contains("cannot be put in place"), "{error}");
assert!(!admin.join("index").exists());
assert_eq!(std::fs::read(admin.join("index.lock")).expect("lock"), b"theirs");
remove(root.as_path());
}