use super::*;
fn run(dir: &Path, args: &[&str]) -> String {
let output = crate::git_backend::git_command()
.arg("-C")
.arg(dir)
.args(["-c", "user.name=Test", "-c", "user.email=test@example.com", "-c", "protocol.file.allow=always"])
.args(args)
.output()
.expect("git runs");
assert!(
output.status.success(),
"git {args:?} in {}: {}",
dir.display(),
String::from_utf8_lossy(&output.stderr)
);
String::from_utf8_lossy(&output.stdout).trim().to_string()
}
fn commit_file(dir: &Path, name: &str, text: &str) -> String {
std::fs::write(dir.join(name), text).expect("a file");
run(dir, &["add", name]);
run(dir, &["commit", "--quiet", "-m", name]);
run(dir, &["rev-parse", "HEAD"])
}
fn origin(dir: &Path) {
std::fs::create_dir_all(dir).expect("mkdir");
run(dir, &["init", "--quiet", "-b", "main"]);
commit_file(dir, "README.md", "readme\n");
}
struct Layout {
_scratch: tempfile::TempDir,
origins: PathBuf,
prj: wsb_prj::Prj,
}
impl Layout {
fn new() -> Self {
let scratch = tempfile::tempdir().expect("a scratch directory");
let base = crate::platform::canonicalize(scratch.path()).expect("resolves");
let origins = base.join("origins");
for name in ["leaf", "mid", "top"] {
origin(&origins.join(name));
}
let leaf = origins.join("leaf");
let mid = origins.join("mid");
run(&mid, &["submodule", "--quiet", "add", leaf.to_str().unwrap(), ".deps/leaf"]);
run(&mid, &["commit", "--quiet", "-m", "leaf"]);
let top = origins.join("top");
run(&top, &["submodule", "--quiet", "add", mid.to_str().unwrap(), "mid"]);
run(&top, &["commit", "--quiet", "-m", "mid"]);
let root = base.join("prj");
let project = root.join("alpha");
std::fs::create_dir_all(&project).expect("mkdir");
run(
&project,
&["clone", "--quiet", "--recurse-submodules", top.to_str().unwrap(), "main-worktree"],
);
let prj = wsb_prj::Prj::at(&root).expect("a PRJ");
Self {
_scratch: scratch,
origins,
prj,
}
}
fn target(&self, name: &str) -> Target {
Target::parse(&self.prj, &format!("alpha/{name}")).expect("a target")
}
fn main(&self) -> PathBuf {
self.prj.project("alpha").expect("alpha").main_worktree()
}
}
#[test]
fn test20037_a_worktrees_submodules_share_mains_repositories() {
let layout = Layout::new();
let main = layout.main();
let target = layout.target("x");
let checkouts = add(&target, None, None).expect("made");
let wt = target.path();
let paths: Vec<&str> = checkouts.iter().map(|c| c.path.as_str()).collect();
assert_eq!(paths, ["", "mid", "mid/.deps/leaf"]);
for checkout in &checkouts {
assert_eq!(checkout.branch.as_deref(), Some("card/x"), "{checkout:?}");
}
for (here, there) in [
(wt.clone(), main.clone()),
(wt.join("mid"), main.join("mid")),
(wt.join("mid/.deps/leaf"), main.join("mid/.deps/leaf")),
] {
assert_eq!(
common_dir(&here).unwrap(),
common_dir(&there).unwrap(),
"{} is not a worktree of {}",
here.display(),
there.display()
);
let registered = registrations(&there).unwrap();
let mine = registered
.iter()
.find(|registration| same_path(®istration.path, &here))
.expect("registered");
assert!(mine.locked, "{} is not locked", here.display());
}
assert!(
!wt.join(".git").is_dir() && !common_dir(&wt).unwrap().join("worktrees/x-worktree/modules").exists(),
"no submodule was cloned into the worktree's git directory"
);
assert_eq!(run(&wt, &["status", "--porcelain"]), "", "the superproject is clean");
let made = commit_file(&wt.join("mid/.deps/leaf"), "new.txt", "new\n");
assert!(has_commit(&main.join("mid/.deps/leaf"), &made).unwrap());
run(&main.join("mid"), &["branch", "card/y"]);
let second = add(&layout.target("y"), None, None).expect("made");
assert_eq!(second[1].branch.as_deref(), Some("card/y"));
}
#[test]
fn test20038_removal_refuses_unsaved_work_and_prunes_nothing_else() {
let layout = Layout::new();
let main = layout.main();
let target = layout.target("x");
add(&target, None, None).expect("made");
let wt = target.path();
let leaf = wt.join("mid/.deps/leaf");
let away = layout.target("away");
add(&away, None, None).expect("made");
let moved = layout.origins.join("away-moved-aside");
std::fs::rename(away.path(), &moved).expect("moved aside");
let by_hand = layout.origins.join("by-hand");
run(&main.join("mid"), &["worktree", "add", "--quiet", "--detach", by_hand.to_str().unwrap()]);
let by_hand_moved = layout.origins.join("by-hand-moved-aside");
std::fs::rename(&by_hand, &by_hand_moved).expect("moved aside");
std::fs::write(leaf.join("scratch.txt"), "untracked\n").expect("a file");
let refusal = remove(&target, false).expect_err("an untracked file refuses");
assert!(refusal.contains("mid/.deps/leaf") && refusal.contains("untracked"), "{refusal}");
assert!(wt.is_dir(), "nothing was removed");
std::fs::remove_file(leaf.join("scratch.txt")).expect("rm");
commit_file(&leaf, "local.txt", "only here\n");
let refusal = remove(&target, false).expect_err("an unpushed commit refuses");
assert!(refusal.contains("on no remote branch"), "{refusal}");
run(&leaf, &["push", "--quiet", "--set-upstream", "origin", "card/x"]);
remove(&target, true).expect("removed");
assert!(!wt.exists());
for repo in [main.clone(), main.join("mid"), main.join("mid/.deps/leaf")] {
assert!(
!registrations(&repo).unwrap().iter().any(|r| r.path.ends_with("x-worktree") || r.path.to_string_lossy().contains("x-worktree/")),
"{} still registers the removed worktree",
repo.display()
);
assert!(
registrations(&repo).unwrap().iter().any(|r| r.path.to_string_lossy().contains("away-worktree")),
"{} lost the other worktree's registration",
repo.display()
);
}
assert!(
registrations(&main.join("mid")).unwrap().iter().any(|r| r.path.ends_with("by-hand")),
"an unlocked registration somebody else made was pruned"
);
assert!(!has_local_branch(&main, "card/x").unwrap(), "--delete-branch deleted the branch");
std::fs::rename(&moved, away.path()).expect("moved back");
assert_eq!(run(&away.path().join("mid"), &["rev-parse", "--is-inside-work-tree"]), "true");
}
#[test]
fn test20039_cloned_submodules_are_migrated_without_losing_a_commit() {
let layout = Layout::new();
let main = layout.main();
let target = layout.target("old");
let wt = target.path();
run(&main, &["worktree", "add", "--quiet", "-b", "card/old", wt.to_str().unwrap()]);
run(&wt, &["submodule", "--quiet", "update", "--init", "--recursive"]);
let leaf = wt.join("mid/.deps/leaf");
run(&leaf, &["checkout", "--quiet", "-b", "feature"]);
let only_in_clone = commit_file(&leaf, "clone-only.txt", "x\n");
assert_ne!(common_dir(&leaf).unwrap(), common_dir(&main.join("mid/.deps/leaf")).unwrap());
let before = list(&layout.prj.project("alpha").unwrap()).unwrap();
let old = before.iter().find(|w| w.name == "old").unwrap();
assert_eq!(old.cloned, ["mid", "mid/.deps/leaf"]);
std::fs::write(wt.join("mid/stray.txt"), "x\n").expect("a file");
assert!(migrate(&target).expect_err("dirty").contains("mid"));
std::fs::remove_file(wt.join("mid/stray.txt")).expect("rm");
let placed = migrate(&target).expect("migrated");
assert_eq!(placed.iter().map(|c| c.path.as_str()).collect::<Vec<_>>(), ["mid", "mid/.deps/leaf"]);
let main_leaf = main.join("mid/.deps/leaf");
assert_eq!(common_dir(&leaf).unwrap(), common_dir(&main_leaf).unwrap(), "now a worktree of main's");
assert_eq!(run(&leaf, &["rev-parse", "HEAD"]), only_in_clone, "on the same commit");
assert_eq!(branch_of(&leaf).unwrap().as_deref(), Some("feature"), "on the same branch");
assert_eq!(
run(&main_leaf, &["rev-parse", "refs/salvage/old/heads/feature"]),
only_in_clone,
"the clone's branch is kept in main's repository"
);
assert!(
!common_dir(&wt).unwrap().join("worktrees/old-worktree/modules").exists(),
"the clones' git directories are gone"
);
let after = list(&layout.prj.project("alpha").unwrap()).unwrap();
let old = after.iter().find(|w| w.name == "old").unwrap();
assert!(old.cloned.is_empty(), "{old:?}");
assert_eq!(old.shared, ["mid", "mid/.deps/leaf"]);
assert!(migrate(&target).expect("again").is_empty(), "a migrated worktree has nothing left to migrate");
}
#[test]
fn test20040_a_failed_add_leaves_nothing_behind() {
let layout = Layout::new();
let main = layout.main();
run(&main, &["submodule", "--quiet", "deinit", "--force", "--all"]);
let target = layout.target("x");
let error = add(&target, None, None).expect_err("mid is not checked out in main");
assert!(error.contains("mid") && error.contains("nothing of alpha/x was left behind"), "{error}");
assert!(!target.path().exists());
assert!(!has_local_branch(&main, "card/x").unwrap(), "the branch made for it is gone");
assert_eq!(registrations(&main).unwrap().len(), 1, "only main is registered");
assert!(Target::parse(&layout.prj, "alpha/main").is_err(), "main is not made by add");
assert!(Target::parse(&layout.prj, "alpha").is_err());
assert!(Target::parse(&layout.prj, "nope/x").is_err());
}
#[test]
fn test20046_a_pruned_checkout_is_repaired_without_losing_work() {
let layout = Layout::new();
let main = layout.main();
let project = layout.prj.project("alpha").unwrap();
for (name, edit) in [("clean", false), ("edited", true)] {
let target = layout.target(name);
add(&target, None, None).expect("made");
let wt = target.path();
let mid = wt.join("mid");
if edit {
std::fs::write(mid.join("README.md"), "an edit nobody committed\n").expect("write");
}
let registration = git_dir(&mid).expect("mid is a worktree");
std::fs::remove_dir_all(®istration).expect("pruned");
let listed = list(&project).unwrap();
let entry = listed.iter().find(|w| w.name == name).unwrap();
assert_eq!(entry.broken, ["mid"], "{entry:?}");
assert!(remove(&target, false).expect_err("refused").contains("migrate"), "rm points at the repair");
let placed = migrate(&target).expect("repaired");
assert!(placed.iter().any(|c| c.path == "mid"), "{placed:?}");
assert_eq!(common_dir(&mid).unwrap(), common_dir(&main.join("mid")).unwrap());
assert_eq!(
run(&wt, &["ls-tree", "HEAD", "mid"]).split_whitespace().nth(2).unwrap(),
run(&mid, &["rev-parse", "HEAD"]),
"put back on the commit the superproject pins"
);
assert_eq!(run(&mid.join(".deps/leaf"), &["rev-parse", "--is-inside-work-tree"]), "true");
let aside = project.state_dir().join("orphaned").join(name);
if edit {
let kept: Vec<PathBuf> = std::fs::read_dir(&aside)
.expect("the edited checkout is kept")
.map(|e| e.unwrap().path().join("mid/README.md"))
.collect();
assert_eq!(kept.len(), 1);
assert_eq!(std::fs::read_to_string(&kept[0]).unwrap(), "an edit nobody committed\n");
} else {
assert!(!aside.exists(), "an orphan holding nothing new is not kept");
}
}
}