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}