use std::cell::OnceCell;
use std::path::Path;
use crate::core::pr_cleanup::plan::{holder_of, kept_branch, parse_worktree_list};
use crate::core::worktree_landed_content::{LandedContent, LandingAdmission};
use super::worktree_reclaim::BranchPrState;
use super::worktree_reclaim_landed::{LandedContentProbe, PUBLISHED_BASE, ReclaimProof};
use super::worktree_reclaim_verdict::{ReclaimGate, ReclaimVerdict};
use super::worktree_safety::{git_command, git_stdout};
pub(super) fn own_pr_gate(
path: &Path,
verdict: ReclaimVerdict,
by_name: &BranchPrState,
landed_content: LandedContentProbe<'_>,
) -> ReclaimVerdict {
let Some(ask) = landed_content else {
return verdict;
};
if !verdict.is_reclaimable() || matches!(by_name, BranchPrState::Merged { .. }) {
return verdict;
}
if let ReclaimVerdict::ReclaimableLandedContent { base } = &verdict
&& base != PUBLISHED_BASE
{
return verdict;
}
let content = ask(path).content;
match own_pr_refusal(by_name, &content) {
None => landed_grant(&content).unwrap_or(verdict),
Some(reason) => ReclaimVerdict::blocked(
ReclaimGate::PrState,
format!(
"{reason}; the earlier grant ({}) was withdrawn",
verdict.decision()
),
),
}
}
pub(super) fn own_pr_refusal(by_name: &BranchPrState, content: &LandedContent) -> Option<String> {
if matches!(by_name, BranchPrState::Merged { .. }) || content.is_landed() {
return None;
}
Some(format!(
"no merged pull request carries this branch's own name ({}), so only content already \
on the base can admit it, and {} (#8109)",
describe(by_name),
content.note()
))
}
fn landed_grant(content: &LandedContent) -> Option<ReclaimVerdict> {
match content {
LandedContent::Landed { base, .. } => {
Some(ReclaimVerdict::ReclaimableLandedContent { base: base.clone() })
}
_ => None,
}
}
fn describe(state: &BranchPrState) -> String {
match state {
BranchPrState::NoPr => "no pull request was found".to_string(),
BranchPrState::Unknown => "the pull-request state could not be determined".to_string(),
BranchPrState::LookupFailed { reason } => format!("the lookup failed: {reason}"),
BranchPrState::Open { pr } => format!("PR #{pr} is open"),
BranchPrState::ClosedUnmerged { pr } => format!("PR #{pr} closed unmerged"),
BranchPrState::Merged { pr } => format!("PR #{pr} merged"),
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum BranchCleanup {
Deleted,
Kept(String),
Absent,
}
pub(crate) fn delete_reclaimed_branch(
repo_root: &Path,
branch: &str,
proven_head: &str,
) -> BranchCleanup {
let proven = proven_head.trim();
if !is_full_object_name(proven) {
return BranchCleanup::Kept(format!(
"the proven HEAD `{proven}` is not a full object name, so no compare-and-delete \
can rest on it"
));
}
let listing = match git_stdout(repo_root, &["worktree", "list", "--porcelain"]) {
Ok(listing) => listing,
Err(e) => return BranchCleanup::Kept(format!("the worktree listing failed: {e}")),
};
let standing = parse_worktree_list(&listing);
if standing.is_empty() {
return BranchCleanup::Kept("the worktree listing named no tree".to_string());
}
if let Some(holder) = holder_of(&standing, branch) {
return BranchCleanup::Kept(kept_branch(branch, holder));
}
let tip_ref = format!("refs/heads/{branch}");
let lookup = git_command(repo_root, &["rev-parse", "--verify", "--quiet", &tip_ref]).output();
match lookup {
Err(e) => return BranchCleanup::Kept(format!("the tip lookup could not run: {e}")),
Ok(out) if !out.status.success() => {
if out.status.code() == Some(1) && out.stdout.is_empty() && out.stderr.is_empty() {
return BranchCleanup::Absent;
}
return BranchCleanup::Kept(format!(
"the tip lookup failed ({}): {}",
out.status,
String::from_utf8_lossy(&out.stderr).trim()
));
}
Ok(_) => {}
}
match git_stdout(
repo_root,
&["update-ref", "--no-deref", "-d", &tip_ref, proven],
) {
Ok(_) => {
let section = format!("branch.{branch}");
if let Err(e) = git_stdout(repo_root, &["config", "--remove-section", §ion]) {
tracing::debug!(branch, "worktree-reclaim: no branch config removed — {e}");
}
BranchCleanup::Deleted
}
Err(e) => BranchCleanup::Kept(format!(
"the compare-and-delete at the proven `{proven}` was refused — its tip moved, or \
the ref could not be locked: {e}"
)),
}
}
fn is_full_object_name(sha: &str) -> bool {
matches!(sha.len(), 40 | 64) && sha.bytes().all(|b| b.is_ascii_hexdigit())
}
pub(super) struct OnceProof<'a> {
path: &'a Path,
prove: &'a dyn Fn(&Path) -> ReclaimProof,
taken: OnceCell<ReclaimProof>,
}
impl<'a> OnceProof<'a> {
pub(super) fn new(path: &'a Path, prove: &'a dyn Fn(&Path) -> ReclaimProof) -> Self {
Self {
path,
prove,
taken: OnceCell::new(),
}
}
pub(super) fn get(&self) -> &ReclaimProof {
self.taken.get_or_init(|| (self.prove)(self.path))
}
pub(super) fn admission(&self, asked: &Path) -> LandingAdmission {
debug_assert_eq!(asked, self.path, "one OnceProof judges one tree");
self.get().admission.clone()
}
}
pub(super) fn deletion_proof(
by_name: &BranchPrState,
proof: &OnceProof<'_>,
) -> Result<ReclaimProof, String> {
let taken = proof.get();
match own_pr_refusal(by_name, &taken.admission.content) {
Some(reason) => Err(reason),
None => Ok(taken.clone()),
}
}
pub(super) fn cleanup_reclaimed_branch(
repo_root: &Path,
path: &Path,
branch: Option<&str>,
proof: &ReclaimProof,
) {
let Some(branch) = branch else {
return;
};
let (Some(head), LandedContent::Landed { base, .. }) =
(proof.head.as_deref(), &proof.admission.content)
else {
tracing::info!(
path = %path.display(),
branch,
"worktree-reclaim: kept the reclaimed worktree's branch — {} (#8109)",
proof.admission.content.note()
);
return;
};
match delete_reclaimed_branch(repo_root, branch, head) {
BranchCleanup::Deleted => tracing::info!(
path = %path.display(),
branch,
head,
"worktree-reclaim: deleted the reclaimed worktree's branch — its content is \
already on {base} (#8109)"
),
BranchCleanup::Kept(why) => tracing::warn!(
path = %path.display(),
branch,
"worktree-reclaim: kept the reclaimed worktree's branch — {why} (#8109)"
),
BranchCleanup::Absent => tracing::debug!(
path = %path.display(),
branch,
"worktree-reclaim: the reclaimed worktree's branch is already gone (#8109)"
),
}
}
#[cfg(test)]
#[path = "worktree_reclaim_branch_tests.rs"]
mod worktree_reclaim_branch_tests;