status.rs

  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}