use std::path::{Path, PathBuf};
use trusty_mpm::core::push_guard::{
GuardState, HookInstall, effective_hooks_dir, inspect_pre_push_guard, install_pre_push_guard,
};
pub(crate) fn repair_push_guard(path: Option<String>, dry_run: bool) -> anyhow::Result<()> {
let target = resolve_target(path.as_deref()).map_err(|e| anyhow::anyhow!(e))?;
println!("Repository: {}", target.display());
match effective_hooks_dir(&target) {
Ok(dir) => println!(
"Hooks directory: {} (shared by {} worktree(s) of this clone)",
dir.display(),
worktree_count(&target)
),
Err(reason) => println!("Hooks directory: unresolved — {reason}"),
}
if dry_run {
report_dry_run(&target);
return Ok(());
}
match install_pre_push_guard(&target).map_err(|e| anyhow::anyhow!(e))? {
HookInstall::Installed(p) => {
println!("INSTALLED cross-branch push guard → {}", p.display());
println!(
"Every worktree of this clone is now covered. A deliberate cross-branch push \
still works with TM_ALLOW_CROSS_BRANCH_PUSH=1."
);
Ok(())
}
HookInstall::AlreadyCurrent(p) => {
println!("UP TO DATE guard already current at {}", p.display());
Ok(())
}
HookInstall::Refused(reason) => Err(anyhow::anyhow!(
"REFUSED — nothing was written: {reason}. trusty-mpm never overwrites a pre-push \
hook it does not own; resolve the conflict by hand (e.g. chain the guard from \
your own hook), then re-run."
)),
}
}
fn report_dry_run(target: &Path) {
match inspect_pre_push_guard(target) {
GuardState::Missing(p) => println!("WOULD INSTALL {} (no hook present)", p.display()),
GuardState::Outdated(p) => {
println!("WOULD UPGRADE {} (older guard revision)", p.display());
}
GuardState::Current(p) => println!("WOULD SKIP {} (already current)", p.display()),
GuardState::Foreign(reason) => println!("WOULD REFUSE {reason}"),
}
println!("(dry run — nothing was written)");
}
fn resolve_target(path: Option<&str>) -> Result<PathBuf, String> {
let start = match path {
Some(p) => PathBuf::from(p),
None => {
std::env::current_dir().map_err(|e| format!("cannot read current directory: {e}"))?
}
};
if !start.exists() {
return Err(format!("{} does not exist", start.display()));
}
if let Some(top) = git_rev_parse(&start, "--show-toplevel") {
return Ok(PathBuf::from(top));
}
if let Some(common) = git_rev_parse(&start, "--git-common-dir") {
return Ok(PathBuf::from(common));
}
Err(format!(
"{} is not inside a git repository",
start.display()
))
}
fn git_rev_parse(start: &Path, flag: &str) -> Option<String> {
let out = std::process::Command::new("git")
.arg("-C")
.arg(start)
.args(["rev-parse", "--path-format=absolute", flag])
.output()
.ok()?;
if !out.status.success() {
return None;
}
let value = String::from_utf8_lossy(&out.stdout).trim().to_string();
if value.is_empty() { None } else { Some(value) }
}
fn worktree_count(repo: &Path) -> String {
let Ok(out) = std::process::Command::new("git")
.arg("-C")
.arg(repo)
.args(["worktree", "list", "--porcelain"])
.output()
else {
return "?".to_string();
};
if !out.status.success() {
return "?".to_string();
}
String::from_utf8_lossy(&out.stdout)
.lines()
.filter(|l| l.starts_with("worktree "))
.count()
.to_string()
}
#[cfg(test)]
#[path = "push_guard_tests.rs"]
mod tests;