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