Skip to main content

ralph/cli/queue/import/
mod.rs

1//! Queue import subcommand for importing tasks from CSV, TSV, or JSON.
2//!
3//! Responsibilities:
4//! - Define the CLI surface for queue import policy and input selection.
5//! - Orchestrate input loading, parsing, normalization, merging, and validation.
6//! - Keep the facade thin while delegating parsing and mutation details to helpers.
7//!
8//! Not handled here:
9//! - Export functionality (see `crate::cli::queue::export`).
10//! - GUI-specific import workflows (this is a CLI command).
11//! - Complex schema migration between versions.
12//!
13//! Invariants/assumptions:
14//! - Always acquire queue lock before mutating queue files.
15//! - Never write to disk on parse or validation failures.
16//! - Undo snapshots are created only after the merged queue validates cleanly.
17
18mod 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/// Arguments for `ralph queue import`.
42#[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    /// Input format.
48    #[arg(long, value_enum)]
49    pub format: QueueImportFormat,
50
51    /// Input file path (default: stdin). Use '-' for stdin.
52    #[arg(long, short)]
53    pub input: Option<PathBuf>,
54
55    /// Show what would change without writing to disk.
56    #[arg(long)]
57    pub dry_run: bool,
58
59    /// What to do if an imported task ID already exists.
60    #[arg(long, value_enum, default_value_t = OnDuplicate::Fail)]
61    pub on_duplicate: OnDuplicate,
62}
63
64/// Policy for handling duplicate task IDs during import.
65#[derive(Clone, Copy, Debug, clap::ValueEnum)]
66#[clap(rename_all = "snake_case")]
67pub enum OnDuplicate {
68    /// Fail with an error if a duplicate ID is found.
69    Fail,
70    /// Skip duplicate tasks and continue importing others.
71    Skip,
72    /// Generate a new ID for duplicate tasks.
73    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}