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