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<()> {
58 let output = Command::new("git").arg("--version").output();
59 match output {
60 Ok(o) if o.status.success() => Ok(()),
61 _ => bail!("git is not installed or not in PATH. Install git and try again."),
62 }
63}
64
65pub fn is_git_repo() -> bool {
67 let cwd = std::env::current_dir().unwrap_or_else(|_| std::path::PathBuf::from("."));
68 is_git_repo_in(&cwd)
69}
70
71pub fn is_git_repo_in(cwd: &Path) -> bool {
75 git_output_in(cwd, &["rev-parse", "--git-dir"]).is_ok()
76}
77
78pub fn git_status_porcelain() -> String {
80 let cwd = std::env::current_dir().unwrap_or_else(|_| std::path::PathBuf::from("."));
81 git_status_porcelain_in(&cwd)
82}
83
84pub fn git_status_porcelain_in(cwd: &Path) -> String {
88 git_output_in(cwd, &["status", "--porcelain"]).unwrap_or_default()
89}
90
91pub fn list_tracked_files_in(cwd: &Path) -> Result<Vec<String>> {
99 let out = git_output_in(cwd, &["ls-files", "-z"])?;
100 Ok(out
101 .split('\0')
102 .filter(|s| !s.is_empty())
103 .map(|s| s.to_string())
104 .collect())
105}
106
107pub fn is_shallow_clone() -> bool {
112 let cwd = std::env::current_dir().unwrap_or_else(|_| std::path::PathBuf::from("."));
113 is_shallow_clone_in(&cwd)
114}
115
116pub fn is_shallow_clone_in(cwd: &Path) -> bool {
123 let git_dir =
126 git_output_in(cwd, &["rev-parse", "--git-dir"]).unwrap_or_else(|_| ".git".to_string());
127 let git_dir_path = Path::new(&git_dir);
128 let shallow = if git_dir_path.is_absolute() {
129 git_dir_path.join("shallow")
130 } else {
131 cwd.join(git_dir_path).join("shallow")
132 };
133 shallow.exists()
134}
135
136#[cfg(test)]
137mod tests {
138 use super::*;
139 use std::process::Command;
140
141 fn init_repo(dir: &Path) {
142 let run = |args: &[&str]| {
143 let out = Command::new("git")
144 .args(args)
145 .current_dir(dir)
146 .env("GIT_AUTHOR_NAME", "test")
147 .env("GIT_AUTHOR_EMAIL", "test@test.com")
148 .env("GIT_COMMITTER_NAME", "test")
149 .env("GIT_COMMITTER_EMAIL", "test@test.com")
150 .output()
151 .unwrap();
152 assert!(
153 out.status.success(),
154 "git {:?} failed: {}",
155 args,
156 String::from_utf8_lossy(&out.stderr)
157 );
158 };
159 run(&["init"]);
160 run(&["config", "user.email", "test@test.com"]);
161 run(&["config", "user.name", "Status Tester"]);
162 std::fs::write(dir.join("README"), "init").unwrap();
163 run(&["add", "."]);
164 run(&["commit", "-m", "initial"]);
165 }
166
167 #[test]
168 fn is_git_repo_in_returns_false_for_non_git_dir() {
169 let tmp = tempfile::tempdir().unwrap();
170 assert!(!is_git_repo_in(tmp.path()));
171 }
172
173 #[test]
174 fn is_git_repo_in_returns_true_for_initialized_repo() {
175 let tmp = tempfile::tempdir().unwrap();
176 init_repo(tmp.path());
177 assert!(is_git_repo_in(tmp.path()));
178 }
179
180 #[test]
181 fn is_git_dirty_in_is_false_for_clean_repo() {
182 let tmp = tempfile::tempdir().unwrap();
183 init_repo(tmp.path());
184 assert!(!is_git_dirty_in(tmp.path()));
185 }
186
187 #[test]
188 fn is_git_dirty_in_is_true_after_untracked_change() {
189 let tmp = tempfile::tempdir().unwrap();
190 init_repo(tmp.path());
191 std::fs::write(tmp.path().join("new.txt"), "hello").unwrap();
192 assert!(is_git_dirty_in(tmp.path()));
193 }
194
195 #[test]
196 fn git_status_porcelain_in_reflects_dirty_state() {
197 let tmp = tempfile::tempdir().unwrap();
198 init_repo(tmp.path());
199 std::fs::write(tmp.path().join("staged.txt"), "x").unwrap();
200 let status = git_status_porcelain_in(tmp.path());
201 assert!(status.contains("staged.txt"), "got: {status:?}");
202 }
203
204 #[test]
205 fn local_git_user_name_in_reads_repo_config() {
206 let tmp = tempfile::tempdir().unwrap();
207 init_repo(tmp.path());
208 assert_eq!(
209 local_git_user_name_in(tmp.path()).as_deref(),
210 Some("Status Tester")
211 );
212 }
213
214 #[test]
215 fn local_git_user_email_in_reads_repo_config() {
216 let tmp = tempfile::tempdir().unwrap();
217 init_repo(tmp.path());
218 assert_eq!(
219 local_git_user_email_in(tmp.path()).as_deref(),
220 Some("test@test.com")
221 );
222 }
223
224 #[test]
225 fn list_tracked_files_in_returns_committed_paths() {
226 let tmp = tempfile::tempdir().unwrap();
227 init_repo(tmp.path());
228 std::fs::write(tmp.path().join("extra.txt"), "x").unwrap();
229 let run = |args: &[&str]| {
230 Command::new("git")
231 .args(args)
232 .current_dir(tmp.path())
233 .output()
234 .unwrap();
235 };
236 run(&["add", "extra.txt"]);
237 run(&["commit", "-m", "add extra"]);
238 let files = list_tracked_files_in(tmp.path()).unwrap();
239 assert!(files.contains(&"README".to_string()), "got: {files:?}");
240 assert!(files.contains(&"extra.txt".to_string()), "got: {files:?}");
241 }
242
243 #[test]
244 fn is_shallow_clone_in_is_false_for_full_clone() {
245 let tmp = tempfile::tempdir().unwrap();
246 init_repo(tmp.path());
247 assert!(!is_shallow_clone_in(tmp.path()));
248 }
249}