#![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>,
}
#[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",
}
}
}
#[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,
_dry_run: bool,
) -> GcVerdict {
if !state.branch_exists {
return GcVerdict::Reap(GcPlan {
remove_worktree: false,
delete_branch: false,
});
}
let authorised = 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("/")
}
}
fn gather_landed(root: &Path, fork: &str) -> anyhow::Result<bool> {
if git::git_status_ok(root, &["merge-base", "--is-ancestor", fork, "HEAD"])? {
return Ok(true);
}
let cherry = git::git_cherry(root, "HEAD", fork)?;
Ok(!cherry.is_empty() && cherry.iter().all(|line| line.starts_with('-')))
}
pub(crate) fn run_gc(
path: Option<PathBuf>,
fork: &str,
superseded_head: Option<&str>,
force: bool,
dry_run: bool,
) -> anyhow::Result<()> {
let root = root::find(path, &root::default_markers())?;
let root =
fs::canonicalize(&root).with_context(|| format!("canonicalize root {}", root.display()))?;
let branch_ref = format!("refs/heads/{fork}");
let branch_head = git::git_opt(
&root,
&[
"rev-parse",
"--verify",
"--quiet",
&format!("{branch_ref}^{{commit}}"),
],
)?;
let branch_exists = branch_head.is_some();
let fork_wt = gather_fork_worktree(&root, fork)?;
let worktree_present = fork_wt.is_some();
let landed_verdict = if branch_exists {
Some(gather_landed(&root, fork)?)
} else {
None
};
let superseded_match = match (superseded_head, &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 state = GcState {
branch_exists,
worktree_present,
landed_verdict,
};
let verdict = classify_gc(state, force, superseded_match, dry_run);
if dry_run {
match verdict {
GcVerdict::Reap(plan) => {
let basis = if !branch_exists {
"already-certified (branch gone)".to_owned()
} else if 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)")
};
writeln!(
io::stdout(),
"{fork}: {basis} — would reap ({})",
reap_targets(plan)
)?;
}
GcVerdict::Refuse(GcRefusal::NotLanded) => {
writeln!(
io::stdout(),
"{fork}: not-landed — `--force` to reap, or `--superseded-head <SHA>` if spent-and-abandoned. If you squash-merged, re-land via `worktree land` (--no-ff)."
)?;
}
}
return Ok(());
}
let plan = match verdict {
GcVerdict::Refuse(GcRefusal::NotLanded) => bail!(
"gc-refused: {} — fork {fork} has not provably landed; `--force` to reap, or `--superseded-head <SHA>` to assert it is spent-and-abandoned. Cannot certify a squash-merge — re-land via `worktree land` (--no-ff), or `--force` knowingly.",
GcRefusal::NotLanded.token()
),
GcVerdict::Reap(plan) => plan,
};
let mut leftovers: Vec<String> = Vec::new();
if let (true, Some(wt)) = (plan.remove_worktree, fork_wt.as_deref()) {
let removed = git::git_opt(
&root,
&["worktree", "remove", "--force", &wt.to_string_lossy()],
)?;
if removed.is_none() {
drop(git::git_opt(&root, &["worktree", "prune"]));
if wt.exists() {
leftovers.push(format!("worktree {}", wt.display()));
}
}
}
if plan.delete_branch {
let deleted = git::git_opt(&root, &["branch", "-D", fork])?;
if deleted.is_none()
&& git::git_opt(&root, &["rev-parse", "--verify", "--quiet", &branch_ref])?.is_some()
{
leftovers.push(format!("branch {fork}"));
}
}
if !leftovers.is_empty() {
bail!(
"gc-incomplete: leftover(s) need manual cleanup: {}",
leftovers.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!(
io::stdout(),
"gc {fork}: reaped (worktree/branch as present)"
)?;
Ok(())
}