ralph/cli/queue/import/
mod.rs1mod input;
19mod merge;
20mod normalize;
21mod parse;
22mod report;
23#[cfg(test)]
24mod tests;
25
26use std::path::PathBuf;
27
28use anyhow::{Context, Result};
29use clap::Args;
30
31use crate::config::Resolved;
32use crate::queue;
33
34use super::QueueImportFormat;
35use input::read_input;
36use merge::merge_imported_tasks;
37use normalize::normalize_task;
38use parse::{parse_csv_tasks, parse_json_tasks};
39use report::ImportReport;
40
41#[derive(Args)]
43#[command(
44 after_long_help = "Examples:\n ralph queue export --format json | ralph queue import --format json --dry-run\n ralph queue import --format csv --input tasks.csv\n ralph queue import --format tsv --input - --on-duplicate rename < tasks.tsv\n ralph queue import --format json --input tasks.json --on-duplicate skip"
45)]
46pub struct QueueImportArgs {
47 #[arg(long, value_enum)]
49 pub format: QueueImportFormat,
50
51 #[arg(long, short)]
53 pub input: Option<PathBuf>,
54
55 #[arg(long)]
57 pub dry_run: bool,
58
59 #[arg(long, value_enum, default_value_t = OnDuplicate::Fail)]
61 pub on_duplicate: OnDuplicate,
62}
63
64#[derive(Clone, Copy, Debug, clap::ValueEnum)]
66#[clap(rename_all = "snake_case")]
67pub enum OnDuplicate {
68 Fail,
70 Skip,
72 Rename,
74}
75
76pub(crate) fn handle(resolved: &Resolved, force: bool, args: QueueImportArgs) -> Result<()> {
77 let _queue_lock = queue::acquire_queue_lock(&resolved.repo_root, "queue import", force)?;
78 let input = read_input(args.input.as_ref()).context("read import input")?;
79
80 let mut imported = match args.format {
81 QueueImportFormat::Json => parse_json_tasks(&input)?,
82 QueueImportFormat::Csv => parse_csv_tasks(&input, b',')?,
83 QueueImportFormat::Tsv => parse_csv_tasks(&input, b'\t')?,
84 };
85
86 let now = crate::timeutil::now_utc_rfc3339_or_fallback();
87 let max_depth = resolved.config.queue.max_dependency_depth.unwrap_or(10);
88
89 let (mut queue_file, done_file) = crate::queue::load_and_validate_queues(resolved, true)?;
90 let done_ref = done_file
91 .as_ref()
92 .filter(|done| !done.tasks.is_empty() || resolved.done_path.exists());
93
94 for task in &mut imported {
95 normalize_task(task, &now);
96 }
97
98 let report = merge_imported_tasks(
99 &mut queue_file,
100 done_ref,
101 imported,
102 &resolved.id_prefix,
103 resolved.id_width,
104 max_depth,
105 &now,
106 args.on_duplicate,
107 )?;
108
109 let warnings = queue::validate_queue_set(
110 &queue_file,
111 done_ref,
112 &resolved.id_prefix,
113 resolved.id_width,
114 max_depth,
115 )?;
116 queue::log_warnings(&warnings);
117
118 if !args.dry_run {
119 crate::undo::create_undo_snapshot(resolved, "queue import")?;
120 }
121
122 if args.dry_run {
123 log::info!("Dry run: no changes written. {}", report.summary());
124 return Ok(());
125 }
126
127 queue::save_queue(&resolved.queue_path, &queue_file)?;
128 log::info!("Imported tasks. {}", report.summary());
129 Ok(())
130}