anodizer_core/git/
status.rs1use anyhow::{Result, bail};
2use std::path::Path;
3use std::process::Command;
4
5use super::git_output_in;
6
7pub fn is_git_dirty() -> bool {
9 let cwd = std::env::current_dir().unwrap_or_else(|_| std::path::PathBuf::from("."));
10 is_git_dirty_in(&cwd)
11}
12
13pub fn is_git_dirty_in(cwd: &Path) -> bool {
18 git_output_in(cwd, &["status", "--porcelain"])
19 .map(|s| !s.is_empty())
20 .unwrap_or(false)
21}
22
23pub fn local_git_user_name() -> Option<String> {
25 let cwd = std::env::current_dir().unwrap_or_else(|_| std::path::PathBuf::from("."));
26 local_git_user_name_in(&cwd)
27}
28
29pub fn local_git_user_name_in(cwd: &Path) -> Option<String> {
33 git_output_in(cwd, &["config", "user.name"])
34 .ok()
35 .filter(|s| !s.is_empty())
36}
37
38pub fn local_git_user_email() -> Option<String> {
40 let cwd = std::env::current_dir().unwrap_or_else(|_| std::path::PathBuf::from("."));
41 local_git_user_email_in(&cwd)
42}
43
44pub fn local_git_user_email_in(cwd: &Path) -> Option<String> {
48 git_output_in(cwd, &["config", "user.email"])
49 .ok()
50 .filter(|s| !s.is_empty())
51}
52
53pub fn check_git_available() -> Result<()> {
60 let output = Command::new("git")
61 .arg("--version")
62 .current_dir(crate::path_util::probe_dir())
63 .output();
64 match output {
65 Ok(o) if o.status.success() => Ok(()),
66 _ => bail!("git is not installed or not in PATH. Install git and try again."),
67 }
68}
69
70pub fn is_git_repo() -> bool {
72 let cwd = std::env::current_dir().unwrap_or_else(|_| std::path::PathBuf::from("."));
73 is_git_repo_in(&cwd)
74}
75
76pub fn is_git_repo_in(cwd: &Path) -> bool {
80 git_output_in(cwd, &["rev-parse", "--git-dir"]).is_ok()
81}
82
83pub fn git_status_porcelain() -> String {
85 let cwd = std::env::current_dir().unwrap_or_else(|_| std::path::PathBuf::from("."));
86 git_status_porcelain_in(&cwd)
87}
88
89pub fn git_status_porcelain_in(cwd: &Path) -> String {
93 git_output_in(cwd, &["status", "--porcelain"]).unwrap_or_default()
94}
95
96pub fn list_tracked_files_in(cwd: &Path) -> Result<Vec<String>> {
104 let out = git_output_in(cwd, &["ls-files", "-z"])?;
105 Ok(out
106 .split('\0')
107 .filter(|s| !s.is_empty())
108 .map(|s| s.to_string())
109 .collect())
110}
111
112pub fn is_shallow_clone() -> bool {
117 let cwd = std::env::current_dir().unwrap_or_else(|_| std::path::PathBuf::from("."));
118 is_shallow_clone_in(&cwd)
119}
120
121pub fn is_shallow_clone_in(cwd: &Path) -> bool {
128 let git_dir =
131 git_output_in(cwd, &["rev-parse", "--git-dir"]).unwrap_or_else(|_| ".git".to_string());
132 let git_dir_path = Path::new(&git_dir);
133 let shallow = if git_dir_path.is_absolute() {
134 git_dir_path.join("shallow")
135 } else {
136 cwd.join(git_dir_path).join("shallow")
137 };
138 shallow.exists()
139}
140
141#[cfg(test)]
142mod tests {
143 use super::*;
144 use std::process::Command;
145
146 fn init_repo(dir: &Path) {
147 let run = |args: &[&str]| {
148 let out = Command::new("git")
149 .args(args)
150 .current_dir(dir)
151 .env("GIT_AUTHOR_NAME", "test")
152 .env("GIT_AUTHOR_EMAIL", "test@test.com")
153 .env("GIT_COMMITTER_NAME", "test")
154 .env("GIT_COMMITTER_EMAIL", "test@test.com")
155 .output()
156 .unwrap();
157 assert!(
158 out.status.success(),
159 "git {:?} failed: {}",
160 args,
161 String::from_utf8_lossy(&out.stderr)
162 );
163 };
164 run(&["init"]);
165 run(&["config", "user.email", "test@test.com"]);
166 run(&["config", "user.name", "Status Tester"]);
167 std::fs::write(dir.join("README"), "init").unwrap();
168 run(&["add", "."]);
169 run(&["commit", "-m", "initial"]);
170 }
171
172 #[test]
173 fn is_git_repo_in_returns_false_for_non_git_dir() {
174 let tmp = tempfile::tempdir().unwrap();
175 assert!(!is_git_repo_in(tmp.path()));
176 }
177
178 #[test]
179 fn is_git_repo_in_returns_true_for_initialized_repo() {
180 let tmp = tempfile::tempdir().unwrap();
181 init_repo(tmp.path());
182 assert!(is_git_repo_in(tmp.path()));
183 }
184
185 #[test]
186 fn is_git_dirty_in_is_false_for_clean_repo() {
187 let tmp = tempfile::tempdir().unwrap();
188 init_repo(tmp.path());
189 assert!(!is_git_dirty_in(tmp.path()));
190 }
191
192 #[test]
193 fn is_git_dirty_in_is_true_after_untracked_change() {
194 let tmp = tempfile::tempdir().unwrap();
195 init_repo(tmp.path());
196 std::fs::write(tmp.path().join("new.txt"), "hello").unwrap();
197 assert!(is_git_dirty_in(tmp.path()));
198 }
199
200 #[test]
201 fn git_status_porcelain_in_reflects_dirty_state() {
202 let tmp = tempfile::tempdir().unwrap();
203 init_repo(tmp.path());
204 std::fs::write(tmp.path().join("staged.txt"), "x").unwrap();
205 let status = git_status_porcelain_in(tmp.path());
206 assert!(status.contains("staged.txt"), "got: {status:?}");
207 }
208
209 #[test]
210 fn local_git_user_name_in_reads_repo_config() {
211 let tmp = tempfile::tempdir().unwrap();
212 init_repo(tmp.path());
213 assert_eq!(
214 local_git_user_name_in(tmp.path()).as_deref(),
215 Some("Status Tester")
216 );
217 }
218
219 #[test]
220 fn local_git_user_email_in_reads_repo_config() {
221 let tmp = tempfile::tempdir().unwrap();
222 init_repo(tmp.path());
223 assert_eq!(
224 local_git_user_email_in(tmp.path()).as_deref(),
225 Some("test@test.com")
226 );
227 }
228
229 #[test]
230 fn list_tracked_files_in_returns_committed_paths() {
231 let tmp = tempfile::tempdir().unwrap();
232 init_repo(tmp.path());
233 std::fs::write(tmp.path().join("extra.txt"), "x").unwrap();
234 let run = |args: &[&str]| {
235 Command::new("git")
236 .args(args)
237 .current_dir(tmp.path())
238 .output()
239 .unwrap();
240 };
241 run(&["add", "extra.txt"]);
242 run(&["commit", "-m", "add extra"]);
243 let files = list_tracked_files_in(tmp.path()).unwrap();
244 assert!(files.contains(&"README".to_string()), "got: {files:?}");
245 assert!(files.contains(&"extra.txt".to_string()), "got: {files:?}");
246 }
247
248 #[test]
249 fn is_shallow_clone_in_is_false_for_full_clone() {
250 let tmp = tempfile::tempdir().unwrap();
251 init_repo(tmp.path());
252 assert!(!is_shallow_clone_in(tmp.path()));
253 }
254}