Skip to main content

git_worktree_manager/operations/
path_cmd.rs

1/// Internal _path command for shell function integration.
2///
3/// Mirrors the _path command in cli.py — used by cw-cd shell function.
4use crate::error::{CwError, Result};
5use crate::git;
6use crate::registry;
7
8/// Resolve branch to path (outputs to stdout for shell consumption).
9pub 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    // Local mode
35    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    // Parse repo:branch notation
78    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    // Multiple matches
118    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(); // (label, path)
131
132    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    // Simple numbered selection (stderr for UI, stdout for path)
194    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}