Skip to main content

rtb_forge/git/
status.rs

1//! `Repo::status` + [`RepoStatus`] value type.
2//!
3//! Surfaces unstaged (worktree vs index) and untracked (paths the
4//! index doesn't know about) changes. The HEAD-tree-to-index diff is
5//! never configured, so `staged` is always empty.
6
7use std::path::PathBuf;
8
9use super::{Repo, RepoError};
10
11/// Working-tree status snapshot.
12///
13/// Returned by [`Repo::status`]. The three lists are mutually
14/// exclusive: a path appears in exactly one bucket.
15#[derive(Debug, Default, Clone, PartialEq, Eq)]
16pub struct RepoStatus {
17    /// Always empty: the index-vs-HEAD diff is not computed. Reserved
18    /// for staged paths.
19    pub staged: Vec<PathBuf>,
20    /// Paths that are tracked but have unstaged worktree changes
21    /// (worktree vs index diff).
22    pub unstaged: Vec<PathBuf>,
23    /// Paths the index doesn't know about.
24    pub untracked: Vec<PathBuf>,
25}
26
27impl Repo {
28    /// Compute the working-tree status.
29    ///
30    /// # Errors
31    ///
32    /// - [`RepoError::StatusFailed`] — gix could not iterate the
33    ///   status (permission errors, malformed index, etc.). `cause`
34    ///   carries the backend's stringified error.
35    pub async fn status(&self) -> Result<RepoStatus, RepoError> {
36        let inner = self.thread_safe();
37        tokio::task::spawn_blocking(move || compute_status(&inner)).await.map_err(|join_err| {
38            RepoError::StatusFailed { cause: format!("spawn_blocking join error: {join_err}") }
39        })?
40    }
41}
42
43fn compute_status(inner: &gix::ThreadSafeRepository) -> Result<RepoStatus, RepoError> {
44    let repo = inner.to_thread_local();
45    let platform = repo
46        .status(gix::progress::Discard)
47        .map_err(|e| RepoError::StatusFailed { cause: e.to_string() })?
48        .untracked_files(gix::status::UntrackedFiles::Files);
49
50    // `head_tree` is never set on the Platform, so nothing routes here.
51    let staged: Vec<PathBuf> = Vec::new();
52    let mut unstaged: Vec<PathBuf> = Vec::new();
53    let mut untracked: Vec<PathBuf> = Vec::new();
54
55    let iter = platform
56        .into_iter(std::iter::empty::<gix::bstr::BString>())
57        .map_err(|e| RepoError::StatusFailed { cause: e.to_string() })?;
58
59    for item in iter {
60        let item = item.map_err(|e| RepoError::StatusFailed { cause: e.to_string() })?;
61        match item {
62            gix::status::Item::IndexWorktree(iw) => match iw {
63                gix::status::index_worktree::Item::Modification { rela_path, .. } => {
64                    unstaged.push(status_path(rela_path)?);
65                }
66                gix::status::index_worktree::Item::Rewrite { dirwalk_entry, .. } => {
67                    // Rewrites surface a directory-walk entry; treat
68                    // the destination as unstaged for v0.5 commit 1.
69                    // Rename / copy classification is a v0.5.x
70                    // concern once a consumer actually needs it.
71                    unstaged.push(status_path(dirwalk_entry.rela_path)?);
72                }
73                gix::status::index_worktree::Item::DirectoryContents { entry, .. } => {
74                    if matches!(entry.status, gix::dir::entry::Status::Untracked) {
75                        untracked.push(status_path(entry.rela_path)?);
76                    }
77                    // Ignored / Pruned / Tracked statuses aren't
78                    // surfaced in the foundation slice.
79                }
80            },
81            gix::status::Item::TreeIndex(_) => {
82                // Only fires when `head_tree(...)` is set, which it is not.
83            }
84        }
85    }
86
87    Ok(RepoStatus { staged, unstaged, untracked })
88}
89
90fn status_path(rela_path: gix::bstr::BString) -> Result<PathBuf, RepoError> {
91    gix::path::from_bstring(rela_path)
92        .map_err(|e| RepoError::StatusFailed { cause: format!("path encoding: {e}") })
93}