1use std::path::{Path, PathBuf};
4
5use console::style;
6
7use crate::constants::{
8 default_worktree_path, format_config_key, CONFIG_KEY_BASE_BRANCH, CONFIG_KEY_BASE_PATH,
9 CONFIG_KEY_INTENDED_BRANCH,
10};
11use crate::error::{CwError, Result};
12use crate::git;
13use crate::shared_files;
14
15use crate::messages;
16
17pub fn create_worktree(
19 branch_name: &str,
20 base_branch: Option<&str>,
21 path: Option<&str>,
22 no_ai: bool,
23 initial_prompt: Option<&str>,
24 term_override: Option<&str>,
25) -> Result<PathBuf> {
26 let repo = git::get_repo_root(None)?;
27
28 if !git::is_valid_branch_name(branch_name, Some(&repo)) {
30 let error_msg = git::get_branch_name_error(branch_name);
31 return Err(CwError::InvalidBranch(messages::invalid_branch_name(
32 &error_msg,
33 )));
34 }
35
36 let existing = git::find_worktree_by_branch(&repo, branch_name)?.or(
38 git::find_worktree_by_branch(&repo, &format!("refs/heads/{}", branch_name))?,
39 );
40
41 if let Some(existing_path) = existing {
42 println!(
43 "\n{}\nBranch '{}' already has a worktree at:\n {}\n",
44 style("! Worktree already exists").yellow().bold(),
45 style(branch_name).cyan(),
46 style(existing_path.display()).blue(),
47 );
48
49 if git::is_non_interactive() {
50 return Err(CwError::InvalidBranch(format!(
51 "Worktree for branch '{}' already exists at {}.\n\
52 Use 'gw resume {}' to continue work.",
53 branch_name,
54 existing_path.display(),
55 branch_name,
56 )));
57 }
58
59 println!(
61 "Use '{}' to resume work in this worktree.\n",
62 style(format!("gw resume {}", branch_name)).cyan()
63 );
64 return Ok(existing_path);
65 }
66
67 let mut branch_already_exists = false;
69 let mut is_remote_only = false;
70
71 if git::branch_exists(branch_name, Some(&repo)) {
72 println!(
73 "\n{}\nBranch '{}' already exists locally but has no worktree.\n",
74 style("! Branch already exists").yellow().bold(),
75 style(branch_name).cyan(),
76 );
77 branch_already_exists = true;
78 } else if git::remote_branch_exists(branch_name, Some(&repo), "origin") {
79 println!(
80 "\n{}\nBranch '{}' exists on remote but not locally.\n",
81 style("! Remote branch found").yellow().bold(),
82 style(branch_name).cyan(),
83 );
84 branch_already_exists = true;
85 is_remote_only = true;
86 }
87
88 let base = if let Some(b) = base_branch {
90 b.to_string()
91 } else {
92 git::detect_default_branch(Some(&repo))
93 };
94
95 if (!is_remote_only || base_branch.is_some()) && !git::branch_exists(&base, Some(&repo)) {
97 return Err(CwError::InvalidBranch(messages::branch_not_found(&base)));
98 }
99
100 let worktree_path = if let Some(p) = path {
102 PathBuf::from(p)
103 .canonicalize()
104 .unwrap_or_else(|_| PathBuf::from(p))
105 } else {
106 default_worktree_path(&repo, branch_name)
107 };
108
109 println!("\n{}", style("Creating new worktree:").cyan().bold());
110 println!(" Base branch: {}", style(&base).green());
111 println!(" New branch: {}", style(branch_name).green());
112 println!(" Path: {}\n", style(worktree_path.display()).blue());
113
114 if let Some(parent) = worktree_path.parent() {
116 let _ = std::fs::create_dir_all(parent);
117 }
118
119 let _ = git::git_command(&["fetch", "--all", "--prune"], Some(&repo), false, false);
121
122 let wt_str = worktree_path.to_string_lossy().to_string();
124 if is_remote_only {
125 git::git_command(
126 &[
127 "worktree",
128 "add",
129 "-b",
130 branch_name,
131 &wt_str,
132 &format!("origin/{}", branch_name),
133 ],
134 Some(&repo),
135 true,
136 false,
137 )?;
138 } else if branch_already_exists {
139 git::git_command(
140 &["worktree", "add", &wt_str, branch_name],
141 Some(&repo),
142 true,
143 false,
144 )?;
145 } else {
146 git::git_command(
147 &["worktree", "add", "-b", branch_name, &wt_str, &base],
148 Some(&repo),
149 true,
150 false,
151 )?;
152 }
153
154 let bb_key = format_config_key(CONFIG_KEY_BASE_BRANCH, branch_name);
156 let bp_key = format_config_key(CONFIG_KEY_BASE_PATH, branch_name);
157 let ib_key = format_config_key(CONFIG_KEY_INTENDED_BRANCH, branch_name);
158 git::set_config(&bb_key, &base, Some(&repo))?;
159 git::set_config(&bp_key, &repo.to_string_lossy(), Some(&repo))?;
160 git::set_config(&ib_key, branch_name, Some(&repo))?;
161
162 println!(
163 "{} Worktree created successfully\n",
164 style("*").green().bold()
165 );
166
167 shared_files::share_files(&repo, &worktree_path);
169
170 crate::hooks::run_event("post_new", &worktree_path)?;
176
177 if !no_ai {
179 let _ = super::ai_tools::spawn_in_worktree(&worktree_path, initial_prompt, term_override);
180 }
181
182 Ok(worktree_path)
183}
184
185#[derive(Debug)]
191pub enum DeletionOutcome {
192 Deleted {
193 branch: Option<String>,
194 path: PathBuf,
195 },
196 Skipped {
197 reason: String,
198 },
199 Failed {
200 error: CwError,
201 },
202}
203
204#[derive(Debug, Clone, Copy)]
206pub struct RmFlags {
207 pub keep_branch: bool,
208 pub delete_remote: bool,
209 pub git_force: bool,
211 pub allow_busy: bool,
213}
214
215pub(crate) fn delete_one(
222 worktree_path: &Path,
223 branch_name: Option<&str>,
224 main_repo: &Path,
225 flags: RmFlags,
226) -> DeletionOutcome {
227 let wt_resolved = git::canonicalize_or(worktree_path);
229 let main_resolved = git::canonicalize_or(main_repo);
230 if wt_resolved == main_resolved {
231 return DeletionOutcome::Failed {
232 error: CwError::Git(messages::cannot_delete_main_worktree()),
233 };
234 }
235
236 if let Ok(cwd) = std::env::current_dir() {
238 let cwd_canon = cwd.canonicalize().unwrap_or(cwd);
239 let wt_canon = worktree_path
240 .canonicalize()
241 .unwrap_or_else(|_| worktree_path.to_path_buf());
242 if cwd_canon.starts_with(&wt_canon) {
243 let _ = std::env::set_current_dir(main_repo);
244 }
245 }
246
247 if let Err(e) = crate::hooks::run_event("pre_rm", worktree_path) {
251 return DeletionOutcome::Failed { error: e };
252 }
253
254 println!(
256 "{}",
257 style(messages::removing_worktree(worktree_path)).yellow()
258 );
259 if let Err(e) = git::remove_worktree_safe(worktree_path, main_repo, flags.git_force) {
260 return DeletionOutcome::Failed { error: e };
261 }
262 println!("{} Worktree removed\n", style("*").green().bold());
263
264 if let Some(branch) = branch_name {
266 if !flags.keep_branch {
267 println!(
268 "{}",
269 style(messages::deleting_local_branch(branch)).yellow()
270 );
271 let _ = git::git_command(&["branch", "-D", branch], Some(main_repo), false, false);
272
273 let bb_key = format_config_key(CONFIG_KEY_BASE_BRANCH, branch);
274 let bp_key = format_config_key(CONFIG_KEY_BASE_PATH, branch);
275 let ib_key = format_config_key(CONFIG_KEY_INTENDED_BRANCH, branch);
276 git::unset_config(&bb_key, Some(main_repo));
277 git::unset_config(&bp_key, Some(main_repo));
278 git::unset_config(&ib_key, Some(main_repo));
279
280 println!(
281 "{} Local branch and metadata removed\n",
282 style("*").green().bold()
283 );
284
285 if flags.delete_remote {
286 println!(
287 "{}",
288 style(messages::deleting_remote_branch(branch)).yellow()
289 );
290 match git::git_command(
291 &["push", "origin", &format!(":{}", branch)],
292 Some(main_repo),
293 false,
294 true,
295 ) {
296 Ok(r) if r.returncode == 0 => {
297 println!("{} Remote branch deleted\n", style("*").green().bold());
298 }
299 _ => {
300 println!("{} Remote branch deletion failed\n", style("!").yellow());
301 }
302 }
303 }
304 }
305 }
306
307 DeletionOutcome::Deleted {
308 branch: branch_name.map(str::to_string),
309 path: worktree_path.to_path_buf(),
310 }
311}
312
313pub fn delete_worktree(
328 target: Option<&str>,
329 keep_branch: bool,
330 delete_remote: bool,
331 force: bool,
332 allow_busy: bool,
333) -> Result<()> {
334 let main_repo = git::get_main_repo_root(None)?;
335 let (worktree_path, branch_name) = resolve_delete_target(target, &main_repo)?;
336
337 let wt_resolved = git::canonicalize_or(&worktree_path);
340 let main_resolved = git::canonicalize_or(&main_repo);
341 if wt_resolved == main_resolved {
342 return Err(CwError::Git(messages::cannot_delete_main_worktree()));
343 }
344
345 if let Ok(cwd) = std::env::current_dir() {
350 let cwd_canon = cwd.canonicalize().unwrap_or(cwd);
351 let wt_canon = worktree_path
352 .canonicalize()
353 .unwrap_or_else(|_| worktree_path.clone());
354 if cwd_canon.starts_with(&wt_canon) {
355 let _ = std::env::set_current_dir(&main_repo);
356 }
357 }
358
359 let (hard, soft) = crate::operations::busy::detect_busy_tiered(&worktree_path);
360 if (!hard.is_empty() || !soft.is_empty()) && !allow_busy {
361 let branch_display = branch_name.clone().unwrap_or_else(|| {
362 worktree_path
363 .file_name()
364 .map(|n| n.to_string_lossy().to_string())
365 .unwrap_or_else(|| worktree_path.to_string_lossy().to_string())
366 });
367 let msg = crate::operations::busy_messages::render_refusal(&branch_display, &hard, &soft);
368 eprint!("{}", msg);
369 return Err(CwError::Other(format!(
370 "worktree '{}' is in use; re-run with --force to override",
371 branch_display
372 )));
373 }
374
375 let flags = RmFlags {
376 keep_branch,
377 delete_remote,
378 git_force: force,
379 allow_busy: true, };
381
382 match delete_one(&worktree_path, branch_name.as_deref(), &main_repo, flags) {
383 DeletionOutcome::Deleted { .. } => Ok(()),
384 DeletionOutcome::Skipped { reason } => Err(CwError::Other(reason)),
385 DeletionOutcome::Failed { error } => Err(error),
386 }
387}
388
389fn resolve_delete_target(
394 target: Option<&str>,
395 main_repo: &Path,
396) -> Result<(PathBuf, Option<String>)> {
397 let target = target.map(|t| t.to_string()).unwrap_or_else(|| {
398 std::env::current_dir()
399 .unwrap_or_default()
400 .to_string_lossy()
401 .to_string()
402 });
403
404 let strict = super::helpers::resolve_target_strict(main_repo, &target)?;
405 Ok((strict.path, strict.branch))
406}