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
204pub fn delete_worktree(
219 target: Option<&str>,
220 keep_branch: bool,
221 delete_remote: bool,
222 force: bool,
223 allow_busy: bool,
224 lookup_mode: Option<&str>,
225) -> Result<()> {
226 let main_repo = git::get_main_repo_root(None)?;
227 let (worktree_path, branch_name) = resolve_delete_target(target, &main_repo, lookup_mode)?;
228
229 let wt_resolved = git::canonicalize_or(&worktree_path);
231 let main_resolved = git::canonicalize_or(&main_repo);
232 if wt_resolved == main_resolved {
233 return Err(CwError::Git(messages::cannot_delete_main_worktree()));
234 }
235
236 if let Ok(cwd) = std::env::current_dir() {
240 let cwd_canon = cwd.canonicalize().unwrap_or(cwd);
241 let wt_canon = worktree_path
242 .canonicalize()
243 .unwrap_or_else(|_| worktree_path.clone());
244 if cwd_canon.starts_with(&wt_canon) {
245 let _ = std::env::set_current_dir(&main_repo);
246 }
247 }
248
249 let busy = crate::operations::busy::detect_busy(&worktree_path);
255 if !busy.is_empty() && !allow_busy {
256 let branch_display = branch_name.clone().unwrap_or_else(|| {
257 worktree_path
258 .file_name()
259 .map(|n| n.to_string_lossy().to_string())
260 .unwrap_or_else(|| worktree_path.to_string_lossy().to_string())
261 });
262 eprintln!(
263 "{} worktree '{}' is in use by:",
264 style("error:").red().bold(),
265 branch_display
266 );
267 for b in &busy {
268 eprintln!(" PID {:>6} {} (source: {:?})", b.pid, b.cmd, b.source);
269 }
270
271 use std::io::IsTerminal;
272 if std::io::stdin().is_terminal() && std::io::stderr().is_terminal() {
273 use std::io::Write;
274 eprint!("Delete anyway? (y/N): ");
275 let _ = std::io::stderr().flush();
276 let mut buf = String::new();
277 std::io::stdin().read_line(&mut buf)?;
278 let ans = buf.trim().to_lowercase();
279 if ans != "y" && ans != "yes" {
280 eprintln!("Aborted.");
281 return Ok(());
282 }
283 } else {
284 return Err(CwError::Other(format!(
285 "worktree '{}' is in use by {} process(es); re-run with --force to override",
286 branch_display,
287 busy.len()
288 )));
289 }
290 }
291
292 let base_branch = branch_name
294 .as_deref()
295 .and_then(|b| {
296 let key = format_config_key(CONFIG_KEY_BASE_BRANCH, b);
297 git::get_config(&key, Some(&main_repo))
298 })
299 .unwrap_or_default();
300
301 let mut hook_ctx = build_hook_context(
302 &branch_name.clone().unwrap_or_default(),
303 &base_branch,
304 &worktree_path,
305 &main_repo,
306 "worktree.pre_delete",
307 "delete",
308 );
309 hooks::run_hooks(
310 "worktree.pre_delete",
311 &hook_ctx,
312 Some(&main_repo),
313 Some(&main_repo),
314 )?;
315
316 println!(
318 "{}",
319 style(messages::removing_worktree(&worktree_path)).yellow()
320 );
321 git::remove_worktree_safe(&worktree_path, &main_repo, force)?;
322 println!("{} Worktree removed\n", style("*").green().bold());
323
324 if let Some(ref branch) = branch_name {
326 if !keep_branch {
327 println!(
328 "{}",
329 style(messages::deleting_local_branch(branch)).yellow()
330 );
331 let _ = git::git_command(&["branch", "-D", branch], Some(&main_repo), false, false);
332
333 let bb_key = format_config_key(CONFIG_KEY_BASE_BRANCH, branch);
335 let bp_key = format_config_key(CONFIG_KEY_BASE_PATH, branch);
336 let ib_key = format_config_key(CONFIG_KEY_INTENDED_BRANCH, branch);
337 git::unset_config(&bb_key, Some(&main_repo));
338 git::unset_config(&bp_key, Some(&main_repo));
339 git::unset_config(&ib_key, Some(&main_repo));
340
341 println!(
342 "{} Local branch and metadata removed\n",
343 style("*").green().bold()
344 );
345
346 if delete_remote {
348 println!(
349 "{}",
350 style(messages::deleting_remote_branch(branch)).yellow()
351 );
352 match git::git_command(
353 &["push", "origin", &format!(":{}", branch)],
354 Some(&main_repo),
355 false,
356 true,
357 ) {
358 Ok(r) if r.returncode == 0 => {
359 println!("{} Remote branch deleted\n", style("*").green().bold());
360 }
361 _ => {
362 println!("{} Remote branch deletion failed\n", style("!").yellow());
363 }
364 }
365 }
366 }
367 }
368
369 hook_ctx.insert("event".into(), "worktree.post_delete".into());
371 let _ = hooks::run_hooks(
372 "worktree.post_delete",
373 &hook_ctx,
374 Some(&main_repo),
375 Some(&main_repo),
376 );
377 let _ = registry::update_last_seen(&main_repo);
378
379 Ok(())
380}
381
382fn resolve_delete_target(
384 target: Option<&str>,
385 main_repo: &Path,
386 lookup_mode: Option<&str>,
387) -> Result<(PathBuf, Option<String>)> {
388 let target = target.map(|t| t.to_string()).unwrap_or_else(|| {
389 std::env::current_dir()
390 .unwrap_or_default()
391 .to_string_lossy()
392 .to_string()
393 });
394
395 let target_path = PathBuf::from(&target);
396
397 if target_path.exists() {
399 let resolved = target_path.canonicalize().unwrap_or(target_path);
400 let branch = super::helpers::get_branch_for_worktree(main_repo, &resolved);
401 return Ok((resolved, branch));
402 }
403
404 if lookup_mode != Some("worktree") {
406 if let Some(path) = git::find_worktree_by_intended_branch(main_repo, &target)? {
407 return Ok((path, Some(target)));
408 }
409 }
410
411 if lookup_mode != Some("branch") {
413 if let Some(path) = git::find_worktree_by_name(main_repo, &target)? {
414 let branch = super::helpers::get_branch_for_worktree(main_repo, &path);
415 return Ok((path, branch));
416 }
417 }
418
419 Err(CwError::WorktreeNotFound(messages::worktree_not_found(
420 &target,
421 )))
422}
423
424pub fn sync_worktree(
426 target: Option<&str>,
427 all: bool,
428 _fetch_only: bool,
429 ai_merge: bool,
430 lookup_mode: Option<&str>,
431) -> Result<()> {
432 let repo = git::get_repo_root(None)?;
433
434 println!("{}", style("Fetching updates from remote...").yellow());
436 let fetch_result = git::git_command(&["fetch", "--all", "--prune"], Some(&repo), false, true)?;
437 if fetch_result.returncode != 0 {
438 println!(
439 "{} Fetch failed or no remote configured\n",
440 style("!").yellow()
441 );
442 }
443
444 if _fetch_only {
445 println!("{} Fetch complete\n", style("*").green().bold());
446 return Ok(());
447 }
448
449 let worktrees_to_sync = if all {
451 let all_wt = git::parse_worktrees(&repo)?;
452 all_wt
453 .into_iter()
454 .filter(|(b, _)| b != "(detached)")
455 .map(|(b, p)| {
456 let branch = git::normalize_branch_name(&b).to_string();
457 (branch, p)
458 })
459 .collect::<Vec<_>>()
460 } else {
461 let resolved = resolve_worktree_target(target, lookup_mode)?;
462 vec![(resolved.branch, resolved.path)]
463 };
464
465 for (branch, wt_path) in &worktrees_to_sync {
466 let base_key = format_config_key(CONFIG_KEY_BASE_BRANCH, branch);
467 let base_branch = git::get_config(&base_key, Some(&repo));
468
469 if let Some(base) = base_branch {
470 println!("\n{}", style("Syncing worktree:").cyan().bold());
471 println!(" Branch: {}", style(branch).green());
472 println!(" Base: {}", style(&base).green());
473 println!(" Path: {}\n", style(wt_path.display()).blue());
474
475 let rebase_target = {
477 let origin_base = format!("origin/{}", base);
478 if git::branch_exists(&origin_base, Some(wt_path)) {
479 origin_base
480 } else {
481 base.clone()
482 }
483 };
484
485 println!(
486 "{}",
487 style(messages::rebase_in_progress(branch, &rebase_target)).yellow()
488 );
489
490 match git::git_command(&["rebase", &rebase_target], Some(wt_path), false, true) {
491 Ok(r) if r.returncode == 0 => {
492 println!("{} Rebase successful\n", style("*").green().bold());
493 }
494 _ => {
495 if ai_merge {
496 let conflicts = git::git_command(
497 &["diff", "--name-only", "--diff-filter=U"],
498 Some(wt_path),
499 false,
500 true,
501 )
502 .ok()
503 .and_then(|r| {
504 if r.returncode == 0 && !r.stdout.trim().is_empty() {
505 Some(r.stdout.trim().to_string())
506 } else {
507 None
508 }
509 });
510
511 let _ =
512 git::git_command(&["rebase", "--abort"], Some(wt_path), false, false);
513
514 let conflict_list = conflicts.as_deref().unwrap_or("(unknown)");
515 let prompt = format!(
516 "Resolve merge conflicts in this repository. The rebase of '{}' onto '{}' \
517 failed with conflicts in: {}\n\
518 Please examine the conflicted files and resolve them.",
519 branch, rebase_target, conflict_list
520 );
521
522 println!(
523 "\n{} Launching AI to resolve conflicts for '{}'...\n",
524 style("*").cyan().bold(),
525 branch
526 );
527 let _ = super::ai_tools::launch_ai_tool(
528 wt_path,
529 None,
530 false,
531 Some(&prompt),
532 None,
533 );
534 } else {
535 let _ =
537 git::git_command(&["rebase", "--abort"], Some(wt_path), false, false);
538 println!(
539 "{} Rebase failed for '{}'. Resolve conflicts manually.\n\
540 Tip: Use --ai-merge flag to get AI assistance with conflicts\n",
541 style("!").yellow(),
542 branch
543 );
544 }
545 }
546 }
547 } else {
548 let origin_ref = format!("origin/{}", branch);
550 if git::branch_exists(&origin_ref, Some(wt_path)) {
551 println!("\n{}", style("Syncing worktree:").cyan().bold());
552 println!(" Branch: {}", style(branch).green());
553 println!(" Path: {}\n", style(wt_path.display()).blue());
554
555 println!(
556 "{}",
557 style(messages::rebase_in_progress(branch, &origin_ref)).yellow()
558 );
559
560 match git::git_command(&["rebase", &origin_ref], Some(wt_path), false, true) {
561 Ok(r) if r.returncode == 0 => {
562 println!("{} Rebase successful\n", style("*").green().bold());
563 }
564 _ => {
565 let _ =
566 git::git_command(&["rebase", "--abort"], Some(wt_path), false, false);
567 println!(
568 "{} Rebase failed for '{}'. Resolve conflicts manually.\n",
569 style("!").yellow(),
570 branch
571 );
572 }
573 }
574 }
575 }
576 }
577
578 Ok(())
579}