Skip to main content

task_graph_mcp/cli/
diff.rs

1//! Diff subcommand for task-graph CLI
2//!
3//! Compares snapshot files against the database or each other.
4
5use clap::Args;
6use std::path::PathBuf;
7
8/// Arguments for the diff subcommand
9#[derive(Args, Debug)]
10pub struct DiffArgs {
11    /// First snapshot file (or database if comparing two snapshots)
12    #[arg(value_name = "FILE")]
13    pub source: PathBuf,
14
15    /// Second snapshot file (optional, compares source against database if not provided)
16    #[arg(value_name = "FILE")]
17    pub target: Option<PathBuf>,
18
19    /// Output format: text (default), json, or summary
20    #[arg(short, long, default_value = "text", value_name = "FORMAT")]
21    pub format: DiffFormat,
22
23    /// Only show changes for specific tables (comma-separated)
24    #[arg(long, value_name = "LIST", value_delimiter = ',')]
25    pub tables: Option<Vec<String>>,
26
27    /// Show only summary counts, not individual changes
28    #[arg(long)]
29    pub summary_only: bool,
30
31    /// Include unchanged tables in output (useful for verification)
32    #[arg(long)]
33    pub include_unchanged: bool,
34}
35
36/// Output format for diff results
37#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
38pub enum DiffFormat {
39    #[default]
40    Text,
41    Json,
42    Summary,
43}
44
45impl std::str::FromStr for DiffFormat {
46    type Err = String;
47
48    fn from_str(s: &str) -> Result<Self, Self::Err> {
49        match s.to_lowercase().as_str() {
50            "text" => Ok(DiffFormat::Text),
51            "json" => Ok(DiffFormat::Json),
52            "summary" => Ok(DiffFormat::Summary),
53            _ => Err(format!(
54                "Invalid format '{}'. Valid options: text, json, summary",
55                s
56            )),
57        }
58    }
59}
60
61impl std::fmt::Display for DiffFormat {
62    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
63        match self {
64            DiffFormat::Text => write!(f, "text"),
65            DiffFormat::Json => write!(f, "json"),
66            DiffFormat::Summary => write!(f, "summary"),
67        }
68    }
69}
70
71impl DiffArgs {
72    /// Check if we're comparing two snapshots or snapshot vs database
73    pub fn is_two_file_diff(&self) -> bool {
74        self.target.is_some()
75    }
76
77    /// Filter diff tables if --tables is specified
78    pub fn should_include_table(&self, table_name: &str) -> bool {
79        match &self.tables {
80            Some(tables) => tables.iter().any(|t| t == table_name),
81            None => true,
82        }
83    }
84}
85
86#[cfg(test)]
87mod tests {
88    use super::*;
89
90    #[test]
91    fn test_diff_format_parse() {
92        assert_eq!("text".parse::<DiffFormat>().unwrap(), DiffFormat::Text);
93        assert_eq!("json".parse::<DiffFormat>().unwrap(), DiffFormat::Json);
94        assert_eq!(
95            "summary".parse::<DiffFormat>().unwrap(),
96            DiffFormat::Summary
97        );
98        assert_eq!("JSON".parse::<DiffFormat>().unwrap(), DiffFormat::Json);
99        assert!("invalid".parse::<DiffFormat>().is_err());
100    }
101
102    #[test]
103    fn test_diff_args_table_filter() {
104        let args = DiffArgs {
105            source: PathBuf::from("test.json"),
106            target: None,
107            format: DiffFormat::Text,
108            tables: Some(vec!["tasks".to_string(), "dependencies".to_string()]),
109            summary_only: false,
110            include_unchanged: false,
111        };
112
113        assert!(args.should_include_table("tasks"));
114        assert!(args.should_include_table("dependencies"));
115        assert!(!args.should_include_table("attachments"));
116    }
117
118    #[test]
119    fn test_diff_args_no_filter() {
120        let args = DiffArgs {
121            source: PathBuf::from("test.json"),
122            target: None,
123            format: DiffFormat::Text,
124            tables: None,
125            summary_only: false,
126            include_unchanged: false,
127        };
128
129        assert!(args.should_include_table("tasks"));
130        assert!(args.should_include_table("attachments"));
131    }
132}