workspacer_git/
ensure_git_clean_for_crate.rs1crate::ix!();
3
4#[async_trait]
5impl EnsureGitClean for CrateHandle {
6 type Error = GitError;
7
8 async fn ensure_git_clean(&self) -> Result<(), Self::Error> {
9 info!("Ensuring Git is clean (single crate) at {:?}", self.as_ref());
10 let output = Command::new("git")
13 .args(["status", "--porcelain"])
14 .current_dir(self.as_ref()) .output()
16 .await
17 .map_err(|e| GitError::IoError {
18 io: Arc::new(e),
19 context: format!("could not run git status --porcelain in current directory: {:?}", self.as_ref()),
20 })?;
21
22 if !output.status.success() {
23 return Err(GitError::FailedToRunGitStatusMakeSureGitIsInstalled);
24 }
25 let stdout_str = String::from_utf8_lossy(&output.stdout);
26 if !stdout_str.trim().is_empty() {
27 return Err(GitError::WorkingDirectoryIsNotCleanAborting);
28 }
29 Ok(())
30 }
31}
32
33#[cfg(test)]
34mod test_ensure_git_clean_for_crate_handle {
35 use super::*;
36 use std::process::Stdio;
37 use std::path::{Path, PathBuf};
38 use tempfile::tempdir;
39 use tokio::process::Command;
40 use tokio::io::AsyncWriteExt;
41 use tokio::fs::{File, create_dir_all};
42
43 #[derive(Clone)]
46 struct TempCratePath(PathBuf);
47
48 impl AsRef<Path> for TempCratePath {
49 fn as_ref(&self) -> &Path {
50 self.0.as_ref()
51 }
52 }
53
54 async fn write_minimal_cargo_toml(dir: &Path) {
56 let cargo_toml = r#"
57 [package]
58 name = "test_crate"
59 version = "0.1.0"
60 authors = ["Someone <someone@example.com>"]
61 license = "MIT"
62 "#;
63 let file_path = dir.join("Cargo.toml");
64 create_dir_all(dir).await.expect("Failed to create directories");
65 let mut f = File::create(&file_path).await.expect("Failed to create Cargo.toml");
66 f.write_all(cargo_toml.as_bytes())
67 .await
68 .expect("Failed to write Cargo.toml");
69 }
70
71 async fn run_in_dir(cmd: &str, args: &[&str], dir: &Path) -> Result<(), String> {
74 let mut command = Command::new(cmd);
75 command.args(args).current_dir(dir).stdout(Stdio::piped()).stderr(Stdio::piped());
76 let output = command
77 .output()
78 .await
79 .map_err(|e| format!("Failed to spawn '{cmd}': {e}"))?;
80
81 if output.status.success() {
82 Ok(())
83 } else {
84 Err(format!(
85 "'{cmd} {:?}' failed with code {:?}\nstdout:\n{}\nstderr:\n{}",
86 args,
87 output.status.code(),
88 String::from_utf8_lossy(&output.stdout),
89 String::from_utf8_lossy(&output.stderr),
90 ))
91 }
92 }
93
94 #[tokio::test]
100 #[ignore = "Requires mocking or a system without git installed to run meaningfully"]
101 async fn test_ensure_git_clean_no_git_available() {
102 let (temp_crate_path, repo_path) = setup_git_repo_for_crate().await;
103 let handle = CrateHandle::new(&temp_crate_path)
106 .await
107 .expect("Failed to create CrateHandle");
108
109 unsafe { std::env::set_var("PATH", "") };
112
113 let result = handle.ensure_git_clean().await;
114 unsafe { std::env::remove_var("PATH") }; assert!(
116 matches!(
117 result,
118 Err(GitError::IoError{..}) | Err(GitError::FailedToRunGitStatusMakeSureGitIsInstalled)
119 ),
120 "Expected IoError or FailedToRunGitStatusMakeSureGitIsInstalled, got: {result:?}"
121 );
122 }
123
124 async fn setup_git_repo_for_crate() -> (TempDir, PathBuf) {
127 let tmp_dir = tempdir().expect("Failed to create temp directory");
128 let repo_path = tmp_dir.path().to_path_buf();
129
130 write_minimal_cargo_toml(tmp_dir.path()).await;
132 run_in_dir("git", &["init"], tmp_dir.path()).await.unwrap();
133 run_in_dir("git", &["add", "."], tmp_dir.path()).await.unwrap();
134 run_in_dir("git", &["commit", "-m", "Initial commit"], tmp_dir.path()).await.unwrap();
135
136 (tmp_dir, repo_path)
138 }
139
140 #[tokio::test]
141 async fn test_ensure_git_clean_pristine_repo() {
142 let (temp_crate_dir, repo_path) = setup_git_repo_for_crate().await;
144
145 let handle = CrateHandle::new(&repo_path)
147 .await
148 .expect("Failed to create CrateHandle");
149
150 handle.ensure_git_clean().await.expect("Expected clean repo");
152
153 }
155
156 #[tokio::test]
158 async fn test_ensure_git_clean_untracked_changes() {
159 let (temp_crate_dir, repo_path) = setup_git_repo_for_crate().await;
160 let handle = CrateHandle::new(&repo_path)
161 .await
162 .expect("Failed to create CrateHandle");
163
164 let untracked_file = repo_path.join("untracked_file.txt");
166 {
167 let mut f = File::create(&untracked_file)
168 .await
169 .expect("Failed to create untracked file");
170 f.write_all(b"This is untracked content.")
171 .await
172 .expect("Failed to write untracked content");
173 }
174
175 let result = handle.ensure_git_clean().await;
177 assert!(
178 matches!(result, Err(GitError::WorkingDirectoryIsNotCleanAborting)),
179 "Expected WorkingDirectoryIsNotCleanAborting, got: {result:?}"
180 );
181 }
183
184 #[tokio::test]
185 async fn test_ensure_git_clean_modified_file() {
186 let (temp_crate_dir, repo_path) = setup_git_repo_for_crate().await;
187 let handle = CrateHandle::new(&repo_path)
188 .await
189 .expect("Failed to create CrateHandle");
190
191 let cargo_toml = repo_path.join("Cargo.toml");
193 {
194 let mut f = File::options()
195 .append(true)
196 .open(&cargo_toml)
197 .await
198 .expect("Failed to open Cargo.toml for appending");
199 f.write_all(b"# Adding a new line to Cargo.toml\n")
200 .await
201 .expect("Failed to write to Cargo.toml");
202 }
203
204 let result = handle.ensure_git_clean().await;
205 assert!(
206 matches!(result, Err(GitError::WorkingDirectoryIsNotCleanAborting)),
207 "Expected WorkingDirectoryIsNotCleanAborting, got: {result:?}"
208 );
209 }
210}