use anyhow::{Context, Result};
use std::path::Path;
use std::process::Command;
use crate::cmd::run::RunArgs;
use crate::types::Task;
pub(crate) fn retry_target(task: &Task) -> Result<(Option<String>, Option<String>)> {
match task.worktree_path.as_ref() {
Some(path) if Path::new(path).is_dir() => {
crate::worktree::ensure_consumed_worktree_path_is_isolated(
task.repo_path.as_deref(),
path,
&format!("recorded worktree path for task {}", task.id),
)?;
ensure_live_worktree_branch(task, path)?;
Ok((Some(path.clone()), task.worktree_branch.clone()))
}
Some(path) if task.worktree_branch.is_none() => {
anyhow::bail!(
"cannot retry task {}: recorded worktree path {} is missing and no worktree_branch was recorded; refusing to run in repo root",
task.id,
path
)
}
Some(_) => Ok((None, task.worktree_branch.clone())),
None => Ok((None, task.worktree_branch.clone())),
}
}
pub(crate) fn apply_retry_target(task: &Task, retry_args: &mut RunArgs) -> Result<()> {
if retry_args.repo.is_none() {
retry_args.repo = task.repo_path.clone();
}
let (dir, worktree) = retry_target(task)?;
if let Some(dir) = dir {
if retry_args.repo.is_none() {
retry_args.repo = find_repo_dir(task, retry_args)?;
}
if retry_args.repo.is_none() {
anyhow::bail!(
"cannot retry task {}: legacy task metadata has no repo_path, so its live worktree cannot be anchored to a repository",
task.id
);
}
retry_args.dir = Some(dir);
retry_args.worktree = worktree
.or_else(|| task.worktree_branch.clone())
.or_else(|| retry_args.worktree.clone());
return Ok(());
}
if let Some(branch) = worktree.clone().or_else(|| task.worktree_branch.clone()) {
let repo = recover_repo_dir(task, retry_args, &branch)?;
ensure_branch_exists(&repo, &branch, task)?;
retry_args.repo = Some(repo.clone());
retry_args.dir = Some(repo);
retry_args.worktree = Some(branch.clone());
if retry_args.base_branch.is_none() {
retry_args.base_branch = Some(branch);
}
return Ok(());
}
retry_args.dir = existing_retry_dir(task, retry_args).or_else(|| repo_dir(task));
retry_args.worktree = worktree
.or_else(|| task.worktree_branch.clone())
.or_else(|| retry_args.worktree.clone());
if retry_args.dir.is_none() {
anyhow::bail!(
"cannot retry task {}: no usable worktree path, retry dir, or repo path",
task.id
);
}
Ok(())
}
fn recover_repo_dir(task: &Task, retry_args: &RunArgs, branch: &str) -> Result<String> {
if let Some(repo) = find_repo_dir(task, retry_args)? {
return Ok(repo);
}
anyhow::bail!(
"cannot retry task {} on branch {}: no recoverable repo_path for missing worktree; refusing to run in repo root",
task.id,
branch
)
}
fn find_repo_dir(task: &Task, retry_args: &RunArgs) -> Result<Option<String>> {
for candidate in [task.repo_path.as_deref(), retry_args.repo.as_deref()].into_iter().flatten() {
if let Some(repo) = preserved_repo_root(candidate)? {
return Ok(Some(repo));
}
}
for candidate in [retry_args.dir.as_deref(), task.worktree_path.as_deref()].into_iter().flatten() {
if let Some(repo) = main_worktree_root(candidate)? {
return Ok(Some(repo));
}
}
Ok(None)
}
fn preserved_repo_root(path: &str) -> Result<Option<String>> {
if !Path::new(path).is_dir() {
return Ok(None);
}
let Some(root) = git_root(path)? else {
return Ok(None);
};
if same_existing_path(path, &root)? {
return Ok(Some(path.to_string()));
}
Ok(Some(root))
}
fn same_existing_path(left: &str, right: &str) -> Result<bool> {
let left = Path::new(left)
.canonicalize()
.with_context(|| format!("failed to canonicalize {left}"))?;
let right = Path::new(right)
.canonicalize()
.with_context(|| format!("failed to canonicalize {right}"))?;
Ok(left == right)
}
fn main_worktree_root(path: &str) -> Result<Option<String>> {
if !Path::new(path).is_dir() {
return Ok(None);
}
if let Some(repo) = worktree_list_root(path)? {
return Ok(Some(repo));
}
git_root(path)
}
fn worktree_list_root(path: &str) -> Result<Option<String>> {
let output = Command::new("git")
.args(["-C", path, "worktree", "list", "--porcelain"])
.output()
.with_context(|| format!("Failed to inspect git worktrees at {path}"))?;
if !output.status.success() {
return Ok(None);
}
for line in String::from_utf8_lossy(&output.stdout).lines() {
let Some(root) = line.strip_prefix("worktree ") else {
continue;
};
return git_root(root.trim());
}
Ok(None)
}
fn repo_dir(task: &Task) -> Option<String> {
let repo_path = task.repo_path.as_ref()?;
Path::new(repo_path).is_dir().then(|| repo_path.clone())
}
fn existing_retry_dir(task: &Task, retry_args: &RunArgs) -> Option<String> {
let dir = retry_args.dir.as_ref()?;
let dir_path = Path::new(dir);
if !dir_path.is_dir() {
return None;
}
let Some(repo_path) = task.repo_path.as_deref() else {
return Some(dir.clone());
};
let repo_path = Path::new(repo_path);
let Ok(dir_path) = dir_path.canonicalize() else {
return None;
};
let Ok(repo_path) = repo_path.canonicalize() else {
return None;
};
dir_path.starts_with(repo_path).then(|| dir.clone())
}
fn git_root(path: &str) -> Result<Option<String>> {
if !Path::new(path).is_dir() {
return Ok(None);
}
let output = Command::new("git")
.args(["-C", path, "rev-parse", "--show-toplevel"])
.output()
.with_context(|| format!("Failed to inspect git repo at {path}"))?;
if !output.status.success() {
return Ok(None);
}
Ok(Some(String::from_utf8_lossy(&output.stdout).trim().to_string()))
}
fn ensure_branch_exists(repo: &str, branch: &str, task: &Task) -> Result<()> {
let status = Command::new("git")
.args(["-C", repo, "rev-parse", "--verify", &format!("refs/heads/{branch}")])
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.status()
.with_context(|| format!("Failed to inspect branch {branch}"))?;
if status.success() {
return Ok(());
}
anyhow::bail!(
"cannot retry task {} on branch {}: recorded worktree is missing and branch does not exist in {}; refusing to run in repo root",
task.id,
branch,
repo
)
}
fn ensure_live_worktree_branch(task: &Task, path: &str) -> Result<()> {
let Some(branch) = task.worktree_branch.as_deref() else {
return Ok(());
};
let current = current_branch(path)?;
if current.as_deref() == Some(branch) {
return Ok(());
}
let observed = current.as_deref().unwrap_or("(detached HEAD)");
anyhow::bail!(
"cannot retry task {}: recorded worktree path {} is on branch {} but expected {}; refusing to run in the wrong worktree",
task.id,
path,
observed,
branch
)
}
fn current_branch(path: &str) -> Result<Option<String>> {
let output = Command::new("git")
.args(["-C", path, "symbolic-ref", "--short", "-q", "HEAD"])
.output()
.with_context(|| format!("Failed to inspect worktree branch at {path}"))?;
if !output.status.success() {
return Ok(None);
}
let branch = String::from_utf8_lossy(&output.stdout).trim().to_string();
Ok((!branch.is_empty()).then_some(branch))
}