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 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/// Options for status.
14#[derive(Clone)]
15pub struct StatusOptions {
16    pub ctx: VoidContext,
17    pub patterns: Vec<String>,
18    /// Optional observer for progress events.
19    pub observer: Option<Arc<dyn VoidObserver>>,
20}
21
22/// Repository status.
23#[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
33/// Compute repository status.
34pub 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 progress periodically (every file processed)
109        emit_workspace(
110            &opts.observer,
111            WorkspaceEvent::Progress {
112                stage: "status".to_string(),
113                current: seen.len() as u64,
114                total: 0, // Unknown until walk completes
115            },
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}