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 {
if let Some(parent) = dir.join(name).parent() {
std::fs::create_dir_all(parent).expect("its folder");
}
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::Workshop,
}
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::Workshop::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.workspace("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.workspace("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.workspace("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.workspace("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");
}
}
}
fn exclude(repo: &Path, patterns: &str) {
let info = common_dir(repo).expect("a repository").join("info");
std::fs::create_dir_all(&info).expect("mkdir");
std::fs::write(info.join("exclude"), patterns).expect("written");
}
fn layout_ignoring() -> Layout {
let layout = Layout::new();
let main = layout.main();
for repo in [main.clone(), main.join("mid"), main.join("mid/.deps/leaf")] {
exclude(&repo, "target/\n");
}
layout
}
#[test]
fn test20056_a_snapshot_is_every_checkout_at_its_commit_taken_again_in_place() {
let layout = layout_ignoring();
let main = layout.main();
commit_file(&main.join("mid"), ".wsb/repo.json", "{\"name\":\"mid\"}\n");
let leaf_head = commit_file(&main.join("mid/.deps/leaf"), "ahead.txt", "ahead\n");
let pinned = run(&main.join("mid"), &["rev-parse", "HEAD:.deps/leaf"]);
assert_ne!(leaf_head, pinned, "the case this holds: a checkout ahead of its pin");
let target = layout.target("snap");
let checkouts = snapshot(&target, "main", false).expect("taken");
let wt = target.path();
assert_eq!(
checkouts.iter().map(|c| c.path.as_str()).collect::<Vec<_>>(),
["", "mid", "mid/.deps/leaf"]
);
for relative in ["", "mid", "mid/.deps/leaf"] {
let (here, there) = (wt.join(relative), main.join(relative));
assert_eq!(run(&here, &["rev-parse", "HEAD"]), run(&there, &["rev-parse", "HEAD"]), "{relative:?} is not at its source's commit");
assert_eq!(run(&here, &["branch", "--show-current"]), "", "{relative:?} is not detached");
assert_eq!(common_dir(&here).unwrap(), common_dir(&there).unwrap(), "{relative:?} is not a worktree of main's repository");
let registered = registrations(&there).unwrap();
assert!(
registered.iter().any(|r| same_path(&r.path, &here) && r.locked),
"{relative:?} is not registered and locked"
);
}
assert_eq!(std::fs::read_to_string(wt.join("mid/.wsb/repo.json")).unwrap(), "{\"name\":\"mid\"}\n");
let record = temporary(&wt).expect("read").expect("a temporary worktree says it is one");
assert_eq!(record.of, "main");
assert!(!record.working);
assert_eq!(record.commits["mid/.deps/leaf"], leaf_head);
assert_eq!(record.commits["."], run(&main, &["rev-parse", "HEAD"]));
assert_eq!(temporary(&main).expect("read"), None, "main took itself for a temporary worktree");
let plain = tempfile::tempdir().expect("a scratch directory");
std::fs::write(plain.path().join("workspace.json"), "{}\n").expect("written");
assert_eq!(temporary(plain.path()).expect("a plain copy is answered, not an error"), None);
std::fs::write(wt.join("mid/left.txt"), "from the last run\n").expect("written");
std::fs::create_dir_all(wt.join("mid/target")).expect("mkdir");
std::fs::write(wt.join("mid/target/built"), "kept\n").expect("written");
let mid_head = commit_file(&main.join("mid"), "two.txt", "two\n");
snapshot(&target, "main", false).expect("taken again");
assert_eq!(run(&wt.join("mid"), &["rev-parse", "HEAD"]), mid_head);
assert!(wt.join("mid/two.txt").is_file());
assert!(!wt.join("mid/left.txt").exists(), "what the last run added is still there");
assert_eq!(std::fs::read_to_string(wt.join("mid/target/built")).unwrap(), "kept\n", "an ignored output was cleared");
assert_eq!(temporary(&wt).unwrap().unwrap().commits["mid"], mid_head);
}
#[test]
fn test20057_a_working_snapshot_holds_the_working_changes() {
let layout = layout_ignoring();
let main = layout.main();
let mid = main.join("mid");
commit_file(&mid, "doomed.txt", "doomed\n");
std::fs::write(mid.join("README.md"), "modified\n").expect("written");
std::fs::remove_file(mid.join("doomed.txt")).expect("deleted");
std::fs::write(mid.join("staged.txt"), "staged\n").expect("written");
run(&mid, &["add", "staged.txt"]);
std::fs::create_dir_all(mid.join("new")).expect("mkdir");
std::fs::write(mid.join("new/untracked.txt"), "untracked\n").expect("written");
std::fs::create_dir_all(mid.join("target")).expect("mkdir");
std::fs::write(mid.join("target/built"), "ignored\n").expect("written");
std::fs::write(main.join("mid/.deps/leaf/README.md"), "leaf modified\n").expect("written");
let target = layout.target("snap");
snapshot(&target, "main", true).expect("taken");
let wt = target.path();
let text = |path: &Path| std::fs::read_to_string(path).ok();
assert_eq!(text(&wt.join("mid/README.md")).as_deref(), Some("modified\n"));
assert_eq!(text(&wt.join("mid/staged.txt")).as_deref(), Some("staged\n"));
assert_eq!(text(&wt.join("mid/new/untracked.txt")).as_deref(), Some("untracked\n"));
assert!(!wt.join("mid/doomed.txt").exists(), "a deleted file is in the snapshot");
assert!(!wt.join("mid/target").exists(), "an ignored output was copied");
assert_eq!(text(&wt.join("mid/.deps/leaf/README.md")).as_deref(), Some("leaf modified\n"));
assert!(temporary(&wt).unwrap().unwrap().working);
}
#[test]
fn test20058_a_snapshot_refuses_what_it_cannot_be() {
let layout = layout_ignoring();
let main = layout.main();
let own = layout.target("x");
add(&own, None, None).expect("made");
let refused = snapshot(&own, "main", false).expect_err("a worktree of its own");
assert!(refused.contains("not a temporary one"), "{refused}");
assert_eq!(run(&own.path(), &["branch", "--show-current"]), "card/x", "the worktree was taken over");
let snap = layout.target("snap");
snapshot(&snap, "main", false).expect("taken");
let refused = snapshot(&snap, "x", false).expect_err("of another source");
assert!(refused.contains("a snapshot of alpha/main"), "{refused}");
let old = layout.target("old");
run(&main, &["worktree", "add", "--quiet", "-b", "card/old", old.path().to_str().unwrap()]);
run(&old.path(), &["submodule", "--quiet", "update", "--init", "--recursive"]);
let of_old = layout.target("of-old");
let refused = snapshot(&of_old, "old", false).expect_err("cloned submodules");
assert!(refused.contains("mid: a separate clone") && refused.contains("migrate alpha/old"), "{refused}");
assert!(!of_old.path().exists(), "a refused snapshot left something behind");
let bare = layout.target("bare");
run(&main, &["worktree", "add", "--quiet", "-b", "card/bare", bare.path().to_str().unwrap()]);
let of_bare = layout.target("of-bare");
let refused = snapshot(&of_bare, "bare", false).expect_err("not checked out");
assert!(refused.contains("mid: not checked out in bare"), "{refused}");
assert!(!of_bare.path().exists(), "a refused snapshot left something behind");
}
#[test]
fn test20059_a_temporary_worktree_is_removed_when_nothing_in_it_would_be_lost() {
let layout = layout_ignoring();
let main = layout.main();
commit_file(&main.join("mid/.deps/leaf"), "unpushed.txt", "only on main's local branch\n");
let target = layout.target("snap");
snapshot(&target, "main", false).expect("taken");
remove(&target, false).expect("its commits are on a branch");
assert!(!target.path().exists());
snapshot(&target, "main", false).expect("taken again");
commit_file(&target.path().join("mid"), "made-here.txt", "a run's own commit\n");
let refused = remove(&target, false).expect_err("a commit made in it");
assert!(refused.contains("mid: 1 commit(s) made here, on no branch"), "{refused}");
assert!(target.path().is_dir());
}