1use crate::repository::{GitFileStatus, RepoPath};
2use anyhow::{anyhow, Result};
3use std::{path::Path, process::Stdio, sync::Arc};
4
5#[derive(Clone, Debug, PartialEq, Eq)]
6pub struct GitStatusPair {
7 // Not both `None`.
8 pub index_status: Option<GitFileStatus>,
9 pub worktree_status: Option<GitFileStatus>,
10}
11
12impl GitStatusPair {
13 pub fn is_staged(&self) -> Option<bool> {
14 match (self.index_status, self.worktree_status) {
15 (Some(_), None) => Some(true),
16 (None, Some(_)) => Some(false),
17 (Some(GitFileStatus::Untracked), Some(GitFileStatus::Untracked)) => Some(false),
18 (Some(_), Some(_)) => None,
19 (None, None) => unreachable!(),
20 }
21 }
22
23 // TODO reconsider uses of this
24 pub fn combined(&self) -> GitFileStatus {
25 self.index_status.or(self.worktree_status).unwrap()
26 }
27}
28
29#[derive(Clone)]
30pub struct GitStatus {
31 pub entries: Arc<[(RepoPath, GitStatusPair)]>,
32}
33
34impl GitStatus {
35 pub(crate) fn new(
36 git_binary: &Path,
37 working_directory: &Path,
38 path_prefixes: &[RepoPath],
39 ) -> Result<Self> {
40 let child = util::command::new_std_command(git_binary)
41 .current_dir(working_directory)
42 .args([
43 "--no-optional-locks",
44 "status",
45 "--porcelain=v1",
46 "--untracked-files=all",
47 "--no-renames",
48 "-z",
49 ])
50 .args(path_prefixes.iter().map(|path_prefix| {
51 if path_prefix.0.as_ref() == Path::new("") {
52 Path::new(".")
53 } else {
54 path_prefix
55 }
56 }))
57 .stdin(Stdio::null())
58 .stdout(Stdio::piped())
59 .stderr(Stdio::piped())
60 .spawn()
61 .map_err(|e| anyhow!("Failed to start git status process: {}", e))?;
62
63 let output = child
64 .wait_with_output()
65 .map_err(|e| anyhow!("Failed to read git blame output: {}", e))?;
66
67 if !output.status.success() {
68 let stderr = String::from_utf8_lossy(&output.stderr);
69 return Err(anyhow!("git status process failed: {}", stderr));
70 }
71 let stdout = String::from_utf8_lossy(&output.stdout);
72 let mut entries = stdout
73 .split('\0')
74 .filter_map(|entry| {
75 let sep = entry.get(2..3)?;
76 if sep != " " {
77 return None;
78 };
79 let path = &entry[3..];
80 let status = entry[0..2].as_bytes();
81 let index_status = GitFileStatus::from_byte(status[0]);
82 let worktree_status = GitFileStatus::from_byte(status[1]);
83 if (index_status, worktree_status) == (None, None) {
84 return None;
85 }
86 let path = RepoPath(Path::new(path).into());
87 Some((
88 path,
89 GitStatusPair {
90 index_status,
91 worktree_status,
92 },
93 ))
94 })
95 .collect::<Vec<_>>();
96 entries.sort_unstable_by(|(a, _), (b, _)| a.cmp(&b));
97 Ok(Self {
98 entries: entries.into(),
99 })
100 }
101}
102
103impl Default for GitStatus {
104 fn default() -> Self {
105 Self {
106 entries: Arc::new([]),
107 }
108 }
109}