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}