git_claw/workflow/
finish.rs1use crate::cli::output::{print_success, print_warning};
4use crate::core::config::Config;
5use crate::infra::git::{
6 git_checkout, git_delete_remote_branch, git_get_remotes, git_is_clean, git_merge,
7 git_remote_branch_exists, resolve_git_common_dir, resolve_primary_repo_root, resolve_repo_name,
8};
9use crate::infra::hook::{run_post_finish_hook, run_pre_finish_hook};
10use crate::infra::lock::SlotLock;
11use crate::infra::registry::SlotRegistry;
12use crate::infra::worktree::{delete_branch, remove_worktree};
13use crate::workflow::error::WorkflowError;
14use std::collections::BTreeMap;
15use std::path::{Path, PathBuf};
16
17#[derive(Debug, Clone, Default)]
34pub struct FinishOptions<'a> {
35 pub name: Option<&'a str>,
36 pub force: bool,
37 pub keep_remote: bool,
38 pub repo_root: Option<&'a Path>,
39}
40
41pub fn finish_worktree(options: FinishOptions<'_>) -> Result<(), WorkflowError> {
60 let toplevel = resolve_primary_repo_root(options.repo_root)?;
61 let common_dir = resolve_git_common_dir(Some(&toplevel))?;
62 let repo_name = resolve_repo_name(&toplevel);
63
64 let config_path = toplevel.join(".git-claw.toml");
65 let config = Config::load_or_default(&config_path, &repo_name)?;
66
67 let registry_path = common_dir.join("claw/slots.json");
68 let lock_path = common_dir.join("claw/slots.lock");
69
70 let _lock = SlotLock::acquire(&lock_path)?;
71 let mut registry = SlotRegistry::load_or_empty(®istry_path)?;
72
73 let target_name = match options.name {
75 Some(n) => n.to_string(),
76 None => {
77 let cwd = options
78 .repo_root
79 .map(|p| p.to_path_buf())
80 .unwrap_or_else(|| std::env::current_dir().unwrap_or_default());
81 let canonical_cwd = cwd.canonicalize().unwrap_or(cwd);
82 let found = registry.slots.iter().find(|s| {
83 let p = PathBuf::from(&s.path);
84 let canonical_p = p.canonicalize().unwrap_or(p);
85 canonical_cwd == canonical_p || canonical_cwd.starts_with(&canonical_p)
86 });
87
88 match found {
89 Some(s) => s.name.clone(),
90 None => {
91 return Err(WorkflowError::CannotInferWorktree(
92 canonical_cwd.to_string_lossy().to_string(),
93 ));
94 }
95 }
96 }
97 };
98
99 let slot = registry
100 .find_by_name(&target_name)
101 .cloned()
102 .ok_or_else(|| WorkflowError::WorktreeNotFound(target_name.clone()))?;
103
104 let worktree_path = PathBuf::from(&slot.path);
105
106 let mut hook_env = BTreeMap::new();
108 hook_env.insert("CLAW_SLOT_ID".to_string(), slot.id.to_string());
109 hook_env.insert("CLAW_WORKTREE_NAME".to_string(), slot.name.clone());
110 hook_env.insert("CLAW_WORKTREE_PATH".to_string(), slot.path.clone());
111
112 run_pre_finish_hook(
114 config.hooks.pre_finish.as_deref(),
115 &worktree_path,
116 &hook_env,
117 options.force,
118 )?;
119
120 if !git_is_clean(&toplevel)? {
122 return Err(WorkflowError::DirtyWorkingTree);
123 }
124
125 git_checkout(&toplevel, config.main_branch())?;
127 git_merge(&toplevel, &slot.branch)?;
128
129 remove_worktree(&toplevel, &worktree_path, true)?;
131
132 delete_branch(&toplevel, &slot.branch, false)?;
134
135 if !options.keep_remote {
137 let remote = config.remote().unwrap_or("origin");
138 let remotes = git_get_remotes(&toplevel).unwrap_or_default();
139 if remotes.iter().any(|r| r == remote) {
140 match git_remote_branch_exists(&toplevel, remote, &slot.branch) {
141 Ok(true) => {
142 if let Err(e) = git_delete_remote_branch(&toplevel, remote, &slot.branch) {
143 print_warning(format!(
144 "Failed to delete remote branch '{}' from '{}': {}",
145 slot.branch, remote, e
146 ));
147 } else {
148 print_success(format!(
149 "Remote branch '{}' deleted from '{}'",
150 slot.branch, remote
151 ));
152 }
153 }
154 Ok(false) => {}
155 Err(e) => {
156 print_warning(format!(
157 "Could not check remote branch '{}' on '{}': {}",
158 slot.branch, remote, e
159 ));
160 }
161 }
162 }
163 }
164
165 registry.remove_by_id(slot.id);
167 registry.save_atomic(®istry_path)?;
168
169 drop(_lock);
171
172 let _ = run_post_finish_hook(config.hooks.post_finish.as_deref(), &toplevel, &hook_env);
174
175 let _ = std::fs::write(
177 "/tmp/.git_claw_target_cd",
178 toplevel.to_string_lossy().as_bytes(),
179 );
180
181 print_success(format!(
182 "Worktree '{}' (Slot {}) successfully merged into '{}' and cleaned up",
183 slot.name,
184 slot.id,
185 config.main_branch()
186 ));
187
188 Ok(())
189}