git_worktree_manager/operations/
path_cmd.rs1use crate::error::{CwError, Result};
5use crate::git;
6use crate::registry;
7
8pub fn worktree_path(
10 branch: Option<&str>,
11 global_mode: bool,
12 list_branches: bool,
13 interactive: bool,
14) -> Result<()> {
15 if interactive {
16 return interactive_path_selection(global_mode);
17 }
18
19 if list_branches {
20 return list_branch_names(global_mode);
21 }
22
23 let branch = branch.ok_or_else(|| {
24 CwError::Git(
25 "branch argument is required (unless --list-branches or --interactive is used)"
26 .to_string(),
27 )
28 })?;
29
30 if global_mode {
31 return resolve_global_path(branch);
32 }
33
34 let repo = git::get_repo_root(None)?;
36 let normalized = git::normalize_branch_name(branch);
37 let path = git::find_worktree_by_branch(&repo, branch)?
38 .or(git::find_worktree_by_branch(
39 &repo,
40 &format!("refs/heads/{}", normalized),
41 )?)
42 .ok_or_else(|| CwError::Git(format!("No worktree found for branch '{}'", branch)))?;
43
44 println!("{}", path.display());
45 Ok(())
46}
47
48fn list_branch_names(global_mode: bool) -> Result<()> {
49 if global_mode {
50 let repos = registry::get_all_registered_repos();
51 for (name, repo_path) in &repos {
52 if !repo_path.exists() {
53 continue;
54 }
55 if let Ok(worktrees) = git::get_feature_worktrees(Some(repo_path)) {
56 for (branch, _) in &worktrees {
57 println!("{}:{}", name, branch);
58 }
59 }
60 }
61 } else if let Ok(repo) = git::get_repo_root(None) {
62 if let Ok(worktrees) = git::parse_worktrees(&repo) {
63 for (branch, _) in &worktrees {
64 let normalized = git::normalize_branch_name(branch);
65 if normalized != "(detached)" {
66 println!("{}", normalized);
67 }
68 }
69 }
70 }
71 Ok(())
72}
73
74fn resolve_global_path(branch: &str) -> Result<()> {
75 let repos = registry::get_all_registered_repos();
76
77 let (repo_filter, branch_target) = if let Some((r, b)) = branch.split_once(':') {
79 (Some(r), b)
80 } else {
81 (None, branch)
82 };
83
84 let mut matches: Vec<(std::path::PathBuf, String, String)> = Vec::new();
85
86 for (name, repo_path) in &repos {
87 if let Some(filter) = repo_filter {
88 if name != filter {
89 continue;
90 }
91 }
92 if !repo_path.exists() {
93 continue;
94 }
95
96 if let Ok(Some(path)) = git::find_worktree_by_branch(repo_path, branch_target) {
97 matches.push((path, branch_target.to_string(), name.clone()));
98 } else if let Ok(Some(path)) =
99 git::find_worktree_by_branch(repo_path, &format!("refs/heads/{}", branch_target))
100 {
101 matches.push((path, branch_target.to_string(), name.clone()));
102 }
103 }
104
105 if matches.is_empty() {
106 return Err(CwError::Git(format!(
107 "No worktree found for '{}' in any registered repository",
108 branch
109 )));
110 }
111
112 if matches.len() == 1 {
113 println!("{}", matches[0].0.display());
114 return Ok(());
115 }
116
117 eprintln!("Multiple worktrees found for '{}':", branch);
119 for (path, branch_name, repo_name) in &matches {
120 eprintln!(" {}:{} ({})", repo_name, branch_name, path.display());
121 }
122 eprintln!("Use 'repo:branch' notation to disambiguate.");
123 Err(CwError::Git(format!(
124 "Multiple worktrees found for '{}'",
125 branch
126 )))
127}
128
129fn interactive_path_selection(global_mode: bool) -> Result<()> {
130 let mut entries: Vec<(String, String)> = Vec::new(); if global_mode {
133 let repos = registry::get_all_registered_repos();
134 for (name, repo_path) in &repos {
135 if !repo_path.exists() {
136 continue;
137 }
138 if let Ok(worktrees) = git::parse_worktrees(repo_path) {
139 let repo_resolved = repo_path
140 .canonicalize()
141 .unwrap_or_else(|_| repo_path.clone());
142 for (branch, path) in &worktrees {
143 let normalized = git::normalize_branch_name(branch);
144 let path_resolved = path.canonicalize().unwrap_or_else(|_| path.clone());
145 if path_resolved == repo_resolved {
146 entries.insert(
147 0,
148 (
149 format!("{} (root)", name),
150 path.to_string_lossy().to_string(),
151 ),
152 );
153 } else if normalized != "(detached)" {
154 entries.push((
155 format!("{}:{}", name, normalized),
156 path.to_string_lossy().to_string(),
157 ));
158 }
159 }
160 }
161 }
162 } else {
163 let repo = git::get_main_repo_root(None)?;
164 let worktrees = git::parse_worktrees(&repo)?;
165 let repo_resolved = repo.canonicalize().unwrap_or_else(|_| repo.clone());
166
167 for (branch, path) in &worktrees {
168 let normalized = git::normalize_branch_name(branch);
169 let path_resolved = path.canonicalize().unwrap_or_else(|_| path.clone());
170 if path_resolved == repo_resolved {
171 let label = if normalized.is_empty() || normalized == "(detached)" {
172 "main (root)".to_string()
173 } else {
174 format!("{} (root)", normalized)
175 };
176 entries.insert(0, (label, path.to_string_lossy().to_string()));
177 } else if normalized != "(detached)" {
178 entries.push((normalized.to_string(), path.to_string_lossy().to_string()));
179 }
180 }
181 }
182
183 if entries.is_empty() {
184 eprintln!("No worktrees found.");
185 std::process::exit(1);
186 }
187
188 if entries.len() == 1 {
189 println!("{}", entries[0].1);
190 return Ok(());
191 }
192
193 if !atty_stderr() {
195 return Err(CwError::Git(
196 "Interactive mode requires a terminal (TTY)".to_string(),
197 ));
198 }
199
200 eprintln!("Select worktree:");
201 for (i, (label, _)) in entries.iter().enumerate() {
202 eprintln!(" [{}] {}", i + 1, label);
203 }
204 eprint!("Choice [1-{}]: ", entries.len());
205
206 let mut input = String::new();
207 std::io::stdin().read_line(&mut input)?;
208 let choice: usize = input.trim().parse().unwrap_or(0);
209
210 if choice >= 1 && choice <= entries.len() {
211 println!("{}", entries[choice - 1].1);
212 Ok(())
213 } else {
214 std::process::exit(1);
215 }
216}
217
218fn atty_stderr() -> bool {
219 std::io::IsTerminal::is_terminal(&std::io::stderr())
220}