git_worktree_manager/operations/
helpers.rs1use 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
11pub struct ResolvedTarget {
13 pub path: PathBuf,
14 pub branch: String,
15 pub repo: PathBuf,
16}
17
18std::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
31pub 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
41pub 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
58pub 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 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 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 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 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 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
152fn 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 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 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
197pub 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 eprintln!(
213 "Warning: Metadata missing for branch '{}'. Attempting to infer...",
214 branch
215 );
216
217 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 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
256pub 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}