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) -> 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 let existing = git::find_worktree_by_branch(&repo, branch_name)?.or(
40 git::find_worktree_by_branch(&repo, &format!("refs/heads/{}", branch_name))?,
41 );
42
43 if let Some(existing_path) = existing {
44 println!(
45 "\n{}\nBranch '{}' already has a worktree at:\n {}\n",
46 style("! Worktree already exists").yellow().bold(),
47 style(branch_name).cyan(),
48 style(existing_path.display()).blue(),
49 );
50
51 if git::is_non_interactive() {
52 return Err(CwError::InvalidBranch(format!(
53 "Worktree for branch '{}' already exists at {}.\n\
54 Use 'gw resume {}' to continue work.",
55 branch_name,
56 existing_path.display(),
57 branch_name,
58 )));
59 }
60
61 println!(
63 "Use '{}' to resume work in this worktree.\n",
64 style(format!("gw resume {}", branch_name)).cyan()
65 );
66 return Ok(existing_path);
67 }
68
69 let mut branch_already_exists = false;
71 let mut is_remote_only = false;
72
73 if git::branch_exists(branch_name, Some(&repo)) {
74 println!(
75 "\n{}\nBranch '{}' already exists locally but has no worktree.\n",
76 style("! Branch already exists").yellow().bold(),
77 style(branch_name).cyan(),
78 );
79 branch_already_exists = true;
80 } else if git::remote_branch_exists(branch_name, Some(&repo), "origin") {
81 println!(
82 "\n{}\nBranch '{}' exists on remote but not locally.\n",
83 style("! Remote branch found").yellow().bold(),
84 style(branch_name).cyan(),
85 );
86 branch_already_exists = true;
87 is_remote_only = true;
88 }
89
90 let base = if is_remote_only && base_branch.is_none() {
92 git::get_current_branch(Some(&repo)).unwrap_or_else(|_| "main".to_string())
93 } else if let Some(b) = base_branch {
94 b.to_string()
95 } else {
96 git::get_current_branch(Some(&repo))
97 .map_err(|_| CwError::InvalidBranch(messages::cannot_determine_base_branch()))?
98 };
99
100 if (!is_remote_only || base_branch.is_some()) && !git::branch_exists(&base, Some(&repo)) {
102 return Err(CwError::InvalidBranch(messages::branch_not_found(&base)));
103 }
104
105 let worktree_path = if let Some(p) = path {
107 PathBuf::from(p)
108 .canonicalize()
109 .unwrap_or_else(|_| PathBuf::from(p))
110 } else {
111 default_worktree_path(&repo, branch_name)
112 };
113
114 println!("\n{}", style("Creating new worktree:").cyan().bold());
115 println!(" Base branch: {}", style(&base).green());
116 println!(" New branch: {}", style(branch_name).green());
117 println!(" Path: {}\n", style(worktree_path.display()).blue());
118
119 let mut hook_ctx = build_hook_context(
121 branch_name,
122 &base,
123 &worktree_path,
124 &repo,
125 "worktree.pre_create",
126 "new",
127 );
128 hooks::run_hooks("worktree.pre_create", &hook_ctx, Some(&repo), Some(&repo))?;
129
130 if let Some(parent) = worktree_path.parent() {
132 let _ = std::fs::create_dir_all(parent);
133 }
134
135 let _ = git::git_command(&["fetch", "--all", "--prune"], Some(&repo), false, false);
137
138 let wt_str = worktree_path.to_string_lossy().to_string();
140 if is_remote_only {
141 git::git_command(
142 &[
143 "worktree",
144 "add",
145 "-b",
146 branch_name,
147 &wt_str,
148 &format!("origin/{}", branch_name),
149 ],
150 Some(&repo),
151 true,
152 false,
153 )?;
154 } else if branch_already_exists {
155 git::git_command(
156 &["worktree", "add", &wt_str, branch_name],
157 Some(&repo),
158 true,
159 false,
160 )?;
161 } else {
162 git::git_command(
163 &["worktree", "add", "-b", branch_name, &wt_str, &base],
164 Some(&repo),
165 true,
166 false,
167 )?;
168 }
169
170 let bb_key = format_config_key(CONFIG_KEY_BASE_BRANCH, branch_name);
172 let bp_key = format_config_key(CONFIG_KEY_BASE_PATH, branch_name);
173 let ib_key = format_config_key(CONFIG_KEY_INTENDED_BRANCH, branch_name);
174 git::set_config(&bb_key, &base, Some(&repo))?;
175 git::set_config(&bp_key, &repo.to_string_lossy(), Some(&repo))?;
176 git::set_config(&ib_key, branch_name, Some(&repo))?;
177
178 let _ = registry::register_repo(&repo);
180
181 println!(
182 "{} Worktree created successfully\n",
183 style("*").green().bold()
184 );
185
186 shared_files::share_files(&repo, &worktree_path);
188
189 hook_ctx.insert("event".into(), "worktree.post_create".into());
191 let _ = hooks::run_hooks(
192 "worktree.post_create",
193 &hook_ctx,
194 Some(&worktree_path),
195 Some(&repo),
196 );
197
198 if !no_ai {
200 let _ = super::ai_tools::launch_ai_tool(&worktree_path, _term, false, None);
201 }
202
203 Ok(worktree_path)
204}
205
206pub fn delete_worktree(
208 target: Option<&str>,
209 keep_branch: bool,
210 delete_remote: bool,
211 force: bool,
212 lookup_mode: Option<&str>,
213) -> Result<()> {
214 let main_repo = git::get_main_repo_root(None)?;
215 let (worktree_path, branch_name) = resolve_delete_target(target, &main_repo, lookup_mode)?;
216
217 let wt_resolved = git::canonicalize_or(&worktree_path);
219 let main_resolved = git::canonicalize_or(&main_repo);
220 if wt_resolved == main_resolved {
221 return Err(CwError::Git(messages::cannot_delete_main_worktree()));
222 }
223
224 if let Ok(cwd) = std::env::current_dir() {
226 let cwd_str = cwd.to_string_lossy().to_string();
227 let wt_str = worktree_path.to_string_lossy().to_string();
228 if cwd_str.starts_with(&wt_str) {
229 let _ = std::env::set_current_dir(&main_repo);
230 }
231 }
232
233 let base_branch = branch_name
235 .as_deref()
236 .and_then(|b| {
237 let key = format_config_key(CONFIG_KEY_BASE_BRANCH, b);
238 git::get_config(&key, Some(&main_repo))
239 })
240 .unwrap_or_default();
241
242 let mut hook_ctx = build_hook_context(
243 &branch_name.clone().unwrap_or_default(),
244 &base_branch,
245 &worktree_path,
246 &main_repo,
247 "worktree.pre_delete",
248 "delete",
249 );
250 hooks::run_hooks(
251 "worktree.pre_delete",
252 &hook_ctx,
253 Some(&main_repo),
254 Some(&main_repo),
255 )?;
256
257 println!(
259 "{}",
260 style(messages::removing_worktree(&worktree_path)).yellow()
261 );
262 git::remove_worktree_safe(&worktree_path, &main_repo, force)?;
263 println!("{} Worktree removed\n", style("*").green().bold());
264
265 if let Some(ref branch) = branch_name {
267 if !keep_branch {
268 println!(
269 "{}",
270 style(messages::deleting_local_branch(branch)).yellow()
271 );
272 let _ = git::git_command(&["branch", "-D", branch], Some(&main_repo), false, false);
273
274 let bb_key = format_config_key(CONFIG_KEY_BASE_BRANCH, branch);
276 let bp_key = format_config_key(CONFIG_KEY_BASE_PATH, branch);
277 let ib_key = format_config_key(CONFIG_KEY_INTENDED_BRANCH, branch);
278 git::unset_config(&bb_key, Some(&main_repo));
279 git::unset_config(&bp_key, Some(&main_repo));
280 git::unset_config(&ib_key, Some(&main_repo));
281
282 println!(
283 "{} Local branch and metadata removed\n",
284 style("*").green().bold()
285 );
286
287 if delete_remote {
289 println!(
290 "{}",
291 style(messages::deleting_remote_branch(branch)).yellow()
292 );
293 match git::git_command(
294 &["push", "origin", &format!(":{}", branch)],
295 Some(&main_repo),
296 false,
297 true,
298 ) {
299 Ok(r) if r.returncode == 0 => {
300 println!("{} Remote branch deleted\n", style("*").green().bold());
301 }
302 _ => {
303 println!("{} Remote branch deletion failed\n", style("!").yellow());
304 }
305 }
306 }
307 }
308 }
309
310 hook_ctx.insert("event".into(), "worktree.post_delete".into());
312 let _ = hooks::run_hooks(
313 "worktree.post_delete",
314 &hook_ctx,
315 Some(&main_repo),
316 Some(&main_repo),
317 );
318 let _ = registry::update_last_seen(&main_repo);
319
320 Ok(())
321}
322
323fn resolve_delete_target(
325 target: Option<&str>,
326 main_repo: &Path,
327 lookup_mode: Option<&str>,
328) -> Result<(PathBuf, Option<String>)> {
329 let target = target.map(|t| t.to_string()).unwrap_or_else(|| {
330 std::env::current_dir()
331 .unwrap_or_default()
332 .to_string_lossy()
333 .to_string()
334 });
335
336 let target_path = PathBuf::from(&target);
337
338 if target_path.exists() {
340 let resolved = target_path.canonicalize().unwrap_or(target_path);
341 let branch = super::helpers::get_branch_for_worktree(main_repo, &resolved);
342 return Ok((resolved, branch));
343 }
344
345 if lookup_mode != Some("worktree") {
347 if let Some(path) = git::find_worktree_by_intended_branch(main_repo, &target)? {
348 return Ok((path, Some(target)));
349 }
350 }
351
352 if lookup_mode != Some("branch") {
354 if let Some(path) = git::find_worktree_by_name(main_repo, &target)? {
355 let branch = super::helpers::get_branch_for_worktree(main_repo, &path);
356 return Ok((path, branch));
357 }
358 }
359
360 Err(CwError::WorktreeNotFound(messages::worktree_not_found(
361 &target,
362 )))
363}
364
365pub fn sync_worktree(
367 target: Option<&str>,
368 all: bool,
369 _fetch_only: bool,
370 ai_merge: bool,
371 lookup_mode: Option<&str>,
372) -> Result<()> {
373 let repo = git::get_repo_root(None)?;
374
375 println!("{}", style("Fetching updates from remote...").yellow());
377 let fetch_result = git::git_command(&["fetch", "--all", "--prune"], Some(&repo), false, true)?;
378 if fetch_result.returncode != 0 {
379 println!(
380 "{} Fetch failed or no remote configured\n",
381 style("!").yellow()
382 );
383 }
384
385 if _fetch_only {
386 println!("{} Fetch complete\n", style("*").green().bold());
387 return Ok(());
388 }
389
390 let worktrees_to_sync = if all {
392 let all_wt = git::parse_worktrees(&repo)?;
393 all_wt
394 .into_iter()
395 .filter(|(b, _)| b != "(detached)")
396 .map(|(b, p)| {
397 let branch = git::normalize_branch_name(&b).to_string();
398 (branch, p)
399 })
400 .collect::<Vec<_>>()
401 } else {
402 let resolved = resolve_worktree_target(target, lookup_mode)?;
403 vec![(resolved.branch, resolved.path)]
404 };
405
406 for (branch, wt_path) in &worktrees_to_sync {
407 let base_key = format_config_key(CONFIG_KEY_BASE_BRANCH, branch);
408 let base_branch = git::get_config(&base_key, Some(&repo));
409
410 if let Some(base) = base_branch {
411 println!("\n{}", style("Syncing worktree:").cyan().bold());
412 println!(" Branch: {}", style(branch).green());
413 println!(" Base: {}", style(&base).green());
414 println!(" Path: {}\n", style(wt_path.display()).blue());
415
416 let rebase_target = {
418 let origin_base = format!("origin/{}", base);
419 if git::branch_exists(&origin_base, Some(wt_path)) {
420 origin_base
421 } else {
422 base.clone()
423 }
424 };
425
426 println!(
427 "{}",
428 style(messages::rebase_in_progress(branch, &rebase_target)).yellow()
429 );
430
431 match git::git_command(&["rebase", &rebase_target], Some(wt_path), false, true) {
432 Ok(r) if r.returncode == 0 => {
433 println!("{} Rebase successful\n", style("*").green().bold());
434 }
435 _ => {
436 if ai_merge {
437 let conflicts = git::git_command(
438 &["diff", "--name-only", "--diff-filter=U"],
439 Some(wt_path),
440 false,
441 true,
442 )
443 .ok()
444 .and_then(|r| {
445 if r.returncode == 0 && !r.stdout.trim().is_empty() {
446 Some(r.stdout.trim().to_string())
447 } else {
448 None
449 }
450 });
451
452 let _ =
453 git::git_command(&["rebase", "--abort"], Some(wt_path), false, false);
454
455 let conflict_list = conflicts.as_deref().unwrap_or("(unknown)");
456 let prompt = format!(
457 "Resolve merge conflicts in this repository. The rebase of '{}' onto '{}' \
458 failed with conflicts in: {}\n\
459 Please examine the conflicted files and resolve them.",
460 branch, rebase_target, conflict_list
461 );
462
463 println!(
464 "\n{} Launching AI to resolve conflicts for '{}'...\n",
465 style("*").cyan().bold(),
466 branch
467 );
468 let _ =
469 super::ai_tools::launch_ai_tool(wt_path, None, false, Some(&prompt));
470 } else {
471 let _ =
473 git::git_command(&["rebase", "--abort"], Some(wt_path), false, false);
474 println!(
475 "{} Rebase failed for '{}'. Resolve conflicts manually.\n\
476 Tip: Use --ai-merge flag to get AI assistance with conflicts\n",
477 style("!").yellow(),
478 branch
479 );
480 }
481 }
482 }
483 } else {
484 let origin_ref = format!("origin/{}", branch);
486 if git::branch_exists(&origin_ref, Some(wt_path)) {
487 println!("\n{}", style("Syncing worktree:").cyan().bold());
488 println!(" Branch: {}", style(branch).green());
489 println!(" Path: {}\n", style(wt_path.display()).blue());
490
491 println!(
492 "{}",
493 style(messages::rebase_in_progress(branch, &origin_ref)).yellow()
494 );
495
496 match git::git_command(&["rebase", &origin_ref], Some(wt_path), false, true) {
497 Ok(r) if r.returncode == 0 => {
498 println!("{} Rebase successful\n", style("*").green().bold());
499 }
500 _ => {
501 let _ =
502 git::git_command(&["rebase", "--abort"], Some(wt_path), false, false);
503 println!(
504 "{} Rebase failed for '{}'. Resolve conflicts manually.\n",
505 style("!").yellow(),
506 branch
507 );
508 }
509 }
510 }
511 }
512 }
513
514 Ok(())
515}