status.rs

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