1use std::collections::HashMap;
5use std::path::{Path, PathBuf};
6
7use console::style;
8
9use crate::constants::{
10 default_worktree_path, format_config_key, CONFIG_KEY_BASE_BRANCH, CONFIG_KEY_BASE_PATH,
11 CONFIG_KEY_INTENDED_BRANCH,
12};
13use crate::error::{CwError, Result};
14use crate::git;
15use crate::hooks;
16use crate::registry;
17use crate::shared_files;
18
19use super::helpers::resolve_worktree_target;
20
21pub 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) -> 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(format!(
35 "Invalid branch name: {}\n\
36 Hint: Use alphanumeric characters, hyphens, and slashes.",
37 error_msg
38 )));
39 }
40
41 let existing = git::find_worktree_by_branch(&repo, branch_name)?.or(
43 git::find_worktree_by_branch(&repo, &format!("refs/heads/{}", branch_name))?,
44 );
45
46 if let Some(existing_path) = existing {
47 println!(
48 "\n{}\nBranch '{}' already has a worktree at:\n {}\n",
49 style("! Worktree already exists").yellow().bold(),
50 style(branch_name).cyan(),
51 style(existing_path.display()).blue(),
52 );
53
54 if git::is_non_interactive() {
55 return Err(CwError::InvalidBranch(format!(
56 "Worktree for branch '{}' already exists at {}.\n\
57 Use 'gw resume {}' to continue work.",
58 branch_name,
59 existing_path.display(),
60 branch_name,
61 )));
62 }
63
64 println!(
66 "Use '{}' to resume work in this worktree.\n",
67 style(format!("gw resume {}", branch_name)).cyan()
68 );
69 return Ok(existing_path);
70 }
71
72 let mut branch_already_exists = false;
74 let mut is_remote_only = false;
75
76 if git::branch_exists(branch_name, Some(&repo)) {
77 println!(
78 "\n{}\nBranch '{}' already exists locally but has no worktree.\n",
79 style("! Branch already exists").yellow().bold(),
80 style(branch_name).cyan(),
81 );
82 branch_already_exists = true;
83 } else if git::remote_branch_exists(branch_name, Some(&repo), "origin") {
84 println!(
85 "\n{}\nBranch '{}' exists on remote but not locally.\n",
86 style("! Remote branch found").yellow().bold(),
87 style(branch_name).cyan(),
88 );
89 branch_already_exists = true;
90 is_remote_only = true;
91 }
92
93 let base = if is_remote_only && base_branch.is_none() {
95 git::get_current_branch(Some(&repo)).unwrap_or_else(|_| "main".to_string())
96 } else if let Some(b) = base_branch {
97 b.to_string()
98 } else {
99 git::get_current_branch(Some(&repo)).map_err(|_| {
100 CwError::InvalidBranch(
101 "Cannot determine base branch. Specify with --branch or checkout a branch first."
102 .to_string(),
103 )
104 })?
105 };
106
107 if (!is_remote_only || base_branch.is_some()) && !git::branch_exists(&base, Some(&repo)) {
109 return Err(CwError::InvalidBranch(format!(
110 "Base branch '{}' not found",
111 base
112 )));
113 }
114
115 let worktree_path = if let Some(p) = path {
117 PathBuf::from(p)
118 .canonicalize()
119 .unwrap_or_else(|_| PathBuf::from(p))
120 } else {
121 default_worktree_path(&repo, branch_name)
122 };
123
124 println!("\n{}", style("Creating new worktree:").cyan().bold());
125 println!(" Base branch: {}", style(&base).green());
126 println!(" New branch: {}", style(branch_name).green());
127 println!(" Path: {}\n", style(worktree_path.display()).blue());
128
129 let mut hook_ctx = HashMap::new();
131 hook_ctx.insert("branch".into(), branch_name.to_string());
132 hook_ctx.insert("base_branch".into(), base.clone());
133 hook_ctx.insert(
134 "worktree_path".into(),
135 worktree_path.to_string_lossy().to_string(),
136 );
137 hook_ctx.insert("repo_path".into(), repo.to_string_lossy().to_string());
138 hook_ctx.insert("event".into(), "worktree.pre_create".into());
139 hook_ctx.insert("operation".into(), "new".into());
140 hooks::run_hooks("worktree.pre_create", &hook_ctx, Some(&repo), Some(&repo))?;
141
142 if let Some(parent) = worktree_path.parent() {
144 let _ = std::fs::create_dir_all(parent);
145 }
146
147 let _ = git::git_command(&["fetch", "--all", "--prune"], Some(&repo), false, false);
149
150 let wt_str = worktree_path.to_string_lossy().to_string();
152 if is_remote_only {
153 git::git_command(
154 &[
155 "worktree",
156 "add",
157 "-b",
158 branch_name,
159 &wt_str,
160 &format!("origin/{}", branch_name),
161 ],
162 Some(&repo),
163 true,
164 false,
165 )?;
166 } else if branch_already_exists {
167 git::git_command(
168 &["worktree", "add", &wt_str, branch_name],
169 Some(&repo),
170 true,
171 false,
172 )?;
173 } else {
174 git::git_command(
175 &["worktree", "add", "-b", branch_name, &wt_str, &base],
176 Some(&repo),
177 true,
178 false,
179 )?;
180 }
181
182 let bb_key = format_config_key(CONFIG_KEY_BASE_BRANCH, branch_name);
184 let bp_key = format_config_key(CONFIG_KEY_BASE_PATH, branch_name);
185 let ib_key = format_config_key(CONFIG_KEY_INTENDED_BRANCH, branch_name);
186 git::set_config(&bb_key, &base, Some(&repo))?;
187 git::set_config(&bp_key, &repo.to_string_lossy(), Some(&repo))?;
188 git::set_config(&ib_key, branch_name, Some(&repo))?;
189
190 let _ = registry::register_repo(&repo);
192
193 println!(
194 "{} Worktree created successfully\n",
195 style("*").green().bold()
196 );
197
198 shared_files::share_files(&repo, &worktree_path);
200
201 hook_ctx.insert("event".into(), "worktree.post_create".into());
203 let _ = hooks::run_hooks(
204 "worktree.post_create",
205 &hook_ctx,
206 Some(&worktree_path),
207 Some(&repo),
208 );
209
210 if !no_ai {
212 }
214
215 Ok(worktree_path)
216}
217
218pub fn delete_worktree(target: Option<&str>, keep_branch: bool, delete_remote: bool) -> Result<()> {
220 let main_repo = git::get_main_repo_root(None)?;
221 let (worktree_path, branch_name) = resolve_delete_target(target, &main_repo)?;
222
223 let wt_resolved = worktree_path
225 .canonicalize()
226 .unwrap_or_else(|_| worktree_path.clone());
227 let main_resolved = main_repo
228 .canonicalize()
229 .unwrap_or_else(|_| main_repo.clone());
230 if wt_resolved == main_resolved {
231 return Err(CwError::Git(
232 "Cannot delete main repository worktree".to_string(),
233 ));
234 }
235
236 if let Ok(cwd) = std::env::current_dir() {
238 let cwd_str = cwd.to_string_lossy().to_string();
239 let wt_str = worktree_path.to_string_lossy().to_string();
240 if cwd_str.starts_with(&wt_str) {
241 let _ = std::env::set_current_dir(&main_repo);
242 }
243 }
244
245 let base_branch = branch_name
247 .as_deref()
248 .and_then(|b| {
249 let key = format_config_key(CONFIG_KEY_BASE_BRANCH, b);
250 git::get_config(&key, Some(&main_repo))
251 })
252 .unwrap_or_default();
253
254 let mut hook_ctx = HashMap::new();
255 hook_ctx.insert("branch".into(), branch_name.clone().unwrap_or_default());
256 hook_ctx.insert("base_branch".into(), base_branch);
257 hook_ctx.insert(
258 "worktree_path".into(),
259 worktree_path.to_string_lossy().to_string(),
260 );
261 hook_ctx.insert("repo_path".into(), main_repo.to_string_lossy().to_string());
262 hook_ctx.insert("event".into(), "worktree.pre_delete".into());
263 hook_ctx.insert("operation".into(), "delete".into());
264 hooks::run_hooks(
265 "worktree.pre_delete",
266 &hook_ctx,
267 Some(&main_repo),
268 Some(&main_repo),
269 )?;
270
271 println!(
273 "{}",
274 style(format!("Removing worktree: {}", worktree_path.display())).yellow()
275 );
276 git::remove_worktree_safe(&worktree_path, &main_repo, true)?;
277 println!("{} Worktree removed\n", style("*").green().bold());
278
279 if let Some(ref branch) = branch_name {
281 if !keep_branch {
282 println!(
283 "{}",
284 style(format!("Deleting local branch: {}", branch)).yellow()
285 );
286 let _ = git::git_command(&["branch", "-D", branch], Some(&main_repo), false, false);
287
288 let bb_key = format_config_key(CONFIG_KEY_BASE_BRANCH, branch);
290 let bp_key = format_config_key(CONFIG_KEY_BASE_PATH, branch);
291 let ib_key = format_config_key(CONFIG_KEY_INTENDED_BRANCH, branch);
292 git::unset_config(&bb_key, Some(&main_repo));
293 git::unset_config(&bp_key, Some(&main_repo));
294 git::unset_config(&ib_key, Some(&main_repo));
295
296 println!(
297 "{} Local branch and metadata removed\n",
298 style("*").green().bold()
299 );
300
301 if delete_remote {
303 println!(
304 "{}",
305 style(format!("Deleting remote branch: origin/{}", branch)).yellow()
306 );
307 match git::git_command(
308 &["push", "origin", &format!(":{}", branch)],
309 Some(&main_repo),
310 false,
311 true,
312 ) {
313 Ok(r) if r.returncode == 0 => {
314 println!("{} Remote branch deleted\n", style("*").green().bold());
315 }
316 _ => {
317 println!("{} Remote branch deletion failed\n", style("!").yellow());
318 }
319 }
320 }
321 }
322 }
323
324 hook_ctx.insert("event".into(), "worktree.post_delete".into());
326 let _ = hooks::run_hooks(
327 "worktree.post_delete",
328 &hook_ctx,
329 Some(&main_repo),
330 Some(&main_repo),
331 );
332 let _ = registry::update_last_seen(&main_repo);
333
334 Ok(())
335}
336
337fn resolve_delete_target(
339 target: Option<&str>,
340 main_repo: &Path,
341) -> Result<(PathBuf, Option<String>)> {
342 let target = target.map(|t| t.to_string()).unwrap_or_else(|| {
343 std::env::current_dir()
344 .unwrap_or_default()
345 .to_string_lossy()
346 .to_string()
347 });
348
349 let target_path = PathBuf::from(&target);
350
351 if target_path.exists() {
353 let resolved = target_path.canonicalize().unwrap_or(target_path);
354 let branch = super::helpers::get_branch_for_worktree(main_repo, &resolved);
355 return Ok((resolved, branch));
356 }
357
358 if let Some(path) = git::find_worktree_by_intended_branch(main_repo, &target)? {
360 return Ok((path, Some(target)));
361 }
362
363 if let Some(path) = git::find_worktree_by_name(main_repo, &target)? {
365 let branch = super::helpers::get_branch_for_worktree(main_repo, &path);
366 return Ok((path, branch));
367 }
368
369 Err(CwError::WorktreeNotFound(format!(
370 "No worktree found for '{}'. Try: full path, branch name, or worktree name.",
371 target
372 )))
373}
374
375pub fn sync_worktree(target: Option<&str>, all: bool, _fetch_only: bool) -> Result<()> {
377 let repo = git::get_repo_root(None)?;
378
379 println!("{}", style("Fetching updates from remote...").yellow());
381 let fetch_result = git::git_command(&["fetch", "--all", "--prune"], Some(&repo), false, true)?;
382 if fetch_result.returncode != 0 {
383 println!(
384 "{} Fetch failed or no remote configured\n",
385 style("!").yellow()
386 );
387 }
388
389 if _fetch_only {
390 println!("{} Fetch complete\n", style("*").green().bold());
391 return Ok(());
392 }
393
394 let worktrees_to_sync = if all {
396 let all_wt = git::parse_worktrees(&repo)?;
397 all_wt
398 .into_iter()
399 .filter(|(b, _)| b != "(detached)")
400 .map(|(b, p)| {
401 let branch = git::normalize_branch_name(&b).to_string();
402 (branch, p)
403 })
404 .collect::<Vec<_>>()
405 } else {
406 let (path, branch, _) = resolve_worktree_target(target, None)?;
407 vec![(branch, path)]
408 };
409
410 for (branch, wt_path) in &worktrees_to_sync {
411 let base_key = format_config_key(CONFIG_KEY_BASE_BRANCH, branch);
412 let base_branch = git::get_config(&base_key, Some(&repo));
413
414 if let Some(base) = base_branch {
415 println!("\n{}", style("Syncing worktree:").cyan().bold());
416 println!(" Branch: {}", style(branch).green());
417 println!(" Base: {}", style(&base).green());
418 println!(" Path: {}\n", style(wt_path.display()).blue());
419
420 let rebase_target = {
422 let origin_base = format!("origin/{}", base);
423 if git::branch_exists(&origin_base, Some(wt_path)) {
424 origin_base
425 } else {
426 base.clone()
427 }
428 };
429
430 println!(
431 "{}",
432 style(format!("Rebasing {} onto {}...", branch, rebase_target)).yellow()
433 );
434
435 match git::git_command(&["rebase", &rebase_target], Some(wt_path), false, true) {
436 Ok(r) if r.returncode == 0 => {
437 println!("{} Rebase successful\n", style("*").green().bold());
438 }
439 _ => {
440 let _ = git::git_command(&["rebase", "--abort"], Some(wt_path), false, false);
442 println!(
443 "{} Rebase failed for '{}'. Resolve conflicts manually.\n",
444 style("!").yellow(),
445 branch
446 );
447 }
448 }
449 } else {
450 let origin_ref = format!("origin/{}", branch);
452 if git::branch_exists(&origin_ref, Some(wt_path)) {
453 println!("\n{}", style("Syncing worktree:").cyan().bold());
454 println!(" Branch: {}", style(branch).green());
455 println!(" Path: {}\n", style(wt_path.display()).blue());
456
457 println!(
458 "{}",
459 style(format!("Rebasing {} onto {}...", branch, origin_ref)).yellow()
460 );
461
462 match git::git_command(&["rebase", &origin_ref], Some(wt_path), false, true) {
463 Ok(r) if r.returncode == 0 => {
464 println!("{} Rebase successful\n", style("*").green().bold());
465 }
466 _ => {
467 let _ =
468 git::git_command(&["rebase", "--abort"], Some(wt_path), false, false);
469 println!(
470 "{} Rebase failed for '{}'. Resolve conflicts manually.\n",
471 style("!").yellow(),
472 branch
473 );
474 }
475 }
476 }
477 }
478 }
479
480 Ok(())
481}