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 let Some(target) = target else {
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 main_repo = git::get_main_repo_root(None)?;
79
80 let branch_match = if lookup_mode != Some("worktree") {
82 git::find_worktree_by_intended_branch(&main_repo, target)?
83 } else {
84 None
85 };
86
87 let worktree_match = if lookup_mode != Some("branch") {
89 git::find_worktree_by_name(&main_repo, target)?
90 } else {
91 None
92 };
93
94 match (branch_match, worktree_match) {
95 (Some(bp), Some(_wp)) => {
96 let repo = git::get_repo_root(Some(&bp))?;
99 Ok(ResolvedTarget {
100 path: bp,
101 branch: target.to_string(),
102 repo,
103 })
104 }
105 (Some(bp), None) => {
106 let repo = git::get_repo_root(Some(&bp))?;
107 Ok(ResolvedTarget {
108 path: bp,
109 branch: target.to_string(),
110 repo,
111 })
112 }
113 (None, Some(wp)) => {
114 let branch =
115 get_branch_for_worktree(&main_repo, &wp).unwrap_or_else(|| target.to_string());
116 let repo = git::get_repo_root(Some(&wp))?;
117 Ok(ResolvedTarget {
118 path: wp,
119 branch,
120 repo,
121 })
122 }
123 (None, None) => Err(CwError::WorktreeNotFound(messages::worktree_not_found(
124 target,
125 ))),
126 }
127}
128
129pub fn get_worktree_metadata(branch: &str, repo: &Path) -> Result<(String, PathBuf)> {
133 let base_key = format_config_key(CONFIG_KEY_BASE_BRANCH, branch);
134 let path_key = format_config_key(CONFIG_KEY_BASE_PATH, branch);
135
136 let base_branch = git::get_config(&base_key, Some(repo));
137 let base_path_str = git::get_config(&path_key, Some(repo));
138
139 if let (Some(bb), Some(bp)) = (base_branch, base_path_str) {
140 return Ok((bb, PathBuf::from(bp)));
141 }
142
143 eprintln!(
145 "Warning: Metadata missing for branch '{}'. Attempting to infer...",
146 branch
147 );
148
149 let worktrees = git::parse_worktrees(repo)?;
151 let inferred_base_path = worktrees.first().map(|(_, p)| p.clone()).ok_or_else(|| {
152 CwError::Other(format!(
153 "Cannot infer base repository path for branch '{}'. Use 'gw new' to create worktrees.",
154 branch
155 ))
156 })?;
157
158 let mut inferred_base_branch: Option<String> = None;
160 for candidate in &["main", "master", "develop"] {
161 if git::branch_exists(candidate, Some(&inferred_base_path)) {
162 inferred_base_branch = Some(candidate.to_string());
163 break;
164 }
165 }
166
167 if inferred_base_branch.is_none() {
168 if let Some((first_branch, _)) = worktrees.first() {
169 if first_branch != "(detached)" {
170 inferred_base_branch = Some(git::normalize_branch_name(first_branch).to_string());
171 }
172 }
173 }
174
175 let base = inferred_base_branch.ok_or_else(|| {
176 CwError::Other(format!(
177 "Cannot infer base branch for '{}'. Use 'gw new' to create worktrees.",
178 branch
179 ))
180 })?;
181
182 eprintln!(" Inferred base branch: {}", base);
183 eprintln!(" Inferred base path: {}", inferred_base_path.display());
184
185 Ok((base, inferred_base_path))
186}
187
188pub fn resolve_target_strict(repo_root: &Path, target: &str) -> Result<StrictTarget> {
200 let worktrees = git::parse_worktrees(repo_root)?;
201
202 let main_path = worktrees
205 .first()
206 .map(|(_, p)| git::canonicalize_or(p))
207 .unwrap_or_default();
208
209 let make = |(branch_raw, path): &(String, PathBuf)| -> StrictTarget {
211 let name = path
212 .file_name()
213 .map(|n| n.to_string_lossy().into_owned())
214 .unwrap_or_default();
215 let normalized = git::normalize_branch_name(branch_raw);
216 let branch = if normalized == "(detached)" {
217 None
218 } else {
219 Some(normalized.to_string())
220 };
221 StrictTarget {
222 name,
223 branch,
224 path: path.clone(),
225 }
226 };
227
228 for entry in &worktrees {
230 let (_, path) = entry;
231 if git::canonicalize_or(path) == main_path {
233 continue;
234 }
235 let name = path
236 .file_name()
237 .map(|n| n.to_string_lossy())
238 .unwrap_or_default();
239 if name == target {
240 return Ok(make(entry));
241 }
242 }
243
244 for entry in &worktrees {
246 let (branch_raw, path) = entry;
247 if git::canonicalize_or(path) == main_path {
248 continue;
249 }
250 if git::normalize_branch_name(branch_raw) == target {
251 return Ok(make(entry));
252 }
253 }
254
255 let abs = PathBuf::from(target);
257 let abs_resolved = git::canonicalize_or(&abs);
258 for entry in &worktrees {
259 let (_, path) = entry;
260 let path_resolved = git::canonicalize_or(path);
261 if path_resolved == abs_resolved {
262 return Ok(make(entry));
263 }
264 }
265
266 Err(CwError::WorktreeNotFound(messages::target_not_found(
267 target,
268 )))
269}
270
271#[cfg(test)]
272mod tests {
273 use super::*;
274
275 #[test]
276 fn test_parse_repo_branch_target() {
277 assert_eq!(
278 parse_repo_branch_target("myrepo:feat/x"),
279 (Some("myrepo"), "feat/x")
280 );
281 assert_eq!(parse_repo_branch_target("feat/x"), (None, "feat/x"));
282 assert_eq!(parse_repo_branch_target(":feat/x"), (None, ":feat/x"));
283 assert_eq!(parse_repo_branch_target("myrepo:"), (None, "myrepo:"));
284 }
285}