Skip to main content

rtb_vcs/git/
status.rs

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