use std::path::{Path, PathBuf};
use crate::ui::warning_sign;
pub fn enabled() -> bool {
crate::config::boolean_or("amont.testPushedTree", false)
}
pub struct PushedTree {
path: PathBuf,
repo: PathBuf,
}
impl PushedTree {
pub fn create(repo: &Path, tip: &str) -> Option<PushedTree> {
let base = std::env::temp_dir().join(unique_name("amont-push"));
Self::create_at(base, repo, tip)
}
fn create_at(base: PathBuf, repo: &Path, tip: &str) -> Option<PushedTree> {
std::fs::create_dir(&base).ok()?;
let ok = crate::git::succeeds(&[
"-C",
repo.to_str()?,
"worktree",
"add",
"--detach",
"--quiet",
base.to_str()?,
tip,
]);
if !ok {
let _ = std::fs::remove_dir_all(&base);
return None;
}
Some(PushedTree {
path: base,
repo: repo.to_path_buf(),
})
}
pub fn path(&self) -> &Path {
&self.path
}
}
fn unique_name(prefix: &str) -> String {
use std::collections::hash_map::RandomState;
use std::hash::{BuildHasher, Hash, Hasher};
let pid = std::process::id();
let mut hasher = RandomState::new().build_hasher();
pid.hash(&mut hasher);
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_nanos()
.hash(&mut hasher);
format!("{prefix}-{pid}-{:016x}", hasher.finish())
}
impl Drop for PushedTree {
fn drop(&mut self) {
let _ = crate::git::succeeds(&[
"-C",
self.repo.to_str().unwrap_or_default(),
"worktree",
"remove",
"--force",
self.path.to_str().unwrap_or_default(),
]);
let _ = std::fs::remove_dir_all(&self.path);
}
}
pub fn where_to_run(tip: &str, fallback: &str) -> (PathBuf, Option<PushedTree>) {
if !enabled() {
println!(
"{} testing the WORKING TREE, not the pushed commits \
(`git config amont.testPushedTree true` to test what you are pushing)",
warning_sign()
);
return (PathBuf::from(fallback), None);
}
match PushedTree::create(Path::new(fallback), tip) {
Some(tree) => {
let path = tree.path().to_path_buf();
(path, Some(tree))
}
None => {
println!(
"{} could not check out {tip} to test it; testing the working tree instead",
warning_sign()
);
(PathBuf::from(fallback), None)
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn unique_name_does_not_repeat() {
let a = unique_name("amont-push");
let b = unique_name("amont-push");
assert_ne!(a, b);
assert!(a.starts_with("amont-push-"));
}
#[test]
fn an_existing_path_is_left_alone_not_cleared() {
let base = std::env::temp_dir().join(format!("pushed-collision-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&base);
std::fs::create_dir_all(&base).unwrap();
std::fs::write(base.join("sentinel.txt"), "do not delete me").unwrap();
let got = PushedTree::create_at(base.clone(), Path::new("/does/not/matter"), "HEAD");
assert!(
got.is_none(),
"must refuse rather than reuse a path it did not create"
);
assert_eq!(
std::fs::read_to_string(base.join("sentinel.txt")).unwrap(),
"do not delete me",
"an existing path must never be cleared to make room"
);
let _ = std::fs::remove_dir_all(&base);
}
fn repo(name: &str) -> PathBuf {
let d = std::env::temp_dir().join(format!("pushed-{name}-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&d);
std::fs::create_dir_all(&d).unwrap();
for args in [
vec!["init", "-q", "--template=", "."],
vec!["config", "user.email", "t@t.test"],
vec!["config", "user.name", "t"],
vec!["config", "core.autocrlf", "false"],
] {
std::process::Command::new("git")
.args(&args)
.current_dir(&d)
.output()
.expect("git");
}
d
}
#[test]
fn the_worktree_holds_the_committed_content() {
let d = repo("tree");
let git = |args: &[&str]| {
std::process::Command::new("git")
.args(args)
.current_dir(&d)
.output()
.expect("git")
};
std::fs::write(d.join("a.txt"), "committed\n").unwrap();
git(&["add", "-A"]);
git(&["commit", "-qm", "seed"]);
let head = String::from_utf8_lossy(&git(&["rev-parse", "HEAD"]).stdout)
.trim()
.to_string();
std::fs::write(d.join("a.txt"), "dirty, not pushed\n").unwrap();
let tree = PushedTree::create(&d, &head).expect("worktree");
let seen = std::fs::read_to_string(tree.path().join("a.txt")).unwrap();
let at = tree.path().to_path_buf();
drop(tree);
assert_eq!(seen, "committed\n", "the worktree saw the dirty tree");
assert!(!at.exists(), "the worktree outlived its guard");
let _ = std::fs::remove_dir_all(&d);
}
}