docker_wrapper/command/
diff.rs

1//! Docker diff command implementation.
2//!
3//! This module provides the `docker diff` command for inspecting filesystem changes in a container.
4
5use super::{CommandExecutor, CommandOutput, DockerCommand};
6use crate::error::Result;
7use async_trait::async_trait;
8
9/// Docker diff command builder
10///
11/// Inspect changes to files or folders on a container's filesystem.
12///
13/// # Example
14///
15/// ```no_run
16/// use docker_wrapper::DiffCommand;
17///
18/// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
19/// // Show filesystem changes
20/// let changes = DiffCommand::new("my-container")
21///     .run()
22///     .await?;
23///
24/// for change in changes.filesystem_changes() {
25///     println!("{}: {}", change.change_type, change.path);
26/// }
27/// # Ok(())
28/// # }
29/// ```
30#[derive(Debug, Clone)]
31pub struct DiffCommand {
32    /// Container name or ID
33    container: String,
34    /// Command executor
35    pub executor: CommandExecutor,
36}
37
38impl DiffCommand {
39    /// Create a new diff command
40    ///
41    /// # Example
42    ///
43    /// ```
44    /// use docker_wrapper::DiffCommand;
45    ///
46    /// let cmd = DiffCommand::new("my-container");
47    /// ```
48    #[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    /// Execute the diff command
57    ///
58    /// # Errors
59    /// Returns an error if:
60    /// - The Docker daemon is not running
61    /// - The container doesn't exist
62    ///
63    /// # Example
64    ///
65    /// ```no_run
66    /// use docker_wrapper::DiffCommand;
67    ///
68    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
69    /// let result = DiffCommand::new("my-container")
70    ///     .run()
71    ///     .await?;
72    ///
73    /// if result.success() {
74    ///     println!("Filesystem changes:");
75    ///     for change in result.filesystem_changes() {
76    ///         println!("{}: {}", change.change_type, change.path);
77    ///     }
78    /// }
79    /// # Ok(())
80    /// # }
81    /// ```
82    pub async fn run(&self) -> Result<DiffResult> {
83        let output = self.execute().await?;
84
85        // Parse filesystem changes from output
86        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    /// Parse filesystem changes from diff command output
96    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            // Format: "C /path/to/file" where C is change type (A, D, C)
106            if line.len() > 2 {
107                let change_char = line.chars().next().unwrap_or(' ');
108                let path = &line[2..]; // Skip change character and space
109
110                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/// Result from the diff command
158#[derive(Debug, Clone)]
159pub struct DiffResult {
160    /// Raw command output
161    pub output: CommandOutput,
162    /// Container that was inspected
163    pub container: String,
164    /// Parsed filesystem changes
165    pub filesystem_changes: Vec<FilesystemChange>,
166}
167
168impl DiffResult {
169    /// Check if the diff command was successful
170    #[must_use]
171    pub fn success(&self) -> bool {
172        self.output.success
173    }
174
175    /// Get the container name
176    #[must_use]
177    pub fn container(&self) -> &str {
178        &self.container
179    }
180
181    /// Get the filesystem changes
182    #[must_use]
183    pub fn filesystem_changes(&self) -> &[FilesystemChange] {
184        &self.filesystem_changes
185    }
186
187    /// Get the raw command output
188    #[must_use]
189    pub fn output(&self) -> &CommandOutput {
190        &self.output
191    }
192
193    /// Get change count
194    #[must_use]
195    pub fn change_count(&self) -> usize {
196        self.filesystem_changes.len()
197    }
198
199    /// Check if there are any changes
200    #[must_use]
201    pub fn has_changes(&self) -> bool {
202        !self.filesystem_changes.is_empty()
203    }
204
205    /// Get changes by type
206    #[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/// Information about a filesystem change in a container
216#[derive(Debug, Clone)]
217pub struct FilesystemChange {
218    /// Type of change (Added, Deleted, Changed)
219    pub change_type: FilesystemChangeType,
220    /// Path that was changed
221    pub path: String,
222    /// Raw output line
223    pub raw_line: String,
224}
225
226/// Type of filesystem change
227#[derive(Debug, Clone, PartialEq, Eq)]
228pub enum FilesystemChangeType {
229    /// File or directory was added
230    Added,
231    /// File or directory was deleted
232    Deleted,
233    /// File or directory was changed
234    Changed,
235    /// Unknown change type with the raw character
236    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}