use super::support::{
Fixture, Observed, fixture, git, observe, remove, repository, run_wrapped, stderr_of, wrapped,
};
use git_policy_cli::worktree_copy_journal::{create_journal, read_pending_journals};
use git_policy_cli::worktree_copy_selection::CopySelection;
use git_policy_cli::worktree_copy_stage::{StagedSnapshot, new_stage_container, stage_ignored_snapshot};
use git_policy_cli::worktree_copy_stage_claim::{claim_stage, stage_claims_exist};
use git_policy_cli::transaction_git::GitContext;
use std::ffi::OsStr;
use std::os::unix::fs::PermissionsExt;
use std::path::{Path, PathBuf};
const RULES_FILE: &str = ".cache/forbidden-strings.rules.txt";
fn needle() -> String {
return ["PLANTED", "COPY", "NEEDLE"].join("_");
}
fn repository_with_rules(fixture: &Fixture, with_rules: bool) -> PathBuf {
let repo: PathBuf = repository(fixture, OsStr::new("repo"));
std::fs::write(
repo.join("cli-git.config.jsonc"),
format!(
"{{ \"policies\": {{ \"security/forbidden-strings\": [\"error\", {{ \"builtinRules\": false, \"rulesFile\": \"{RULES_FILE}\" }}] }} }}\n"
),
)
.expect("configuration");
std::fs::write(repo.join(".gitignore"), b".cache/\nheavy/\n").expect("gitignore");
std::fs::write(repo.join("a.txt"), format!("first\n{}\n", needle())).expect("needle file");
git(
fixture,
repo.as_path(),
&["add", "--", "cli-git.config.jsonc", ".gitignore", "a.txt"],
);
git(
fixture,
repo.as_path(),
&["commit", "--quiet", "--message=configuration"],
);
if with_rules {
std::fs::create_dir_all(repo.join(".cache")).expect("cache directory");
std::fs::write(repo.join(RULES_FILE), format!("{}\n", needle())).expect("rules");
std::fs::write(repo.join(".cache/other.txt"), b"other\n").expect("other cache file");
std::fs::create_dir_all(repo.join("heavy")).expect("heavy directory");
std::fs::write(repo.join("heavy/data"), b"payload\n").expect("heavy data");
}
return repo;
}
fn check_all(fixture: &Fixture, worktree: &Path) -> Observed {
return run_wrapped(
fixture,
worktree,
&["cli-git", "check", "--policy", "security/forbidden-strings", "--all"],
);
}
fn assert_scanner_has_rules(observed: &Observed) {
let stdout: String = String::from_utf8_lossy(&observed.stdout).into_owned();
assert_eq!(observed.code, Some(1), "{stdout} {}", stderr_of(observed));
assert!(
stdout.contains("security/forbidden-strings/forbidden-string"),
"{stdout}"
);
}
fn assert_scanner_failed_closed(observed: &Observed, worktree: &Path) {
let stdout: String = String::from_utf8_lossy(&observed.stdout).into_owned();
assert_eq!(observed.code, Some(2), "{stdout}");
assert!(stdout.contains("\"code\":\"policy-incomplete\""), "{stdout}");
assert!(
stdout.contains(worktree.join(RULES_FILE).to_string_lossy().as_ref()),
"{stdout}"
);
}
#[test]
fn worktree_added_from_the_main_checkout_receives_the_rules_file() {
let fixture: Fixture = fixture("copy-main");
let repo: PathBuf = repository_with_rules(&fixture, true);
let created: Observed = run_wrapped(
&fixture,
repo.as_path(),
&["worktree", "add", "--quiet", "-b", "one", "../one"],
);
assert_eq!(created.code, Some(0), "{}", stderr_of(&created));
assert_eq!(
stderr_of(&created),
format!(
"cli-git: copied 1 ignored filesystem entry from \"{}\" into 1 new worktree.\n",
repo.display()
)
);
let one: PathBuf = fixture.root.join("one");
assert_eq!(
std::fs::read(one.join(RULES_FILE)).expect("copied rules"),
format!("{}\n", needle()).into_bytes()
);
assert!(!one.join(".cache/other.txt").exists(), "siblings stay behind");
assert!(!one.join("heavy").exists(), "other ignored trees stay behind");
assert_scanner_has_rules(&check_all(&fixture, one.as_path()));
let opted_out: Observed = run_wrapped(
&fixture,
repo.as_path(),
&["worktree", "add", "--no-worktree-copy", "--quiet", "-b", "two", "../two"],
);
assert_eq!(opted_out.code, Some(0), "{}", stderr_of(&opted_out));
assert!(opted_out.stderr.is_empty(), "{}", stderr_of(&opted_out));
let two: PathBuf = fixture.root.join("two");
assert!(!two.join(".cache").exists());
assert_scanner_failed_closed(&check_all(&fixture, two.as_path()), two.as_path());
remove(&fixture);
}
#[test]
fn worktree_added_from_a_linked_worktree_receives_all_ignored_state() {
let fixture: Fixture = fixture("copy-linked");
let repo: PathBuf = repository_with_rules(&fixture, true);
assert_eq!(
run_wrapped(
&fixture,
repo.as_path(),
&["worktree", "add", "--quiet", "-b", "one", "../one"],
)
.code,
Some(0)
);
let one: PathBuf = fixture.root.join("one");
std::fs::create_dir_all(one.join("heavy")).expect("heavy in the source");
std::fs::write(one.join("heavy/data"), b"payload\n").expect("heavy data");
let created: Observed = run_wrapped(
&fixture,
one.as_path(),
&["worktree", "add", "--quiet", "-b", "two", "../two"],
);
assert_eq!(created.code, Some(0), "{}", stderr_of(&created));
assert_eq!(
stderr_of(&created),
format!(
"cli-git: copied 4 ignored filesystem entries from \"{}\" into 1 new worktree.\n",
one.display()
)
);
let two: PathBuf = fixture.root.join("two");
assert!(two.join(RULES_FILE).exists());
assert_eq!(
std::fs::read(two.join("heavy/data")).expect("heavy copied"),
b"payload\n"
);
assert_scanner_has_rules(&check_all(&fixture, two.as_path()));
remove(&fixture);
}
#[test]
fn a_source_without_the_rules_file_copies_nothing_and_the_scanner_fails_closed() {
let fixture: Fixture = fixture("copy-absent");
let repo: PathBuf = repository_with_rules(&fixture, false);
let created: Observed = run_wrapped(
&fixture,
repo.as_path(),
&["worktree", "add", "--quiet", "-b", "one", "../one"],
);
assert_eq!(created.code, Some(0), "{}", stderr_of(&created));
let one: PathBuf = fixture.root.join("one");
assert!(!one.join(".cache").exists());
assert_scanner_failed_closed(&check_all(&fixture, one.as_path()), one.as_path());
remove(&fixture);
}
#[test]
fn a_main_checkout_without_a_configured_rules_file_is_forwarded_plainly() {
let fixture: Fixture = fixture("copy-unconfigured");
let repo: PathBuf = repository(&fixture, OsStr::new("repo"));
std::fs::write(repo.join(".gitignore"), b".cache/\n").expect("gitignore");
std::fs::create_dir_all(repo.join(".cache")).expect("cache");
std::fs::write(repo.join(RULES_FILE), b"rules\n").expect("rules");
let created: Observed = run_wrapped(
&fixture,
repo.as_path(),
&["worktree", "add", "--quiet", "-b", "one", "../one"],
);
assert_eq!(created.code, Some(0), "{}", stderr_of(&created));
assert!(created.stderr.is_empty(), "{}", stderr_of(&created));
assert!(!fixture.root.join("one/.cache").exists());
remove(&fixture);
}
#[test]
fn an_aliased_worktree_add_is_copied_and_git_failures_keep_their_status() {
let fixture: Fixture = fixture("copy-alias");
let repo: PathBuf = repository_with_rules(&fixture, true);
let aliased: Observed = run_wrapped(
&fixture,
repo.as_path(),
&["-c", "alias.wta=worktree add", "wta", "--quiet", "-b", "one", "../one"],
);
assert_eq!(aliased.code, Some(0), "{}", stderr_of(&aliased));
assert!(fixture.root.join("one").join(RULES_FILE).exists());
let refused: Observed = run_wrapped(
&fixture,
repo.as_path(),
&["worktree", "add", "--quiet", "-b", "one", "../again"],
);
assert_eq!(refused.code, Some(255), "{}", stderr_of(&refused));
assert!(!stderr_of(&refused).contains("cli-git: copied"));
assert!(!fixture.root.join("again").exists());
remove(&fixture);
}
#[test]
fn an_interrupted_copy_is_recovered_by_the_next_command() {
let fixture: Fixture = fixture("copy-recovery");
let repo: PathBuf = repository_with_rules(&fixture, true);
let opted_out: Observed = run_wrapped(
&fixture,
repo.as_path(),
&["worktree", "add", "--no-worktree-copy", "--quiet", "-b", "one", "../one"],
);
assert_eq!(opted_out.code, Some(0));
let one: PathBuf = std::fs::canonicalize(fixture.root.join("one")).expect("canonical worktree");
assert!(!one.join(RULES_FILE).exists());
let common: PathBuf = std::fs::canonicalize(repo.join(".git")).expect("common directory");
let context: GitContext = GitContext {
real_git: PathBuf::from(super::support::REAL_GIT),
overlay: Vec::new(),
global_prefix: Vec::new(),
};
let snapshot: StagedSnapshot = stage_ignored_snapshot(
&context,
std::fs::canonicalize(repo.as_path()).expect("canonical source").as_path(),
new_stage_container(one.as_path()).expect("container").as_path(),
&[],
&CopySelection::Paths(vec![String::from(RULES_FILE)]),
)
.expect("staged");
create_journal(common.as_path(), one.as_path(), &snapshot).expect("journal");
assert_eq!(read_pending_journals(common.as_path()).expect("pending").len(), 1);
let status: Observed = run_wrapped(&fixture, one.as_path(), &["rev-parse", "--is-inside-work-tree"]);
assert_eq!(status.code, Some(0), "{}", stderr_of(&status));
assert_eq!(status.stdout, b"true\n");
assert_eq!(
stderr_of(&status),
"cli-git: recovered ignored-state copies for 1 worktree transaction.\n"
);
assert!(read_pending_journals(common.as_path()).expect("pending").is_empty());
assert!(one.join(RULES_FILE).exists());
assert_scanner_has_rules(&check_all(&fixture, one.as_path()));
let again: Observed = run_wrapped(&fixture, one.as_path(), &["rev-parse", "--is-inside-work-tree"]);
assert!(again.stderr.is_empty(), "{}", stderr_of(&again));
remove(&fixture);
}
#[test]
fn a_stage_abandoned_before_its_journal_is_removed_by_the_next_command() {
let fixture: Fixture = fixture("copy-abandoned-stage");
let repo: PathBuf = repository_with_rules(&fixture, true);
let opted_out: Observed = run_wrapped(
&fixture,
repo.as_path(),
&["worktree", "add", "--no-worktree-copy", "--quiet", "-b", "one", "../one"],
);
assert_eq!(opted_out.code, Some(0));
let one: PathBuf = std::fs::canonicalize(fixture.root.join("one")).expect("canonical worktree");
let common: PathBuf = std::fs::canonicalize(repo.join(".git")).expect("common directory");
let claim = claim_stage(common.as_path(), one.as_path()).expect("claim");
let stage: PathBuf = claim.container().to_path_buf();
std::fs::create_dir(stage.as_path()).expect("stage");
std::fs::set_permissions(stage.as_path(), std::fs::Permissions::from_mode(0o700)).expect("private");
std::fs::create_dir(stage.join("payload")).expect("payload");
std::fs::write(stage.join("payload/partial"), b"copied so far").expect("partial copy");
let status: Observed = run_wrapped(&fixture, one.as_path(), &["rev-parse", "--is-inside-work-tree"]);
assert_eq!(status.code, Some(0), "{}", stderr_of(&status));
assert_eq!(status.stdout, b"true\n");
assert!(stderr_of(&status).starts_with("cli-git: removed the stage "), "{}", stderr_of(&status));
assert!(!stage.exists());
assert!(!stage_claims_exist(common.as_path()).expect("claims"));
let again: Observed = run_wrapped(&fixture, one.as_path(), &["rev-parse", "--is-inside-work-tree"]);
assert!(again.stderr.is_empty(), "{}", stderr_of(&again));
remove(&fixture);
}
#[test]
fn a_nested_command_with_a_foreign_lease_still_runs() {
let fixture: Fixture = fixture("copy-lease");
let repo: PathBuf = repository_with_rules(&fixture, true);
let created: Observed = run_wrapped(
&fixture,
repo.as_path(),
&["worktree", "add", "--quiet", "-b", "one", "../one"],
);
assert_eq!(created.code, Some(0));
let one: PathBuf = fixture.root.join("one");
let observed: Observed = observe(
wrapped(&fixture)
.current_dir(&one)
.env("CLI_GIT_WORKTREE_COPY_LEASE", "not-the-lock-token")
.args(["rev-parse", "--is-inside-work-tree"]),
b"",
);
assert_eq!(observed.code, Some(0), "{}", stderr_of(&observed));
assert_eq!(observed.stdout, b"true\n");
remove(&fixture);
}
#[test]
fn a_hook_that_creates_a_worktree_runs_under_the_inherited_lease() {
let fixture: Fixture = fixture("copy-hook-lease");
let repo: PathBuf = repository_with_rules(&fixture, true);
let hook: PathBuf = repo.join(".git/hooks/post-checkout");
super::support::executable(
hook.as_path(),
b"#!/bin/sh\nif [ -n \"$CLI_GIT_WORKTREE_COPY_LEASE\" ] && [ ! -e ../nested ]; then\n printf 'leased' > ../hook-saw-lease\n \"$CLI_GIT_TEST_WRAPPER\" worktree add --quiet -b nested ../nested || exit 1\nfi\n",
);
let created: Observed = observe(
wrapped(&fixture)
.current_dir(&repo)
.env("CLI_GIT_TEST_WRAPPER", fixture.root.join("bin/git"))
.args(["worktree", "add", "--quiet", "-b", "one", "../one"]),
b"",
);
assert_eq!(created.code, Some(0), "{}", stderr_of(&created));
assert_eq!(
std::fs::read(fixture.root.join("hook-saw-lease")).expect("the hook saw the lease"),
b"leased"
);
assert!(fixture.root.join("nested/.git").exists(), "the nested creation ran");
assert!(fixture.root.join("one").join(RULES_FILE).exists(), "the outer copy finished");
assert!(fixture.root.join("nested").join(RULES_FILE).exists());
assert!(stderr_of(&created).ends_with(" into 2 new worktrees.\n"), "{}", stderr_of(&created));
remove(&fixture);
}
#[test]
fn an_interrupted_main_checkout_copy_is_recovered_by_the_next_creation() {
let fixture: Fixture = fixture("copy-recovery-main");
let repo: PathBuf = repository_with_rules(&fixture, true);
assert_eq!(
run_wrapped(
&fixture,
repo.as_path(),
&["worktree", "add", "--no-worktree-copy", "--quiet", "-b", "one", "../one"],
)
.code,
Some(0)
);
let one: PathBuf = std::fs::canonicalize(fixture.root.join("one")).expect("canonical worktree");
let common: PathBuf = std::fs::canonicalize(repo.join(".git")).expect("common directory");
let context: GitContext = GitContext {
real_git: PathBuf::from(super::support::REAL_GIT),
overlay: Vec::new(),
global_prefix: Vec::new(),
};
let snapshot: StagedSnapshot = stage_ignored_snapshot(
&context,
std::fs::canonicalize(repo.as_path()).expect("canonical source").as_path(),
new_stage_container(one.as_path()).expect("container").as_path(),
&[],
&CopySelection::Paths(vec![String::from(RULES_FILE)]),
)
.expect("staged");
create_journal(common.as_path(), one.as_path(), &snapshot).expect("journal");
let next: Observed = run_wrapped(
&fixture,
repo.as_path(),
&["worktree", "add", "--quiet", "-b", "two", "../two"],
);
assert_eq!(next.code, Some(0), "{}", stderr_of(&next));
assert!(
stderr_of(&next).starts_with("cli-git: recovered ignored-state copies for 1 worktree transaction.\n"),
"{}",
stderr_of(&next)
);
assert!(one.join(RULES_FILE).exists());
assert!(fixture.root.join("two").join(RULES_FILE).exists());
assert!(read_pending_journals(common.as_path()).expect("pending").is_empty());
remove(&fixture);
}
#[test]
fn moving_a_worktree_is_forwarded_and_prints_no_summary() {
let fixture: Fixture = fixture("copy-move");
let repo: PathBuf = repository_with_rules(&fixture, true);
assert_eq!(
run_wrapped(
&fixture,
repo.as_path(),
&["worktree", "add", "--quiet", "-b", "one", "../one"],
)
.code,
Some(0)
);
let moved: Observed = run_wrapped(&fixture, repo.as_path(), &["worktree", "move", "../one", "../moved"]);
assert_eq!(moved.code, Some(0), "{}", stderr_of(&moved));
assert!(moved.stderr.is_empty(), "{}", stderr_of(&moved));
assert!(fixture.root.join("moved").join(RULES_FILE).exists());
let moved_again: Observed = run_wrapped(
&fixture,
fixture.root.join("moved").as_path(),
&["worktree", "move", "../moved", "../final"],
);
assert_eq!(moved_again.code, Some(0), "{}", stderr_of(&moved_again));
assert!(moved_again.stderr.is_empty(), "{}", stderr_of(&moved_again));
remove(&fixture);
}
#[test]
fn a_failed_copy_ends_with_status_two_and_its_message() {
let fixture: Fixture = fixture("copy-failure");
let repo: PathBuf = repository_with_rules(&fixture, false);
std::fs::create_dir_all(repo.join(".cache")).expect("cache directory");
std::fs::write(repo.join(RULES_FILE), b"rules\n").expect("rules");
std::fs::set_permissions(repo.join(RULES_FILE), std::fs::Permissions::from_mode(0o000))
.expect("unreadable");
let created: Observed = run_wrapped(
&fixture,
repo.as_path(),
&["worktree", "add", "--quiet", "-b", "one", "../one"],
);
assert_eq!(created.code, Some(2), "{}", stderr_of(&created));
assert!(
stderr_of(&created).starts_with("cli-git: could not stage ignored state from "),
"{}",
stderr_of(&created)
);
assert!(!stderr_of(&created).contains("cli-git: copied"), "{}", stderr_of(&created));
assert!(fixture.root.join("one/.git").exists(), "Git created the worktree");
assert!(!fixture.root.join("one").join(RULES_FILE).exists());
let common: PathBuf = std::fs::canonicalize(repo.join(".git")).expect("common directory");
assert!(read_pending_journals(common.as_path()).expect("pending").is_empty());
std::fs::set_permissions(repo.join(RULES_FILE), std::fs::Permissions::from_mode(0o600))
.expect("readable again");
let next: Observed = run_wrapped(
&fixture,
repo.as_path(),
&["worktree", "add", "--no-worktree-copy", "--quiet", "-b", "two", "../two"],
);
assert_eq!(next.code, Some(0), "{}", stderr_of(&next));
remove(&fixture);
}