1use crate::core::GitStatus;
6use crate::error::{DevSweepError, Result};
7use std::path::{Path, PathBuf};
8use std::process::Command;
9
10pub fn get_git_status(project_root: &Path) -> Result<Option<GitStatus>> {
14 let git_dir = project_root.join(".git");
16 if !git_dir.exists() {
17 let output = Command::new("git")
19 .args(["rev-parse", "--git-dir"])
20 .current_dir(project_root)
21 .output();
22
23 match output {
24 Ok(o) if o.status.success() => {
25 }
27 _ => return Ok(None), }
29 }
30
31 let mut status = GitStatus {
32 is_repo: true,
33 ..Default::default()
34 };
35
36 if let Ok(output) = Command::new("git")
38 .args(["branch", "--show-current"])
39 .current_dir(project_root)
40 .output()
41 {
42 if output.status.success() {
43 let branch = String::from_utf8_lossy(&output.stdout).trim().to_string();
44 if !branch.is_empty() {
45 status.branch = Some(branch);
46 }
47 }
48 }
49
50 if let Ok(output) = Command::new("git")
52 .args(["remote", "get-url", "origin"])
53 .current_dir(project_root)
54 .output()
55 {
56 if output.status.success() {
57 let remote = String::from_utf8_lossy(&output.stdout).trim().to_string();
58 if !remote.is_empty() {
59 status.remote = Some(remote);
60 }
61 }
62 }
63
64 if let Ok(output) = Command::new("git")
66 .args(["status", "--porcelain"])
67 .current_dir(project_root)
68 .output()
69 {
70 if output.status.success() {
71 let status_output = String::from_utf8_lossy(&output.stdout);
72
73 for line in status_output.lines() {
74 if line.len() < 3 {
75 continue;
76 }
77
78 let status_code = &line[..2];
79 let file_path = &line[3..];
80
81 let first = status_code.chars().next().unwrap_or(' ');
83 let second = status_code.chars().nth(1).unwrap_or(' ');
84
85 if first == '?' && second == '?' {
87 status.has_untracked = true;
88 } else {
89 if first != ' ' || second != ' ' {
91 status.has_uncommitted = true;
92 status.dirty_paths.push(PathBuf::from(file_path));
93 }
94 }
95 }
96 }
97 }
98
99 if let Ok(output) = Command::new("git")
101 .args(["stash", "list"])
102 .current_dir(project_root)
103 .output()
104 {
105 if output.status.success() {
106 let stash_output = String::from_utf8_lossy(&output.stdout);
107 status.has_stashed = !stash_output.trim().is_empty();
108 }
109 }
110
111 Ok(Some(status))
112}
113
114pub fn has_uncommitted_changes(path: &Path) -> Result<bool> {
116 match get_git_status(path)? {
117 Some(status) => Ok(status.has_uncommitted),
118 None => Ok(false),
119 }
120}
121
122pub fn is_git_tracked(repo_root: &Path, path: &Path) -> Result<bool> {
124 let relative = path
125 .strip_prefix(repo_root)
126 .unwrap_or(path)
127 .to_string_lossy();
128
129 let output = Command::new("git")
130 .args(["ls-files", &relative])
131 .current_dir(repo_root)
132 .output()
133 .map_err(|e| DevSweepError::Git(e.to_string()))?;
134
135 Ok(output.status.success() && !output.stdout.is_empty())
136}
137
138pub fn find_repo_root(path: &Path) -> Result<Option<PathBuf>> {
140 let output = Command::new("git")
141 .args(["rev-parse", "--show-toplevel"])
142 .current_dir(path)
143 .output();
144
145 match output {
146 Ok(o) if o.status.success() => {
147 let root = String::from_utf8_lossy(&o.stdout).trim().to_string();
148 Ok(Some(PathBuf::from(root)))
149 }
150 _ => Ok(None),
151 }
152}
153
154#[cfg(test)]
155mod tests {
156 use super::*;
157 use tempfile::TempDir;
158
159 fn init_git_repo(path: &Path) {
160 Command::new("git")
161 .args(["init"])
162 .current_dir(path)
163 .output()
164 .expect("git init failed");
165
166 Command::new("git")
167 .args(["config", "user.email", "test@test.com"])
168 .current_dir(path)
169 .output()
170 .ok();
171
172 Command::new("git")
173 .args(["config", "user.name", "Test"])
174 .current_dir(path)
175 .output()
176 .ok();
177 }
178
179 #[test]
180 fn test_non_git_repo() {
181 let temp = TempDir::new().unwrap();
182 let status = get_git_status(temp.path()).unwrap();
183 assert!(status.is_none());
184 }
185
186 #[test]
187 fn test_clean_repo() {
188 let temp = TempDir::new().unwrap();
189 init_git_repo(temp.path());
190
191 std::fs::write(temp.path().join("test.txt"), "hello").unwrap();
193 Command::new("git")
194 .args(["add", "."])
195 .current_dir(temp.path())
196 .output()
197 .unwrap();
198 Command::new("git")
199 .args(["commit", "-m", "initial"])
200 .current_dir(temp.path())
201 .output()
202 .unwrap();
203
204 let status = get_git_status(temp.path()).unwrap().unwrap();
205 assert!(status.is_repo);
206 assert!(!status.has_uncommitted);
207 assert!(!status.has_untracked);
208 }
209
210 #[test]
211 fn test_uncommitted_changes() {
212 let temp = TempDir::new().unwrap();
213 init_git_repo(temp.path());
214
215 std::fs::write(temp.path().join("test.txt"), "hello").unwrap();
217 Command::new("git")
218 .args(["add", "."])
219 .current_dir(temp.path())
220 .output()
221 .unwrap();
222 Command::new("git")
223 .args(["commit", "-m", "initial"])
224 .current_dir(temp.path())
225 .output()
226 .unwrap();
227
228 std::fs::write(temp.path().join("test.txt"), "modified").unwrap();
230
231 let status = get_git_status(temp.path()).unwrap().unwrap();
232 assert!(status.has_uncommitted);
233 }
234
235 #[test]
236 fn test_untracked_files() {
237 let temp = TempDir::new().unwrap();
238 init_git_repo(temp.path());
239
240 std::fs::write(temp.path().join("new.txt"), "new file").unwrap();
242
243 let status = get_git_status(temp.path()).unwrap().unwrap();
244 assert!(status.has_untracked);
245 }
246
247 #[test]
248 fn test_has_uncommitted_changes_helper() {
249 let temp = TempDir::new().unwrap();
250
251 assert!(!has_uncommitted_changes(temp.path()).unwrap());
253
254 init_git_repo(temp.path());
256 std::fs::write(temp.path().join("test.txt"), "hello").unwrap();
257 Command::new("git")
258 .args(["add", "."])
259 .current_dir(temp.path())
260 .output()
261 .unwrap();
262
263 assert!(has_uncommitted_changes(temp.path()).unwrap());
264 }
265}