status.rs

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