use super::*;
use crate::recording_git::{recorded_starts, recording_context};
use crate::repo_read_hook::SWITCH_VARIABLE;
use crate::index_reset_file::support::git_with;
use crate::test_support::{fixture, git, git_context, git_output, git_text, remove, repository};
use std::os::unix::ffi::OsStrExt;
use std::path::PathBuf;
fn index_behind(name: &str, files: &[Vec<u8>]) -> (PathBuf, PathBuf, String, String) {
let root: PathBuf = fixture(name);
let repo: PathBuf = repository(root.as_path(), "repo");
let first: String = git_text(repo.as_path(), &["rev-parse", "HEAD"]);
for file in files {
let path: PathBuf = repo.join(std::ffi::OsStr::from_bytes(file.as_slice()));
std::fs::create_dir_all(path.parent().expect("parent")).expect("directories");
std::fs::write(path, b"x\n").expect("write");
}
git(repo.as_path(), &["add", "--all"]);
git(repo.as_path(), &["commit", "--quiet", "--message=second"]);
let second: String = git_text(repo.as_path(), &["rev-parse", "HEAD"]);
git(repo.as_path(), &["read-tree", first.as_str()]);
return (root, repo, first, second);
}
fn differs(repo: &Path, commit: &str) -> bool {
return !git_output(repo, &["diff", "--cached", "--quiet", commit]).status.success();
}
#[test]
fn derived_paths_cover_names_that_are_not_text_and_lists_past_the_request_limit() {
let mut files: Vec<Vec<u8>> = vec![b"not-utf8-\xff\xfe.txt".to_vec(), b"back\\slash \"quoted\".txt".to_vec(), b"line\nbreak.txt".to_vec()];
files.extend((0..crate::service_protocol::MAX_CHANGES + 50).map(|number| { return format!("many/file-{number}.txt").into_bytes(); }));
let (root, repo, first, second) = index_behind("service-follow-derived", files.as_slice());
assert!(differs(repo.as_path(), second.as_str()), "the index starts behind the commit");
update_index(&git_context(), repo.as_path(), repo.join(".git/index").as_path(), second.as_str(), IndexPaths::Derived { old: first.as_str() }).expect("updated");
assert!(!differs(repo.as_path(), second.as_str()));
remove(root.as_path());
}
#[test]
fn a_root_commit_is_updated_from_the_all_zero_old_value() {
let (root, repo, first, _second) = index_behind("service-follow-root", &[b"new.txt".to_vec()]);
git(repo.as_path(), &["read-tree", "--empty"]);
let zero: String = "0".repeat(first.len());
update_index(&git_context(), repo.as_path(), repo.join(".git/index").as_path(), first.as_str(), IndexPaths::Derived { old: zero.as_str() }).expect("updated");
assert!(!differs(repo.as_path(), first.as_str()));
remove(root.as_path());
}
#[test]
fn an_empty_list_leaves_the_index_alone() {
let (root, repo, first, second) = index_behind("service-follow-empty", &[b"new.txt".to_vec()]);
update_index(&git_context(), repo.as_path(), repo.join(".git/index").as_path(), second.as_str(), IndexPaths::Listed(&[])).expect("nothing to do");
assert!(differs(repo.as_path(), second.as_str()));
update_index(&git_context(), repo.as_path(), repo.join(".git/index").as_path(), second.as_str(), IndexPaths::Derived { old: second.as_str() }).expect("no difference");
assert!(differs(repo.as_path(), second.as_str()), "{first}");
remove(root.as_path());
}
#[test]
fn a_holder_proven_alive_is_waited_for_past_all_the_tries() {
let (root, repo, _first, second) = index_behind("service-follow-proven-holder", &[b"new.txt".to_vec()]);
let index: PathBuf = repo.join(".git/index");
let lock: PathBuf = repo.join(".git/index.lock");
std::fs::write(&lock, b"").expect("a lock");
std::fs::write(crate::recovery_files::lock_pid_path(index.as_path()), format!("pid {}\n", std::process::id())).expect("PID file");
let holding: std::time::Duration = INDEX_PAUSE * u32::try_from(INDEX_ATTEMPTS * 3).expect("a small count");
let holder = std::thread::spawn(move || {
std::thread::sleep(holding);
std::fs::remove_file(&lock).expect("the holder lets go");
});
let began: std::time::Instant = std::time::Instant::now();
update_index(&git_context(), repo.as_path(), index.as_path(), second.as_str(), IndexPaths::Named(&[b"new.txt".to_vec()])).expect("updated once the holder let go");
holder.join().expect("holder");
assert!(began.elapsed() >= holding, "{:?}", began.elapsed());
assert!(!differs(repo.as_path(), second.as_str()));
remove(root.as_path());
}
#[test]
fn an_owner_nothing_proves_alive_is_waited_for_like_a_proven_one() {
let (root, repo, _first, second) = index_behind("service-follow-unproven-unbounded", &[b"new.txt".to_vec()]);
let index: PathBuf = repo.join(".git/index");
let lock: PathBuf = repo.join(".git/index.lock");
std::fs::write(&lock, b"").expect("a lock nobody holds");
let holding: std::time::Duration = std::time::Duration::from_millis(2500);
let holder = std::thread::spawn(move || {
std::thread::sleep(holding);
std::fs::remove_file(&lock).expect("the lock goes");
});
let began: std::time::Instant = std::time::Instant::now();
update_index(&git_context(), repo.as_path(), index.as_path(), second.as_str(), IndexPaths::Named(&[b"new.txt".to_vec()])).expect("updated once the lock went");
holder.join().expect("holder");
assert!(began.elapsed() >= holding, "{:?}", began.elapsed());
assert!(!differs(repo.as_path(), second.as_str()));
remove(root.as_path());
}
#[test]
fn a_cleanup_without_its_scratch_directory_is_done() {
let root: PathBuf = fixture("service-follow-gone");
let repo: PathBuf = repository(root.as_path(), "repo");
let cleanup: Cleanup = Cleanup { git_dir: repo.join(".git"), scratch: repo.join(".git").join("gone"), reftable: false };
assert!(remove_conclusion_state(&git_context(), repo.as_path(), &cleanup));
remove(root.as_path());
}
#[test]
fn the_index_update_starts_reset_without_quiet_in_every_form() {
let (root, repo, first, second) = index_behind("service-follow-no-quiet", &[b"new.txt".to_vec()]);
let (mut context, log) = crate::recording_git::recording_context(root.as_path());
context.common_dir = Some(repo.join(".git"));
let listed: Vec<String> = vec![String::from("new.txt")];
let named: Vec<Vec<u8>> = vec![b"new.txt".to_vec()];
for paths in [IndexPaths::Listed(listed.as_slice()), IndexPaths::Named(named.as_slice()), IndexPaths::Derived { old: first.as_str() }] {
git(repo.as_path(), &["read-tree", first.as_str()]);
update_index(&context, repo.as_path(), repo.join(".git/index").as_path(), second.as_str(), paths).expect("updated");
assert!(!differs(repo.as_path(), second.as_str()), "{paths:?} brings the index to the commit");
}
let resets: Vec<String> = crate::recording_git::recorded_starts(log.as_path()).into_iter().filter(|start| { return start.starts_with("reset "); }).collect();
assert_eq!(resets.len(), 3, "one reset for each of the three forms: {resets:?}");
assert_eq!(crate::recording_git::quiet_starts(log.as_path()), Vec::<String>::new(), "no start carries --quiet or -q");
remove(root.as_path());
}
#[test]
fn what_reset_says_is_kept_in_the_journal() {
let (root, repo, _first, second) = index_behind("service-follow-words", &[b"new.txt".to_vec()]);
let shim: PathBuf = root.join("saying-git");
let words: &str = "Unstaged changes after reset:";
let script: String = format!("#!/bin/sh\ncase \" $* \" in *\" reset \"*) printf '%s\\n' '{words}';; esac\nexec {} \"$@\"\n", crate::test_support::REAL_GIT);
crate::test_support::executable(shim.as_path(), script.as_bytes());
let mut context: GitContext = git_context();
context.real_git = shim;
let listed: Vec<String> = vec![String::from("new.txt")];
let entries = crate::service_timing::capture::entries_of(|| {
update_index(&context, repo.as_path(), repo.join(".git/index").as_path(), second.as_str(), IndexPaths::Listed(listed.as_slice())).expect("updated");
});
assert_eq!(entries.len(), 1, "{entries:?}");
assert!(entries[0].message.contains(words), "{:?}", entries[0]);
remove(root.as_path());
}
#[test]
fn the_follow_up_starts_no_git_reset_and_no_git_diff_tree() {
let files: Vec<Vec<u8>> = vec![b"top.txt".to_vec(), b"dir/inner.txt".to_vec()];
let listed: Vec<String> = vec![String::from("top.txt"), String::from("dir/inner.txt")];
for form in ["listed", "named", "derived"] {
let (root, repo, first, second) = index_behind(format!("service-follow-starts-{form}").as_str(), files.as_slice());
let (mut context, log) = recording_context(root.as_path());
context.overlay.retain(|(name, _)| { return name != SWITCH_VARIABLE; });
let paths: IndexPaths<'_> = match form {
"listed" => IndexPaths::Listed(listed.as_slice()),
"named" => IndexPaths::Named(files.as_slice()),
_ => IndexPaths::Derived { old: first.as_str() },
};
let index: PathBuf = repo.join(".git").join("index");
let held = crate::service_index_lock::try_acquire(index.as_path(), 1).expect("the lock is free");
let built = build_index(&context, repo.as_path(), second.as_str(), paths, &held, 1).expect("built");
let words: Vec<String> = recorded_starts(log.as_path()).iter().map(|start| { return start.split(' ').next().unwrap_or_default().to_string(); }).collect();
assert!(!words.iter().any(|word| { return word == "reset" || word == "diff-tree"; }), "{form} started {words:?}");
held.release_with(built.path()).expect("installed");
assert!(!differs(repo.as_path(), second.as_str()), "{form} left the index behind the commit");
remove(root.as_path());
}
}
fn staged_against(repo: &Path, commit: &str) -> Vec<String> {
return git_text(repo, &["diff", "--cached", "--name-only", commit]).lines().map(str::to_string).collect();
}
#[test]
fn what_was_staged_before_the_lock_survives_and_a_stage_after_it_is_refused() {
use crate::service_index_lock::{lock_path_of, try_acquire};
let files: Vec<Vec<u8>> = vec![b"top.txt".to_vec(), b"dir/inner.txt".to_vec()];
let (root, repo, _first, second) = index_behind("service-follow-staged", files.as_slice());
std::fs::write(repo.join("unselected.txt"), b"staged by another session\n").expect("write");
crate::test_support::age(repo.join("unselected.txt").as_path());
git(repo.as_path(), &["add", "unselected.txt"]);
let (mut context, log) = recording_context(root.as_path());
context.overlay.retain(|(name, _)| { return name != SWITCH_VARIABLE; });
context.common_dir = Some(repo.join(".git"));
let index: PathBuf = repo.join(".git").join("index");
let held = try_acquire(index.as_path(), 3).expect("the lock is free");
std::fs::write(repo.join("late.txt"), b"staged too late\n").expect("write");
assert!(!git_output(repo.as_path(), &["add", "late.txt"]).status.success(), "a stage after the lock is taken is refused");
let built = build_index(&context, repo.as_path(), second.as_str(), IndexPaths::Named(&files), &held, 3).expect("built");
assert!(lock_path_of(index.as_path()).exists(), "the lock is held until the installation");
held.release_with(built.path()).expect("installed");
let words: Vec<String> = recorded_starts(log.as_path()).iter().map(|start| { return start.split(' ').next().unwrap_or_default().to_string(); }).collect();
assert!(!words.iter().any(|word| { return word == "reset"; }), "no Git reset started: {words:?}");
assert_eq!(staged_against(repo.as_path(), second.as_str()), vec![String::from("unselected.txt")], "only the earlier stage is left, and it survived");
assert!(git_output(repo.as_path(), &["add", "late.txt"]).status.success(), "the lock is gone, so the stage works now");
remove(root.as_path());
}
#[test]
fn a_failed_installation_keeps_the_index_and_a_replaced_lock_is_left_alone() {
use crate::service_index_lock::{lock_path_of, try_acquire};
let files: Vec<Vec<u8>> = vec![b"top.txt".to_vec()];
let (root, repo, _first, second) = index_behind("service-follow-install", files.as_slice());
let mut context = git_context();
context.common_dir = Some(repo.join(".git"));
let index: PathBuf = repo.join(".git").join("index");
let before: Vec<u8> = std::fs::read(&index).expect("index");
let held = try_acquire(index.as_path(), 4).expect("the lock is free");
let built = build_index(&context, repo.as_path(), second.as_str(), IndexPaths::Named(&files), &held, 4).expect("built");
std::fs::remove_file(lock_path_of(index.as_path())).expect("removed");
std::fs::write(lock_path_of(index.as_path()), b"another program's lock").expect("foreign lock");
assert!(held.release_with(built.path()).is_err());
assert_eq!(std::fs::read(&index).expect("index"), before, "the index was not replaced");
assert_eq!(std::fs::read(lock_path_of(index.as_path())).expect("lock"), b"another program's lock".to_vec());
std::fs::remove_file(lock_path_of(index.as_path())).expect("removed");
std::fs::remove_file(&built).expect("built removed");
let second_hold = try_acquire(index.as_path(), 5).expect("the lock is free");
let second_built = build_index(&context, repo.as_path(), second.as_str(), IndexPaths::Named(&files), &second_hold, 5).expect("built");
std::fs::remove_file(&index).expect("index removed");
std::fs::create_dir_all(index.join("occupied")).expect("a directory where the index goes");
assert!(second_hold.release_with(second_built.path()).is_err());
assert!(!lock_path_of(index.as_path()).exists(), "the lock is removed with the failure");
assert!(index.join("occupied").is_dir());
remove(root.as_path());
}
#[test]
fn a_racily_clean_entry_sends_the_follow_up_to_git_with_the_same_result() {
use crate::service_index_lock::try_acquire;
let files: Vec<Vec<u8>> = vec![b"top.txt".to_vec()];
let (root, repo, _first, second) = index_behind("service-follow-racy", files.as_slice());
let racy: PathBuf = repo.join("racy.txt");
std::fs::write(&racy, b"staged an instant ago\n").expect("write");
let ahead: std::time::SystemTime = std::time::SystemTime::now() + std::time::Duration::from_secs(3600);
std::fs::OpenOptions::new().write(true).open(&racy).expect("open").set_modified(ahead).expect("set the time");
git(repo.as_path(), &["add", "racy.txt"]);
let (mut context, log) = recording_context(root.as_path());
context.overlay.retain(|(name, _)| { return name != SWITCH_VARIABLE; });
context.common_dir = Some(repo.join(".git"));
let index: PathBuf = repo.join(".git").join("index");
let held = try_acquire(index.as_path(), 6).expect("the lock is free");
let built = build_index(&context, repo.as_path(), second.as_str(), IndexPaths::Named(&files), &held, 6).expect("built");
let starts: Vec<String> = recorded_starts(log.as_path());
assert!(starts.iter().any(|start| { return start.starts_with("reset --no-refresh "); }), "Git was not asked: {starts:?}");
held.release_with(built.path()).expect("installed");
assert_eq!(staged_against(repo.as_path(), second.as_str()), vec![String::from("racy.txt")]);
remove(root.as_path());
}
#[test]
fn the_record_is_made_between_the_installation_and_the_unlock_and_a_failed_record_keeps_the_lock() {
use crate::service_index_lock::{lock_path_of, try_acquire};
let files: Vec<Vec<u8>> = vec![b"top.txt".to_vec()];
let (root, repo, _first, second) = index_behind("service-follow-record", files.as_slice());
let mut context = git_context();
context.common_dir = Some(repo.join(".git"));
let index: PathBuf = repo.join(".git").join("index");
let lock: PathBuf = lock_path_of(index.as_path());
let held = try_acquire(index.as_path(), 8).expect("the lock is free");
let built = build_index(&context, repo.as_path(), second.as_str(), IndexPaths::Named(&files), &held, 8).expect("built");
let built_bytes: Vec<u8> = std::fs::read(&built).expect("built bytes");
let seen: std::cell::RefCell<Vec<(bool, bool)>> = std::cell::RefCell::new(Vec::new());
let record = || -> std::io::Result<()> {
seen.borrow_mut().push((lock.exists(), std::fs::read(&index)? == built_bytes));
return Ok(());
};
held.release_with_record(built.path(), &record).expect("installed and recorded");
assert_eq!(seen.into_inner(), vec![(true, true)], "the lock was held and the index installed when the record was made");
assert!(!lock.exists());
let second_hold = try_acquire(index.as_path(), 9).expect("the lock is free");
let second_built = build_index(&context, repo.as_path(), second.as_str(), IndexPaths::Named(&files), &second_hold, 9).expect("built");
let broken = || -> std::io::Result<()> { return Err(std::io::Error::other("no space left")); };
assert!(second_hold.release_with_record(second_built.path(), &broken).is_err());
assert!(lock.exists(), "the lock stays for the restart to reclaim");
assert!(!differs(repo.as_path(), second.as_str()), "the installed index stays");
std::fs::remove_file(&lock).expect("the restart reclaims the lock");
remove(root.as_path());
}
#[test]
fn a_live_index_named_like_a_private_copy_is_not_taken_for_one() {
use crate::service_index_lock::try_acquire;
let files: Vec<Vec<u8>> = vec![b"top.txt".to_vec()];
let (root, repo, _first, second) = index_behind("service-follow-alias-live", files.as_slice());
std::fs::write(repo.join("unselected.txt"), b"staged\n").expect("write");
crate::test_support::age(repo.join("unselected.txt").as_path());
git(repo.as_path(), &["add", "unselected.txt"]);
let live: PathBuf = repo.join(".git").join("index.cli-git-7");
std::fs::copy(repo.join(".git").join("index"), &live).expect("a client's index named like a private copy");
let before: Vec<u8> = std::fs::read(&live).expect("live");
let mut context = git_context();
context.common_dir = Some(repo.join(".git"));
let held = try_acquire(live.as_path(), 7).expect("the lock is free");
let built = build_index(&context, repo.as_path(), second.as_str(), IndexPaths::Named(&files), &held, 7).expect("built");
assert_ne!(built.path(), live, "the private copy is not the live index");
assert_eq!(std::fs::read(&live).expect("the live index stays until the installation"), before);
held.release_with(built.path()).expect("installed");
let output = git_with(repo.as_path(), &[("GIT_INDEX_FILE", live.as_os_str().to_os_string())], &["diff", "--cached", "--name-only", second.as_str()], None);
assert_eq!(String::from_utf8_lossy(&output.stdout).trim(), "unselected.txt", "the unselected stage is still in the client's index");
remove(root.as_path());
}
#[test]
fn another_clients_file_with_the_private_name_is_left_alone() {
use crate::service_index_lock::try_acquire;
let files: Vec<Vec<u8>> = vec![b"top.txt".to_vec()];
let (root, repo, _first, second) = index_behind("service-follow-alias-other", files.as_slice());
let index: PathBuf = repo.join(".git").join("index");
let other: PathBuf = repo.join(".git").join("index.cli-git-7");
std::fs::write(&other, b"another client's index").expect("other");
let mut context = git_context();
context.common_dir = Some(repo.join(".git"));
let held = try_acquire(index.as_path(), 7).expect("the lock is free");
let built = build_index(&context, repo.as_path(), second.as_str(), IndexPaths::Named(&files), &held, 7).expect("built");
assert_ne!(built.path(), other);
held.release_with(built.path()).expect("installed");
assert_eq!(std::fs::read(&other).expect("other"), b"another client's index".to_vec());
assert!(!differs(repo.as_path(), second.as_str()));
remove(root.as_path());
}
fn locked_index(name: &str) -> (PathBuf, PathBuf, PathBuf, crate::service_index_lock::IndexLock) {
let (root, repo, _first, _second) = index_behind(name, &[b"top.txt".to_vec()]);
let index: PathBuf = repo.join(".git").join("index");
let held = crate::service_index_lock::try_acquire(index.as_path(), 1).expect("the lock is free");
return (root, repo, index, held);
}
#[test]
fn a_long_index_name_still_gets_a_private_copy() {
let (root, repo, _first, _second) = index_behind("service-follow-long-name", &[b"top.txt".to_vec()]);
let dot_git: PathBuf = repo.join(".git");
let (_long, held) = (1..=246_usize)
.rev()
.find_map(|length| {
let long: PathBuf = dot_git.join("i".repeat(length));
std::fs::copy(dot_git.join("index"), &long).ok()?;
let held = crate::service_index_lock::try_acquire(long.as_path(), 1)?;
return Some((long, held));
})
.expect("some index name takes the lock");
let copy = PrivateCopy::make(&held, 1).expect("a private copy of the longest index name that takes the lock");
assert!(copy.path().exists());
drop(copy);
drop(held);
remove(root.as_path());
}
#[test]
fn leftovers_under_the_old_names_do_not_exhaust_the_private_copies() {
let (root, repo, _index, held) = locked_index("service-follow-exhausted");
let dot_git: PathBuf = repo.join(".git");
let mut foreign: Vec<PathBuf> = vec![dot_git.join("index.cli-git-1")];
foreign.extend((1..32).map(|attempt| { return dot_git.join(format!("index.cli-git-1-{attempt}")); }));
for path in &foreign {
std::fs::write(path, b"another program's file").expect("foreign file");
}
let copy = PrivateCopy::make(&held, 1).expect("a private copy although every old name is taken");
assert!(copy.path().exists());
for path in &foreign {
assert_eq!(std::fs::read(path).expect("the foreign file stays"), b"another program's file".to_vec());
}
drop(copy);
drop(held);
remove(root.as_path());
}
#[test]
fn only_a_dead_services_leftover_is_removed() {
use std::os::unix::fs::PermissionsExt as _;
let (root, repo, _index, held) = locked_index("service-follow-sweep");
let dot_git: PathBuf = repo.join(".git");
let named = |tail: &str| -> PathBuf { return dot_git.join(format!("cli-git-private-{tail}")); };
let dead: PathBuf = named("00000000-0000-4000-8000-000000000001");
let unmarked: PathBuf = named("00000000-0000-4000-8000-000000000002");
let reused: PathBuf = named("00000000-0000-4000-8000-000000000003");
let plain: PathBuf = named("00000000-0000-4000-8000-000000000004");
let not_ours: PathBuf = named("00000000-0000-4000-8000-000000000005");
for directory in [&dead, &unmarked, &reused, ¬_ours] {
std::fs::create_dir(directory).expect("leftover");
std::fs::set_permissions(directory, std::fs::Permissions::from_mode(0o700)).expect("owner only");
std::fs::write(directory.join("index"), b"copy").expect("copy");
}
std::fs::set_permissions(¬_ours, std::fs::Permissions::from_mode(0o755)).expect("open to others");
std::fs::write(dead.join("owner"), format!("cli-git-private-copy v1\npid={}\n", i32::MAX)).expect("marker");
std::fs::write(reused.join("owner"), format!("cli-git-private-copy v1\npid={}\n", std::process::id())).expect("marker");
std::fs::write(&plain, b"not a directory").expect("plain");
std::fs::copy(dot_git.join("index"), dot_git.join("other-index")).expect("another index");
let owner = crate::service_index_lock::try_acquire(dot_git.join("other-index").as_path(), 2).expect("a lock on another index in the same directory");
let alive = PrivateCopy::make(&owner, 2).expect("a live owner's copy");
std::fs::write(alive.directory().join("owner"), "cli-git-private-copy v1\npid=1\n").ok();
let copy = PrivateCopy::make(&held, 1).expect("a private copy");
assert!(!dead.exists(), "the dead service's copy is removed");
assert!(!unmarked.exists(), "the copy of a service that died before it wrote a marker is removed");
assert!(!reused.exists(), "a marker whose process number a living process took over decides nothing");
assert!(not_ours.join("index").exists() && plain.exists(), "nothing else is touched");
assert!(alive.path().exists(), "a copy a live owner holds stays, which only the owner's hold can tell");
drop(copy);
drop(alive);
drop(owner);
drop(held);
remove(root.as_path());
}
#[test]
fn a_foreign_lock_at_the_old_copy_name_does_not_stop_a_landing_without_an_index() {
use crate::service_index_lock::try_acquire;
let files: Vec<Vec<u8>> = vec![b"top.txt".to_vec()];
let (root, repo, _first, second) = index_behind("service-follow-foreign-lock", files.as_slice());
let index: PathBuf = repo.join(".git").join("index");
std::fs::remove_file(&index).expect("no live index");
let foreign: PathBuf = repo.join(".git").join("index.cli-git-7.lock");
std::fs::write(&foreign, b"another client holds this").expect("foreign lock");
let mut context = git_context();
context.common_dir = Some(repo.join(".git"));
let held = try_acquire(index.as_path(), 7).expect("the lock is free");
let built = build_index(&context, repo.as_path(), second.as_str(), IndexPaths::Named(&files), &held, 7);
assert!(built.is_ok(), "the landing builds its index: {:?}", built.as_ref().err());
assert_eq!(std::fs::read(&foreign).expect("the foreign lock stays"), b"another client holds this".to_vec());
drop(built);
drop(held);
remove(root.as_path());
}
#[test]
fn the_private_copy_is_owner_only_and_a_no_change_keeps_the_live_mode() {
if crate::umask_child_test_support::ran_in_child(module_path!(), "the_private_copy_is_owner_only_and_a_no_change_keeps_the_live_mode", 0o000) {
return;
}
use std::os::unix::fs::PermissionsExt as _;
let (root, repo, index, held) = locked_index("service-follow-private-mode");
std::fs::set_permissions(&index, std::fs::Permissions::from_mode(0o640)).expect("a live mode");
let mut context = git_context();
context.common_dir = Some(repo.join(".git"));
let none: Vec<Vec<u8>> = Vec::new();
let built = build_index(&context, repo.as_path(), "0".repeat(40).as_str(), IndexPaths::Named(&none), &held, 1).expect("built");
let path: PathBuf = AsRef::<Path>::as_ref(&built).to_path_buf();
let directory: &Path = path.parent().expect("a directory");
assert_ne!(directory, repo.join(".git").as_path(), "the copy is not loose in the live index's directory");
assert_eq!(std::fs::metadata(directory).expect("directory").permissions().mode() & 0o777, 0o700, "only the owner opens the directory");
assert_eq!(std::fs::metadata(&path).expect("copy").permissions().mode() & 0o777, 0o640, "the live index's mode");
drop(built);
drop(held);
remove(root.as_path());
}