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
61 let stdout = String::from_utf8_lossy(&output.stdout);
62 let mut entries = stdout
63 .split('\0')
64 .filter_map(|entry| {
65 if entry.is_char_boundary(3) {
66 let (status, path) = entry.split_at(3);
67 let status = status.trim();
68 Some((
69 RepoPath(PathBuf::from(path)),
70 match status {
71 "A" | "??" => GitFileStatus::Added,
72 "M" => GitFileStatus::Modified,
73 _ => return None,
74 },
75 ))
76 } else {
77 None
78 }
79 })
80 .collect::<Vec<_>>();
81 entries.sort_unstable_by(|a, b| a.0.cmp(&b.0));
82 Ok(Self {
83 entries: entries.into(),
84 })
85 }
86
87 pub fn get(&self, path: &Path) -> Option<GitFileStatus> {
88 self.entries
89 .binary_search_by(|(repo_path, _)| repo_path.0.as_path().cmp(path))
90 .ok()
91 .map(|index| self.entries[index].1)
92 }
93}
94
95impl Default for GitStatus {
96 fn default() -> Self {
97 Self {
98 entries: Arc::new([]),
99 }
100 }
101}