Skip to main content

void_core/workspace/
status.rs

1//! Status operations for workspace.
2
3use std::collections::{HashMap, HashSet};
4use std::sync::Arc;
5
6use rayon::prelude::*;
7
8use crate::VoidContext;
9use crate::index::{entry_matches_file, IndexEntry};
10use crate::support::events::{emit_workspace, VoidObserver, WorkspaceEvent};
11use crate::{Result, VoidError};
12
13use super::stage::{build_pathspec, load_head_entries, load_index_or_empty, parallel_walk};
14
15/// Options for status.
16#[derive(Clone)]
17pub struct StatusOptions {
18    pub ctx: VoidContext,
19    pub patterns: Vec<String>,
20    /// Optional observer for progress events.
21    pub observer: Option<Arc<dyn VoidObserver>>,
22}
23
24/// Repository status.
25#[derive(Debug, Clone)]
26pub struct StatusResult {
27    pub staged_added: Vec<String>,
28    pub staged_modified: Vec<String>,
29    pub staged_deleted: Vec<String>,
30    pub unstaged_modified: Vec<String>,
31    pub unstaged_deleted: Vec<String>,
32    pub untracked: Vec<String>,
33}
34
35/// Compute repository status.
36pub fn status_workspace(opts: StatusOptions) -> Result<StatusResult> {
37    let root = &opts.ctx.paths.root;
38
39    let t0 = std::time::Instant::now();
40    let index = load_index_or_empty(&opts.ctx)?;
41    let t1 = t0.elapsed();
42    let head_entries = load_head_entries(&opts.ctx)?;
43    let t2 = t0.elapsed();
44    let pathspec = build_pathspec(&opts.patterns)?;
45
46    let mut head_map: HashMap<String, IndexEntry> = HashMap::new();
47    for entry in head_entries {
48        head_map.insert(entry.path.clone(), entry);
49    }
50
51    let mut index_map: HashMap<String, IndexEntry> = HashMap::new();
52    for entry in index.entries.iter() {
53        index_map.insert(entry.path.clone(), entry.clone());
54    }
55
56    let mut staged_added = Vec::new();
57    let mut staged_modified = Vec::new();
58    let mut staged_deleted = Vec::new();
59
60    for entry in index.entries.iter() {
61        if !pathspec.matches(&entry.path) {
62            continue;
63        }
64        match head_map.get(&entry.path) {
65            Some(head) => {
66                if head.content_hash != entry.content_hash {
67                    staged_modified.push(entry.path.clone());
68                }
69            }
70            None => staged_added.push(entry.path.clone()),
71        }
72    }
73
74    for (path, _head_entry) in head_map.iter() {
75        if !pathspec.matches(path) {
76            continue;
77        }
78        if !index_map.contains_key(path) {
79            staged_deleted.push(path.clone());
80        }
81    }
82
83    let void_dir_name = opts
84        .ctx
85        .paths
86        .void_dir
87        .file_name()
88        .unwrap_or(".void")
89        .to_string();
90
91    // Phase 1: Walk filesystem in parallel and collect all matching paths
92    let file_paths = parallel_walk(opts.ctx.paths.root.as_std_path(), void_dir_name.clone(), pathspec.clone());
93    let t3 = t0.elapsed();
94
95    // Phase 2: Check each file against index in parallel
96    // Returns: true = modified (unstaged), false = matches index, None = untracked
97    let check_results: Vec<Option<bool>> = file_paths
98        .par_iter()
99        .map(|rel| {
100            match index_map.get(rel.as_str()) {
101                Some(entry) => {
102                    let matches = match entry_matches_file(entry, root) {
103                        Ok(value) => value,
104                        Err(VoidError::NotFound(_)) => false,
105                        Err(err) => return Err(err),
106                    };
107                    Ok(Some(!matches))
108                }
109                None => Ok(None),
110            }
111        })
112        .collect::<Result<Vec<_>>>()?;
113    let t4 = t0.elapsed();
114
115    eprintln!("[status timing] index={:?} head={:?} walk={:?} check={:?} files={}",
116        t1, t2 - t1, t3 - t2, t4 - t3, file_paths.len());
117
118    // Phase 3: Collect results
119    let mut seen = HashSet::new();
120    let mut unstaged_modified = Vec::new();
121    let mut untracked = Vec::new();
122
123    for (i, result) in check_results.into_iter().enumerate() {
124        let rel = &file_paths[i];
125        seen.insert(rel.clone());
126        match result {
127            Some(true) => unstaged_modified.push(rel.clone()),
128            Some(false) => {}
129            None => untracked.push(rel.clone()),
130        }
131    }
132
133    let mut unstaged_deleted = Vec::new();
134    for entry in index.entries.iter() {
135        if !pathspec.matches(&entry.path) {
136            continue;
137        }
138        // Non-materialized files (large files not checked out) are expected
139        // to be absent from disk — don't report them as deleted.
140        if !entry.materialized {
141            continue;
142        }
143        if !seen.contains(&entry.path) {
144            unstaged_deleted.push(entry.path.clone());
145        }
146    }
147
148    staged_added.sort();
149    staged_modified.sort();
150    staged_deleted.sort();
151    unstaged_modified.sort();
152    unstaged_deleted.sort();
153    untracked.sort();
154
155    Ok(StatusResult {
156        staged_added,
157        staged_modified,
158        staged_deleted,
159        unstaged_modified,
160        unstaged_deleted,
161        untracked,
162    })
163}