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