void_core/workspace/
status.rs1use 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#[derive(Clone)]
17pub struct StatusOptions {
18 pub ctx: VoidContext,
19 pub patterns: Vec<String>,
20 pub observer: Option<Arc<dyn VoidObserver>>,
22}
23
24#[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
35pub 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 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 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 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 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}