workspacer_git/
ensure_git_clean.rs

1// ---------------- [ File: workspacer-git/src/ensure_git_clean.rs ]
2crate::ix!();
3
4#[async_trait]
5pub trait EnsureGitClean {
6    type Error;
7    async fn ensure_git_clean(&self) -> Result<(), Self::Error>;
8}
9
10#[async_trait]
11impl<P,H> EnsureGitClean for Workspace<P,H>
12where
13    // your existing constraints:
14    for<'async_trait> P: From<PathBuf> + AsRef<Path> + Clone + Send + Sync + 'async_trait,
15    H: CrateHandleInterface<P> + Send + Sync,
16{
17    type Error = GitError;
18
19    /// Checks that the Git working directory is clean:
20    ///  - If `git status --porcelain` returns any output, we fail.
21    ///  - If there's no .git folder or `git` isn't installed, this will also error out.
22    async fn ensure_git_clean(&self) -> Result<(), Self::Error> {
23        // Run `git status --porcelain`; if it returns any output, that means dirty/untracked changes
24        let output = Command::new("git")
25            .args(["status", "--porcelain"])
26            .current_dir(self.as_ref()) // important: run in workspace directory
27            .output()
28            .await
29            .map_err(|e|
30                GitError::IoError { 
31                    io: Arc::new(e),
32                    context: format!("could not run git status --porcelain in current directory: {:?}", self.as_ref()),
33                }
34            )?;
35
36        if !output.status.success() {
37            return Err(GitError::FailedToRunGitStatusMakeSureGitIsInstalled);
38        }
39
40        let stdout_str = String::from_utf8_lossy(&output.stdout);
41        if !stdout_str.trim().is_empty() {
42            return Err(GitError::WorkingDirectoryIsNotCleanAborting);
43        }
44
45        Ok(())
46    }
47}
48
49#[cfg(test)]
50mod test_ensure_git_clean_for_workspace {
51    use super::*;
52
53    // We'll define a minimal "MockWorkspace" or actually a real `Workspace<_,_>` if you can:
54    // For demonstration, let's say your real code can do:
55    //   let ws = Workspace::new(&some_path).await?
56    // and `ws.ensure_git_clean().await?` calls the trait method.
57    //
58    // If you need a minimal test-only struct that implements `EnsureGitClean`, we can do that.
59
60    /// Helper to run a shell command in the given directory (blocking). 
61    /// For a purely async test, you might do tokio::process::Command -> await.
62    async fn run_in_dir(dir: &std::path::Path, cmd: &str, args: &[&str]) -> Result<(), String> {
63        let mut command = Command::new(cmd);
64        command.args(args).current_dir(dir);
65        let output = command.output().await.map_err(|e| format!("Failed to spawn {}: {}", cmd, e))?;
66        if !output.status.success() {
67            let stderr = String::from_utf8_lossy(&output.stderr);
68            return Err(format!("Command {} {:?} failed: {}", cmd, args, stderr));
69        }
70        Ok(())
71    }
72
73    #[tokio::test]
74    async fn test_clean_repo_succeeds() {
75        // 1) Create a temp directory
76        let tmp_dir = tempdir().expect("failed to create temp dir");
77        let path = tmp_dir.path();
78
79        // 2) Initialize a git repo
80        //    We'll skip if `git` is not installed, or just let it fail with "Failed to spawn"
81        run_in_dir(path, "git", &["init", "."]).await.expect("git init should succeed");
82        // 3) Create a file
83        let file_path = path.join("hello.txt");
84        tokio::fs::write(&file_path, b"hello").await.expect("write file");
85        // 4) git add + commit
86        run_in_dir(path, "git", &["add", "."]).await.expect("git add .");
87        run_in_dir(path, "git", &["commit", "-m", "Initial commit"]).await.expect("git commit");
88
89        // 5) Build or mock your workspace that references `path`.
90        //    For demonstration, let's define a small struct implementing `AsRef<Path>`.
91        let ws = MockWorkspace { root: path.to_path_buf() };
92
93        // 6) Call ensure_git_clean
94        let result = ws.ensure_git_clean().await;
95        assert!(result.is_ok(), "A fully committed repo is clean => Ok(())");
96    }
97
98    #[tokio::test]
99    async fn test_dirty_repo_fails() {
100        let tmp_dir = tempdir().expect("failed to create temp dir");
101        let path = tmp_dir.path();
102
103        run_in_dir(path, "git", &["init", "."]).await.expect("git init");
104        // create a file
105        let file_path = path.join("hello.txt");
106        tokio::fs::write(&file_path, b"hello").await.expect("write file");
107        // We do NOT commit it => working dir is dirty
108
109        let ws = MockWorkspace { root: path.to_path_buf() };
110
111        let result = ws.ensure_git_clean().await;
112        match result {
113            Err(GitError::WorkingDirectoryIsNotCleanAborting) => {
114                // expected
115            }
116            other => panic!("Expected WorkingDirectoryIsNotCleanAborting, got {:?}", other),
117        }
118    }
119
120    /// If there's no .git folder or `git` isn't installed, we likely get an IoError or
121    /// GitError::FailedToRunGitStatusMakeSureGitIsInstalled, depending on the error message.
122    /// We'll demonstrate a scenario with no .git:
123    #[tokio::test]
124    async fn test_not_git_repo_errors() {
125        let tmp_dir = tempdir().expect("failed to create temp dir");
126        let path = tmp_dir.path();
127
128        let ws = MockWorkspace { root: path.to_path_buf() };
129
130        let result = ws.ensure_git_clean().await;
131        match result {
132            Err(GitError::IoError{..}) 
133            | Err(GitError::FailedToRunGitStatusMakeSureGitIsInstalled) => {
134                // Either error is plausible if there's no .git or no git command
135            }
136            other => panic!("Expected IoError or FailedToRunGitStatus..., got {:?}", other),
137        }
138    }
139
140    // A minimal workspace that implements `AsRef<Path>` and references the same `ensure_git_clean`
141    // logic you posted.  If you have a real `Workspace<P,H>`, just use that.
142    #[derive(Debug)]
143    struct MockWorkspace {
144        root: PathBuf,
145    }
146
147    impl AsRef<std::path::Path> for MockWorkspace {
148        fn as_ref(&self) -> &std::path::Path {
149            &self.root
150        }
151    }
152
153    #[async_trait]
154    impl EnsureGitClean for MockWorkspace {
155        type Error = GitError;
156
157        async fn ensure_git_clean(&self) -> Result<(), Self::Error> {
158            // replicate your code:
159            let output = Command::new("git")
160                .args(["status", "--porcelain"])
161                .current_dir(self.as_ref())
162                .output()
163                .await
164                .map_err(|e| GitError::IoError { 
165                    io: Arc::new(e),
166                    context: format!("could not run git status --porcelain in current directory: {:?}", self.as_ref()),
167                })?;
168
169            if !output.status.success() {
170                return Err(GitError::FailedToRunGitStatusMakeSureGitIsInstalled);
171            }
172            let stdout_str = String::from_utf8_lossy(&output.stdout);
173            if !stdout_str.trim().is_empty() {
174                return Err(GitError::WorkingDirectoryIsNotCleanAborting);
175            }
176            Ok(())
177        }
178    }
179}