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 super::ai_tools::LaunchOptions;
16use crate::cli::EmitFormat;
17use crate::messages;
18
19pub fn create_worktree(
21 branch_name: &str,
22 base_branch: Option<&str>,
23 path: Option<&str>,
24 initial_prompt: Option<&str>,
25 launch_opts: &LaunchOptions<'_>,
26 emit: EmitFormat,
27) -> Result<PathBuf> {
28 let repo = git::get_repo_root(None)?;
29
30 if !git::is_valid_branch_name(branch_name, Some(&repo)) {
32 let error_msg = git::get_branch_name_error(branch_name);
33 return Err(CwError::InvalidBranch(messages::invalid_branch_name(
34 &error_msg,
35 )));
36 }
37
38 macro_rules! say {
41 ($($arg:tt)*) => {
42 if emit == EmitFormat::Json {
43 eprintln!($($arg)*);
44 } else {
45 println!($($arg)*);
46 }
47 };
48 }
49
50 let existing = git::find_worktree_by_branch(&repo, branch_name)?.or(
52 git::find_worktree_by_branch(&repo, &format!("refs/heads/{}", branch_name))?,
53 );
54
55 if let Some(existing_path) = existing {
56 say!(
57 "\n{}\nBranch '{}' already has a worktree at:\n {}\n",
58 style("! Worktree already exists").yellow().bold(),
59 style(branch_name).cyan(),
60 style(existing_path.display()).blue(),
61 );
62
63 if git::is_non_interactive() {
64 return Err(CwError::InvalidBranch(format!(
65 "Worktree for branch '{}' already exists at {}.\n\
66 Use 'gw resume {}' to continue work.",
67 branch_name,
68 existing_path.display(),
69 branch_name,
70 )));
71 }
72
73 say!(
75 "Use '{}' to resume work in this worktree.\n",
76 style(format!("gw resume {}", branch_name)).cyan()
77 );
78 return Ok(existing_path);
79 }
80
81 let mut branch_already_exists = false;
83 let mut is_remote_only = false;
84
85 if git::branch_exists(branch_name, Some(&repo)) {
86 say!(
87 "\n{}\nBranch '{}' already exists locally but has no worktree.\n",
88 style("! Branch already exists").yellow().bold(),
89 style(branch_name).cyan(),
90 );
91 branch_already_exists = true;
92 } else if git::remote_branch_exists(branch_name, Some(&repo), "origin") {
93 say!(
94 "\n{}\nBranch '{}' exists on remote but not locally.\n",
95 style("! Remote branch found").yellow().bold(),
96 style(branch_name).cyan(),
97 );
98 branch_already_exists = true;
99 is_remote_only = true;
100 }
101
102 let base = if let Some(b) = base_branch {
104 b.to_string()
105 } else {
106 git::detect_default_branch(Some(&repo))
107 };
108
109 if (!is_remote_only || base_branch.is_some()) && !git::branch_exists(&base, Some(&repo)) {
111 return Err(CwError::InvalidBranch(messages::branch_not_found(&base)));
112 }
113
114 let worktree_path = if let Some(p) = path {
120 let raw = PathBuf::from(p);
121 let abs = if raw.is_absolute() {
122 raw
123 } else {
124 std::env::current_dir()
125 .map_err(|e| CwError::Other(format!("cannot determine current directory: {e}")))?
126 .join(raw)
127 };
128 abs.canonicalize().unwrap_or(abs)
129 } else {
130 default_worktree_path(&repo, branch_name)
131 };
132
133 say!("\n{}", style("Creating new worktree:").cyan().bold());
134 say!(" Base branch: {}", style(&base).green());
135 say!(" New branch: {}", style(branch_name).green());
136 say!(" Path: {}\n", style(worktree_path.display()).blue());
137
138 if let Some(parent) = worktree_path.parent() {
140 let _ = std::fs::create_dir_all(parent);
141 }
142
143 let _ = git::git_command(&["fetch", "--all", "--prune"], Some(&repo), false, false);
145
146 let wt_str = worktree_path.to_string_lossy().to_string();
148 if is_remote_only {
149 git::git_command(
150 &[
151 "worktree",
152 "add",
153 "-b",
154 branch_name,
155 &wt_str,
156 &format!("origin/{}", branch_name),
157 ],
158 Some(&repo),
159 true,
160 false,
161 )?;
162 } else if branch_already_exists {
163 git::git_command(
164 &["worktree", "add", &wt_str, branch_name],
165 Some(&repo),
166 true,
167 false,
168 )?;
169 } else {
170 git::git_command(
171 &["worktree", "add", "-b", branch_name, &wt_str, &base],
172 Some(&repo),
173 true,
174 false,
175 )?;
176 }
177
178 let bb_key = format_config_key(CONFIG_KEY_BASE_BRANCH, branch_name);
180 let bp_key = format_config_key(CONFIG_KEY_BASE_PATH, branch_name);
181 let ib_key = format_config_key(CONFIG_KEY_INTENDED_BRANCH, branch_name);
182 git::set_config(&bb_key, &base, Some(&repo))?;
183 git::set_config(&bp_key, &repo.to_string_lossy(), Some(&repo))?;
184 git::set_config(&ib_key, branch_name, Some(&repo))?;
185
186 say!(
187 "{} Worktree created successfully\n",
188 style("*").green().bold()
189 );
190
191 shared_files::share_files(&repo, &worktree_path);
193
194 crate::hooks::run_event("post_new", &worktree_path)?;
200
201 if emit == EmitFormat::Json {
204 println!(
205 "{}",
206 serde_json::to_string(&serde_json::json!({
207 "worktree_path": worktree_path.display().to_string(),
208 "branch": branch_name,
209 "base": base,
210 }))
211 .map_err(|e| CwError::Other(format!("json serialization failed: {e}")))?
212 );
213 return Ok(worktree_path);
214 }
215
216 let _ = super::ai_tools::spawn_in_worktree(&worktree_path, initial_prompt, launch_opts);
222
223 Ok(worktree_path)
224}
225
226#[derive(Debug)]
232pub enum DeletionOutcome {
233 Deleted {
234 branch: Option<String>,
235 path: PathBuf,
236 },
237 Skipped {
238 reason: String,
239 },
240 Failed {
241 error: CwError,
242 },
243}
244
245#[derive(Debug, Clone, Copy)]
247pub struct RmFlags {
248 pub keep_branch: bool,
249 pub delete_remote: bool,
250 pub git_force: bool,
252 pub allow_busy: bool,
254}
255
256pub(crate) fn delete_one(
263 worktree_path: &Path,
264 branch_name: Option<&str>,
265 main_repo: &Path,
266 flags: RmFlags,
267) -> DeletionOutcome {
268 let wt_resolved = git::canonicalize_or(worktree_path);
270 let main_resolved = git::canonicalize_or(main_repo);
271 if wt_resolved == main_resolved {
272 return DeletionOutcome::Failed {
273 error: CwError::Other(messages::cannot_delete_main_worktree()),
274 };
275 }
276
277 if let Ok(cwd) = std::env::current_dir() {
279 let cwd_canon = cwd.canonicalize().unwrap_or(cwd);
280 let wt_canon = worktree_path
281 .canonicalize()
282 .unwrap_or_else(|_| worktree_path.to_path_buf());
283 if cwd_canon.starts_with(&wt_canon) {
284 let _ = std::env::set_current_dir(main_repo);
285 }
286 }
287
288 if let Err(e) = crate::hooks::run_event("pre_rm", worktree_path) {
293 eprintln!(
294 "{} pre_rm hook failed (continuing anyway): {}",
295 style("!").yellow().bold(),
296 e
297 );
298 }
299
300 println!(
302 "{}",
303 style(messages::removing_worktree(worktree_path)).yellow()
304 );
305 if let Err(e) = git::remove_worktree_safe(worktree_path, main_repo, flags.git_force) {
306 return DeletionOutcome::Failed { error: e };
307 }
308 println!("{} Worktree removed\n", style("*").green().bold());
309
310 if let Some(branch) = branch_name {
312 if !flags.keep_branch {
313 println!(
314 "{}",
315 style(messages::deleting_local_branch(branch)).yellow()
316 );
317 let _ = git::git_command(&["branch", "-D", branch], Some(main_repo), false, false);
318
319 let bb_key = format_config_key(CONFIG_KEY_BASE_BRANCH, branch);
320 let bp_key = format_config_key(CONFIG_KEY_BASE_PATH, branch);
321 let ib_key = format_config_key(CONFIG_KEY_INTENDED_BRANCH, branch);
322 git::unset_config(&bb_key, Some(main_repo));
323 git::unset_config(&bp_key, Some(main_repo));
324 git::unset_config(&ib_key, Some(main_repo));
325
326 println!(
327 "{} Local branch and metadata removed\n",
328 style("*").green().bold()
329 );
330
331 if flags.delete_remote {
332 println!(
333 "{}",
334 style(messages::deleting_remote_branch(branch)).yellow()
335 );
336 match git::git_command(
337 &["push", "origin", &format!(":{}", branch)],
338 Some(main_repo),
339 false,
340 true,
341 ) {
342 Ok(r) if r.returncode == 0 => {
343 println!("{} Remote branch deleted\n", style("*").green().bold());
344 }
345 _ => {
346 println!("{} Remote branch deletion failed\n", style("!").yellow());
347 }
348 }
349 }
350 }
351 }
352
353 DeletionOutcome::Deleted {
354 branch: branch_name.map(str::to_string),
355 path: worktree_path.to_path_buf(),
356 }
357}
358
359pub fn delete_worktree(
374 target: Option<&str>,
375 keep_branch: bool,
376 delete_remote: bool,
377 force: bool,
378 allow_busy: bool,
379) -> Result<()> {
380 let main_repo = git::get_main_repo_root(None)?;
381 let (worktree_path, branch_name) = resolve_delete_target(target, &main_repo)?;
382
383 let wt_resolved = git::canonicalize_or(&worktree_path);
386 let main_resolved = git::canonicalize_or(&main_repo);
387 if wt_resolved == main_resolved {
388 return Err(CwError::Other(messages::cannot_delete_main_worktree()));
389 }
390
391 if let Ok(cwd) = std::env::current_dir() {
396 let cwd_canon = cwd.canonicalize().unwrap_or(cwd);
397 let wt_canon = worktree_path
398 .canonicalize()
399 .unwrap_or_else(|_| worktree_path.clone());
400 if cwd_canon.starts_with(&wt_canon) {
401 let _ = std::env::set_current_dir(&main_repo);
402 }
403 }
404
405 let (hard, soft) = crate::operations::busy::detect_busy_tiered(&worktree_path);
406 if (!hard.is_empty() || !soft.is_empty()) && !allow_busy {
407 let branch_display = branch_name.clone().unwrap_or_else(|| {
408 worktree_path
409 .file_name()
410 .map(|n| n.to_string_lossy().to_string())
411 .unwrap_or_else(|| worktree_path.to_string_lossy().to_string())
412 });
413 let msg = crate::operations::busy_messages::render_refusal(&branch_display, &hard, &soft);
414 eprint!("{}", msg);
415 return Err(CwError::Other(format!(
416 "worktree '{}' is in use; re-run with --force to override",
417 branch_display
418 )));
419 }
420
421 let flags = RmFlags {
422 keep_branch,
423 delete_remote,
424 git_force: force,
425 allow_busy: true, };
427
428 match delete_one(&worktree_path, branch_name.as_deref(), &main_repo, flags) {
429 DeletionOutcome::Deleted { .. } => Ok(()),
430 DeletionOutcome::Skipped { reason } => Err(CwError::Other(reason)),
431 DeletionOutcome::Failed { error } => Err(error),
432 }
433}
434
435fn resolve_delete_target(
440 target: Option<&str>,
441 main_repo: &Path,
442) -> Result<(PathBuf, Option<String>)> {
443 let target = match target {
444 Some(t) => t.to_string(),
445 None => std::env::current_dir()
446 .map(|p| p.to_string_lossy().into_owned())
447 .map_err(|e| CwError::Other(format!("cannot determine current directory: {e}")))?,
448 };
449
450 let strict = super::helpers::resolve_target_strict(main_repo, &target)?;
451 Ok((strict.path, strict.branch))
452}