docker_wrapper/command/
history.rs

1//! Docker history command implementation.
2//!
3//! This module provides the `docker history` command for showing image layer history.
4
5use super::{CommandExecutor, CommandOutput, DockerCommand};
6use crate::error::Result;
7use async_trait::async_trait;
8use serde::{Deserialize, Serialize};
9
10/// Docker history command builder
11///
12/// Show the history of an image, including layer information.
13///
14/// # Example
15///
16/// ```no_run
17/// use docker_wrapper::HistoryCommand;
18///
19/// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
20/// // Show image history
21/// let history = HistoryCommand::new("nginx:latest")
22///     .run()
23///     .await?;
24///
25/// for layer in history.layers() {
26///     println!("{}: {} ({})", layer.id, layer.created_by, layer.size);
27/// }
28/// # Ok(())
29/// # }
30/// ```
31#[derive(Debug, Clone)]
32pub struct HistoryCommand {
33    /// Image name or ID
34    image: String,
35    /// Show human readable sizes
36    human: bool,
37    /// Don't truncate output
38    no_trunc: bool,
39    /// Show quiet output (only image IDs)
40    quiet: bool,
41    /// Format output using a Go template
42    format: Option<String>,
43    /// Command executor
44    pub executor: CommandExecutor,
45}
46
47impl HistoryCommand {
48    /// Create a new history command
49    ///
50    /// # Example
51    ///
52    /// ```
53    /// use docker_wrapper::HistoryCommand;
54    ///
55    /// let cmd = HistoryCommand::new("nginx:latest");
56    /// ```
57    #[must_use]
58    pub fn new(image: impl Into<String>) -> Self {
59        Self {
60            image: image.into(),
61            human: false,
62            no_trunc: false,
63            quiet: false,
64            format: None,
65            executor: CommandExecutor::new(),
66        }
67    }
68
69    /// Show human readable sizes
70    ///
71    /// # Example
72    ///
73    /// ```
74    /// use docker_wrapper::HistoryCommand;
75    ///
76    /// let cmd = HistoryCommand::new("nginx:latest")
77    ///     .human(true);
78    /// ```
79    #[must_use]
80    pub fn human(mut self, human: bool) -> Self {
81        self.human = human;
82        self
83    }
84
85    /// Don't truncate output
86    ///
87    /// # Example
88    ///
89    /// ```
90    /// use docker_wrapper::HistoryCommand;
91    ///
92    /// let cmd = HistoryCommand::new("nginx:latest")
93    ///     .no_trunc(true);
94    /// ```
95    #[must_use]
96    pub fn no_trunc(mut self, no_trunc: bool) -> Self {
97        self.no_trunc = no_trunc;
98        self
99    }
100
101    /// Show quiet output (only image IDs)
102    ///
103    /// # Example
104    ///
105    /// ```
106    /// use docker_wrapper::HistoryCommand;
107    ///
108    /// let cmd = HistoryCommand::new("nginx:latest")
109    ///     .quiet(true);
110    /// ```
111    #[must_use]
112    pub fn quiet(mut self, quiet: bool) -> Self {
113        self.quiet = quiet;
114        self
115    }
116
117    /// Format output using a Go template
118    ///
119    /// # Example
120    ///
121    /// ```
122    /// use docker_wrapper::HistoryCommand;
123    ///
124    /// let cmd = HistoryCommand::new("nginx:latest")
125    ///     .format("{{.ID}}: {{.CreatedBy}}");
126    /// ```
127    #[must_use]
128    pub fn format(mut self, format: impl Into<String>) -> Self {
129        self.format = Some(format.into());
130        self
131    }
132
133    /// Execute the history command
134    ///
135    /// # Errors
136    /// Returns an error if:
137    /// - The Docker daemon is not running
138    /// - The image doesn't exist
139    ///
140    /// # Example
141    ///
142    /// ```no_run
143    /// use docker_wrapper::HistoryCommand;
144    ///
145    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
146    /// let result = HistoryCommand::new("nginx:latest")
147    ///     .run()
148    ///     .await?;
149    ///
150    /// if result.success() {
151    ///     println!("Image layers:");
152    ///     for layer in result.layers() {
153    ///         println!("{}: {}", layer.id, layer.size);
154    ///     }
155    /// }
156    /// # Ok(())
157    /// # }
158    /// ```
159    pub async fn run(&self) -> Result<HistoryResult> {
160        let output = self.execute().await?;
161
162        // Parse layers from output
163        let layers = if self.format.as_deref() == Some("json") {
164            Self::parse_json_layers(&output.stdout)
165        } else {
166            Self::parse_table_layers(&output.stdout)
167        };
168
169        Ok(HistoryResult {
170            output,
171            image: self.image.clone(),
172            layers,
173        })
174    }
175
176    /// Parse JSON layer output
177    fn parse_json_layers(stdout: &str) -> Vec<ImageLayer> {
178        let mut layers = Vec::new();
179
180        for line in stdout.lines() {
181            let line = line.trim();
182            if line.is_empty() {
183                continue;
184            }
185
186            if let Ok(layer) = serde_json::from_str::<ImageLayer>(line) {
187                layers.push(layer);
188            }
189        }
190
191        layers
192    }
193
194    /// Parse table format layer output
195    fn parse_table_layers(stdout: &str) -> Vec<ImageLayer> {
196        let mut layers = Vec::new();
197        let lines: Vec<&str> = stdout.lines().collect();
198
199        if lines.len() < 2 {
200            return layers;
201        }
202
203        // Skip header line
204        for line in lines.iter().skip(1) {
205            let parts: Vec<&str> = line.split_whitespace().collect();
206
207            if parts.len() >= 4 {
208                let layer = ImageLayer {
209                    id: parts[0].to_string(),
210                    created: if parts.len() > 1 {
211                        parts[1].to_string()
212                    } else {
213                        String::new()
214                    },
215                    created_by: if parts.len() > 3 {
216                        parts[3..].join(" ")
217                    } else {
218                        String::new()
219                    },
220                    size: if parts.len() > 2 {
221                        parts[2].to_string()
222                    } else {
223                        String::new()
224                    },
225                    comment: String::new(),
226                };
227                layers.push(layer);
228            }
229        }
230
231        layers
232    }
233}
234
235#[async_trait]
236impl DockerCommand for HistoryCommand {
237    type Output = CommandOutput;
238
239    fn build_command_args(&self) -> Vec<String> {
240        let mut args = vec!["history".to_string()];
241
242        if self.human {
243            args.push("--human".to_string());
244        }
245
246        if self.no_trunc {
247            args.push("--no-trunc".to_string());
248        }
249
250        if self.quiet {
251            args.push("--quiet".to_string());
252        }
253
254        if let Some(ref format) = self.format {
255            args.push("--format".to_string());
256            args.push(format.clone());
257        }
258
259        args.push(self.image.clone());
260        args.extend(self.executor.raw_args.clone());
261        args
262    }
263
264    async fn execute(&self) -> Result<Self::Output> {
265        let args = self.build_command_args();
266        let command_name = args[0].clone();
267        let command_args = args[1..].to_vec();
268        self.executor
269            .execute_command(&command_name, command_args)
270            .await
271    }
272
273    fn get_executor(&self) -> &CommandExecutor {
274        &self.executor
275    }
276
277    fn get_executor_mut(&mut self) -> &mut CommandExecutor {
278        &mut self.executor
279    }
280}
281
282/// Result from the history command
283#[derive(Debug, Clone)]
284pub struct HistoryResult {
285    /// Raw command output
286    pub output: CommandOutput,
287    /// Image that was inspected
288    pub image: String,
289    /// Parsed image layers
290    pub layers: Vec<ImageLayer>,
291}
292
293impl HistoryResult {
294    /// Check if the history command was successful
295    #[must_use]
296    pub fn success(&self) -> bool {
297        self.output.success
298    }
299
300    /// Get the image name
301    #[must_use]
302    pub fn image(&self) -> &str {
303        &self.image
304    }
305
306    /// Get the image layers
307    #[must_use]
308    pub fn layers(&self) -> &[ImageLayer] {
309        &self.layers
310    }
311
312    /// Get the raw command output
313    #[must_use]
314    pub fn output(&self) -> &CommandOutput {
315        &self.output
316    }
317
318    /// Get layer count
319    #[must_use]
320    pub fn layer_count(&self) -> usize {
321        self.layers.len()
322    }
323
324    /// Get total size of all layers (if parseable)
325    #[must_use]
326    pub fn total_size_bytes(&self) -> Option<u64> {
327        let mut total = 0u64;
328
329        for layer in &self.layers {
330            if let Some(size) = Self::parse_size(&layer.size) {
331                total = total.saturating_add(size);
332            } else {
333                return None; // If any layer size can't be parsed, return None
334            }
335        }
336
337        Some(total)
338    }
339
340    /// Parse size string to bytes
341    fn parse_size(size_str: &str) -> Option<u64> {
342        if size_str.is_empty() || size_str == "0B" {
343            return Some(0);
344        }
345
346        let size_str = size_str.trim();
347        if let Some(stripped) = size_str.strip_suffix("B") {
348            if let Ok(bytes) = stripped.parse::<u64>() {
349                return Some(bytes);
350            }
351        }
352
353        None
354    }
355}
356
357/// Information about an image layer
358#[derive(Debug, Clone, Serialize, Deserialize)]
359pub struct ImageLayer {
360    /// Layer ID
361    #[serde(rename = "ID")]
362    pub id: String,
363    /// Creation timestamp
364    #[serde(rename = "Created")]
365    pub created: String,
366    /// Command that created this layer
367    #[serde(rename = "CreatedBy")]
368    pub created_by: String,
369    /// Size of this layer
370    #[serde(rename = "Size")]
371    pub size: String,
372    /// Comment for this layer
373    #[serde(rename = "Comment")]
374    pub comment: String,
375}
376
377#[cfg(test)]
378mod tests {
379    use super::*;
380
381    #[test]
382    fn test_history_basic() {
383        let cmd = HistoryCommand::new("nginx:latest");
384        let args = cmd.build_command_args();
385        assert_eq!(args, vec!["history", "nginx:latest"]);
386    }
387
388    #[test]
389    fn test_history_all_options() {
390        let cmd = HistoryCommand::new("nginx:latest")
391            .human(true)
392            .no_trunc(true)
393            .quiet(true)
394            .format("json");
395        let args = cmd.build_command_args();
396        assert_eq!(
397            args,
398            vec![
399                "history",
400                "--human",
401                "--no-trunc",
402                "--quiet",
403                "--format",
404                "json",
405                "nginx:latest"
406            ]
407        );
408    }
409
410    #[test]
411    fn test_history_with_format() {
412        let cmd = HistoryCommand::new("ubuntu").format("{{.ID}}: {{.Size}}");
413        let args = cmd.build_command_args();
414        assert_eq!(
415            args,
416            vec!["history", "--format", "{{.ID}}: {{.Size}}", "ubuntu"]
417        );
418    }
419
420    #[test]
421    fn test_parse_table_layers() {
422        let output = "IMAGE          CREATED        SIZE      COMMENT\nabc123         2023-01-01     100MB     layer-comment\ndef456         2023-01-02     50MB      another-comment";
423
424        let layers = HistoryCommand::parse_table_layers(output);
425        assert_eq!(layers.len(), 2);
426
427        assert_eq!(layers[0].id, "abc123");
428        assert_eq!(layers[0].created, "2023-01-01");
429        assert_eq!(layers[0].size, "100MB");
430        assert_eq!(layers[0].created_by, "layer-comment");
431
432        assert_eq!(layers[1].id, "def456");
433        assert_eq!(layers[1].created, "2023-01-02");
434        assert_eq!(layers[1].size, "50MB");
435        assert_eq!(layers[1].created_by, "another-comment");
436    }
437
438    #[test]
439    fn test_parse_json_layers() {
440        let output = r#"{"ID":"abc123","Created":"2023-01-01","CreatedBy":"RUN apt-get update","Size":"100MB","Comment":""}
441{"ID":"def456","Created":"2023-01-02","CreatedBy":"COPY . /app","Size":"50MB","Comment":""}"#;
442
443        let layers = HistoryCommand::parse_json_layers(output);
444        assert_eq!(layers.len(), 2);
445
446        assert_eq!(layers[0].id, "abc123");
447        assert_eq!(layers[0].created_by, "RUN apt-get update");
448        assert_eq!(layers[0].size, "100MB");
449
450        assert_eq!(layers[1].id, "def456");
451        assert_eq!(layers[1].created_by, "COPY . /app");
452        assert_eq!(layers[1].size, "50MB");
453    }
454
455    #[test]
456    fn test_parse_json_layers_empty() {
457        let layers = HistoryCommand::parse_json_layers("");
458        assert!(layers.is_empty());
459    }
460
461    #[test]
462    fn test_history_result_helpers() {
463        let result = HistoryResult {
464            output: CommandOutput {
465                stdout: String::new(),
466                stderr: String::new(),
467                exit_code: 0,
468                success: true,
469            },
470            image: "nginx".to_string(),
471            layers: vec![
472                ImageLayer {
473                    id: "layer1".to_string(),
474                    created: "2023-01-01".to_string(),
475                    created_by: "RUN command".to_string(),
476                    size: "100B".to_string(),
477                    comment: String::new(),
478                },
479                ImageLayer {
480                    id: "layer2".to_string(),
481                    created: "2023-01-02".to_string(),
482                    created_by: "COPY files".to_string(),
483                    size: "200B".to_string(),
484                    comment: String::new(),
485                },
486            ],
487        };
488
489        assert_eq!(result.layer_count(), 2);
490        assert_eq!(result.total_size_bytes(), Some(300));
491    }
492
493    #[test]
494    fn test_parse_size() {
495        assert_eq!(HistoryResult::parse_size("100B"), Some(100));
496        assert_eq!(HistoryResult::parse_size("0B"), Some(0));
497        assert_eq!(HistoryResult::parse_size(""), Some(0));
498        assert_eq!(HistoryResult::parse_size("invalid"), None);
499    }
500}