#![expect(unused, reason = "extraction; PHASE-03 prunes")]
use super::allowlist::{
Allowlist, allowlist_violations, is_withheld, parse_allowlist, select_copies,
};
use super::marker::{DISPATCH_WORKER_AGENT_TYPE, marker_present, write_marker};
use super::shared::{
gather_fork_worktree, gather_tree_clean, is_linked_worktree, matches, resolve_commit,
resolve_common_dir,
};
use crate::fsutil::{self, CopyOutcome};
use crate::git;
use crate::root;
use anyhow::{Context, bail};
use std::fs;
use std::io::{self, ErrorKind, Write};
use std::path::{Path, PathBuf};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct GcState {
pub(crate) branch_exists: bool,
pub(crate) worktree_present: bool,
pub(crate) landed_verdict: Option<bool>,
pub(crate) funnel_landed: bool,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct GcPlan {
pub(crate) remove_worktree: bool,
pub(crate) delete_branch: bool,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum GcRefusal {
NotLanded,
}
impl GcRefusal {
pub(crate) fn token(self) -> &'static str {
match self {
GcRefusal::NotLanded => "not-landed",
}
}
pub(crate) fn remedy(self) -> &'static str {
match self {
GcRefusal::NotLanded => {
"if this fork is funnel-managed, reap it with `dispatch_reap` (the funnel record is its landing proof); \
otherwise `--force` to reap knowingly, or `--superseded-head <SHA>` to assert it is spent-and-abandoned. \
A squash-merge cannot be certified — re-land via `worktree land` (--no-ff)."
}
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum GcVerdict {
Reap(GcPlan),
Refuse(GcRefusal),
}
pub(crate) fn classify_gc(state: GcState, force: bool, superseded_match: bool) -> GcVerdict {
if !state.branch_exists {
return GcVerdict::Reap(GcPlan {
remove_worktree: false,
delete_branch: false,
});
}
let authorised =
state.funnel_landed || force || superseded_match || state.landed_verdict == Some(true);
if !authorised {
return GcVerdict::Refuse(GcRefusal::NotLanded);
}
GcVerdict::Reap(GcPlan {
remove_worktree: state.worktree_present,
delete_branch: true,
})
}
fn reap_targets(plan: GcPlan) -> String {
let mut parts: Vec<&str> = Vec::new();
if plan.remove_worktree {
parts.push("worktree");
}
if plan.delete_branch {
parts.push("branch");
}
if parts.is_empty() {
"nothing".to_owned()
} else {
parts.join("/")
}
}
pub(crate) fn landed_against(root: &Path, target: &str, fork: &str) -> anyhow::Result<bool> {
if git::git_status_ok(root, &["merge-base", "--is-ancestor", fork, target])? {
return Ok(true);
}
let cherry = git::git_cherry(root, target, fork)?;
Ok(!cherry.is_empty() && cherry.iter().all(|line| line.starts_with('-')))
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub(crate) struct Residue {
pub(crate) worktree: bool,
pub(crate) branch: bool,
pub(crate) dispatch_record: Option<String>,
}
impl Residue {
pub(crate) fn is_empty(&self) -> bool {
!self.worktree && !self.branch && self.dispatch_record.is_none()
}
pub(crate) fn members(&self, fork: &str, worktree_dir: Option<&Path>) -> Vec<String> {
let mut parts: Vec<String> = Vec::new();
if self.worktree {
parts.push(match worktree_dir {
Some(dir) => format!("worktree {}", dir.display()),
None => format!("worktree of {fork} (see `doctrine worktree list`)"),
});
}
if self.branch {
parts.push(format!("branch {fork}"));
}
if let Some(cause) = self.dispatch_record.as_deref() {
parts.push(format!("dispatch record: {cause}"));
}
parts
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum GcOutcome {
Reaped,
AlreadyAbsent,
Busy,
NotLanded,
Residual(Residue),
}
impl GcOutcome {
pub(crate) fn advances(&self) -> bool {
match self {
GcOutcome::Reaped | GcOutcome::AlreadyAbsent => true,
GcOutcome::Busy | GcOutcome::NotLanded => false,
GcOutcome::Residual(residue) => !residue.worktree && !residue.branch,
}
}
}
struct ClaimHold {
name: Option<String>,
_lock: Option<super::claim_lock::ClaimLock>,
}
fn hold_claim(root: &Path, fork: &str) -> anyhow::Result<Option<ClaimHold>> {
let name = fork
.strip_prefix("dispatch/")
.and_then(|n| super::create::sanitise_name(n).ok());
let lock = match name.as_deref() {
Some(name) => super::claim_lock::try_acquire(root, name)?,
None => None,
};
if name.is_some() && lock.is_none() {
return Ok(None);
}
Ok(Some(ClaimHold { name, _lock: lock }))
}
struct Gathered {
state: GcState,
branch_head: Option<String>,
fork_wt: Option<PathBuf>,
}
fn gather(root: &Path, fork: &str, funnel_landed: bool) -> anyhow::Result<Gathered> {
let branch_head = git::git_opt(
root,
&[
"rev-parse",
"--verify",
"--quiet",
&format!("refs/heads/{fork}^{{commit}}"),
],
)?;
let branch_exists = branch_head.is_some();
let fork_wt = gather_fork_worktree(root, fork)?;
let landed_verdict = if branch_exists && !funnel_landed {
Some(landed_against(root, "HEAD", fork)?)
} else {
None
};
Ok(Gathered {
state: GcState {
branch_exists,
worktree_present: fork_wt.is_some(),
landed_verdict,
funnel_landed,
},
branch_head,
fork_wt,
})
}
fn act(
root: &Path,
plan: GcPlan,
fork: &str,
fork_wt: Option<&Path>,
record_name: &str,
) -> Residue {
let mut residue = Residue::default();
if let (true, Some(wt)) = (plan.remove_worktree, fork_wt) {
let removed = git::git_opt(
root,
&["worktree", "remove", "--force", &wt.to_string_lossy()],
)
.ok()
.flatten();
if removed.is_none() {
drop(git::git_opt(root, &["worktree", "prune"]));
residue.worktree = wt.exists();
}
}
if plan.delete_branch {
let deleted = git::git_opt(root, &["branch", "-D", fork]).ok().flatten();
let branch_ref = format!("refs/heads/{fork}");
if deleted.is_none() {
residue.branch = git::git_opt(root, &["rev-parse", "--verify", "--quiet", &branch_ref])
.ok()
.flatten()
.is_some();
}
}
if let Err(cause) = super::dispatch_record::delete_dispatch_record(root, record_name) {
residue.dispatch_record = Some(format!("{cause:#}"));
}
residue
}
fn dry_run_basis(state: GcState, force: bool) -> String {
if !state.branch_exists {
"already-certified (branch gone)".to_owned()
} else if state.funnel_landed {
"landed ✓ (funnel record)".to_owned()
} else if state.landed_verdict == Some(true) {
"landed ✓ (oracle)".to_owned()
} else {
let how = if force {
"--force"
} else {
"--superseded-head"
};
format!("NOT landed — reap authorised by {how} (oracle override)")
}
}
pub(crate) fn reap_fork(
path: Option<PathBuf>,
fork: &str,
funnel_landed: bool,
) -> anyhow::Result<GcOutcome> {
let root = root::find(path, &root::default_markers())?;
let root =
fs::canonicalize(&root).with_context(|| format!("canonicalize root {}", root.display()))?;
let Some(claim) = hold_claim(&root, fork)? else {
return Ok(GcOutcome::Busy);
};
let gathered = gather(&root, fork, funnel_landed)?;
let plan = match classify_gc(gathered.state, false, false) {
GcVerdict::Refuse(GcRefusal::NotLanded) => return Ok(GcOutcome::NotLanded),
GcVerdict::Reap(plan) => plan,
};
let record_name = claim.name.as_deref().unwrap_or(fork);
let residue = act(&root, plan, fork, gathered.fork_wt.as_deref(), record_name);
Ok(if residue.is_empty() {
if gathered.state.branch_exists {
GcOutcome::Reaped
} else {
GcOutcome::AlreadyAbsent
}
} else {
GcOutcome::Residual(residue)
})
}
pub(crate) fn run_gc(
path: Option<PathBuf>,
fork: &str,
superseded_head: Option<&str>,
force: bool,
dry_run: bool,
) -> anyhow::Result<()> {
run_gc_to(
path,
fork,
superseded_head,
force,
dry_run,
&mut io::stdout().lock(),
)
}
pub(crate) fn run_gc_to(
path: Option<PathBuf>,
fork: &str,
superseded_head: Option<&str>,
force: bool,
dry_run: bool,
out: &mut dyn io::Write,
) -> anyhow::Result<()> {
let root = root::find(path, &root::default_markers())?;
let root =
fs::canonicalize(&root).with_context(|| format!("canonicalize root {}", root.display()))?;
let Some(claim) = hold_claim(&root, fork)? else {
writeln!(
out,
"{fork}: skipped — an active claimant holds the claim lock (a spawn is mid claim→bind→act); nothing classified, nothing deleted"
)?;
return Ok(());
};
let gathered = gather(&root, fork, false)?;
let state = gathered.state;
let superseded_match = match (superseded_head, &gathered.branch_head) {
(Some(sha), Some(head)) => {
match git::git_opt(
&root,
&[
"rev-parse",
"--verify",
"--quiet",
&format!("{sha}^{{commit}}"),
],
)? {
Some(resolved) => matches(&resolved, head),
None => false,
}
}
_ => false,
};
let verdict = classify_gc(state, force, superseded_match);
if dry_run {
match verdict {
GcVerdict::Reap(plan) => {
writeln!(
out,
"{fork}: {} — would reap ({})",
dry_run_basis(state, force),
reap_targets(plan)
)?;
}
GcVerdict::Refuse(GcRefusal::NotLanded) => {
writeln!(
out,
"{fork}: {} — {}",
GcRefusal::NotLanded.token(),
GcRefusal::NotLanded.remedy()
)?;
}
}
return Ok(());
}
let plan = match verdict {
GcVerdict::Refuse(GcRefusal::NotLanded) => bail!(
"gc-refused: {} — fork {fork} has not provably landed; {}",
GcRefusal::NotLanded.token(),
GcRefusal::NotLanded.remedy()
),
GcVerdict::Reap(plan) => plan,
};
let record_name = claim.name.as_deref().unwrap_or(fork);
let residue = act(&root, plan, fork, gathered.fork_wt.as_deref(), record_name);
if !residue.is_empty() {
bail!(
"gc-incomplete: leftover(s) need manual cleanup: {}",
residue
.members(fork, gathered.fork_wt.as_deref())
.join(", ")
);
}
writeln!(
io::stderr(),
"warning: test binaries baked with the reaped fork's CARGO_MANIFEST_DIR are now stale — recompile before trusting a RED"
)?;
writeln!(out, "gc {fork}: reaped (worktree/branch as present)")?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::landed_against;
use std::path::{Path, PathBuf};
use std::process::Command;
struct ScratchRepo {
_dir: tempfile::TempDir,
path: PathBuf,
}
impl ScratchRepo {
fn new() -> Self {
let dir = tempfile::tempdir().expect("tempdir");
let path = dir.path().to_path_buf();
let repo = Self { _dir: dir, path };
repo.git(&["init", "-q", "-b", "main"]);
repo.git(&["config", "user.email", "t@example.com"]);
repo.git(&["config", "user.name", "Test"]);
repo
}
fn path(&self) -> &Path {
&self.path
}
fn git(&self, args: &[&str]) -> String {
let out = Command::new("git")
.arg("-C")
.arg(&self.path)
.args(args)
.output()
.expect("spawn git");
assert!(
out.status.success(),
"git {args:?} failed: {}",
String::from_utf8_lossy(&out.stderr).trim()
);
String::from_utf8_lossy(&out.stdout).trim().to_string()
}
fn branch_exists(&self, branch: &str) -> bool {
crate::git::git_opt(
&self.path,
&[
"rev-parse",
"--verify",
"--quiet",
&format!("refs/heads/{branch}"),
],
)
.expect("probe branch")
.is_some()
}
fn commit(&self, rel: &str, contents: &str, message: &str) -> String {
std::fs::write(self.path.join(rel), contents).expect("write file");
self.git(&["add", rel]);
self.git(&["commit", "-q", "-m", message]);
self.git(&["rev-parse", "HEAD"])
}
}
#[test]
fn landed_against_non_head_target_via_ancestry() {
let repo = ScratchRepo::new();
repo.commit("base.txt", "0", "C0");
repo.git(&["checkout", "-q", "-b", "feature"]);
let fork = "feature";
repo.commit("feat.txt", "f", "feature work");
repo.git(&["checkout", "-q", "main"]);
repo.git(&["checkout", "-q", "-b", "release"]);
repo.git(&["merge", "--no-ff", "-q", "-m", "land feature", fork]);
repo.git(&["checkout", "-q", "main"]);
let target = "release";
assert!(landed_against(repo.path(), target, fork).expect("oracle"));
assert!(!landed_against(repo.path(), "HEAD", fork).expect("oracle"));
}
#[test]
fn landed_against_non_head_target_via_patch_id_cherry() {
let repo = ScratchRepo::new();
repo.commit("base.txt", "0", "C0");
repo.git(&["checkout", "-q", "-b", "feature"]);
let fork = "feature";
let fork_tip = repo.commit("feat.txt", "content", "add feat");
repo.git(&["checkout", "-q", "main"]);
repo.git(&["checkout", "-q", "-b", "release"]);
repo.git(&["cherry-pick", &fork_tip]);
repo.git(&["checkout", "-q", "main"]);
let target = "release";
assert!(landed_against(repo.path(), target, fork).expect("oracle"));
}
#[test]
fn not_landed_against_non_head_target() {
let repo = ScratchRepo::new();
repo.commit("base.txt", "0", "C0");
repo.git(&["checkout", "-q", "-b", "feature"]);
let fork = "feature";
repo.commit("only.txt", "only", "unlanded work");
repo.git(&["checkout", "-q", "main"]);
repo.git(&["checkout", "-q", "-b", "release"]);
repo.commit("other.txt", "other", "unrelated release work");
repo.git(&["checkout", "-q", "main"]);
let target = "release";
assert!(!landed_against(repo.path(), target, fork).expect("oracle"));
}
#[test]
fn reap_deletes_dispatch_record_and_resolve_yields_unknown_agent() {
use super::run_gc;
use crate::worktree::dispatch_record::{
ForkExpect, RECORD_SUBPATH, ResolveRefusal, provision_dispatch_record, resolve_agent,
};
let repo = ScratchRepo::new();
let base = repo.commit("a.txt", "0", "base");
let coord = std::fs::canonicalize(repo.path()).unwrap();
let name = "agent-cafe";
let branch = format!("dispatch/{name}");
let dir = coord.join(".worktrees").join(name);
let dir_s = dir.to_string_lossy().to_string();
repo.git(&["worktree", "add", "-b", &branch, &dir_s, &base]);
provision_dispatch_record(&coord, name, &base, &dir, &branch, None).unwrap();
let record_file = coord.join(RECORD_SUBPATH).join(format!("{name}.toml"));
assert!(
record_file.exists(),
"the DispatchRecord is written for a live worker"
);
assert!(
resolve_agent(&coord, name, ForkExpect::AtBase).is_ok(),
"a live, consistent worker resolves pre-reap"
);
run_gc(Some(coord.clone()), &branch, None, true, false).expect("gc reap");
assert!(
!record_file.exists(),
"reap deleted the DispatchRecord — none survives a reaped worktree"
);
assert_eq!(
resolve_agent(&coord, name, ForkExpect::AtBase),
Err(ResolveRefusal::UnknownAgent),
"post-reap resolve of the same agent yields unknown-agent"
);
}
fn state(
branch_exists: bool,
worktree_present: bool,
landed_verdict: Option<bool>,
funnel_landed: bool,
) -> super::GcState {
super::GcState {
branch_exists,
worktree_present,
landed_verdict,
funnel_landed,
}
}
#[test]
fn the_injected_funnel_proof_authorises_without_force_or_superseded() {
use super::{GcPlan, GcRefusal, GcVerdict, classify_gc};
assert_eq!(
classify_gc(state(true, true, None, true), false, false),
GcVerdict::Reap(GcPlan {
remove_worktree: true,
delete_branch: true,
}),
"the funnel record certifies the fork; no operator override is needed"
);
assert!(matches!(
classify_gc(state(true, true, Some(false), true), false, false),
GcVerdict::Reap(_)
));
assert_eq!(
classify_gc(state(true, true, Some(false), false), false, false),
GcVerdict::Refuse(GcRefusal::NotLanded)
);
assert!(matches!(
classify_gc(state(true, true, Some(true), false), false, false),
GcVerdict::Reap(_)
));
}
#[test]
fn an_import_superset_patch_still_reads_not_landed_and_the_funnel_fact_reaps_it() {
use super::{GcOutcome, GcVerdict, classify_gc, gather, landed_against, reap_fork};
let repo = ScratchRepo::new();
repo.commit("base.txt", "0", "C0");
let base = repo.git(&["rev-parse", "HEAD"]);
let name = "agent-superset";
let branch = format!("dispatch/{name}");
repo.git(&["checkout", "-q", "-b", &branch]);
repo.commit("a.rs", "fn a() {}\n", "worker work");
repo.git(&["checkout", "-q", "main"]);
std::fs::write(repo.path().join("a.rs"), "fn a() {}\n").expect("a.rs");
std::fs::create_dir_all(repo.path().join(".doctrine/dispatch/228")).expect("dir");
std::fs::write(
repo.path().join(".doctrine/dispatch/228/funnel.toml"),
"schema = 1\n",
)
.expect("funnel.toml");
repo.git(&["add", "a.rs", ".doctrine"]);
repo.git(&["commit", "-q", "-m", "import: delta ⊕ funnel row"]);
let coord = std::fs::canonicalize(repo.path()).expect("canonicalize");
assert!(
!landed_against(&coord, "HEAD", &branch).expect("oracle"),
"a superset landing commit matches no patch-id: `git cherry` reports `+`"
);
assert!(matches!(
classify_gc(
gather(&coord, &branch, false).expect("gather").state,
false,
false
),
GcVerdict::Refuse(_)
));
let gathered = gather(&coord, &branch, true).expect("gather");
assert_eq!(
gathered.state.landed_verdict, None,
"the `git cherry` read is skipped entirely under the funnel proof"
);
assert_eq!(
reap_fork(Some(coord.clone()), &branch, true).expect("reap"),
GcOutcome::Reaped,
"no --force anywhere: the funnel record is the landing proof"
);
assert!(!repo.branch_exists(&branch), "the branch is gone");
assert_ne!(
base, "",
"the base commit is real (the fork was a descendant, not an orphan)"
);
}
#[test]
fn an_unproven_fork_is_a_typed_outcome_and_a_gone_fork_is_already_absent() {
use super::{GcOutcome, reap_fork};
let repo = ScratchRepo::new();
repo.commit("a.txt", "0", "base");
let name = "agent-unproven";
let branch = format!("dispatch/{name}");
repo.git(&["checkout", "-q", "-b", &branch]);
repo.commit("only.txt", "unlanded", "fork work");
repo.git(&["checkout", "-q", "main"]);
repo.commit("other.txt", "other", "diverging coord work");
let coord = std::fs::canonicalize(repo.path()).expect("canonicalize");
assert_eq!(
reap_fork(Some(coord.clone()), &branch, false).expect("no Err on a refusal"),
GcOutcome::NotLanded
);
assert!(
!reap_fork(Some(coord.clone()), &branch, false)
.expect("outcome")
.advances(),
"a refusal never advances the funnel position"
);
assert!(
repo.branch_exists(&branch),
"the unproven fork survives the refusal"
);
assert_eq!(
reap_fork(Some(coord.clone()), &branch, true).expect("reap"),
GcOutcome::Reaped
);
let replay = reap_fork(Some(coord), &branch, true).expect("replay");
assert_eq!(replay, GcOutcome::AlreadyAbsent);
assert!(replay.advances(), "already-absent completes the run");
}
#[test]
fn a_busy_claim_lock_is_a_returned_outcome_not_a_printed_skip() {
use super::{GcOutcome, reap_fork};
use crate::worktree::claim_lock;
let repo = ScratchRepo::new();
let base = repo.commit("a.txt", "0", "base");
let coord = std::fs::canonicalize(repo.path()).expect("canonicalize");
let name = "agent-busy";
let branch = format!("dispatch/{name}");
repo.git(&["branch", &branch, &base]);
let held = claim_lock::acquire(&coord, name).expect("the claimant takes the name");
let outcome = reap_fork(Some(coord.clone()), &branch, true).expect("busy is not an error");
assert_eq!(outcome, GcOutcome::Busy);
assert!(
!outcome.advances(),
"a skipped name never advances position"
);
assert!(
repo.branch_exists(&branch),
"the live claimant's branch is NOT swept, funnel proof or not"
);
drop(held);
assert_eq!(
reap_fork(Some(coord), &branch, true).expect("reap"),
GcOutcome::Reaped,
"with the claim released the reap proceeds"
);
}
#[test]
fn the_dry_run_basis_names_the_authority_that_authorised_the_reap() {
use super::dry_run_basis;
assert_eq!(
dry_run_basis(state(false, false, None, false), false),
"already-certified (branch gone)"
);
assert_eq!(
dry_run_basis(state(true, true, None, true), false),
"landed ✓ (funnel record)"
);
assert_eq!(
dry_run_basis(state(true, true, Some(true), false), false),
"landed ✓ (oracle)"
);
assert!(
dry_run_basis(state(true, true, Some(false), false), true).contains("--force"),
"an override reap says so, and says it is NOT landed"
);
}
#[test]
fn residue_classification_puts_critical_above_administrative() {
use super::{GcOutcome, Residue};
let admin = Residue {
worktree: false,
branch: false,
dispatch_record: Some("permission denied".to_owned()),
};
assert!(
GcOutcome::Residual(admin.clone()).advances(),
"the fork IS gone; only a stale record survives"
);
let critical = Residue {
worktree: true,
branch: true,
dispatch_record: Some("permission denied".to_owned()),
};
assert!(!GcOutcome::Residual(critical.clone()).advances());
let members = critical.members("dispatch/x", Some(Path::new("/w/x")));
assert_eq!(
members,
vec![
"worktree /w/x".to_owned(),
"branch dispatch/x".to_owned(),
"dispatch record: permission denied".to_owned(),
]
);
assert!(Residue::default().is_empty());
assert!(!admin.is_empty());
}
#[test]
fn run_gc_to_writes_the_report_to_the_injected_sink() {
use super::run_gc_to;
let repo = ScratchRepo::new();
let base = repo.commit("a.txt", "0", "base");
let coord = std::fs::canonicalize(repo.path()).unwrap();
let name = "agent-feed";
let branch = format!("dispatch/{name}");
let dir = coord.join(".worktrees").join(name);
repo.git(&[
"worktree",
"add",
"-b",
&branch,
&dir.to_string_lossy(),
&base,
]);
let mut sink: Vec<u8> = Vec::new();
run_gc_to(Some(coord.clone()), &branch, None, true, false, &mut sink).expect("gc reap");
let report = String::from_utf8(sink).expect("utf8 report");
assert!(
report.contains(&format!("gc {branch}: reaped")),
"the reap report lands in the injected sink, not stdout: {report:?}"
);
assert!(
!repo.path().join(".worktrees").join(name).exists(),
"the fork worktree is actually reaped (side effects unchanged)"
);
}
#[test]
fn a_busy_claim_lock_makes_the_sweep_skip_the_name_and_destroy_nothing() {
use super::run_gc_to;
use crate::worktree::claim_lock;
use crate::worktree::dispatch_record::{RECORD_SUBPATH, provision_dispatch_record};
let repo = ScratchRepo::new();
let base = repo.commit("a.txt", "0", "base");
let coord = std::fs::canonicalize(repo.path()).unwrap();
let name = "agent-claimed";
let branch = format!("dispatch/{name}");
let dir = coord.join(".worktrees").join(name);
repo.git(&["branch", &branch, &base]);
provision_dispatch_record(&coord, name, &base, &dir, &branch, None).unwrap();
let record_file = coord.join(RECORD_SUBPATH).join(format!("{name}.toml"));
let held = claim_lock::acquire(&coord, name).expect("the claimant takes the name");
let mut sink: Vec<u8> = Vec::new();
run_gc_to(Some(coord.clone()), &branch, None, true, false, &mut sink)
.expect("a busy name is a skip, not an error");
let report = String::from_utf8(sink).expect("utf8 report");
assert!(
report.contains("skipped") && report.contains("claim lock"),
"the skip names its reason: {report:?}"
);
assert!(
repo.git(&["rev-parse", "--verify", "--quiet", &branch]) != *"",
"the live claimant's branch is NOT swept"
);
assert!(
record_file.exists(),
"the live claimant's record is NOT deleted"
);
drop(held);
let mut sink2: Vec<u8> = Vec::new();
run_gc_to(Some(coord.clone()), &branch, None, true, false, &mut sink2).expect("gc reap");
assert!(
!record_file.exists(),
"with the claim released the residue sweeps"
);
assert!(
String::from_utf8(sink2).unwrap().contains("reaped"),
"and reports a real reap"
);
}
}