use std::path::Path;
use serde::Serialize;
use super::worktree_adopt::LockEvidence;
use super::worktree_safety::{count_dirty_files, git_stdout, is_worktree_root};
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
#[serde(tag = "outcome", rename_all = "snake_case")]
pub(crate) enum BranchRelease {
Released { branch: String },
AlreadyDetached,
Kept { reason: String },
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
#[serde(tag = "outcome", rename_all = "snake_case")]
pub(crate) enum LockRelease {
Cleared { pid: u32 },
LeftRunning { pid: u32 },
Failed { pid: u32, reason: String },
NotCleared,
}
pub(crate) fn release_branch_if_clean(path: &Path) -> BranchRelease {
let kept = |reason: String| BranchRelease::Kept { reason };
match is_worktree_root(path) {
Ok(true) => {}
Ok(false) => {
return kept(format!(
"{} is not the root of a git worktree, so its cleanliness cannot be checked",
path.display()
));
}
Err(e) => return kept(format!("the clean check could not complete: {e}")),
}
match is_linked_worktree(path) {
Ok(true) => {}
Ok(false) => {
return kept(format!(
"{} is the repository's main checkout, not a linked worktree; its branch is \
never released",
path.display()
));
}
Err(e) => {
return kept(format!(
"could not tell a linked worktree from the main one: {e}"
));
}
}
let dirty = match count_dirty_files(path) {
Ok(n) => n,
Err(e) => return kept(format!("the clean check could not complete: {e}")),
};
if dirty > 0 {
return kept(format!(
"the tree holds {dirty} uncommitted, staged or untracked file(s). Commit or discard \
them, then free the branch with `git -C {} switch --detach`",
path.display()
));
}
let head = match git_stdout(path, &["rev-parse", "--abbrev-ref", "HEAD"]) {
Ok(out) => out.trim().to_string(),
Err(e) => return kept(format!("could not read the checked-out branch: {e}")),
};
if head == "HEAD" {
return BranchRelease::AlreadyDetached;
}
match git_stdout(path, &["switch", "--detach", "--quiet"]) {
Ok(_) => BranchRelease::Released { branch: head },
Err(e) => kept(format!("detaching HEAD failed: {e}")),
}
}
fn is_linked_worktree(path: &Path) -> Result<bool, String> {
let out = git_stdout(path, &["rev-parse", "--git-dir", "--git-common-dir"])?;
let mut dirs = out.lines().map(|l| {
let p = path.join(l.trim());
std::fs::canonicalize(&p).map_err(|e| format!("cannot resolve {}: {e}", p.display()))
});
match (dirs.next(), dirs.next()) {
(Some(git_dir), Some(common)) => Ok(git_dir? != common?),
_ => Err(format!("`git rev-parse` named no git directory: {out:?}")),
}
}
pub(crate) fn clear_dead_agent_lock(path: &Path, evidence: LockEvidence) -> LockRelease {
match evidence {
LockEvidence::HolderPidGone(pid) => {
let target = path.to_string_lossy();
match git_stdout(path, &["worktree", "unlock", target.as_ref()]) {
Ok(_) => LockRelease::Cleared { pid },
Err(reason) => LockRelease::Failed { pid, reason },
}
}
LockEvidence::HolderPidRunning(pid) => LockRelease::LeftRunning { pid },
LockEvidence::Silent => LockRelease::NotCleared,
}
}
#[cfg(test)]
#[path = "worktree_adopt_release_tests.rs"]
mod worktree_adopt_release_tests;