Skip to main content

git_worktree_manager/operations/
helpers.rs

1/// Helper functions shared across operations modules.
2///
3use std::collections::HashMap;
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;
9use crate::messages;
10
11/// Resolved worktree target with named fields for clarity.
12pub struct ResolvedTarget {
13    pub path: PathBuf,
14    pub branch: String,
15    pub repo: PathBuf,
16}
17
18// Thread-local global mode flag.
19std::thread_local! {
20    static GLOBAL_MODE: std::cell::Cell<bool> = const { std::cell::Cell::new(false) };
21}
22
23pub fn set_global_mode(enabled: bool) {
24    GLOBAL_MODE.with(|g| g.set(enabled));
25}
26
27pub fn is_global_mode() -> bool {
28    GLOBAL_MODE.with(|g| g.get())
29}
30
31/// Parse 'repo:branch' notation.
32pub fn parse_repo_branch_target(target: &str) -> (Option<&str>, &str) {
33    if let Some((repo, branch)) = target.split_once(':') {
34        if !repo.is_empty() && !branch.is_empty() {
35            return (Some(repo), branch);
36        }
37    }
38    (None, target)
39}
40
41/// Get the branch for a worktree path from parse_worktrees output.
42pub fn get_branch_for_worktree(repo: &Path, worktree_path: &Path) -> Option<String> {
43    let worktrees = git::parse_worktrees(repo).ok()?;
44    let resolved = git::canonicalize_or(worktree_path);
45
46    for (branch, path) in &worktrees {
47        let p_resolved = git::canonicalize_or(path);
48        if p_resolved == resolved {
49            if branch == "(detached)" {
50                return None;
51            }
52            return Some(git::normalize_branch_name(branch).to_string());
53        }
54    }
55    None
56}
57
58/// Resolve worktree target to a [`ResolvedTarget`] with path, branch, and repo.
59///
60/// Supports branch name lookup, worktree directory name lookup,
61/// and disambiguation when both match.
62pub fn resolve_worktree_target(
63    target: Option<&str>,
64    lookup_mode: Option<&str>,
65) -> Result<ResolvedTarget> {
66    if target.is_none() && is_global_mode() {
67        return Err(CwError::WorktreeNotFound(
68            "Global mode requires an explicit target (branch or worktree name).".to_string(),
69        ));
70    }
71
72    if target.is_none() {
73        // Use current directory
74        let cwd = std::env::current_dir()?;
75        let branch = git::get_current_branch(Some(&cwd))?;
76        let repo = git::get_repo_root(Some(&cwd))?;
77        return Ok(ResolvedTarget {
78            path: cwd,
79            branch,
80            repo,
81        });
82    }
83
84    let target = target.unwrap();
85
86    // Global mode: search all registered repositories
87    if is_global_mode() {
88        return resolve_global_target(target, lookup_mode);
89    }
90
91    let main_repo = git::get_main_repo_root(None)?;
92
93    // Try branch lookup (skip if lookup_mode is "worktree")
94    let branch_match = if lookup_mode != Some("worktree") {
95        git::find_worktree_by_intended_branch(&main_repo, target)?
96    } else {
97        None
98    };
99
100    // Try worktree name lookup (skip if lookup_mode is "branch")
101    let worktree_match = if lookup_mode != Some("branch") {
102        git::find_worktree_by_name(&main_repo, target)?
103    } else {
104        None
105    };
106
107    match (branch_match, worktree_match) {
108        (Some(bp), Some(wp)) => {
109            let bp_resolved = git::canonicalize_or(&bp);
110            let wp_resolved = git::canonicalize_or(&wp);
111            if bp_resolved == wp_resolved {
112                let repo = git::get_repo_root(Some(&bp))?;
113                Ok(ResolvedTarget {
114                    path: bp,
115                    branch: target.to_string(),
116                    repo,
117                })
118            } else {
119                // Ambiguous — prefer branch match
120                let repo = git::get_repo_root(Some(&bp))?;
121                Ok(ResolvedTarget {
122                    path: bp,
123                    branch: target.to_string(),
124                    repo,
125                })
126            }
127        }
128        (Some(bp), None) => {
129            let repo = git::get_repo_root(Some(&bp))?;
130            Ok(ResolvedTarget {
131                path: bp,
132                branch: target.to_string(),
133                repo,
134            })
135        }
136        (None, Some(wp)) => {
137            let branch =
138                get_branch_for_worktree(&main_repo, &wp).unwrap_or_else(|| target.to_string());
139            let repo = git::get_repo_root(Some(&wp))?;
140            Ok(ResolvedTarget {
141                path: wp,
142                branch,
143                repo,
144            })
145        }
146        (None, None) => Err(CwError::WorktreeNotFound(messages::worktree_not_found(
147            target,
148        ))),
149    }
150}
151
152/// Global mode target resolution.
153fn resolve_global_target(target: &str, lookup_mode: Option<&str>) -> Result<ResolvedTarget> {
154    let repos = crate::registry::get_all_registered_repos();
155    let (repo_filter, branch_target) = parse_repo_branch_target(target);
156
157    for (name, repo_path) in &repos {
158        if let Some(filter) = repo_filter {
159            if name != filter {
160                continue;
161            }
162        }
163        if !repo_path.exists() {
164            continue;
165        }
166
167        // Try branch lookup (skip if lookup_mode is "worktree")
168        if lookup_mode != Some("worktree") {
169            if let Ok(Some(path)) = git::find_worktree_by_intended_branch(repo_path, branch_target)
170            {
171                let repo = git::get_repo_root(Some(&path)).unwrap_or(repo_path.clone());
172                return Ok(ResolvedTarget {
173                    path,
174                    branch: branch_target.to_string(),
175                    repo,
176                });
177            }
178        }
179
180        // Try worktree name lookup (skip if lookup_mode is "branch")
181        if lookup_mode != Some("branch") {
182            if let Ok(Some(path)) = git::find_worktree_by_name(repo_path, branch_target) {
183                let branch = get_branch_for_worktree(repo_path, &path)
184                    .unwrap_or_else(|| branch_target.to_string());
185                let repo = git::get_repo_root(Some(&path)).unwrap_or(repo_path.clone());
186                return Ok(ResolvedTarget { path, branch, repo });
187            }
188        }
189    }
190
191    Err(CwError::WorktreeNotFound(format!(
192        "'{}' not found in any registered repository. Run 'gw scan' to register repos.",
193        target
194    )))
195}
196
197/// Get worktree metadata (base branch and base repository path).
198///
199/// If metadata is missing, tries to infer from common defaults.
200pub fn get_worktree_metadata(branch: &str, repo: &Path) -> Result<(String, PathBuf)> {
201    let base_key = format_config_key(CONFIG_KEY_BASE_BRANCH, branch);
202    let path_key = format_config_key(CONFIG_KEY_BASE_PATH, branch);
203
204    let base_branch = git::get_config(&base_key, Some(repo));
205    let base_path_str = git::get_config(&path_key, Some(repo));
206
207    if let (Some(bb), Some(bp)) = (base_branch, base_path_str) {
208        return Ok((bb, PathBuf::from(bp)));
209    }
210
211    // Metadata missing — try to infer
212    eprintln!(
213        "Warning: Metadata missing for branch '{}'. Attempting to infer...",
214        branch
215    );
216
217    // Infer base_path from first worktree entry
218    let worktrees = git::parse_worktrees(repo)?;
219    let inferred_base_path = worktrees.first().map(|(_, p)| p.clone()).ok_or_else(|| {
220        CwError::Git(format!(
221            "Cannot infer base repository path for branch '{}'. Use 'gw new' to create worktrees.",
222            branch
223        ))
224    })?;
225
226    // Infer base_branch from common defaults
227    let mut inferred_base_branch: Option<String> = None;
228    for candidate in &["main", "master", "develop"] {
229        if git::branch_exists(candidate, Some(&inferred_base_path)) {
230            inferred_base_branch = Some(candidate.to_string());
231            break;
232        }
233    }
234
235    if inferred_base_branch.is_none() {
236        if let Some((first_branch, _)) = worktrees.first() {
237            if first_branch != "(detached)" {
238                inferred_base_branch = Some(git::normalize_branch_name(first_branch).to_string());
239            }
240        }
241    }
242
243    let base = inferred_base_branch.ok_or_else(|| {
244        CwError::Git(format!(
245            "Cannot infer base branch for '{}'. Use 'gw new' to create worktrees.",
246            branch
247        ))
248    })?;
249
250    eprintln!("  Inferred base branch: {}", base);
251    eprintln!("  Inferred base path: {}", inferred_base_path.display());
252
253    Ok((base, inferred_base_path))
254}
255
256/// Build a hook context HashMap with standard fields.
257pub fn build_hook_context(
258    branch: &str,
259    base_branch: &str,
260    worktree_path: &Path,
261    repo_path: &Path,
262    event: &str,
263    operation: &str,
264) -> HashMap<String, String> {
265    HashMap::from([
266        ("branch".into(), branch.to_string()),
267        ("base_branch".into(), base_branch.to_string()),
268        (
269            "worktree_path".into(),
270            worktree_path.to_string_lossy().to_string(),
271        ),
272        ("repo_path".into(), repo_path.to_string_lossy().to_string()),
273        ("event".into(), event.to_string()),
274        ("operation".into(), operation.to_string()),
275    ])
276}
277
278#[cfg(test)]
279mod tests {
280    use super::*;
281    #[test]
282    fn test_build_hook_context_all_fields() {
283        let ctx = build_hook_context(
284            "feat/login",
285            "main",
286            Path::new("/tmp/worktree"),
287            Path::new("/tmp/repo"),
288            "worktree.pre_create",
289            "new",
290        );
291
292        assert_eq!(ctx.len(), 6);
293        assert_eq!(ctx["branch"], "feat/login");
294        assert_eq!(ctx["base_branch"], "main");
295        assert_eq!(ctx["worktree_path"], "/tmp/worktree");
296        assert_eq!(ctx["repo_path"], "/tmp/repo");
297        assert_eq!(ctx["event"], "worktree.pre_create");
298        assert_eq!(ctx["operation"], "new");
299    }
300
301    #[test]
302    fn test_parse_repo_branch_target() {
303        assert_eq!(
304            parse_repo_branch_target("myrepo:feat/x"),
305            (Some("myrepo"), "feat/x")
306        );
307        assert_eq!(parse_repo_branch_target("feat/x"), (None, "feat/x"));
308        assert_eq!(parse_repo_branch_target(":feat/x"), (None, ":feat/x"));
309        assert_eq!(parse_repo_branch_target("myrepo:"), (None, "myrepo:"));
310    }
311}