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::hooks;
14use crate::registry;
15use crate::shared_files;
16
17use super::helpers::{build_hook_context, resolve_worktree_target};
18use crate::messages;
19
20pub fn create_worktree(
22 branch_name: &str,
23 base_branch: Option<&str>,
24 path: Option<&str>,
25 _term: Option<&str>,
26 no_ai: bool,
27 initial_prompt: Option<&str>,
28) -> Result<PathBuf> {
29 let repo = git::get_repo_root(None)?;
30
31 if !git::is_valid_branch_name(branch_name, Some(&repo)) {
33 let error_msg = git::get_branch_name_error(branch_name);
34 return Err(CwError::InvalidBranch(messages::invalid_branch_name(
35 &error_msg,
36 )));
37 }
38
39 let existing = git::find_worktree_by_branch(&repo, branch_name)?.or(
41 git::find_worktree_by_branch(&repo, &format!("refs/heads/{}", branch_name))?,
42 );
43
44 if let Some(existing_path) = existing {
45 println!(
46 "\n{}\nBranch '{}' already has a worktree at:\n {}\n",
47 style("! Worktree already exists").yellow().bold(),
48 style(branch_name).cyan(),
49 style(existing_path.display()).blue(),
50 );
51
52 if git::is_non_interactive() {
53 return Err(CwError::InvalidBranch(format!(
54 "Worktree for branch '{}' already exists at {}.\n\
55 Use 'gw resume {}' to continue work.",
56 branch_name,
57 existing_path.display(),
58 branch_name,
59 )));
60 }
61
62 println!(
64 "Use '{}' to resume work in this worktree.\n",
65 style(format!("gw resume {}", branch_name)).cyan()
66 );
67 return Ok(existing_path);
68 }
69
70 let mut branch_already_exists = false;
72 let mut is_remote_only = false;
73
74 if git::branch_exists(branch_name, Some(&repo)) {
75 println!(
76 "\n{}\nBranch '{}' already exists locally but has no worktree.\n",
77 style("! Branch already exists").yellow().bold(),
78 style(branch_name).cyan(),
79 );
80 branch_already_exists = true;
81 } else if git::remote_branch_exists(branch_name, Some(&repo), "origin") {
82 println!(
83 "\n{}\nBranch '{}' exists on remote but not locally.\n",
84 style("! Remote branch found").yellow().bold(),
85 style(branch_name).cyan(),
86 );
87 branch_already_exists = true;
88 is_remote_only = true;
89 }
90
91 let base = if let Some(b) = base_branch {
93 b.to_string()
94 } else {
95 git::detect_default_branch(Some(&repo))
96 };
97
98 if (!is_remote_only || base_branch.is_some()) && !git::branch_exists(&base, Some(&repo)) {
100 return Err(CwError::InvalidBranch(messages::branch_not_found(&base)));
101 }
102
103 let worktree_path = if let Some(p) = path {
105 PathBuf::from(p)
106 .canonicalize()
107 .unwrap_or_else(|_| PathBuf::from(p))
108 } else {
109 default_worktree_path(&repo, branch_name)
110 };
111
112 println!("\n{}", style("Creating new worktree:").cyan().bold());
113 println!(" Base branch: {}", style(&base).green());
114 println!(" New branch: {}", style(branch_name).green());
115 println!(" Path: {}\n", style(worktree_path.display()).blue());
116
117 let mut hook_ctx = build_hook_context(
119 branch_name,
120 &base,
121 &worktree_path,
122 &repo,
123 "worktree.pre_create",
124 "new",
125 );
126 hooks::run_hooks("worktree.pre_create", &hook_ctx, Some(&repo), Some(&repo))?;
127
128 if let Some(parent) = worktree_path.parent() {
130 let _ = std::fs::create_dir_all(parent);
131 }
132
133 let _ = git::git_command(&["fetch", "--all", "--prune"], Some(&repo), false, false);
135
136 let wt_str = worktree_path.to_string_lossy().to_string();
138 if is_remote_only {
139 git::git_command(
140 &[
141 "worktree",
142 "add",
143 "-b",
144 branch_name,
145 &wt_str,
146 &format!("origin/{}", branch_name),
147 ],
148 Some(&repo),
149 true,
150 false,
151 )?;
152 } else if branch_already_exists {
153 git::git_command(
154 &["worktree", "add", &wt_str, branch_name],
155 Some(&repo),
156 true,
157 false,
158 )?;
159 } else {
160 git::git_command(
161 &["worktree", "add", "-b", branch_name, &wt_str, &base],
162 Some(&repo),
163 true,
164 false,
165 )?;
166 }
167
168 let bb_key = format_config_key(CONFIG_KEY_BASE_BRANCH, branch_name);
170 let bp_key = format_config_key(CONFIG_KEY_BASE_PATH, branch_name);
171 let ib_key = format_config_key(CONFIG_KEY_INTENDED_BRANCH, branch_name);
172 git::set_config(&bb_key, &base, Some(&repo))?;
173 git::set_config(&bp_key, &repo.to_string_lossy(), Some(&repo))?;
174 git::set_config(&ib_key, branch_name, Some(&repo))?;
175
176 let _ = registry::register_repo(&repo);
178
179 println!(
180 "{} Worktree created successfully\n",
181 style("*").green().bold()
182 );
183
184 shared_files::share_files(&repo, &worktree_path);
186
187 hook_ctx.insert("event".into(), "worktree.post_create".into());
189 let _ = hooks::run_hooks(
190 "worktree.post_create",
191 &hook_ctx,
192 Some(&worktree_path),
193 Some(&repo),
194 );
195
196 if !no_ai {
198 let _ = super::ai_tools::launch_ai_tool(&worktree_path, _term, false, None, initial_prompt);
199 }
200
201 Ok(worktree_path)
202}
203
204#[derive(Debug)]
210pub enum DeletionOutcome {
211 Deleted {
212 branch: Option<String>,
213 path: PathBuf,
214 },
215 Skipped {
216 reason: String,
217 },
218 Failed {
219 error: CwError,
220 },
221}
222
223#[derive(Debug, Clone, Copy)]
225pub struct DeleteFlags {
226 pub keep_branch: bool,
227 pub delete_remote: bool,
228 pub git_force: bool,
230 pub allow_busy: bool,
232}
233
234pub(crate) fn delete_one(
241 worktree_path: &Path,
242 branch_name: Option<&str>,
243 main_repo: &Path,
244 flags: DeleteFlags,
245) -> DeletionOutcome {
246 let wt_resolved = git::canonicalize_or(worktree_path);
248 let main_resolved = git::canonicalize_or(main_repo);
249 if wt_resolved == main_resolved {
250 return DeletionOutcome::Failed {
251 error: CwError::Git(messages::cannot_delete_main_worktree()),
252 };
253 }
254
255 if let Ok(cwd) = std::env::current_dir() {
257 let cwd_canon = cwd.canonicalize().unwrap_or(cwd);
258 let wt_canon = worktree_path
259 .canonicalize()
260 .unwrap_or_else(|_| worktree_path.to_path_buf());
261 if cwd_canon.starts_with(&wt_canon) {
262 let _ = std::env::set_current_dir(main_repo);
263 }
264 }
265
266 let base_branch = branch_name
268 .and_then(|b| {
269 let key = format_config_key(CONFIG_KEY_BASE_BRANCH, b);
270 git::get_config(&key, Some(main_repo))
271 })
272 .unwrap_or_default();
273
274 let mut hook_ctx = build_hook_context(
275 branch_name.unwrap_or(""),
276 &base_branch,
277 worktree_path,
278 main_repo,
279 "worktree.pre_delete",
280 "delete",
281 );
282 if let Err(e) = hooks::run_hooks(
283 "worktree.pre_delete",
284 &hook_ctx,
285 Some(main_repo),
286 Some(main_repo),
287 ) {
288 return DeletionOutcome::Failed { error: e };
289 }
290
291 println!(
293 "{}",
294 style(messages::removing_worktree(worktree_path)).yellow()
295 );
296 if let Err(e) = git::remove_worktree_safe(worktree_path, main_repo, flags.git_force) {
297 return DeletionOutcome::Failed { error: e };
298 }
299 println!("{} Worktree removed\n", style("*").green().bold());
300
301 if let Some(branch) = branch_name {
303 if !flags.keep_branch {
304 println!(
305 "{}",
306 style(messages::deleting_local_branch(branch)).yellow()
307 );
308 let _ = git::git_command(&["branch", "-D", branch], Some(main_repo), false, false);
309
310 let bb_key = format_config_key(CONFIG_KEY_BASE_BRANCH, branch);
311 let bp_key = format_config_key(CONFIG_KEY_BASE_PATH, branch);
312 let ib_key = format_config_key(CONFIG_KEY_INTENDED_BRANCH, branch);
313 git::unset_config(&bb_key, Some(main_repo));
314 git::unset_config(&bp_key, Some(main_repo));
315 git::unset_config(&ib_key, Some(main_repo));
316
317 println!(
318 "{} Local branch and metadata removed\n",
319 style("*").green().bold()
320 );
321
322 if flags.delete_remote {
323 println!(
324 "{}",
325 style(messages::deleting_remote_branch(branch)).yellow()
326 );
327 match git::git_command(
328 &["push", "origin", &format!(":{}", branch)],
329 Some(main_repo),
330 false,
331 true,
332 ) {
333 Ok(r) if r.returncode == 0 => {
334 println!("{} Remote branch deleted\n", style("*").green().bold());
335 }
336 _ => {
337 println!("{} Remote branch deletion failed\n", style("!").yellow());
338 }
339 }
340 }
341 }
342 }
343
344 hook_ctx.insert("event".into(), "worktree.post_delete".into());
346 let _ = hooks::run_hooks(
347 "worktree.post_delete",
348 &hook_ctx,
349 Some(main_repo),
350 Some(main_repo),
351 );
352 let _ = registry::update_last_seen(main_repo);
353
354 DeletionOutcome::Deleted {
355 branch: branch_name.map(str::to_string),
356 path: worktree_path.to_path_buf(),
357 }
358}
359
360pub fn delete_worktree(
375 target: Option<&str>,
376 keep_branch: bool,
377 delete_remote: bool,
378 force: bool,
379 allow_busy: bool,
380 lookup_mode: Option<&str>,
381) -> Result<()> {
382 let main_repo = git::get_main_repo_root(None)?;
383 let (worktree_path, branch_name) = resolve_delete_target(target, &main_repo, lookup_mode)?;
384
385 let wt_resolved = git::canonicalize_or(&worktree_path);
388 let main_resolved = git::canonicalize_or(&main_repo);
389 if wt_resolved == main_resolved {
390 return Err(CwError::Git(messages::cannot_delete_main_worktree()));
391 }
392
393 if let Ok(cwd) = std::env::current_dir() {
398 let cwd_canon = cwd.canonicalize().unwrap_or(cwd);
399 let wt_canon = worktree_path
400 .canonicalize()
401 .unwrap_or_else(|_| worktree_path.clone());
402 if cwd_canon.starts_with(&wt_canon) {
403 let _ = std::env::set_current_dir(&main_repo);
404 }
405 }
406
407 let (hard, soft) = crate::operations::busy::detect_busy_tiered(&worktree_path);
408 if (!hard.is_empty() || !soft.is_empty()) && !allow_busy {
409 let branch_display = branch_name.clone().unwrap_or_else(|| {
410 worktree_path
411 .file_name()
412 .map(|n| n.to_string_lossy().to_string())
413 .unwrap_or_else(|| worktree_path.to_string_lossy().to_string())
414 });
415 let msg = crate::operations::busy_messages::render_refusal(&branch_display, &hard, &soft);
416 eprint!("{}", msg);
417 return Err(CwError::Other(format!(
418 "worktree '{}' is in use; re-run with --force to override",
419 branch_display
420 )));
421 }
422
423 let flags = DeleteFlags {
424 keep_branch,
425 delete_remote,
426 git_force: force,
427 allow_busy: true, };
429
430 match delete_one(&worktree_path, branch_name.as_deref(), &main_repo, flags) {
431 DeletionOutcome::Deleted { .. } => Ok(()),
432 DeletionOutcome::Skipped { reason } => Err(CwError::Other(reason)),
433 DeletionOutcome::Failed { error } => Err(error),
434 }
435}
436
437fn resolve_delete_target(
439 target: Option<&str>,
440 main_repo: &Path,
441 lookup_mode: Option<&str>,
442) -> Result<(PathBuf, Option<String>)> {
443 let target = target.map(|t| t.to_string()).unwrap_or_else(|| {
444 std::env::current_dir()
445 .unwrap_or_default()
446 .to_string_lossy()
447 .to_string()
448 });
449
450 let target_path = PathBuf::from(&target);
451
452 if target_path.exists() {
454 let resolved = target_path.canonicalize().unwrap_or(target_path);
455 let branch = super::helpers::get_branch_for_worktree(main_repo, &resolved);
456 return Ok((resolved, branch));
457 }
458
459 if lookup_mode != Some("worktree") {
461 if let Some(path) = git::find_worktree_by_intended_branch(main_repo, &target)? {
462 return Ok((path, Some(target)));
463 }
464 }
465
466 if lookup_mode != Some("branch") {
468 if let Some(path) = git::find_worktree_by_name(main_repo, &target)? {
469 let branch = super::helpers::get_branch_for_worktree(main_repo, &path);
470 return Ok((path, branch));
471 }
472 }
473
474 Err(CwError::WorktreeNotFound(messages::worktree_not_found(
475 &target,
476 )))
477}
478
479pub fn sync_worktree(
481 target: Option<&str>,
482 all: bool,
483 _fetch_only: bool,
484 ai_merge: bool,
485 lookup_mode: Option<&str>,
486) -> Result<()> {
487 let repo = git::get_repo_root(None)?;
488
489 println!("{}", style("Fetching updates from remote...").yellow());
491 let fetch_result = git::git_command(&["fetch", "--all", "--prune"], Some(&repo), false, true)?;
492 if fetch_result.returncode != 0 {
493 println!(
494 "{} Fetch failed or no remote configured\n",
495 style("!").yellow()
496 );
497 }
498
499 if _fetch_only {
500 println!("{} Fetch complete\n", style("*").green().bold());
501 return Ok(());
502 }
503
504 let worktrees_to_sync = if all {
506 let all_wt = git::parse_worktrees(&repo)?;
507 all_wt
508 .into_iter()
509 .filter(|(b, _)| b != "(detached)")
510 .map(|(b, p)| {
511 let branch = git::normalize_branch_name(&b).to_string();
512 (branch, p)
513 })
514 .collect::<Vec<_>>()
515 } else {
516 let resolved = resolve_worktree_target(target, lookup_mode)?;
517 vec![(resolved.branch, resolved.path)]
518 };
519
520 for (branch, wt_path) in &worktrees_to_sync {
521 let base_key = format_config_key(CONFIG_KEY_BASE_BRANCH, branch);
522 let base_branch = git::get_config(&base_key, Some(&repo));
523
524 if let Some(base) = base_branch {
525 println!("\n{}", style("Syncing worktree:").cyan().bold());
526 println!(" Branch: {}", style(branch).green());
527 println!(" Base: {}", style(&base).green());
528 println!(" Path: {}\n", style(wt_path.display()).blue());
529
530 let rebase_target = {
532 let origin_base = format!("origin/{}", base);
533 if git::branch_exists(&origin_base, Some(wt_path)) {
534 origin_base
535 } else {
536 base.clone()
537 }
538 };
539
540 println!(
541 "{}",
542 style(messages::rebase_in_progress(branch, &rebase_target)).yellow()
543 );
544
545 match git::git_command(&["rebase", &rebase_target], Some(wt_path), false, true) {
546 Ok(r) if r.returncode == 0 => {
547 println!("{} Rebase successful\n", style("*").green().bold());
548 }
549 _ => {
550 if ai_merge {
551 let conflicts = git::list_conflicted_files(wt_path);
552 let _ =
553 git::git_command(&["rebase", "--abort"], Some(wt_path), false, false);
554
555 let conflict_list = conflicts.as_deref().unwrap_or("(unknown)");
556 let prompt = format!(
557 "Resolve merge conflicts in this repository. The rebase of '{}' onto '{}' \
558 failed with conflicts in: {}\n\
559 Please examine the conflicted files and resolve them.",
560 branch, rebase_target, conflict_list
561 );
562
563 println!(
564 "\n{} Launching AI to resolve conflicts for '{}'...\n",
565 style("*").cyan().bold(),
566 branch
567 );
568 let _ = super::ai_tools::launch_ai_tool(
569 wt_path,
570 None,
571 false,
572 Some(&prompt),
573 None,
574 );
575 } else {
576 let _ =
578 git::git_command(&["rebase", "--abort"], Some(wt_path), false, false);
579 println!(
580 "{} Rebase failed for '{}'. Resolve conflicts manually.\n\
581 Tip: Use --ai-merge flag to get AI assistance with conflicts\n",
582 style("!").yellow(),
583 branch
584 );
585 }
586 }
587 }
588 } else {
589 let origin_ref = format!("origin/{}", branch);
591 if git::branch_exists(&origin_ref, Some(wt_path)) {
592 println!("\n{}", style("Syncing worktree:").cyan().bold());
593 println!(" Branch: {}", style(branch).green());
594 println!(" Path: {}\n", style(wt_path.display()).blue());
595
596 println!(
597 "{}",
598 style(messages::rebase_in_progress(branch, &origin_ref)).yellow()
599 );
600
601 match git::git_command(&["rebase", &origin_ref], Some(wt_path), false, true) {
602 Ok(r) if r.returncode == 0 => {
603 println!("{} Rebase successful\n", style("*").green().bold());
604 }
605 _ => {
606 let _ =
607 git::git_command(&["rebase", "--abort"], Some(wt_path), false, false);
608 println!(
609 "{} Rebase failed for '{}'. Resolve conflicts manually.\n",
610 style("!").yellow(),
611 branch
612 );
613 }
614 }
615 }
616 }
617 }
618
619 Ok(())
620}