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(
206 target: Option<&str>,
207 keep_branch: bool,
208 delete_remote: bool,
209 force: bool,
210 lookup_mode: Option<&str>,
211) -> Result<()> {
212 let main_repo = git::get_main_repo_root(None)?;
213 let (worktree_path, branch_name) = resolve_delete_target(target, &main_repo, lookup_mode)?;
214
215 let wt_resolved = git::canonicalize_or(&worktree_path);
217 let main_resolved = git::canonicalize_or(&main_repo);
218 if wt_resolved == main_resolved {
219 return Err(CwError::Git(messages::cannot_delete_main_worktree()));
220 }
221
222 if let Ok(cwd) = std::env::current_dir() {
224 let cwd_str = cwd.to_string_lossy().to_string();
225 let wt_str = worktree_path.to_string_lossy().to_string();
226 if cwd_str.starts_with(&wt_str) {
227 let _ = std::env::set_current_dir(&main_repo);
228 }
229 }
230
231 let base_branch = branch_name
233 .as_deref()
234 .and_then(|b| {
235 let key = format_config_key(CONFIG_KEY_BASE_BRANCH, b);
236 git::get_config(&key, Some(&main_repo))
237 })
238 .unwrap_or_default();
239
240 let mut hook_ctx = build_hook_context(
241 &branch_name.clone().unwrap_or_default(),
242 &base_branch,
243 &worktree_path,
244 &main_repo,
245 "worktree.pre_delete",
246 "delete",
247 );
248 hooks::run_hooks(
249 "worktree.pre_delete",
250 &hook_ctx,
251 Some(&main_repo),
252 Some(&main_repo),
253 )?;
254
255 println!(
257 "{}",
258 style(messages::removing_worktree(&worktree_path)).yellow()
259 );
260 git::remove_worktree_safe(&worktree_path, &main_repo, force)?;
261 println!("{} Worktree removed\n", style("*").green().bold());
262
263 if let Some(ref branch) = branch_name {
265 if !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);
274 let bp_key = format_config_key(CONFIG_KEY_BASE_PATH, branch);
275 let ib_key = format_config_key(CONFIG_KEY_INTENDED_BRANCH, branch);
276 git::unset_config(&bb_key, Some(&main_repo));
277 git::unset_config(&bp_key, Some(&main_repo));
278 git::unset_config(&ib_key, Some(&main_repo));
279
280 println!(
281 "{} Local branch and metadata removed\n",
282 style("*").green().bold()
283 );
284
285 if delete_remote {
287 println!(
288 "{}",
289 style(messages::deleting_remote_branch(branch)).yellow()
290 );
291 match git::git_command(
292 &["push", "origin", &format!(":{}", branch)],
293 Some(&main_repo),
294 false,
295 true,
296 ) {
297 Ok(r) if r.returncode == 0 => {
298 println!("{} Remote branch deleted\n", style("*").green().bold());
299 }
300 _ => {
301 println!("{} Remote branch deletion failed\n", style("!").yellow());
302 }
303 }
304 }
305 }
306 }
307
308 hook_ctx.insert("event".into(), "worktree.post_delete".into());
310 let _ = hooks::run_hooks(
311 "worktree.post_delete",
312 &hook_ctx,
313 Some(&main_repo),
314 Some(&main_repo),
315 );
316 let _ = registry::update_last_seen(&main_repo);
317
318 Ok(())
319}
320
321fn resolve_delete_target(
323 target: Option<&str>,
324 main_repo: &Path,
325 lookup_mode: Option<&str>,
326) -> Result<(PathBuf, Option<String>)> {
327 let target = target.map(|t| t.to_string()).unwrap_or_else(|| {
328 std::env::current_dir()
329 .unwrap_or_default()
330 .to_string_lossy()
331 .to_string()
332 });
333
334 let target_path = PathBuf::from(&target);
335
336 if target_path.exists() {
338 let resolved = target_path.canonicalize().unwrap_or(target_path);
339 let branch = super::helpers::get_branch_for_worktree(main_repo, &resolved);
340 return Ok((resolved, branch));
341 }
342
343 if lookup_mode != Some("worktree") {
345 if let Some(path) = git::find_worktree_by_intended_branch(main_repo, &target)? {
346 return Ok((path, Some(target)));
347 }
348 }
349
350 if lookup_mode != Some("branch") {
352 if let Some(path) = git::find_worktree_by_name(main_repo, &target)? {
353 let branch = super::helpers::get_branch_for_worktree(main_repo, &path);
354 return Ok((path, branch));
355 }
356 }
357
358 Err(CwError::WorktreeNotFound(messages::worktree_not_found(
359 &target,
360 )))
361}
362
363pub fn sync_worktree(
365 target: Option<&str>,
366 all: bool,
367 _fetch_only: bool,
368 ai_merge: bool,
369 lookup_mode: Option<&str>,
370) -> Result<()> {
371 let repo = git::get_repo_root(None)?;
372
373 println!("{}", style("Fetching updates from remote...").yellow());
375 let fetch_result = git::git_command(&["fetch", "--all", "--prune"], Some(&repo), false, true)?;
376 if fetch_result.returncode != 0 {
377 println!(
378 "{} Fetch failed or no remote configured\n",
379 style("!").yellow()
380 );
381 }
382
383 if _fetch_only {
384 println!("{} Fetch complete\n", style("*").green().bold());
385 return Ok(());
386 }
387
388 let worktrees_to_sync = if all {
390 let all_wt = git::parse_worktrees(&repo)?;
391 all_wt
392 .into_iter()
393 .filter(|(b, _)| b != "(detached)")
394 .map(|(b, p)| {
395 let branch = git::normalize_branch_name(&b).to_string();
396 (branch, p)
397 })
398 .collect::<Vec<_>>()
399 } else {
400 let resolved = resolve_worktree_target(target, lookup_mode)?;
401 vec![(resolved.branch, resolved.path)]
402 };
403
404 for (branch, wt_path) in &worktrees_to_sync {
405 let base_key = format_config_key(CONFIG_KEY_BASE_BRANCH, branch);
406 let base_branch = git::get_config(&base_key, Some(&repo));
407
408 if let Some(base) = base_branch {
409 println!("\n{}", style("Syncing worktree:").cyan().bold());
410 println!(" Branch: {}", style(branch).green());
411 println!(" Base: {}", style(&base).green());
412 println!(" Path: {}\n", style(wt_path.display()).blue());
413
414 let rebase_target = {
416 let origin_base = format!("origin/{}", base);
417 if git::branch_exists(&origin_base, Some(wt_path)) {
418 origin_base
419 } else {
420 base.clone()
421 }
422 };
423
424 println!(
425 "{}",
426 style(messages::rebase_in_progress(branch, &rebase_target)).yellow()
427 );
428
429 match git::git_command(&["rebase", &rebase_target], Some(wt_path), false, true) {
430 Ok(r) if r.returncode == 0 => {
431 println!("{} Rebase successful\n", style("*").green().bold());
432 }
433 _ => {
434 if ai_merge {
435 let conflicts = git::git_command(
436 &["diff", "--name-only", "--diff-filter=U"],
437 Some(wt_path),
438 false,
439 true,
440 )
441 .ok()
442 .and_then(|r| {
443 if r.returncode == 0 && !r.stdout.trim().is_empty() {
444 Some(r.stdout.trim().to_string())
445 } else {
446 None
447 }
448 });
449
450 let _ =
451 git::git_command(&["rebase", "--abort"], Some(wt_path), false, false);
452
453 let conflict_list = conflicts.as_deref().unwrap_or("(unknown)");
454 let prompt = format!(
455 "Resolve merge conflicts in this repository. The rebase of '{}' onto '{}' \
456 failed with conflicts in: {}\n\
457 Please examine the conflicted files and resolve them.",
458 branch, rebase_target, conflict_list
459 );
460
461 println!(
462 "\n{} Launching AI to resolve conflicts for '{}'...\n",
463 style("*").cyan().bold(),
464 branch
465 );
466 let _ = super::ai_tools::launch_ai_tool(
467 wt_path,
468 None,
469 false,
470 Some(&prompt),
471 None,
472 );
473 } else {
474 let _ =
476 git::git_command(&["rebase", "--abort"], Some(wt_path), false, false);
477 println!(
478 "{} Rebase failed for '{}'. Resolve conflicts manually.\n\
479 Tip: Use --ai-merge flag to get AI assistance with conflicts\n",
480 style("!").yellow(),
481 branch
482 );
483 }
484 }
485 }
486 } else {
487 let origin_ref = format!("origin/{}", branch);
489 if git::branch_exists(&origin_ref, Some(wt_path)) {
490 println!("\n{}", style("Syncing worktree:").cyan().bold());
491 println!(" Branch: {}", style(branch).green());
492 println!(" Path: {}\n", style(wt_path.display()).blue());
493
494 println!(
495 "{}",
496 style(messages::rebase_in_progress(branch, &origin_ref)).yellow()
497 );
498
499 match git::git_command(&["rebase", &origin_ref], Some(wt_path), false, true) {
500 Ok(r) if r.returncode == 0 => {
501 println!("{} Rebase successful\n", style("*").green().bold());
502 }
503 _ => {
504 let _ =
505 git::git_command(&["rebase", "--abort"], Some(wt_path), false, false);
506 println!(
507 "{} Rebase failed for '{}'. Resolve conflicts manually.\n",
508 style("!").yellow(),
509 branch
510 );
511 }
512 }
513 }
514 }
515 }
516
517 Ok(())
518}