shipper_core/ops/git/
cleanliness.rs1use std::env;
19use std::path::Path;
20use std::process::Command;
21
22use anyhow::{Context, Result};
23
24use super::bin_override;
25
26pub fn is_git_clean(repo_root: &Path) -> Result<bool> {
32 if let Ok(git_program) = env::var("SHIPPER_GIT_BIN") {
33 return bin_override::local_is_git_clean(repo_root, &git_program);
34 }
35
36 is_git_clean_default(repo_root).map_err(|err| anyhow::anyhow!("git status failed: {err}"))
37}
38
39pub(super) fn is_git_clean_default(path: &Path) -> Result<bool> {
46 let output = Command::new("git")
47 .args(["status", "--porcelain"])
48 .current_dir(path)
49 .output()
50 .context("failed to run git status")?;
51
52 if !output.status.success() {
53 return Err(anyhow::anyhow!(
54 "git status failed: {}",
55 String::from_utf8_lossy(&output.stderr)
56 ));
57 }
58
59 Ok(output.stdout.is_empty())
61}
62
63pub fn ensure_git_clean(repo_root: &Path) -> Result<()> {
68 if !is_git_clean(repo_root)? {
69 anyhow::bail!("git working tree is not clean; commit/stash changes or use --allow-dirty");
70 }
71 Ok(())
72}
73
74#[cfg(test)]
81pub(super) fn ensure_git_clean_legacy(path: &Path) -> Result<()> {
82 if !is_git_clean_default(path)? {
83 return Err(anyhow::anyhow!(
84 "git working tree has uncommitted changes. Use --allow-dirty to bypass."
85 ));
86 }
87 Ok(())
88}
89
90#[cfg(test)]
91mod tests {
92 use super::*;
93 use std::fs;
94 use tempfile::tempdir;
95
96 fn init_git_repo(dir: &Path) {
97 Command::new("git")
98 .args(["init"])
99 .current_dir(dir)
100 .output()
101 .expect("git init");
102
103 Command::new("git")
104 .args(["config", "user.email", "test@example.com"])
105 .current_dir(dir)
106 .output()
107 .expect("git config");
108
109 Command::new("git")
110 .args(["config", "user.name", "Test User"])
111 .current_dir(dir)
112 .output()
113 .expect("git config");
114 }
115
116 fn make_commit(dir: &Path, msg: &str) {
117 Command::new("git")
118 .args(["commit", "--allow-empty", "-m", msg])
119 .current_dir(dir)
120 .output()
121 .expect("git commit");
122 }
123
124 #[test]
125 fn is_git_clean_for_empty_repo() {
126 let td = tempdir().expect("tempdir");
127 init_git_repo(td.path());
128 assert!(is_git_clean_default(td.path()).unwrap_or(false));
130 }
131
132 #[test]
133 fn is_git_clean_dirty_with_untracked_file() {
134 let td = tempdir().expect("tempdir");
135 init_git_repo(td.path());
136 make_commit(td.path(), "initial");
137
138 fs::write(td.path().join("untracked.txt"), "hello").expect("write file");
139 assert!(!is_git_clean_default(td.path()).expect("git status"));
140 }
141
142 #[test]
143 fn is_git_clean_dirty_with_modified_tracked_file() {
144 let td = tempdir().expect("tempdir");
145 init_git_repo(td.path());
146
147 fs::write(td.path().join("file.txt"), "initial").expect("write file");
149 Command::new("git")
150 .args(["add", "."])
151 .current_dir(td.path())
152 .output()
153 .expect("git add");
154 make_commit(td.path(), "initial");
155
156 fs::write(td.path().join("file.txt"), "modified").expect("write file");
158 assert!(!is_git_clean_default(td.path()).expect("git status"));
159 }
160
161 #[test]
162 fn ensure_git_clean_ok_on_clean_repo() {
163 let td = tempdir().expect("tempdir");
164 init_git_repo(td.path());
165 assert!(ensure_git_clean_legacy(td.path()).is_ok());
166 }
167
168 #[test]
169 fn ensure_git_clean_errors_with_allow_dirty_hint() {
170 let td = tempdir().expect("tempdir");
171 init_git_repo(td.path());
172 make_commit(td.path(), "initial");
173
174 fs::write(td.path().join("dirty.txt"), "x").expect("write");
175 let err = ensure_git_clean_legacy(td.path()).unwrap_err();
176 let msg = err.to_string();
177 assert!(msg.contains("--allow-dirty"));
178 assert!(msg.contains("uncommitted changes"));
179 }
180
181 #[test]
182 fn ensure_git_clean_new_phrasing() {
183 let td = tempdir().expect("tempdir");
184 init_git_repo(td.path());
185 make_commit(td.path(), "initial");
186
187 fs::write(td.path().join("dirty.txt"), "x").expect("write");
188 let err = ensure_git_clean(td.path()).unwrap_err();
189 let msg = err.to_string();
190 assert!(msg.contains("--allow-dirty"));
192 assert!(msg.contains("not clean"));
193 }
194}