Skip to main content

git_worktree_manager/operations/
helpers.rs

1/// Helper functions shared across operations modules.
2///
3/// Mirrors src/git_worktree_manager/operations/helpers.py (444 lines).
4use std::path::{Path, PathBuf};
5
6use crate::constants::{format_config_key, CONFIG_KEY_BASE_BRANCH, CONFIG_KEY_BASE_PATH};
7use crate::error::{CwError, Result};
8use crate::git;
9
10// Thread-local global mode flag.
11std::thread_local! {
12    static GLOBAL_MODE: std::cell::Cell<bool> = const { std::cell::Cell::new(false) };
13}
14
15pub fn set_global_mode(enabled: bool) {
16    GLOBAL_MODE.with(|g| g.set(enabled));
17}
18
19pub fn is_global_mode() -> bool {
20    GLOBAL_MODE.with(|g| g.get())
21}
22
23/// Parse 'repo:branch' notation.
24pub fn parse_repo_branch_target(target: &str) -> (Option<&str>, &str) {
25    if let Some((repo, branch)) = target.split_once(':') {
26        if !repo.is_empty() && !branch.is_empty() {
27            return (Some(repo), branch);
28        }
29    }
30    (None, target)
31}
32
33/// Get the branch for a worktree path from parse_worktrees output.
34pub fn get_branch_for_worktree(repo: &Path, worktree_path: &Path) -> Option<String> {
35    let worktrees = git::parse_worktrees(repo).ok()?;
36    let resolved = worktree_path
37        .canonicalize()
38        .unwrap_or_else(|_| worktree_path.to_path_buf());
39
40    for (branch, path) in &worktrees {
41        let p_resolved = path.canonicalize().unwrap_or_else(|_| path.clone());
42        if p_resolved == resolved {
43            if branch == "(detached)" {
44                return None;
45            }
46            return Some(git::normalize_branch_name(branch).to_string());
47        }
48    }
49    None
50}
51
52/// Resolve worktree target to (worktree_path, branch_name, worktree_repo).
53///
54/// Supports branch name lookup, worktree directory name lookup,
55/// and disambiguation when both match.
56pub fn resolve_worktree_target(
57    target: Option<&str>,
58    _lookup_mode: Option<&str>,
59) -> Result<(PathBuf, String, PathBuf)> {
60    if target.is_none() && is_global_mode() {
61        return Err(CwError::WorktreeNotFound(
62            "Global mode requires an explicit target (branch or worktree name).".to_string(),
63        ));
64    }
65
66    if target.is_none() {
67        // Use current directory
68        let cwd = std::env::current_dir()?;
69        let branch = git::get_current_branch(Some(&cwd))?;
70        let repo = git::get_repo_root(Some(&cwd))?;
71        return Ok((cwd, branch, repo));
72    }
73
74    let target = target.unwrap();
75
76    // Global mode: search all registered repositories
77    if is_global_mode() {
78        return resolve_global_target(target, _lookup_mode);
79    }
80
81    let main_repo = git::get_main_repo_root(None)?;
82
83    // Try branch lookup
84    let branch_match = git::find_worktree_by_intended_branch(&main_repo, target)?;
85
86    // Try worktree name lookup
87    let worktree_match = git::find_worktree_by_name(&main_repo, target)?;
88
89    match (branch_match, worktree_match) {
90        (Some(bp), Some(wp)) => {
91            let bp_resolved = bp.canonicalize().unwrap_or_else(|_| bp.clone());
92            let wp_resolved = wp.canonicalize().unwrap_or_else(|_| wp.clone());
93            if bp_resolved == wp_resolved {
94                let repo = git::get_repo_root(Some(&bp))?;
95                Ok((bp, target.to_string(), repo))
96            } else {
97                // Ambiguous — in non-interactive mode, prefer branch match
98                if git::is_non_interactive() {
99                    let repo = git::get_repo_root(Some(&bp))?;
100                    Ok((bp, target.to_string(), repo))
101                } else {
102                    // Default to branch match
103                    let repo = git::get_repo_root(Some(&bp))?;
104                    Ok((bp, target.to_string(), repo))
105                }
106            }
107        }
108        (Some(bp), None) => {
109            let repo = git::get_repo_root(Some(&bp))?;
110            Ok((bp, target.to_string(), repo))
111        }
112        (None, Some(wp)) => {
113            let branch =
114                get_branch_for_worktree(&main_repo, &wp).unwrap_or_else(|| target.to_string());
115            let repo = git::get_repo_root(Some(&wp))?;
116            Ok((wp, branch, repo))
117        }
118        (None, None) => Err(CwError::WorktreeNotFound(format!(
119            "No worktree found for '{}'. \
120             Try: full path, branch name, or worktree name.",
121            target
122        ))),
123    }
124}
125
126/// Global mode target resolution.
127fn resolve_global_target(
128    target: &str,
129    _lookup_mode: Option<&str>,
130) -> Result<(PathBuf, String, PathBuf)> {
131    let repos = crate::registry::get_all_registered_repos();
132    let (repo_filter, branch_target) = parse_repo_branch_target(target);
133
134    for (name, repo_path) in &repos {
135        if let Some(filter) = repo_filter {
136            if name != filter {
137                continue;
138            }
139        }
140        if !repo_path.exists() {
141            continue;
142        }
143
144        // Try branch lookup
145        if let Ok(Some(path)) = git::find_worktree_by_intended_branch(repo_path, branch_target) {
146            let repo = git::get_repo_root(Some(&path)).unwrap_or(repo_path.clone());
147            return Ok((path, branch_target.to_string(), repo));
148        }
149
150        // Try worktree name lookup
151        if let Ok(Some(path)) = git::find_worktree_by_name(repo_path, branch_target) {
152            let branch = get_branch_for_worktree(repo_path, &path)
153                .unwrap_or_else(|| branch_target.to_string());
154            let repo = git::get_repo_root(Some(&path)).unwrap_or(repo_path.clone());
155            return Ok((path, branch, repo));
156        }
157    }
158
159    Err(CwError::WorktreeNotFound(format!(
160        "'{}' not found in any registered repository. Run 'gw scan' to register repos.",
161        target
162    )))
163}
164
165/// Get worktree metadata (base branch and base repository path).
166///
167/// If metadata is missing, tries to infer from common defaults.
168pub fn get_worktree_metadata(branch: &str, repo: &Path) -> Result<(String, PathBuf)> {
169    let base_key = format_config_key(CONFIG_KEY_BASE_BRANCH, branch);
170    let path_key = format_config_key(CONFIG_KEY_BASE_PATH, branch);
171
172    let base_branch = git::get_config(&base_key, Some(repo));
173    let base_path_str = git::get_config(&path_key, Some(repo));
174
175    if let (Some(bb), Some(bp)) = (base_branch, base_path_str) {
176        return Ok((bb, PathBuf::from(bp)));
177    }
178
179    // Metadata missing — try to infer
180    eprintln!(
181        "Warning: Metadata missing for branch '{}'. Attempting to infer...",
182        branch
183    );
184
185    // Infer base_path from first worktree entry
186    let worktrees = git::parse_worktrees(repo)?;
187    let inferred_base_path = worktrees.first().map(|(_, p)| p.clone()).ok_or_else(|| {
188        CwError::Git(format!(
189            "Cannot infer base repository path for branch '{}'. Use 'gw new' to create worktrees.",
190            branch
191        ))
192    })?;
193
194    // Infer base_branch from common defaults
195    let mut inferred_base_branch: Option<String> = None;
196    for candidate in &["main", "master", "develop"] {
197        if git::branch_exists(candidate, Some(&inferred_base_path)) {
198            inferred_base_branch = Some(candidate.to_string());
199            break;
200        }
201    }
202
203    if inferred_base_branch.is_none() {
204        if let Some((first_branch, _)) = worktrees.first() {
205            if first_branch != "(detached)" {
206                inferred_base_branch = Some(git::normalize_branch_name(first_branch).to_string());
207            }
208        }
209    }
210
211    let base = inferred_base_branch.ok_or_else(|| {
212        CwError::Git(format!(
213            "Cannot infer base branch for '{}'. Use 'gw new' to create worktrees.",
214            branch
215        ))
216    })?;
217
218    eprintln!("  Inferred base branch: {}", base);
219    eprintln!("  Inferred base path: {}", inferred_base_path.display());
220
221    Ok((base, inferred_base_path))
222}