feat: one store directory per checkout
This commit is contained in:
@@ -109,7 +109,24 @@ fn git_root(ctx: &Ctx) -> Result<PathBuf> {
|
|||||||
)))?)
|
)))?)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// the remote names the project and the checkout names its clone, so two clones
|
||||||
|
// never share a directory
|
||||||
fn repo_components(ctx: &Ctx, root: &Path) -> Result<PathBuf> {
|
fn repo_components(ctx: &Ctx, root: &Path) -> Result<PathBuf> {
|
||||||
|
Ok(project_components(ctx, root)?.join(checkout_name(root)))
|
||||||
|
}
|
||||||
|
|
||||||
|
// `<dir>-<hash>`: the directory for a human, the hash of the canonical path
|
||||||
|
// for two checkouts in directories of one name
|
||||||
|
fn checkout_name(root: &Path) -> String {
|
||||||
|
let hash = fingerprint(root.as_os_str().as_bytes());
|
||||||
|
|
||||||
|
match root.file_name() {
|
||||||
|
Some(dir) => format!("{}-{hash}", cleaned(&dir.to_string_lossy())),
|
||||||
|
None => hash,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn project_components(ctx: &Ctx, root: &Path) -> Result<PathBuf> {
|
||||||
if let Some(url) = git_origin_url(ctx) {
|
if let Some(url) = git_origin_url(ctx) {
|
||||||
if let Some(components) = components_from_remote(&url) {
|
if let Some(components) = components_from_remote(&url) {
|
||||||
return Ok(components);
|
return Ok(components);
|
||||||
@@ -302,11 +319,13 @@ pub(super) fn leads(path: &Path, root: &Path) -> Leads {
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::{components_from_remote, fingerprint, normalized, sanitize, sanitize_name};
|
use super::{
|
||||||
|
checkout_name, components_from_remote, fingerprint, normalized, sanitize, sanitize_name,
|
||||||
|
};
|
||||||
use std::collections::HashSet;
|
use std::collections::HashSet;
|
||||||
use std::ffi::OsStr;
|
use std::ffi::OsStr;
|
||||||
use std::os::unix::ffi::OsStrExt;
|
use std::os::unix::ffi::OsStrExt;
|
||||||
use std::path::PathBuf;
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn parses_every_spelling_of_a_remote() {
|
fn parses_every_spelling_of_a_remote() {
|
||||||
@@ -407,6 +426,17 @@ mod tests {
|
|||||||
assert_eq!(sanitize_name(OsStr::new("afurnik")), "afurnik");
|
assert_eq!(sanitize_name(OsStr::new("afurnik")), "afurnik");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_checkout_is_named_by_its_directory_and_its_path() {
|
||||||
|
let one = checkout_name(Path::new("/home/a/afurnik"));
|
||||||
|
let two = checkout_name(Path::new("/home/b/afurnik"));
|
||||||
|
|
||||||
|
assert!(one.starts_with("afurnik-"), "{one}");
|
||||||
|
assert_ne!(one, two);
|
||||||
|
// the root itself has no directory to be named after
|
||||||
|
assert_eq!(checkout_name(Path::new("/")), fingerprint(b"/"));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn a_store_root_is_absolute_with_the_dots_folded_out() {
|
fn a_store_root_is_absolute_with_the_dots_folded_out() {
|
||||||
// it is written into every symlink add creates, and compared against the
|
// it is written into every symlink add creates, and compared against the
|
||||||
|
|||||||
@@ -260,3 +260,25 @@ fn distinct_remotes_do_not_share_one_store_directory() {
|
|||||||
// the ordinary remote keeps the path it already had
|
// the ordinary remote keeps the path it already had
|
||||||
assert!(plain.contains("/a/my_api"), "{plain}");
|
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");
|
||||||
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
#![allow(dead_code)]
|
#![allow(dead_code)]
|
||||||
use std::ffi::OsStr;
|
use std::ffi::OsStr;
|
||||||
use std::fmt::Write as _;
|
use std::fmt::Write as _;
|
||||||
|
use std::os::unix::ffi::OsStrExt;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use std::process::{Command, Output};
|
use std::process::{Command, Output};
|
||||||
use std::sync::atomic::{AtomicU32, Ordering};
|
use std::sync::atomic::{AtomicU32, Ordering};
|
||||||
@@ -15,8 +16,12 @@ use std::{env, fs};
|
|||||||
// unique per case even with the suite running in parallel
|
// unique per case even with the suite running in parallel
|
||||||
static NEXT: AtomicU32 = AtomicU32::new(0);
|
static NEXT: AtomicU32 = AtomicU32::new(0);
|
||||||
|
|
||||||
|
pub const REMOTE: &str = "https://git.example.org/acme/proj.git";
|
||||||
|
|
||||||
pub struct Case {
|
pub struct Case {
|
||||||
root: PathBuf,
|
root: PathBuf,
|
||||||
|
// the data home every checkout of a case shares, as one user's would
|
||||||
|
xdg: PathBuf,
|
||||||
pub repo: PathBuf,
|
pub repo: PathBuf,
|
||||||
pub store: PathBuf,
|
pub store: PathBuf,
|
||||||
pub outside: PathBuf,
|
pub outside: PathBuf,
|
||||||
@@ -32,29 +37,41 @@ impl Case {
|
|||||||
));
|
));
|
||||||
let _ = fs::remove_dir_all(&root);
|
let _ = fs::remove_dir_all(&root);
|
||||||
|
|
||||||
let case = Self {
|
Self::checkout(root.clone(), root.join("xdg"))
|
||||||
repo: root.join("repo"),
|
}
|
||||||
store: root
|
|
||||||
.join("xdg/ahab/git.example.org/acme/proj")
|
|
||||||
.to_path_buf(),
|
|
||||||
outside: root.join("outside"),
|
|
||||||
root,
|
|
||||||
};
|
|
||||||
|
|
||||||
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");
|
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(&["init", "-q", "."]);
|
||||||
case.git(&["config", "user.email", "test@example.org"]);
|
case.git(&["config", "user.email", "test@example.org"]);
|
||||||
case.git(&["config", "user.name", "test"]);
|
case.git(&["config", "user.name", "test"]);
|
||||||
case.git(&["config", "commit.gpgsign", "false"]);
|
case.git(&["config", "commit.gpgsign", "false"]);
|
||||||
case.git(&[
|
case.git(&["remote", "add", "origin", REMOTE]);
|
||||||
"remote",
|
|
||||||
"add",
|
|
||||||
"origin",
|
|
||||||
"https://git.example.org/acme/proj.git",
|
|
||||||
]);
|
|
||||||
case.write("tracked.txt", b"tracked\n");
|
case.write("tracked.txt", b"tracked\n");
|
||||||
case.git(&["add", "tracked.txt"]);
|
case.git(&["add", "tracked.txt"]);
|
||||||
case.git(&["commit", "-qm", "init"]);
|
case.git(&["commit", "-qm", "init"]);
|
||||||
@@ -84,7 +101,7 @@ impl Case {
|
|||||||
pub fn ahab<S: AsRef<OsStr>>(&self, args: &[S]) -> Run {
|
pub fn ahab<S: AsRef<OsStr>>(&self, args: &[S]) -> Run {
|
||||||
let out = Command::new(env!("CARGO_BIN_EXE_ahab"))
|
let out = Command::new(env!("CARGO_BIN_EXE_ahab"))
|
||||||
.current_dir(&self.repo)
|
.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
|
// the git commands ahab runs must not read the developer's own config
|
||||||
.env("GIT_CONFIG_GLOBAL", "/dev/null")
|
.env("GIT_CONFIG_GLOBAL", "/dev/null")
|
||||||
.env("GIT_CONFIG_SYSTEM", "/dev/null")
|
.env("GIT_CONFIG_SYSTEM", "/dev/null")
|
||||||
@@ -205,3 +222,15 @@ impl Run {
|
|||||||
out
|
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)
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user