mod common;
use std::path::PathBuf;
use std::process::Output;
use common::{Repo, mkit};
fn wt_add(repo: &Repo, extra: &[&str], name: &str) -> (tempfile::TempDir, PathBuf, Output) {
let host = tempfile::tempdir().expect("worktree host dir");
let tree = host.path().join(name);
let target_arg = tree.to_string_lossy().into_owned();
let mut argv = vec!["worktree", "add", &target_arg];
argv.extend_from_slice(extra);
let out = repo.run(&argv);
(host, tree, out)
}
#[test]
fn gc_keeps_sibling_in_progress_merge_state() {
let repo = Repo::new();
repo.commit_file("a.txt", b"base\n", "seed");
repo.ok(&["branch", "feature"]);
let (_host, tree, out) = wt_add(&repo, &["feature"], "wt1");
assert!(
out.status.success(),
"{}",
String::from_utf8_lossy(&out.stderr)
);
repo.write("a.txt", b"ours-main\n");
repo.ok(&["add", "a.txt"]);
repo.ok(&["commit", "-m", "main change"]);
std::fs::write(tree.join("a.txt"), b"theirs-linked\n").unwrap();
let out = mkit(&tree, repo.xdg(), &["add", "a.txt"]);
assert!(out.status.success());
let out = mkit(&tree, repo.xdg(), &["commit", "-m", "linked change"]);
assert!(
out.status.success(),
"{}",
String::from_utf8_lossy(&out.stderr)
);
let merge = mkit(&tree, repo.xdg(), &["merge", "main"]);
assert!(
!merge.status.success(),
"merge should conflict: {}",
String::from_utf8_lossy(&merge.stdout)
);
assert!(
repo.mkit_dir().join("worktrees/wt1/MERGE_HEAD").is_file(),
"conflict state must live in the sibling's state dir"
);
repo.ok(&["gc", "--grace-secs", "0"]);
std::fs::write(tree.join("a.txt"), b"resolved\n").unwrap();
let out = mkit(&tree, repo.xdg(), &["add", "a.txt"]);
assert!(
out.status.success(),
"{}",
String::from_utf8_lossy(&out.stderr)
);
let out = mkit(&tree, repo.xdg(), &["merge", "--continue"]);
assert!(
out.status.success(),
"merge --continue after cross-tree gc: {}",
String::from_utf8_lossy(&out.stderr)
);
common::check_invariants(repo.path(), "after gc + sibling merge").unwrap();
}
#[test]
fn destructive_commands_in_linked_tree_stay_tree_local() {
let repo = Repo::new();
repo.commit_file("a.txt", b"one\n", "seed");
let (_host, tree, out) = wt_add(&repo, &[], "topic");
assert!(out.status.success());
repo.write("a.txt", b"main-dirty\n");
repo.write("untracked-main.txt", b"keep\n");
std::fs::write(tree.join("a.txt"), b"linked-dirty\n").unwrap();
std::fs::write(tree.join("junk.txt"), b"drop\n").unwrap();
let out = mkit(&tree, repo.xdg(), &["reset", "--hard", "-f", "HEAD"]);
assert!(
out.status.success(),
"{}",
String::from_utf8_lossy(&out.stderr)
);
let out = mkit(&tree, repo.xdg(), &["clean", "-f"]);
assert!(
out.status.success(),
"{}",
String::from_utf8_lossy(&out.stderr)
);
assert_eq!(std::fs::read(tree.join("a.txt")).unwrap(), b"one\n");
assert!(!tree.join("junk.txt").exists());
assert_eq!(
std::fs::read(repo.path().join("a.txt")).unwrap(),
b"main-dirty\n"
);
assert!(repo.path().join("untracked-main.txt").exists());
}
#[test]
#[ignore = "deliberate ~5s lock-timeout; serial CI lane (#505)"]
fn gc_blocks_on_a_sibling_worktree_lock() {
let repo = Repo::new();
repo.commit_file("a.txt", b"one\n", "seed");
let (_host, _tree, out) = wt_add(&repo, &[], "topic");
assert!(out.status.success());
let lock = repo.mkit_dir().join("worktrees/topic/worktree.lock");
std::fs::write(&lock, b"").unwrap();
let out = repo.run(&["gc", "--grace-secs", "0"]);
assert!(
!out.status.success(),
"gc must not run while a sibling tree is mid-mutation"
);
assert_eq!(out.status.code(), Some(75), "TEMPFAIL on lock contention");
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
stderr.contains("lock"),
"diagnostic must name the lock: {stderr}"
);
std::fs::remove_file(&lock).unwrap();
repo.ok(&["gc", "--grace-secs", "0"]);
}