use camino::Utf8Path;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Deletion {
Deleted,
ConfigSurvived {
detail: String,
},
Refused {
detail: String,
},
}
#[must_use]
pub fn delete_branch(target: &Utf8Path, branch: &str, verified_tip: &str) -> Deletion {
let ref_name = format!("refs/heads/{branch}");
let deleted = match git(target, &["update-ref", "-d", &ref_name, verified_tip]) {
Ok(output) => output,
Err(detail) => return Deletion::Refused { detail },
};
if !deleted.status.success() {
return Deletion::Refused {
detail: last_line(&deleted.stderr),
};
}
let section = format!("branch.{branch}");
let survives = match git(target, &["config", "--remove-section", §ion]) {
Ok(removed) if removed.status.success() => false,
Ok(_) => {
let prefix = format!("branch.{branch}.");
match git(target, &["config", "--get-regexp", "^branch\\."]) {
Ok(leftover) => {
leftover.status.success()
&& String::from_utf8_lossy(&leftover.stdout)
.lines()
.any(|line| line.starts_with(&prefix))
}
Err(_) => true,
}
}
Err(_) => true,
};
if survives {
return Deletion::ConfigSurvived {
detail: format!(
"the branch configuration survives: git config --remove-section branch.{branch}"
),
};
}
Deletion::Deleted
}
#[must_use]
pub fn row_owes(status: &str, detail: Option<&str>) -> bool {
match status {
"deleted" | "pruned" => detail.is_some(),
_ => true,
}
}
pub(crate) const GIT_HOOK_VARS: [&str; 4] = [
"GIT_DIR",
"GIT_WORK_TREE",
"GIT_INDEX_FILE",
"GIT_COMMON_DIR",
];
fn git(target: &Utf8Path, args: &[&str]) -> Result<std::process::Output, String> {
let mut command = std::process::Command::new(crate::probes::git_bin());
for var in GIT_HOOK_VARS {
command.env_remove(var);
}
command
.arg("-C")
.arg(target.as_std_path())
.args(args)
.output()
.map_err(|source| format!("git did not run: {source}"))
}
#[must_use]
pub fn integration_ledger(target: &Utf8Path, trunk: &str) -> crate::integrate::Ledger {
let Some(path) = ledger_path(target) else {
return crate::integrate::Ledger::default();
};
let mut ledger: crate::integrate::Ledger = std::fs::read_to_string(path)
.ok()
.and_then(|text| crate::integrate::Ledger::parse(&text).ok())
.unwrap_or_default();
ledger
.entries
.retain(|entry| trunk_reaches(target, trunk, &entry.trunk_commit));
ledger
}
fn trunk_reaches(target: &Utf8Path, trunk: &str, commit: &str) -> bool {
git(target, &["merge-base", "--is-ancestor", commit, trunk])
.is_ok_and(|answer| answer.status.success())
}
pub fn forget_integration(target: &Utf8Path, branch: &str) {
let Some(path) = ledger_path(target) else {
return;
};
let Ok(text) = std::fs::read_to_string(&path) else {
return;
};
let Ok(mut ledger) = crate::integrate::Ledger::parse(&text) else {
return;
};
if !ledger.names(branch) {
return;
}
ledger.forget(branch);
if let Ok(rendered) = ledger.render() {
let _ = crate::atomic::write(&path, rendered.as_bytes());
}
}
fn ledger_path(target: &Utf8Path) -> Option<std::path::PathBuf> {
let answer = git(
target,
&["rev-parse", "--path-format=absolute", "--git-common-dir"],
)
.ok()
.filter(|answer| answer.status.success())?;
let dir = String::from_utf8_lossy(&answer.stdout).trim().to_owned();
Some(std::path::Path::new(&dir).join(crate::integrate::LEDGER_PATH))
}
pub(crate) fn last_line(bytes: &[u8]) -> String {
String::from_utf8_lossy(bytes)
.lines()
.rev()
.find(|line| !line.trim().is_empty())
.unwrap_or("no output")
.to_owned()
}
#[cfg(test)]
mod tests {
use super::row_owes;
#[test]
fn a_row_owes_until_nothing_is_left_to_ask() {
for status in [
"candidate",
"kept",
"stale",
"confirmed",
"unconfirmed",
"unknown",
"worktree-bound",
"delete-failed",
"remove-failed",
"branch-delete-failed",
] {
assert!(row_owes(status, None), "{status} names a move");
assert!(row_owes(status, Some("detail")), "{status} names a move");
}
for finished in ["deleted", "pruned"] {
assert!(
row_owes(finished, Some("the branch configuration survives")),
"surviving residue is still owed"
);
assert!(!row_owes(finished, None), "done is done");
}
}
}