Skip to main content

task_graph_mcp/cli/
import.rs

1//! Import subcommand for task-graph CLI
2//!
3//! Imports task data from a structured JSON export file back into
4//! the database.
5
6use clap::Args;
7use std::path::PathBuf;
8
9/// Arguments for the import subcommand
10#[derive(Args, Debug)]
11pub struct ImportArgs {
12    /// Path to the export file to import
13    #[arg(value_name = "FILE")]
14    pub file: PathBuf,
15
16    /// Validate import without modifying database
17    ///
18    /// Parses the file, validates schema compatibility, and reports
19    /// what would be imported without making any changes.
20    #[arg(long)]
21    pub dry_run: bool,
22
23    /// Merge mode: add missing items, skip existing
24    ///
25    /// By default, import replaces all project data. With --merge:
26    /// - Tasks: skip if ID already exists, insert if new
27    /// - Dependencies: skip if exact match exists
28    /// - Attachments: configurable via --attachment-mode
29    #[arg(long)]
30    pub merge: bool,
31
32    /// Force overwrite of existing data without prompting
33    ///
34    /// In replace mode (default): skip confirmation prompt
35    /// In merge mode: overwrite conflicts instead of skipping
36    #[arg(long)]
37    pub force: bool,
38
39    /// Enable strict validation mode
40    ///
41    /// Rejects imports with:
42    /// - Circular dependencies (normally just warned)
43    /// - Missing referenced tasks
44    /// - Invalid status values
45    #[arg(long)]
46    pub strict: bool,
47}
48
49impl ImportArgs {
50    /// Check if this is a gzipped file based on extension
51    pub fn is_gzipped(&self) -> bool {
52        self.file.extension().is_some_and(|ext| ext == "gz")
53    }
54
55    /// Describe the import mode for logging
56    pub fn import_mode(&self) -> &'static str {
57        if self.dry_run {
58            "dry-run"
59        } else if self.merge {
60            if self.force {
61                "merge-overwrite"
62            } else {
63                "merge-skip"
64            }
65        } else {
66            "replace"
67        }
68    }
69}
70
71#[cfg(test)]
72mod tests {
73    use super::*;
74
75    #[test]
76    fn test_is_gzipped() {
77        let args = ImportArgs {
78            file: PathBuf::from("snapshot.json"),
79            dry_run: false,
80            merge: false,
81            force: false,
82            strict: false,
83        };
84        assert!(!args.is_gzipped());
85
86        let args = ImportArgs {
87            file: PathBuf::from("snapshot.json.gz"),
88            dry_run: false,
89            merge: false,
90            force: false,
91            strict: false,
92        };
93        assert!(args.is_gzipped());
94    }
95
96    #[test]
97    fn test_import_mode() {
98        // Dry run
99        let args = ImportArgs {
100            file: PathBuf::from("test.json"),
101            dry_run: true,
102            merge: false,
103            force: false,
104            strict: false,
105        };
106        assert_eq!(args.import_mode(), "dry-run");
107
108        // Replace (default)
109        let args = ImportArgs {
110            file: PathBuf::from("test.json"),
111            dry_run: false,
112            merge: false,
113            force: false,
114            strict: false,
115        };
116        assert_eq!(args.import_mode(), "replace");
117
118        // Merge skip
119        let args = ImportArgs {
120            file: PathBuf::from("test.json"),
121            dry_run: false,
122            merge: true,
123            force: false,
124            strict: false,
125        };
126        assert_eq!(args.import_mode(), "merge-skip");
127
128        // Merge overwrite
129        let args = ImportArgs {
130            file: PathBuf::from("test.json"),
131            dry_run: false,
132            merge: true,
133            force: true,
134            strict: false,
135        };
136        assert_eq!(args.import_mode(), "merge-overwrite");
137    }
138}