1use std::fs;
2use std::path::{Path, PathBuf};
3
4use tokio::task::spawn_blocking;
5
6use super::error::GitError;
7use super::repo::{main_repo_root_sync, resolve_git_dir, run_git_command_sync};
8
9pub async fn detect_git_info(dir: PathBuf) -> Option<String> {
12 spawn_blocking(move || detect_git_info_sync(&dir))
13 .await
14 .ok()
15 .flatten()
16}
17
18pub async fn find_git_repo_root(dir: PathBuf) -> Option<PathBuf> {
22 spawn_blocking(move || find_git_repo_root_sync(&dir))
23 .await
24 .ok()
25 .flatten()
26}
27
28pub async fn create_worktree(
43 repo_path: PathBuf,
44 worktree_path: PathBuf,
45 branch_name: String,
46 start_ref: String,
47) -> Result<(), GitError> {
48 spawn_blocking(move || {
49 let worktree_path = worktree_path.to_string_lossy().to_string();
50 run_git_command_sync(
51 &repo_path,
52 &[
53 "worktree",
54 "add",
55 "-b",
56 branch_name.as_str(),
57 worktree_path.as_str(),
58 start_ref.as_str(),
59 ],
60 "Git worktree command failed",
61 )?;
62
63 Ok(())
64 })
65 .await?
66}
67
68pub async fn remove_worktree(worktree_path: PathBuf) -> Result<(), GitError> {
83 spawn_blocking(move || {
84 let repo_root = main_repo_root_sync(&worktree_path)?;
85 let worktree_path = worktree_path.to_string_lossy().to_string();
86 run_git_command_sync(
87 &repo_root,
88 &["worktree", "remove", "--force", worktree_path.as_str()],
89 "Git worktree command failed",
90 )?;
91
92 Ok(())
93 })
94 .await?
95}
96
97pub(super) fn detect_git_info_sync(dir: &Path) -> Option<String> {
99 let repo_dir = find_git_repo(dir)?;
100
101 get_git_branch(&repo_dir)
102}
103
104fn find_git_repo(dir: &Path) -> Option<PathBuf> {
106 find_git_repo_root_sync(dir)
107}
108
109fn find_git_repo_root_sync(dir: &Path) -> Option<PathBuf> {
111 let mut current = dir.to_path_buf();
112 loop {
113 let git_dir = current.join(".git");
114 if git_dir.exists() {
115 return Some(current);
116 }
117
118 if !current.pop() {
119 return None;
120 }
121 }
122}
123
124fn get_git_branch(repo_dir: &Path) -> Option<String> {
126 let git_dir = resolve_git_dir(repo_dir)?;
127 let head_path = git_dir.join("HEAD");
128 let content = fs::read_to_string(head_path).ok()?;
129 let content = content.trim();
130
131 if let Some(branch_ref) = content.strip_prefix("ref: refs/heads/") {
132 return Some(branch_ref.to_string());
133 }
134
135 if content.len() >= 7
136 && content
137 .chars()
138 .all(|character| character.is_ascii_hexdigit())
139 {
140 return Some(format!("HEAD@{}", &content[..7]));
141 }
142
143 None
144}
145
146#[cfg(test)]
147mod tests {
148 use super::*;
149
150 #[test]
151 fn find_git_repo_root_sync_finds_repo_at_current_dir() {
152 let temp_dir = tempfile::tempdir().expect("should create temp dir");
154 fs::create_dir(temp_dir.path().join(".git")).expect("should create .git");
155
156 let result = find_git_repo_root_sync(temp_dir.path());
158
159 assert_eq!(result, Some(temp_dir.path().to_path_buf()));
161 }
162
163 #[test]
164 fn find_git_repo_root_sync_walks_up_to_parent() {
165 let temp_dir = tempfile::tempdir().expect("should create temp dir");
167 fs::create_dir(temp_dir.path().join(".git")).expect("should create .git");
168 let nested = temp_dir.path().join("src").join("lib");
169 fs::create_dir_all(&nested).expect("should create nested dirs");
170
171 let result = find_git_repo_root_sync(&nested);
173
174 assert_eq!(result, Some(temp_dir.path().to_path_buf()));
176 }
177
178 #[test]
179 fn find_git_repo_root_sync_returns_none_when_no_git_dir() {
180 let temp_dir = tempfile::tempdir().expect("should create temp dir");
182
183 let result = find_git_repo_root_sync(temp_dir.path());
185
186 assert!(result.is_none());
188 }
189
190 #[test]
191 fn get_git_branch_returns_branch_from_ref() {
192 let temp_dir = tempfile::tempdir().expect("should create temp dir");
194 let git_dir = temp_dir.path().join(".git");
195 fs::create_dir(&git_dir).expect("should create .git");
196 fs::write(git_dir.join("HEAD"), "ref: refs/heads/main\n").expect("should write HEAD");
197
198 let result = get_git_branch(temp_dir.path());
200
201 assert_eq!(result, Some("main".to_string()));
203 }
204
205 #[test]
206 fn get_git_branch_returns_detached_head_for_commit_hash() {
207 let temp_dir = tempfile::tempdir().expect("should create temp dir");
209 let git_dir = temp_dir.path().join(".git");
210 fs::create_dir(&git_dir).expect("should create .git");
211 fs::write(git_dir.join("HEAD"), "abc1234def5678\n").expect("should write HEAD");
212
213 let result = get_git_branch(temp_dir.path());
215
216 assert_eq!(result, Some("HEAD@abc1234".to_string()));
218 }
219
220 #[test]
221 fn get_git_branch_returns_none_for_unrecognized_content() {
222 let temp_dir = tempfile::tempdir().expect("should create temp dir");
224 let git_dir = temp_dir.path().join(".git");
225 fs::create_dir(&git_dir).expect("should create .git");
226 fs::write(git_dir.join("HEAD"), "unknown\n").expect("should write HEAD");
227
228 let result = get_git_branch(temp_dir.path());
230
231 assert!(result.is_none());
233 }
234
235 #[test]
236 fn get_git_branch_returns_none_when_no_git_dir() {
237 let temp_dir = tempfile::tempdir().expect("should create temp dir");
239
240 let result = get_git_branch(temp_dir.path());
242
243 assert!(result.is_none());
245 }
246
247 #[test]
248 fn detect_git_info_sync_returns_branch_for_repo() {
249 let temp_dir = tempfile::tempdir().expect("should create temp dir");
251 let git_dir = temp_dir.path().join(".git");
252 fs::create_dir(&git_dir).expect("should create .git");
253 fs::write(git_dir.join("HEAD"), "ref: refs/heads/feature\n").expect("should write HEAD");
254
255 let result = detect_git_info_sync(temp_dir.path());
257
258 assert_eq!(result, Some("feature".to_string()));
260 }
261
262 #[test]
263 fn detect_git_info_sync_returns_none_for_non_repo() {
264 let temp_dir = tempfile::tempdir().expect("should create temp dir");
266
267 let result = detect_git_info_sync(temp_dir.path());
269
270 assert!(result.is_none());
272 }
273}