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::messages;
7use crate::registry;
8
9/// Resolve branch to path (outputs to stdout for shell consumption).
10pub fn worktree_path(
11    branch: Option<&str>,
12    global_mode: bool,
13    list_branches: bool,
14    interactive: bool,
15) -> Result<()> {
16    if interactive {
17        return interactive_path_selection(global_mode);
18    }
19
20    if list_branches {
21        return list_branch_names(global_mode);
22    }
23
24    let branch = branch.ok_or_else(|| {
25        CwError::Git(
26            "branch argument is required (unless --list-branches or --interactive is used)"
27                .to_string(),
28        )
29    })?;
30
31    if global_mode {
32        return resolve_global_path(branch);
33    }
34
35    // Local mode
36    let repo = git::get_repo_root(None)?;
37    let normalized = git::normalize_branch_name(branch);
38    let path = git::find_worktree_by_branch(&repo, branch)?
39        .or(git::find_worktree_by_branch(
40            &repo,
41            &format!("refs/heads/{}", normalized),
42        )?)
43        .ok_or_else(|| CwError::Git(messages::worktree_not_found(branch)))?;
44
45    println!("{}", path.display());
46    Ok(())
47}
48
49fn list_branch_names(global_mode: bool) -> Result<()> {
50    if global_mode {
51        let repos = registry::get_all_registered_repos();
52        for (name, repo_path) in &repos {
53            if !repo_path.exists() {
54                continue;
55            }
56            if let Ok(worktrees) = git::get_feature_worktrees(Some(repo_path)) {
57                for (branch, _) in &worktrees {
58                    println!("{}:{}", name, branch);
59                }
60            }
61        }
62    } else if let Ok(repo) = git::get_repo_root(None) {
63        if let Ok(worktrees) = git::parse_worktrees(&repo) {
64            for (branch, _) in &worktrees {
65                let normalized = git::normalize_branch_name(branch);
66                if normalized != "(detached)" {
67                    println!("{}", normalized);
68                }
69            }
70        }
71    }
72    Ok(())
73}
74
75fn resolve_global_path(branch: &str) -> Result<()> {
76    let repos = registry::get_all_registered_repos();
77
78    // Parse repo:branch notation
79    let (repo_filter, branch_target) = if let Some((r, b)) = branch.split_once(':') {
80        (Some(r), b)
81    } else {
82        (None, branch)
83    };
84
85    let mut matches: Vec<(std::path::PathBuf, String, String)> = Vec::new();
86
87    for (name, repo_path) in &repos {
88        if let Some(filter) = repo_filter {
89            if name != filter {
90                continue;
91            }
92        }
93        if !repo_path.exists() {
94            continue;
95        }
96
97        if let Ok(Some(path)) = git::find_worktree_by_branch(repo_path, branch_target) {
98            matches.push((path, branch_target.to_string(), name.clone()));
99        } else if let Ok(Some(path)) =
100            git::find_worktree_by_branch(repo_path, &format!("refs/heads/{}", branch_target))
101        {
102            matches.push((path, branch_target.to_string(), name.clone()));
103        }
104    }
105
106    if matches.is_empty() {
107        return Err(CwError::Git(format!(
108            "No worktree found for '{}' in any registered repository",
109            branch
110        )));
111    }
112
113    if matches.len() == 1 {
114        println!("{}", matches[0].0.display());
115        return Ok(());
116    }
117
118    // Multiple matches
119    eprintln!("Multiple worktrees found for '{}':", branch);
120    for (path, branch_name, repo_name) in &matches {
121        eprintln!("  {}:{}  ({})", repo_name, branch_name, path.display());
122    }
123    eprintln!("Use 'repo:branch' notation to disambiguate.");
124    Err(CwError::Git(format!(
125        "Multiple worktrees found for '{}'",
126        branch
127    )))
128}
129
130fn interactive_path_selection(global_mode: bool) -> Result<()> {
131    let mut entries: Vec<(String, String)> = Vec::new(); // (label, path)
132
133    if global_mode {
134        let repos = registry::get_all_registered_repos();
135        for (name, repo_path) in &repos {
136            if !repo_path.exists() {
137                continue;
138            }
139            if let Ok(worktrees) = git::parse_worktrees(repo_path) {
140                let repo_resolved = git::canonicalize_or(repo_path);
141                for (branch, path) in &worktrees {
142                    let normalized = git::normalize_branch_name(branch);
143                    let path_resolved = git::canonicalize_or(path);
144                    if path_resolved == repo_resolved {
145                        entries.insert(
146                            0,
147                            (
148                                format!("{} (root)", name),
149                                path.to_string_lossy().to_string(),
150                            ),
151                        );
152                    } else if normalized != "(detached)" {
153                        entries.push((
154                            format!("{}:{}", name, normalized),
155                            path.to_string_lossy().to_string(),
156                        ));
157                    }
158                }
159            }
160        }
161    } else {
162        let repo = git::get_main_repo_root(None)?;
163        let worktrees = git::parse_worktrees(&repo)?;
164        let repo_resolved = git::canonicalize_or(&repo);
165
166        for (branch, path) in &worktrees {
167            let normalized = git::normalize_branch_name(branch);
168            let path_resolved = git::canonicalize_or(path);
169            if path_resolved == repo_resolved {
170                let label = if normalized.is_empty() || normalized == "(detached)" {
171                    "main (root)".to_string()
172                } else {
173                    format!("{} (root)", normalized)
174                };
175                entries.insert(0, (label, path.to_string_lossy().to_string()));
176            } else if normalized != "(detached)" {
177                entries.push((normalized.to_string(), path.to_string_lossy().to_string()));
178            }
179        }
180    }
181
182    if entries.is_empty() {
183        eprintln!("No worktrees found.");
184        std::process::exit(1);
185    }
186
187    if entries.len() == 1 {
188        println!("{}", entries[0].1);
189        return Ok(());
190    }
191
192    // Try arrow-key TUI selector, fall back to numbered selection
193    match crate::tui::arrow_select(&entries, "Select worktree:", 0) {
194        Some(selected_path) => {
195            println!("{}", selected_path);
196            Ok(())
197        }
198        None => {
199            std::process::exit(1);
200        }
201    }
202}