1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
//! The `void rm` command implementation.
//!
//! Removes files from the index and optionally from the working tree.
use std::path::Path;
use serde::Serialize;
use void_core::workspace::stage::{remove_paths, RemoveOptions};
use crate::context::{build_void_context, void_err_to_cli};
use crate::output::{run_command, CliError, CliOptions};
/// JSON output structure for the rm command.
#[derive(Debug, Clone, Serialize)]
pub struct RmOutput {
/// Files that were removed.
pub removed: Vec<String>,
/// Total count of files removed.
pub count: usize,
}
/// Run the rm command.
///
/// # Arguments
///
/// * `cwd` - Current working directory.
/// * `paths` - Paths to remove.
/// * `cached_only` - If true, only remove from index (keep files on disk).
/// * `force` - If true, remove even if file has uncommitted changes.
/// * `recursive` - If true, recursively remove directories.
/// * `opts` - CLI options.
///
/// # Returns
///
/// Returns `Ok(())` on success, or a `CliError` on failure.
pub fn run(
cwd: &Path,
paths: Vec<String>,
cached_only: bool,
force: bool,
recursive: bool,
opts: &CliOptions,
) -> Result<(), CliError> {
// Filter empty/whitespace paths
let paths: Vec<String> = paths.into_iter().filter(|p| !p.trim().is_empty()).collect();
run_command("rm", opts, |ctx| {
// Validate paths provided (inside run_command for proper JSON error output)
if paths.is_empty() {
return Err(CliError::invalid_args(
"No paths specified. Provide files or directories to remove.",
));
}
// Get repository context
let void_ctx = build_void_context(cwd)?;
ctx.progress("Removing files...");
// Remove the paths
let remove_opts = RemoveOptions {
ctx: void_ctx,
paths,
cached_only,
force,
recursive,
};
let result = remove_paths(remove_opts).map_err(void_err_to_cli)?;
// Print human-readable summary if not in JSON mode
if !ctx.use_json() {
let total_removed = result.removed.len();
if total_removed == 0 {
ctx.info("Nothing to remove");
} else {
for path in &result.removed {
ctx.info(format!("rm '{}'", path));
}
}
}
let count = result.removed.len();
Ok(RmOutput {
removed: result.removed,
count,
})
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_rm_output_serialization() {
let output = RmOutput {
removed: vec!["src/main.rs".to_string(), "src/lib.rs".to_string()],
count: 2,
};
let json = serde_json::to_string(&output).unwrap();
assert!(json.contains("\"removed\""));
assert!(json.contains("\"count\":2"));
assert!(json.contains("src/main.rs"));
assert!(json.contains("src/lib.rs"));
}
#[test]
fn test_rm_output_empty() {
let output = RmOutput {
removed: vec![],
count: 0,
};
let json = serde_json::to_string(&output).unwrap();
assert!(json.contains("\"removed\":[]"));
assert!(json.contains("\"count\":0"));
}
}