docker_wrapper/command/
diff.rs1use super::{CommandExecutor, CommandOutput, DockerCommand};
6use crate::error::Result;
7use async_trait::async_trait;
8
9#[derive(Debug, Clone)]
31pub struct DiffCommand {
32 container: String,
34 pub executor: CommandExecutor,
36}
37
38impl DiffCommand {
39 #[must_use]
49 pub fn new(container: impl Into<String>) -> Self {
50 Self {
51 container: container.into(),
52 executor: CommandExecutor::new(),
53 }
54 }
55
56 pub async fn run(&self) -> Result<DiffResult> {
83 let output = self.execute().await?;
84
85 let filesystem_changes = Self::parse_filesystem_changes(&output.stdout);
87
88 Ok(DiffResult {
89 output,
90 container: self.container.clone(),
91 filesystem_changes,
92 })
93 }
94
95 fn parse_filesystem_changes(stdout: &str) -> Vec<FilesystemChange> {
97 let mut changes = Vec::new();
98
99 for line in stdout.lines() {
100 let line = line.trim();
101 if line.is_empty() {
102 continue;
103 }
104
105 if line.len() > 2 {
107 let change_char = line.chars().next().unwrap_or(' ');
108 let path = &line[2..]; let change_type = match change_char {
111 'A' => FilesystemChangeType::Added,
112 'D' => FilesystemChangeType::Deleted,
113 'C' => FilesystemChangeType::Changed,
114 _ => FilesystemChangeType::Unknown(change_char.to_string()),
115 };
116
117 changes.push(FilesystemChange {
118 change_type,
119 path: path.to_string(),
120 raw_line: line.to_string(),
121 });
122 }
123 }
124
125 changes
126 }
127}
128
129#[async_trait]
130impl DockerCommand for DiffCommand {
131 type Output = CommandOutput;
132
133 fn build_command_args(&self) -> Vec<String> {
134 let mut args = vec!["diff".to_string(), self.container.clone()];
135 args.extend(self.executor.raw_args.clone());
136 args
137 }
138
139 fn get_executor(&self) -> &CommandExecutor {
140 &self.executor
141 }
142
143 fn get_executor_mut(&mut self) -> &mut CommandExecutor {
144 &mut self.executor
145 }
146
147 async fn execute(&self) -> Result<Self::Output> {
148 let args = self.build_command_args();
149 let command_name = args[0].clone();
150 let command_args = args[1..].to_vec();
151 self.executor
152 .execute_command(&command_name, command_args)
153 .await
154 }
155}
156
157#[derive(Debug, Clone)]
159pub struct DiffResult {
160 pub output: CommandOutput,
162 pub container: String,
164 pub filesystem_changes: Vec<FilesystemChange>,
166}
167
168impl DiffResult {
169 #[must_use]
171 pub fn success(&self) -> bool {
172 self.output.success
173 }
174
175 #[must_use]
177 pub fn container(&self) -> &str {
178 &self.container
179 }
180
181 #[must_use]
183 pub fn filesystem_changes(&self) -> &[FilesystemChange] {
184 &self.filesystem_changes
185 }
186
187 #[must_use]
189 pub fn output(&self) -> &CommandOutput {
190 &self.output
191 }
192
193 #[must_use]
195 pub fn change_count(&self) -> usize {
196 self.filesystem_changes.len()
197 }
198
199 #[must_use]
201 pub fn has_changes(&self) -> bool {
202 !self.filesystem_changes.is_empty()
203 }
204
205 #[must_use]
207 pub fn changes_by_type(&self, change_type: &FilesystemChangeType) -> Vec<&FilesystemChange> {
208 self.filesystem_changes
209 .iter()
210 .filter(|change| &change.change_type == change_type)
211 .collect()
212 }
213}
214
215#[derive(Debug, Clone)]
217pub struct FilesystemChange {
218 pub change_type: FilesystemChangeType,
220 pub path: String,
222 pub raw_line: String,
224}
225
226#[derive(Debug, Clone, PartialEq, Eq)]
228pub enum FilesystemChangeType {
229 Added,
231 Deleted,
233 Changed,
235 Unknown(String),
237}
238
239impl std::fmt::Display for FilesystemChangeType {
240 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
241 match self {
242 Self::Added => write!(f, "Added"),
243 Self::Deleted => write!(f, "Deleted"),
244 Self::Changed => write!(f, "Changed"),
245 Self::Unknown(char) => write!(f, "Unknown({char})"),
246 }
247 }
248}
249
250#[cfg(test)]
251mod tests {
252 use super::*;
253
254 #[test]
255 fn test_diff_basic() {
256 let cmd = DiffCommand::new("test-container");
257 let args = cmd.build_command_args();
258 assert_eq!(args, vec!["diff", "test-container"]);
259 }
260
261 #[test]
262 fn test_parse_filesystem_changes() {
263 let output = "A /new/file.txt\nD /deleted/file.txt\nC /changed/file.txt";
264 let changes = DiffCommand::parse_filesystem_changes(output);
265
266 assert_eq!(changes.len(), 3);
267
268 assert_eq!(changes[0].change_type, FilesystemChangeType::Added);
269 assert_eq!(changes[0].path, "/new/file.txt");
270
271 assert_eq!(changes[1].change_type, FilesystemChangeType::Deleted);
272 assert_eq!(changes[1].path, "/deleted/file.txt");
273
274 assert_eq!(changes[2].change_type, FilesystemChangeType::Changed);
275 assert_eq!(changes[2].path, "/changed/file.txt");
276 }
277
278 #[test]
279 fn test_parse_filesystem_changes_empty() {
280 let changes = DiffCommand::parse_filesystem_changes("");
281 assert!(changes.is_empty());
282 }
283
284 #[test]
285 fn test_parse_filesystem_changes_unknown_type() {
286 let output = "X /unknown/file.txt";
287 let changes = DiffCommand::parse_filesystem_changes(output);
288
289 assert_eq!(changes.len(), 1);
290 assert_eq!(
291 changes[0].change_type,
292 FilesystemChangeType::Unknown("X".to_string())
293 );
294 assert_eq!(changes[0].path, "/unknown/file.txt");
295 }
296
297 #[test]
298 fn test_filesystem_change_type_display() {
299 assert_eq!(FilesystemChangeType::Added.to_string(), "Added");
300 assert_eq!(FilesystemChangeType::Deleted.to_string(), "Deleted");
301 assert_eq!(FilesystemChangeType::Changed.to_string(), "Changed");
302 assert_eq!(
303 FilesystemChangeType::Unknown("X".to_string()).to_string(),
304 "Unknown(X)"
305 );
306 }
307
308 #[test]
309 fn test_diff_result_helpers() {
310 let result = DiffResult {
311 output: CommandOutput {
312 stdout: "A /new\nD /old".to_string(),
313 stderr: String::new(),
314 exit_code: 0,
315 success: true,
316 },
317 container: "test".to_string(),
318 filesystem_changes: vec![
319 FilesystemChange {
320 change_type: FilesystemChangeType::Added,
321 path: "/new".to_string(),
322 raw_line: "A /new".to_string(),
323 },
324 FilesystemChange {
325 change_type: FilesystemChangeType::Deleted,
326 path: "/old".to_string(),
327 raw_line: "D /old".to_string(),
328 },
329 ],
330 };
331
332 assert!(result.has_changes());
333 assert_eq!(result.change_count(), 2);
334
335 let added = result.changes_by_type(&FilesystemChangeType::Added);
336 assert_eq!(added.len(), 1);
337 assert_eq!(added[0].path, "/new");
338
339 let deleted = result.changes_by_type(&FilesystemChangeType::Deleted);
340 assert_eq!(deleted.len(), 1);
341 assert_eq!(deleted[0].path, "/old");
342 }
343}