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}