git_worktree_manager/operations/
helpers.rs1use 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
10std::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
23pub 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
33pub 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
52pub 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 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 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 let branch_match = git::find_worktree_by_intended_branch(&main_repo, target)?;
85
86 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 if git::is_non_interactive() {
99 let repo = git::get_repo_root(Some(&bp))?;
100 Ok((bp, target.to_string(), repo))
101 } else {
102 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
126fn 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 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 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
165pub 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 eprintln!(
181 "Warning: Metadata missing for branch '{}'. Attempting to infer...",
182 branch
183 );
184
185 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 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}