use super::{CommandExecutor, CommandOutput, DockerCommand};
use crate::error::Result;
use async_trait::async_trait;
#[derive(Debug, Clone)]
pub struct DiffCommand {
container: String,
pub executor: CommandExecutor,
}
impl DiffCommand {
#[must_use]
pub fn new(container: impl Into<String>) -> Self {
Self {
container: container.into(),
executor: CommandExecutor::new(),
}
}
pub async fn run(&self) -> Result<DiffResult> {
let output = self.execute().await?;
let filesystem_changes = Self::parse_filesystem_changes(&output.stdout);
Ok(DiffResult {
output,
container: self.container.clone(),
filesystem_changes,
})
}
fn parse_filesystem_changes(stdout: &str) -> Vec<FilesystemChange> {
let mut changes = Vec::new();
for line in stdout.lines() {
let line = line.trim();
if line.is_empty() {
continue;
}
if line.len() > 2 {
let change_char = line.chars().next().unwrap_or(' ');
let path = &line[2..];
let change_type = match change_char {
'A' => FilesystemChangeType::Added,
'D' => FilesystemChangeType::Deleted,
'C' => FilesystemChangeType::Changed,
_ => FilesystemChangeType::Unknown(change_char.to_string()),
};
changes.push(FilesystemChange {
change_type,
path: path.to_string(),
raw_line: line.to_string(),
});
}
}
changes
}
}
#[async_trait]
impl DockerCommand for DiffCommand {
type Output = CommandOutput;
fn build_command_args(&self) -> Vec<String> {
let mut args = vec!["diff".to_string(), self.container.clone()];
args.extend(self.executor.raw_args.clone());
args
}
fn get_executor(&self) -> &CommandExecutor {
&self.executor
}
fn get_executor_mut(&mut self) -> &mut CommandExecutor {
&mut self.executor
}
async fn execute(&self) -> Result<Self::Output> {
let args = self.build_command_args();
let command_name = args[0].clone();
let command_args = args[1..].to_vec();
self.executor
.execute_command(&command_name, command_args)
.await
}
}
#[derive(Debug, Clone)]
pub struct DiffResult {
pub output: CommandOutput,
pub container: String,
pub filesystem_changes: Vec<FilesystemChange>,
}
impl DiffResult {
#[must_use]
pub fn success(&self) -> bool {
self.output.success
}
#[must_use]
pub fn container(&self) -> &str {
&self.container
}
#[must_use]
pub fn filesystem_changes(&self) -> &[FilesystemChange] {
&self.filesystem_changes
}
#[must_use]
pub fn output(&self) -> &CommandOutput {
&self.output
}
#[must_use]
pub fn change_count(&self) -> usize {
self.filesystem_changes.len()
}
#[must_use]
pub fn has_changes(&self) -> bool {
!self.filesystem_changes.is_empty()
}
#[must_use]
pub fn changes_by_type(&self, change_type: &FilesystemChangeType) -> Vec<&FilesystemChange> {
self.filesystem_changes
.iter()
.filter(|change| &change.change_type == change_type)
.collect()
}
}
#[derive(Debug, Clone)]
pub struct FilesystemChange {
pub change_type: FilesystemChangeType,
pub path: String,
pub raw_line: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum FilesystemChangeType {
Added,
Deleted,
Changed,
Unknown(String),
}
impl std::fmt::Display for FilesystemChangeType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Added => write!(f, "Added"),
Self::Deleted => write!(f, "Deleted"),
Self::Changed => write!(f, "Changed"),
Self::Unknown(char) => write!(f, "Unknown({char})"),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_diff_basic() {
let cmd = DiffCommand::new("test-container");
let args = cmd.build_command_args();
assert_eq!(args, vec!["diff", "test-container"]);
}
#[test]
fn test_parse_filesystem_changes() {
let output = "A /new/file.txt\nD /deleted/file.txt\nC /changed/file.txt";
let changes = DiffCommand::parse_filesystem_changes(output);
assert_eq!(changes.len(), 3);
assert_eq!(changes[0].change_type, FilesystemChangeType::Added);
assert_eq!(changes[0].path, "/new/file.txt");
assert_eq!(changes[1].change_type, FilesystemChangeType::Deleted);
assert_eq!(changes[1].path, "/deleted/file.txt");
assert_eq!(changes[2].change_type, FilesystemChangeType::Changed);
assert_eq!(changes[2].path, "/changed/file.txt");
}
#[test]
fn test_parse_filesystem_changes_empty() {
let changes = DiffCommand::parse_filesystem_changes("");
assert!(changes.is_empty());
}
#[test]
fn test_parse_filesystem_changes_unknown_type() {
let output = "X /unknown/file.txt";
let changes = DiffCommand::parse_filesystem_changes(output);
assert_eq!(changes.len(), 1);
assert_eq!(
changes[0].change_type,
FilesystemChangeType::Unknown("X".to_string())
);
assert_eq!(changes[0].path, "/unknown/file.txt");
}
#[test]
fn test_filesystem_change_type_display() {
assert_eq!(FilesystemChangeType::Added.to_string(), "Added");
assert_eq!(FilesystemChangeType::Deleted.to_string(), "Deleted");
assert_eq!(FilesystemChangeType::Changed.to_string(), "Changed");
assert_eq!(
FilesystemChangeType::Unknown("X".to_string()).to_string(),
"Unknown(X)"
);
}
#[test]
fn test_diff_result_helpers() {
let result = DiffResult {
output: CommandOutput {
stdout: "A /new\nD /old".to_string(),
stderr: String::new(),
exit_code: 0,
success: true,
},
container: "test".to_string(),
filesystem_changes: vec![
FilesystemChange {
change_type: FilesystemChangeType::Added,
path: "/new".to_string(),
raw_line: "A /new".to_string(),
},
FilesystemChange {
change_type: FilesystemChangeType::Deleted,
path: "/old".to_string(),
raw_line: "D /old".to_string(),
},
],
};
assert!(result.has_changes());
assert_eq!(result.change_count(), 2);
let added = result.changes_by_type(&FilesystemChangeType::Added);
assert_eq!(added.len(), 1);
assert_eq!(added[0].path, "/new");
let deleted = result.changes_by_type(&FilesystemChangeType::Deleted);
assert_eq!(deleted.len(), 1);
assert_eq!(deleted[0].path, "/old");
}
}