feat: one store directory per checkout

This commit is contained in:
2026-09-21 13:02:26 +00:00
parent 9adcfed28e
commit c96211a28b
3 changed files with 99 additions and 18 deletions

View File

@@ -260,3 +260,25 @@ fn distinct_remotes_do_not_share_one_store_directory() {
// the ordinary remote keeps the path it already had
assert!(plain.contains("/a/my_api"), "{plain}");
}
#[test]
fn two_checkouts_of_one_remote_keep_their_own_files() {
let case = Case::new("clones");
let second = case.second_checkout();
case.write(".env", b"FIRST\n");
second.write(".env", b"SECOND\n");
case.ahab(&["link", "add", ".env"]).ok();
// no collision, and no --force needed
second.ahab(&["link", "add", ".env"]).ok();
assert_ne!(case.store, second.store);
assert_eq!(fs::read(case.path(".env")).unwrap(), b"FIRST\n");
assert_eq!(fs::read(second.path(".env")).unwrap(), b"SECOND\n");
// one restoring leaves the other linked
case.ahab(&["link", "restore", "--all"]).ok();
assert!(second.is_symlink(".env"));
assert_eq!(fs::read(second.path(".env")).unwrap(), b"SECOND\n");
second.ahab(&["link", "list"]).ok().says("linked: .env");
}

View File

@@ -7,6 +7,7 @@
#![allow(dead_code)]
use std::ffi::OsStr;
use std::fmt::Write as _;
use std::os::unix::ffi::OsStrExt;
use std::path::{Path, PathBuf};
use std::process::{Command, Output};
use std::sync::atomic::{AtomicU32, Ordering};
@@ -15,8 +16,12 @@ use std::{env, fs};
// unique per case even with the suite running in parallel
static NEXT: AtomicU32 = AtomicU32::new(0);
pub const REMOTE: &str = "https://git.example.org/acme/proj.git";
pub struct Case {
root: PathBuf,
// the data home every checkout of a case shares, as one user's would
xdg: PathBuf,
pub repo: PathBuf,
pub store: PathBuf,
pub outside: PathBuf,
@@ -32,29 +37,41 @@ impl Case {
));
let _ = fs::remove_dir_all(&root);
let case = Self {
repo: root.join("repo"),
store: root
.join("xdg/ahab/git.example.org/acme/proj")
.to_path_buf(),
outside: root.join("outside"),
root,
};
Self::checkout(root.clone(), root.join("xdg"))
}
for dir in [&case.repo, &case.outside] {
// a second clone of the same remote, sharing the first one's data home
pub fn second_checkout(&self) -> Self {
Self::checkout(self.root.join("second"), self.xdg.clone())
}
fn checkout(root: PathBuf, xdg: PathBuf) -> Self {
let repo = root.join("repo");
let outside = root.join("outside");
for dir in [&repo, &outside] {
fs::create_dir_all(dir).expect("creating the case directories");
}
// `<dir>-<hash>` of the canonical checkout, as ahab spells it
let canonical = fs::canonicalize(&repo).expect("resolving the repository");
let store = xdg.join("ahab/git.example.org/acme/proj").join(format!(
"repo-{}",
fingerprint(canonical.as_os_str().as_bytes())
));
let case = Self {
root,
xdg,
repo,
store,
outside,
};
case.git(&["init", "-q", "."]);
case.git(&["config", "user.email", "test@example.org"]);
case.git(&["config", "user.name", "test"]);
case.git(&["config", "commit.gpgsign", "false"]);
case.git(&[
"remote",
"add",
"origin",
"https://git.example.org/acme/proj.git",
]);
case.git(&["remote", "add", "origin", REMOTE]);
case.write("tracked.txt", b"tracked\n");
case.git(&["add", "tracked.txt"]);
case.git(&["commit", "-qm", "init"]);
@@ -84,7 +101,7 @@ impl Case {
pub fn ahab<S: AsRef<OsStr>>(&self, args: &[S]) -> Run {
let out = Command::new(env!("CARGO_BIN_EXE_ahab"))
.current_dir(&self.repo)
.env("XDG_DATA_HOME", self.root.join("xdg"))
.env("XDG_DATA_HOME", &self.xdg)
// the git commands ahab runs must not read the developer's own config
.env("GIT_CONFIG_GLOBAL", "/dev/null")
.env("GIT_CONFIG_SYSTEM", "/dev/null")
@@ -205,3 +222,15 @@ impl Run {
out
}
}
// fnv-1a, as the store spells a checkout
pub fn fingerprint(bytes: &[u8]) -> String {
let mut hash: u64 = 0xcbf2_9ce4_8422_2325;
for byte in bytes {
hash ^= u64::from(*byte);
hash = hash.wrapping_mul(0x100_0000_01b3);
}
format!("{:08x}", hash as u32)
}