git_worktree_manager/operations/
helpers.rs1use std::path::{Path, PathBuf};
4
5use crate::constants::{format_config_key, CONFIG_KEY_BASE_BRANCH, CONFIG_KEY_BASE_PATH};
6use crate::error::{CwError, Result};
7use crate::git;
8use crate::messages;
9
10pub struct ResolvedTarget {
12 pub path: PathBuf,
13 pub branch: String,
14 pub repo: PathBuf,
15}
16
17#[derive(Debug)]
21pub struct StrictTarget {
22 pub name: String,
24 pub branch: Option<String>,
27 pub path: PathBuf,
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() {
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(ResolvedTarget {
72 path: cwd,
73 branch,
74 repo,
75 });
76 }
77
78 let target = target.unwrap();
79
80 let main_repo = git::get_main_repo_root(None)?;
81
82 let branch_match = if lookup_mode != Some("worktree") {
84 git::find_worktree_by_intended_branch(&main_repo, target)?
85 } else {
86 None
87 };
88
89 let worktree_match = if lookup_mode != Some("branch") {
91 git::find_worktree_by_name(&main_repo, target)?
92 } else {
93 None
94 };
95
96 match (branch_match, worktree_match) {
97 (Some(bp), Some(wp)) => {
98 let bp_resolved = git::canonicalize_or(&bp);
99 let wp_resolved = git::canonicalize_or(&wp);
100 if bp_resolved == wp_resolved {
101 let repo = git::get_repo_root(Some(&bp))?;
102 Ok(ResolvedTarget {
103 path: bp,
104 branch: target.to_string(),
105 repo,
106 })
107 } else {
108 let repo = git::get_repo_root(Some(&bp))?;
110 Ok(ResolvedTarget {
111 path: bp,
112 branch: target.to_string(),
113 repo,
114 })
115 }
116 }
117 (Some(bp), None) => {
118 let repo = git::get_repo_root(Some(&bp))?;
119 Ok(ResolvedTarget {
120 path: bp,
121 branch: target.to_string(),
122 repo,
123 })
124 }
125 (None, Some(wp)) => {
126 let branch =
127 get_branch_for_worktree(&main_repo, &wp).unwrap_or_else(|| target.to_string());
128 let repo = git::get_repo_root(Some(&wp))?;
129 Ok(ResolvedTarget {
130 path: wp,
131 branch,
132 repo,
133 })
134 }
135 (None, None) => Err(CwError::WorktreeNotFound(messages::worktree_not_found(
136 target,
137 ))),
138 }
139}
140
141pub fn get_worktree_metadata(branch: &str, repo: &Path) -> Result<(String, PathBuf)> {
145 let base_key = format_config_key(CONFIG_KEY_BASE_BRANCH, branch);
146 let path_key = format_config_key(CONFIG_KEY_BASE_PATH, branch);
147
148 let base_branch = git::get_config(&base_key, Some(repo));
149 let base_path_str = git::get_config(&path_key, Some(repo));
150
151 if let (Some(bb), Some(bp)) = (base_branch, base_path_str) {
152 return Ok((bb, PathBuf::from(bp)));
153 }
154
155 eprintln!(
157 "Warning: Metadata missing for branch '{}'. Attempting to infer...",
158 branch
159 );
160
161 let worktrees = git::parse_worktrees(repo)?;
163 let inferred_base_path = worktrees.first().map(|(_, p)| p.clone()).ok_or_else(|| {
164 CwError::Git(format!(
165 "Cannot infer base repository path for branch '{}'. Use 'gw new' to create worktrees.",
166 branch
167 ))
168 })?;
169
170 let mut inferred_base_branch: Option<String> = None;
172 for candidate in &["main", "master", "develop"] {
173 if git::branch_exists(candidate, Some(&inferred_base_path)) {
174 inferred_base_branch = Some(candidate.to_string());
175 break;
176 }
177 }
178
179 if inferred_base_branch.is_none() {
180 if let Some((first_branch, _)) = worktrees.first() {
181 if first_branch != "(detached)" {
182 inferred_base_branch = Some(git::normalize_branch_name(first_branch).to_string());
183 }
184 }
185 }
186
187 let base = inferred_base_branch.ok_or_else(|| {
188 CwError::Git(format!(
189 "Cannot infer base branch for '{}'. Use 'gw new' to create worktrees.",
190 branch
191 ))
192 })?;
193
194 eprintln!(" Inferred base branch: {}", base);
195 eprintln!(" Inferred base path: {}", inferred_base_path.display());
196
197 Ok((base, inferred_base_path))
198}
199
200pub fn resolve_target_strict(repo_root: &Path, target: &str) -> Result<StrictTarget> {
212 let worktrees = git::parse_worktrees(repo_root)?;
213
214 let main_path = worktrees
217 .first()
218 .map(|(_, p)| git::canonicalize_or(p))
219 .unwrap_or_default();
220
221 let make = |(branch_raw, path): &(String, PathBuf)| -> StrictTarget {
223 let name = path
224 .file_name()
225 .map(|n| n.to_string_lossy().into_owned())
226 .unwrap_or_default();
227 let normalized = git::normalize_branch_name(branch_raw);
228 let branch = if normalized == "(detached)" {
229 None
230 } else {
231 Some(normalized.to_string())
232 };
233 StrictTarget {
234 name,
235 branch,
236 path: path.clone(),
237 }
238 };
239
240 for entry in &worktrees {
242 let (_, path) = entry;
243 if git::canonicalize_or(path) == main_path {
245 continue;
246 }
247 let name = path
248 .file_name()
249 .map(|n| n.to_string_lossy())
250 .unwrap_or_default();
251 if name == target {
252 return Ok(make(entry));
253 }
254 }
255
256 for entry in &worktrees {
258 let (branch_raw, path) = entry;
259 if git::canonicalize_or(path) == main_path {
260 continue;
261 }
262 if git::normalize_branch_name(branch_raw) == target {
263 return Ok(make(entry));
264 }
265 }
266
267 let abs = PathBuf::from(target);
269 let abs_resolved = git::canonicalize_or(&abs);
270 for entry in &worktrees {
271 let (_, path) = entry;
272 let path_resolved = git::canonicalize_or(path);
273 if path_resolved == abs_resolved {
274 return Ok(make(entry));
275 }
276 }
277
278 Err(CwError::WorktreeNotFound(messages::target_not_found(
279 target,
280 )))
281}
282
283#[cfg(test)]
284mod tests {
285 use super::*;
286
287 #[test]
288 fn test_parse_repo_branch_target() {
289 assert_eq!(
290 parse_repo_branch_target("myrepo:feat/x"),
291 (Some("myrepo"), "feat/x")
292 );
293 assert_eq!(parse_repo_branch_target("feat/x"), (None, "feat/x"));
294 assert_eq!(parse_repo_branch_target(":feat/x"), (None, ":feat/x"));
295 assert_eq!(parse_repo_branch_target("myrepo:"), (None, "myrepo:"));
296 }
297}