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