use crate::cli::output::{print_success, print_warning};
use crate::core::config::Config;
use crate::infra::git::{
git_checkout, git_delete_remote_branch, git_get_remotes, git_is_clean, git_merge,
git_remote_branch_exists, resolve_git_common_dir, resolve_repo_name, resolve_toplevel,
};
use crate::infra::hook::{run_post_finish_hook, run_pre_finish_hook};
use crate::infra::lock::SlotLock;
use crate::infra::registry::SlotRegistry;
use crate::infra::worktree::{delete_branch, remove_worktree};
use crate::workflow::error::WorkflowError;
use std::collections::BTreeMap;
use std::path::{Path, PathBuf};
#[derive(Debug, Clone, Default)]
pub struct FinishOptions<'a> {
pub name: Option<&'a str>,
pub force: bool,
pub keep_remote: bool,
pub repo_root: Option<&'a Path>,
}
pub fn finish_worktree(options: FinishOptions<'_>) -> Result<(), WorkflowError> {
let toplevel = resolve_toplevel(options.repo_root)?;
let common_dir = resolve_git_common_dir(Some(&toplevel))?;
let repo_name = resolve_repo_name(&toplevel);
let config_path = toplevel.join(".git-claw.toml");
let config = Config::load_or_default(&config_path, &repo_name)?;
let registry_path = common_dir.join("claw/slots.json");
let lock_path = common_dir.join("claw/slots.lock");
let _lock = SlotLock::acquire(&lock_path)?;
let mut registry = SlotRegistry::load_or_empty(®istry_path)?;
let target_name = match options.name {
Some(n) => n.to_string(),
None => {
let cwd = std::env::current_dir().unwrap_or_default();
let canonical_cwd = cwd.canonicalize().unwrap_or(cwd);
let found = registry.slots.iter().find(|s| {
let p = PathBuf::from(&s.path);
p.canonicalize().unwrap_or(p) == canonical_cwd
});
match found {
Some(s) => s.name.clone(),
None => {
return Err(WorkflowError::CannotInferWorktree(
canonical_cwd.to_string_lossy().to_string(),
));
}
}
}
};
let slot = registry
.find_by_name(&target_name)
.cloned()
.ok_or_else(|| WorkflowError::WorktreeNotFound(target_name.clone()))?;
let worktree_path = PathBuf::from(&slot.path);
let mut hook_env = BTreeMap::new();
hook_env.insert("CLAW_SLOT_ID".to_string(), slot.id.to_string());
hook_env.insert("CLAW_WORKTREE_NAME".to_string(), slot.name.clone());
hook_env.insert("CLAW_WORKTREE_PATH".to_string(), slot.path.clone());
run_pre_finish_hook(
config.hooks.pre_finish.as_deref(),
&worktree_path,
&hook_env,
options.force,
)?;
if !git_is_clean(&toplevel)? {
return Err(WorkflowError::DirtyWorkingTree);
}
git_checkout(&toplevel, config.main_branch())?;
git_merge(&toplevel, &slot.branch)?;
remove_worktree(&toplevel, &worktree_path, true)?;
delete_branch(&toplevel, &slot.branch, false)?;
if !options.keep_remote {
let remote = config.remote().unwrap_or("origin");
let remotes = git_get_remotes(&toplevel).unwrap_or_default();
if remotes.iter().any(|r| r == remote) {
match git_remote_branch_exists(&toplevel, remote, &slot.branch) {
Ok(true) => {
if let Err(e) = git_delete_remote_branch(&toplevel, remote, &slot.branch) {
print_warning(format!(
"Failed to delete remote branch '{}' from '{}': {}",
slot.branch, remote, e
));
} else {
print_success(format!(
"Remote branch '{}' deleted from '{}'",
slot.branch, remote
));
}
}
Ok(false) => {}
Err(e) => {
print_warning(format!(
"Could not check remote branch '{}' on '{}': {}",
slot.branch, remote, e
));
}
}
}
}
registry.remove_by_id(slot.id);
registry.save_atomic(®istry_path)?;
drop(_lock);
let _ = run_post_finish_hook(config.hooks.post_finish.as_deref(), &toplevel, &hook_env);
print_success(format!(
"Worktree '{}' (Slot {}) successfully merged into '{}' and cleaned up",
slot.name,
slot.id,
config.main_branch()
));
Ok(())
}