use super::support::{Fixture, Observed, fixture, git, remove, repository, run_direct, run_wrapped, stderr_of};
use git_policy_cli::fs_btrfs_ioctl::subvolume_create_at;
use git_policy_cli::fs_kind::{FilesystemKind, filesystem_kind, is_subvolume_root};
use git_policy_cli::fs_mount_options::read_mount_facts;
use std::ffi::OsStr;
use std::path::{Path, PathBuf};
fn on_btrfs_or_acknowledged(fixture: &Fixture, control: &str) -> bool {
let kind = filesystem_kind(fixture.root.as_path());
if kind == Ok(FilesystemKind::Btrfs) {
return true;
}
let acknowledged: bool = std::env::var_os("CLI_GIT_BTRFS_ABSENT_ACKNOWLEDGED").is_some_and(|value| { return !value.is_empty(); });
assert!(
acknowledged,
"the control {control} needs a btrfs fixture root, but {} is on {kind:?}; set CLI_GIT_TEST_ROOT to a directory on btrfs, or set CLI_GIT_BTRFS_ABSENT_ACKNOWLEDGED=1",
fixture.root.display()
);
eprintln!("BTRFS CONTROL NOT RUN: {control} (fixture root {} is on {kind:?})", fixture.root.display());
return false;
}
fn removal_allowed(path: &Path) -> bool {
return read_mount_facts(path).ok().flatten().is_some_and(|facts| { return facts.has_option("user_subvol_rm_allowed"); });
}
fn populated_repository(fixture: &Fixture, as_subvolume: bool) -> PathBuf {
if as_subvolume {
subvolume_create_at(fixture.root.join("repo").as_path()).expect("repository subvolume");
git(fixture, fixture.root.join("repo").as_path(), &["init", "--quiet", "--initial-branch=main"]);
git(fixture, fixture.root.join("repo").as_path(), &["commit", "--quiet", "--allow-empty", "--message=initial"]);
}
let repo: PathBuf = if as_subvolume { fixture.root.join("repo") } else { repository(fixture, OsStr::new("repo")) };
std::fs::create_dir_all(repo.join("src/deep")).expect("directories");
std::fs::write(repo.join("README.md"), b"readme\n").expect("readme");
std::fs::write(repo.join("src/deep/leaf.txt"), vec![b'x'; 5_000]).expect("leaf");
std::fs::write(repo.join(".gitignore"), b"ignored/\n").expect("gitignore");
git(fixture, repo.as_path(), &["add", "--all"]);
git(fixture, repo.as_path(), &["commit", "--quiet", "--message=files"]);
return repo;
}
fn listing(fixture: &Fixture, repo: &Path) -> String {
return String::from_utf8_lossy(&git(fixture, repo, &["worktree", "list", "--porcelain"]).stdout).into_owned();
}
#[test]
fn a_worktree_added_from_the_main_checkout_is_a_subvolume() {
let fixture: Fixture = fixture("ladder-add-main");
if !on_btrfs_or_acknowledged(&fixture, "ladder-add-main") {
remove(&fixture);
return;
}
let repo: PathBuf = populated_repository(&fixture, false);
let created: Observed = run_wrapped(&fixture, repo.as_path(), &["worktree", "add", "-b", "topic", "../topic"]);
assert_eq!(created.code, Some(0), "{}", stderr_of(&created));
let control: Observed = run_direct(&fixture, repo.as_path(), &["worktree", "add", "-b", "control", "../control"]);
let wrapped: String = stderr_of(&created).replace("topic", "NAME");
let git_words: String = stderr_of(&control).replace("control", "NAME");
assert!(wrapped.starts_with(git_words.as_str()), "the caller sees Git's own words first: {wrapped}");
let after_git: Vec<&str> = wrapped[git_words.len()..].lines().collect();
assert!(after_git.first().is_some_and(|line: &&str| { return line.starts_with("cli-git: this worktree was not made as a snapshot"); }), "{wrapped}");
assert!(after_git.get(1).is_some_and(|line: &&str| { return line.starts_with("cli-git: copied "); }), "{wrapped}");
let topic: PathBuf = fixture.root.join("topic");
assert!(is_subvolume_root(topic.as_path()).expect("probe"), "the new worktree is a subvolume");
assert!(!is_subvolume_root(fixture.root.join("control").as_path()).expect("probe"), "real Git makes an ordinary directory");
assert_eq!(std::fs::read(topic.join("src/deep/leaf.txt")).expect("checked out").len(), 5_000);
assert!(listing(&fixture, repo.as_path()).contains(format!("worktree {}\n", topic.display()).as_str()));
remove(&fixture);
}
#[test]
fn a_worktree_added_from_a_linked_worktree_is_a_subvolume_with_the_ignored_state() {
let fixture: Fixture = fixture("ladder-add-linked");
if !on_btrfs_or_acknowledged(&fixture, "ladder-add-linked") {
remove(&fixture);
return;
}
let repo: PathBuf = populated_repository(&fixture, false);
let first: Observed = run_wrapped(&fixture, repo.as_path(), &["worktree", "add", "--quiet", "-b", "first", "../first"]);
assert_eq!(first.code, Some(0), "{}", stderr_of(&first));
let first_root: PathBuf = fixture.root.join("first");
std::fs::create_dir_all(first_root.join("ignored/pkg")).expect("ignored directory");
std::fs::write(first_root.join("ignored/pkg/index.js"), b"module.exports = 1;\n").expect("ignored file");
let second: Observed = run_wrapped(&fixture, first_root.as_path(), &["worktree", "add", "--quiet", "-b", "second", "../second"]);
assert_eq!(second.code, Some(0), "{}", stderr_of(&second));
let second_root: PathBuf = fixture.root.join("second");
assert!(is_subvolume_root(second_root.as_path()).expect("probe"));
assert_eq!(std::fs::read(second_root.join("ignored/pkg/index.js")).expect("ignored state"), b"module.exports = 1;\n");
assert_eq!(std::fs::read(second_root.join("README.md")).expect("tracked"), b"readme\n");
remove(&fixture);
}
#[test]
fn the_opt_out_and_unknown_forms_keep_git_s_ordinary_directory() {
let fixture: Fixture = fixture("ladder-add-plain");
if !on_btrfs_or_acknowledged(&fixture, "ladder-add-plain") {
remove(&fixture);
return;
}
let repo: PathBuf = populated_repository(&fixture, false);
for (name, arguments) in [
("optout", vec!["worktree", "add", "--no-worktree-copy", "--quiet", "-b", "optout", "../optout"]),
("forced", vec!["worktree", "add", "--force", "--quiet", "-b", "forced", "../forced"]),
("nocheckout", vec!["worktree", "add", "--no-checkout", "--quiet", "-b", "nocheckout", "../nocheckout"]),
] {
let created: Observed = run_wrapped(&fixture, repo.as_path(), &arguments);
assert_eq!(created.code, Some(0), "{name}: {}", stderr_of(&created));
assert!(!is_subvolume_root(fixture.root.join(name).as_path()).expect("probe"), "{name}");
}
git(&fixture, repo.as_path(), &["branch", "taken"]);
let failed: Observed = run_wrapped(&fixture, repo.as_path(), &["worktree", "add", "-b", "taken", "../failed"]);
let control: Observed = run_direct(&fixture, repo.as_path(), &["worktree", "add", "-b", "taken", "../failed-control"]);
assert_eq!(failed.code, control.code);
assert_ne!(failed.code, Some(0));
assert!(!fixture.root.join("failed").exists(), "the failed add leaves nothing");
remove(&fixture);
}
#[test]
fn a_subvolume_worktree_is_removed_and_a_dirty_one_is_refused() {
let fixture: Fixture = fixture("ladder-remove");
if !on_btrfs_or_acknowledged(&fixture, "ladder-remove") {
remove(&fixture);
return;
}
let repo: PathBuf = populated_repository(&fixture, false);
for name in ["clean", "dirty"] {
let created: Observed = run_wrapped(&fixture, repo.as_path(), &["worktree", "add", "--quiet", "-b", name, format!("../{name}").as_str()]);
assert_eq!(created.code, Some(0), "{}", stderr_of(&created));
}
std::fs::write(fixture.root.join("dirty/README.md"), b"changed\n").expect("modify");
let refused: Observed = run_wrapped(&fixture, repo.as_path(), &["worktree", "remove", "../dirty"]);
assert_eq!(refused.code, Some(128), "{}", stderr_of(&refused));
assert!(stderr_of(&refused).contains("contains modified or untracked files, use --force to delete it"), "{}", stderr_of(&refused));
assert!(fixture.root.join("dirty/README.md").is_file());
let settled: Observed = run_wrapped(&fixture, repo.as_path(), &["worktree", "remove", "../clean"]);
assert_eq!(settled.code, Some(0), "{}", stderr_of(&settled));
assert!(!fixture.root.join("clean").exists());
assert!(!listing(&fixture, repo.as_path()).contains("/clean\n"));
eprintln!("the mount allows the one-call destroy: {}", removal_allowed(fixture.root.as_path()));
remove(&fixture);
}
fn age_tracked_files(fixture: &Fixture, worktree: &Path) {
for name in ["README.md", ".gitignore", "src/deep/leaf.txt"] {
let Ok(file) = std::fs::OpenOptions::new().write(true).open(worktree.join(name)) else { continue };
file.set_modified(std::time::SystemTime::now() - std::time::Duration::from_secs(120)).expect("age the tracked file");
}
git(fixture, worktree, &["update-index", "--refresh"]);
}
#[test]
fn a_worktree_of_another_repository_and_an_empty_name_are_not_removed() {
let fixture: Fixture = fixture("ladder-remove-foreign");
if !on_btrfs_or_acknowledged(&fixture, "ladder-remove-foreign") {
remove(&fixture);
return;
}
let repo: PathBuf = populated_repository(&fixture, false);
let other: PathBuf = repository(&fixture, OsStr::new("other"));
std::fs::write(other.join("README.md"), b"readme\n").expect("readme");
git(&fixture, other.as_path(), &["add", "README.md"]);
git(&fixture, other.as_path(), &["commit", "--quiet", "--message=files"]);
for (repository_root, branch) in [(other.as_path(), "foreign"), (repo.as_path(), "own")] {
let created: Observed = run_wrapped(&fixture, repository_root, &["worktree", "add", "--quiet", "-b", branch, format!("../{branch}").as_str()]);
assert_eq!(created.code, Some(0), "{}", stderr_of(&created));
}
age_tracked_files(&fixture, fixture.root.join("own").as_path());
age_tracked_files(&fixture, fixture.root.join("foreign").as_path());
let foreign: Observed = run_wrapped(&fixture, repo.as_path(), &["worktree", "remove", "../foreign"]);
assert_eq!(foreign.code, Some(128), "{}", stderr_of(&foreign));
assert!(stderr_of(&foreign).contains("is not a working tree"), "{}", stderr_of(&foreign));
assert!(fixture.root.join("foreign/.git").is_file(), "the other repository's worktree is untouched");
let own_root: PathBuf = fixture.root.join("own");
let empty: Observed = run_wrapped(&fixture, own_root.as_path(), &["worktree", "remove", ""]);
assert_eq!(empty.code, Some(128), "{}", stderr_of(&empty));
assert!(stderr_of(&empty).contains("is not a working tree"), "{}", stderr_of(&empty));
assert!(own_root.join("README.md").is_file(), "an empty name does not name the current worktree");
remove(&fixture);
}
#[test]
fn a_registration_left_after_the_destroy_fails_the_command_with_the_repair_steps() {
use std::os::unix::fs::PermissionsExt;
let fixture: Fixture = fixture("ladder-remove-incomplete");
if !on_btrfs_or_acknowledged(&fixture, "ladder-remove-incomplete") {
remove(&fixture);
return;
}
if !removal_allowed(fixture.root.as_path()) {
eprintln!("CONTROL NOT RUN: ladder-remove-incomplete (the mount of {} lacks user_subvol_rm_allowed)", fixture.root.display());
remove(&fixture);
return;
}
let repo: PathBuf = populated_repository(&fixture, false);
let created: Observed = run_wrapped(&fixture, repo.as_path(), &["worktree", "add", "--quiet", "-b", "clean", "../clean"]);
assert_eq!(created.code, Some(0), "{}", stderr_of(&created));
age_tracked_files(&fixture, fixture.root.join("clean").as_path());
let worktrees: PathBuf = repo.join(".git/worktrees");
std::fs::set_permissions(worktrees.as_path(), std::fs::Permissions::from_mode(0o555)).expect("read-only registrations");
let failed: Observed = run_wrapped(&fixture, repo.as_path(), &["worktree", "remove", "../clean"]);
std::fs::set_permissions(worktrees.as_path(), std::fs::Permissions::from_mode(0o755)).expect("restore");
assert_ne!(failed.code, Some(0), "{}", stderr_of(&failed));
assert!(!fixture.root.join("clean").exists(), "the files were destroyed");
let report: String = stderr_of(&failed);
assert!(report.contains(worktrees.join("clean").to_str().expect("UTF-8")), "the report names the leftover directory: {report}");
assert!(report.contains(format!("git -C {} worktree prune", repo.display()).as_str()), "and the command that finishes the removal: {report}");
let finished: Observed = run_wrapped(&fixture, repo.as_path(), &["worktree", "prune"]);
assert_eq!(finished.code, Some(0), "{}", stderr_of(&finished));
assert!(!listing(&fixture, repo.as_path()).contains("/clean\n"));
assert!(!worktrees.join("clean").exists(), "the prune removed the leftover registration");
remove(&fixture);
}
#[test]
fn the_destroy_of_a_subvolume_worktree_says_it_cannot_be_undone() {
let fixture: Fixture = fixture("ladder-remove-notice");
if !on_btrfs_or_acknowledged(&fixture, "ladder-remove-notice") {
remove(&fixture);
return;
}
if !removal_allowed(fixture.root.as_path()) {
eprintln!("CONTROL NOT RUN: ladder-remove-notice (the mount of {} lacks user_subvol_rm_allowed)", fixture.root.display());
remove(&fixture);
return;
}
let repo: PathBuf = populated_repository(&fixture, false);
let created: Observed = run_wrapped(&fixture, repo.as_path(), &["worktree", "add", "--quiet", "-b", "notice", "../notice"]);
assert_eq!(created.code, Some(0), "{}", stderr_of(&created));
age_tracked_files(&fixture, fixture.root.join("notice").as_path());
let removed: Observed = run_wrapped(&fixture, repo.as_path(), &["worktree", "remove", "../notice"]);
assert_eq!(removed.code, Some(0), "{}", stderr_of(&removed));
assert!(!fixture.root.join("notice").exists());
assert!(stderr_of(&removed).contains("this cannot be undone"), "{}", stderr_of(&removed));
remove(&fixture);
}