void_core/workspace/
remove.rs1use std::collections::HashMap;
4use std::fs;
5
6use crate::VoidContext;
7use crate::index::{entry_matches_file, write_workspace_index, IndexEntry};
8use crate::{Result, VoidError};
9
10use super::stage::{load_head_entries, load_index_or_empty};
11
12#[derive(Clone)]
14pub struct RemoveOptions {
15 pub ctx: VoidContext,
16 pub paths: Vec<String>,
17 pub cached_only: bool,
19 pub force: bool,
21 pub recursive: bool,
23}
24
25#[derive(Debug, Clone)]
27pub struct RemoveResult {
28 pub removed: Vec<String>,
29}
30
31pub fn remove_paths(opts: RemoveOptions) -> Result<RemoveResult> {
42 let root = &opts.ctx.paths.root;
43
44 let mut index = load_index_or_empty(&opts.ctx)?;
45 let head_entries = load_head_entries(&opts.ctx)?;
46
47 let head_map: HashMap<String, IndexEntry> = head_entries
48 .into_iter()
49 .map(|e| (e.path.clone(), e))
50 .collect();
51
52 let mut to_remove: Vec<String> = Vec::new();
53
54 for path in &opts.paths {
56 let path = path.replace('\\', "/").trim_start_matches('/').to_string();
57
58 if let Some(_entry) = index.get(&path) {
60 to_remove.push(path);
61 } else if opts.recursive {
62 let prefix = if path.ends_with('/') {
64 path.clone()
65 } else {
66 format!("{}/", path)
67 };
68
69 let matching: Vec<String> = index
70 .entries
71 .iter()
72 .filter(|e| e.path.starts_with(&prefix) || e.path == path)
73 .map(|e| e.path.clone())
74 .collect();
75
76 if matching.is_empty() {
77 return Err(VoidError::NotFound(format!(
78 "pathspec '{}' did not match any files",
79 path
80 )));
81 }
82
83 to_remove.extend(matching);
84 } else {
85 let prefix = format!("{}/", path);
87 let has_children = index.entries.iter().any(|e| e.path.starts_with(&prefix));
88 if has_children {
89 return Err(VoidError::InvalidPattern(format!(
90 "'{}' is a directory. Use -r to remove recursively.",
91 path
92 )));
93 }
94
95 return Err(VoidError::NotFound(format!(
96 "pathspec '{}' did not match any files",
97 path
98 )));
99 }
100 }
101
102 to_remove.sort();
104 to_remove.dedup();
105
106 if !opts.force && !opts.cached_only {
108 for path in &to_remove {
109 if let Some(index_entry) = index.get(path) {
110 let file_path = crate::util::safe_join(opts.ctx.paths.root.as_std_path(), path)?;
112
113 if file_path.exists() {
115 let matches = match entry_matches_file(index_entry, root) {
116 Ok(m) => m,
117 Err(VoidError::NotFound(_)) => true, Err(e) => return Err(e),
119 };
120
121 if !matches {
122 return Err(VoidError::InvalidPattern(format!(
124 "file '{}' has local modifications (use --force to remove anyway)",
125 path
126 )));
127 }
128 }
129
130 if let Some(head_entry) = head_map.get(path) {
132 if index_entry.content_hash != head_entry.content_hash {
133 return Err(VoidError::InvalidPattern(format!(
134 "file '{}' has staged changes (use --force to remove anyway)",
135 path
136 )));
137 }
138 } else {
139 return Err(VoidError::InvalidPattern(format!(
141 "file '{}' has staged changes (use --force to remove anyway)",
142 path
143 )));
144 }
145 }
146 }
147 }
148
149 let mut removed = Vec::new();
151 for path in to_remove {
152 index.entries.retain(|e| e.path != path);
154 removed.push(path.clone());
155
156 if !opts.cached_only {
158 let file_path = crate::util::safe_join(opts.ctx.paths.root.as_std_path(), &path)?;
160 if file_path.exists() {
161 fs::remove_file(&file_path)?;
162 }
163 }
164 }
165
166 write_workspace_index(opts.ctx.paths.workspace_dir.as_std_path(), opts.ctx.crypto.vault.index_key()?, &index)?;
167
168 removed.sort();
169 Ok(RemoveResult { removed })
170}