status.rs

  1use crate::repository::{GitFileStatus, RepoPath};
  2use anyhow::{anyhow, Result};
  3use std::{
  4    path::{Path, PathBuf},
  5    process::{Command, Stdio},
  6    sync::Arc,
  7};
  8
  9#[derive(Clone)]
 10pub struct GitStatus {
 11    pub entries: Arc<[(RepoPath, GitFileStatus)]>,
 12}
 13
 14impl GitStatus {
 15    pub(crate) fn new(
 16        git_binary: &Path,
 17        working_directory: &Path,
 18        path_prefixes: &[PathBuf],
 19    ) -> Result<Self> {
 20        let mut child = Command::new(git_binary);
 21
 22        child
 23            .current_dir(working_directory)
 24            .args([
 25                "--no-optional-locks",
 26                "status",
 27                "--porcelain=v1",
 28                "--untracked-files=all",
 29                "-z",
 30            ])
 31            .args(path_prefixes.iter().map(|path_prefix| {
 32                if *path_prefix == Path::new("") {
 33                    Path::new(".")
 34                } else {
 35                    path_prefix
 36                }
 37            }))
 38            .stdin(Stdio::null())
 39            .stdout(Stdio::piped())
 40            .stderr(Stdio::piped());
 41
 42        #[cfg(windows)]
 43        {
 44            use std::os::windows::process::CommandExt;
 45            child.creation_flags(windows::Win32::System::Threading::CREATE_NO_WINDOW.0);
 46        }
 47
 48        let child = child
 49            .spawn()
 50            .map_err(|e| anyhow!("Failed to start git status process: {}", e))?;
 51
 52        let output = child
 53            .wait_with_output()
 54            .map_err(|e| anyhow!("Failed to read git blame output: {}", e))?;
 55
 56        if !output.status.success() {
 57            let stderr = String::from_utf8_lossy(&output.stderr);
 58            return Err(anyhow!("git status process failed: {}", stderr));
 59        }
 60        let stdout = String::from_utf8_lossy(&output.stdout);
 61        let mut entries = stdout
 62            .split('\0')
 63            .filter_map(|entry| {
 64                if entry.is_char_boundary(3) {
 65                    let (status, path) = entry.split_at(3);
 66                    let status = status.trim();
 67                    Some((
 68                        RepoPath(PathBuf::from(path)),
 69                        match status {
 70                            "A" | "??" => GitFileStatus::Added,
 71                            "M" => GitFileStatus::Modified,
 72                            _ => return None,
 73                        },
 74                    ))
 75                } else {
 76                    None
 77                }
 78            })
 79            .collect::<Vec<_>>();
 80        entries.sort_unstable_by(|a, b| a.0.cmp(&b.0));
 81        Ok(Self {
 82            entries: entries.into(),
 83        })
 84    }
 85
 86    pub fn get(&self, path: &Path) -> Option<GitFileStatus> {
 87        self.entries
 88            .binary_search_by(|(repo_path, _)| repo_path.0.as_path().cmp(path))
 89            .ok()
 90            .map(|index| self.entries[index].1)
 91    }
 92}
 93
 94impl Default for GitStatus {
 95    fn default() -> Self {
 96        Self {
 97            entries: Arc::new([]),
 98        }
 99    }
100}