void_core/workspace/
status.rs1use std::collections::{HashMap, HashSet};
4use std::sync::Arc;
5
6use crate::VoidContext;
7use crate::index::{entry_matches_file, IndexEntry};
8use crate::support::events::{emit_workspace, VoidObserver, WorkspaceEvent};
9use crate::{Result, VoidError};
10
11use super::stage::{build_pathspec, build_walker, load_head_entries, load_index_or_empty};
12
13#[derive(Clone)]
15pub struct StatusOptions {
16 pub ctx: VoidContext,
17 pub patterns: Vec<String>,
18 pub observer: Option<Arc<dyn VoidObserver>>,
20}
21
22#[derive(Debug, Clone)]
24pub struct StatusResult {
25 pub staged_added: Vec<String>,
26 pub staged_modified: Vec<String>,
27 pub staged_deleted: Vec<String>,
28 pub unstaged_modified: Vec<String>,
29 pub unstaged_deleted: Vec<String>,
30 pub untracked: Vec<String>,
31}
32
33pub fn status_workspace(opts: StatusOptions) -> Result<StatusResult> {
35 let root = &opts.ctx.paths.root;
36
37 let index = load_index_or_empty(&opts.ctx)?;
38 let head_entries = load_head_entries(&opts.ctx)?;
39 let pathspec = build_pathspec(&opts.patterns)?;
40
41 let mut head_map: HashMap<String, IndexEntry> = HashMap::new();
42 for entry in head_entries {
43 head_map.insert(entry.path.clone(), entry);
44 }
45
46 let mut index_map: HashMap<String, IndexEntry> = HashMap::new();
47 for entry in index.entries.iter() {
48 index_map.insert(entry.path.clone(), entry.clone());
49 }
50
51 let mut staged_added = Vec::new();
52 let mut staged_modified = Vec::new();
53 let mut staged_deleted = Vec::new();
54
55 for entry in index.entries.iter() {
56 if !pathspec.matches(&entry.path) {
57 continue;
58 }
59 match head_map.get(&entry.path) {
60 Some(head) => {
61 if head.content_hash != entry.content_hash {
62 staged_modified.push(entry.path.clone());
63 }
64 }
65 None => staged_added.push(entry.path.clone()),
66 }
67 }
68
69 for (path, _head_entry) in head_map.iter() {
70 if !pathspec.matches(path) {
71 continue;
72 }
73 if !index_map.contains_key(path) {
74 staged_deleted.push(path.clone());
75 }
76 }
77
78 let void_dir_name = opts
79 .ctx
80 .paths
81 .void_dir
82 .file_name()
83 .unwrap_or(".void")
84 .to_string();
85
86 let mut seen = HashSet::new();
87 let mut unstaged_modified = Vec::new();
88 let mut untracked = Vec::new();
89
90 let walker = build_walker(opts.ctx.paths.root.as_std_path(), void_dir_name.clone(), pathspec.clone());
91 for entry in walker.build().flatten() {
92 if !entry.file_type().map(|t| t.is_file()).unwrap_or(false) {
93 continue;
94 }
95
96 let path = entry.path().to_path_buf();
97 let rel = match path.strip_prefix(opts.ctx.paths.root.as_std_path()) {
98 Ok(r) => r.to_string_lossy().replace('\\', "/"),
99 Err(_) => continue,
100 };
101
102 if !pathspec.matches(&rel) {
103 continue;
104 }
105
106 seen.insert(rel.clone());
107
108 emit_workspace(
110 &opts.observer,
111 WorkspaceEvent::Progress {
112 stage: "status".to_string(),
113 current: seen.len() as u64,
114 total: 0, },
116 );
117
118 match index_map.get(&rel) {
119 Some(entry) => {
120 let matches = match entry_matches_file(entry, root) {
121 Ok(value) => value,
122 Err(VoidError::NotFound(_)) => false,
123 Err(err) => return Err(err),
124 };
125 if !matches {
126 unstaged_modified.push(rel.clone());
127 }
128 }
129 None => {
130 untracked.push(rel.clone());
131 }
132 }
133 }
134
135 let mut unstaged_deleted = Vec::new();
136 for entry in index.entries.iter() {
137 if !pathspec.matches(&entry.path) {
138 continue;
139 }
140 if !seen.contains(&entry.path) {
141 unstaged_deleted.push(entry.path.clone());
142 }
143 }
144
145 staged_added.sort();
146 staged_modified.sort();
147 staged_deleted.sort();
148 unstaged_modified.sort();
149 unstaged_deleted.sort();
150 untracked.sort();
151
152 Ok(StatusResult {
153 staged_added,
154 staged_modified,
155 staged_deleted,
156 unstaged_modified,
157 unstaged_deleted,
158 untracked,
159 })
160}