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("git");
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}"))
}
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");
}
}
}