1use super::{CommandExecutor, CommandOutput, DockerCommand};
6use crate::error::Result;
7use async_trait::async_trait;
8use serde::{Deserialize, Serialize};
9
10#[derive(Debug, Clone)]
32pub struct HistoryCommand {
33 image: String,
35 human: bool,
37 no_trunc: bool,
39 quiet: bool,
41 format: Option<String>,
43 pub executor: CommandExecutor,
45}
46
47impl HistoryCommand {
48 #[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 #[must_use]
80 pub fn human(mut self, human: bool) -> Self {
81 self.human = human;
82 self
83 }
84
85 #[must_use]
96 pub fn no_trunc(mut self, no_trunc: bool) -> Self {
97 self.no_trunc = no_trunc;
98 self
99 }
100
101 #[must_use]
112 pub fn quiet(mut self, quiet: bool) -> Self {
113 self.quiet = quiet;
114 self
115 }
116
117 #[must_use]
128 pub fn format(mut self, format: impl Into<String>) -> Self {
129 self.format = Some(format.into());
130 self
131 }
132
133 pub async fn run(&self) -> Result<HistoryResult> {
160 let output = self.execute().await?;
161
162 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 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 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 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#[derive(Debug, Clone)]
284pub struct HistoryResult {
285 pub output: CommandOutput,
287 pub image: String,
289 pub layers: Vec<ImageLayer>,
291}
292
293impl HistoryResult {
294 #[must_use]
296 pub fn success(&self) -> bool {
297 self.output.success
298 }
299
300 #[must_use]
302 pub fn image(&self) -> &str {
303 &self.image
304 }
305
306 #[must_use]
308 pub fn layers(&self) -> &[ImageLayer] {
309 &self.layers
310 }
311
312 #[must_use]
314 pub fn output(&self) -> &CommandOutput {
315 &self.output
316 }
317
318 #[must_use]
320 pub fn layer_count(&self) -> usize {
321 self.layers.len()
322 }
323
324 #[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; }
335 }
336
337 Some(total)
338 }
339
340 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#[derive(Debug, Clone, Serialize, Deserialize)]
359pub struct ImageLayer {
360 #[serde(rename = "ID")]
362 pub id: String,
363 #[serde(rename = "Created")]
365 pub created: String,
366 #[serde(rename = "CreatedBy")]
368 pub created_by: String,
369 #[serde(rename = "Size")]
371 pub size: String,
372 #[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}