import.rs

  1use anyhow::Result;
  2use serde::Deserialize;
  3use std::io::BufRead;
  4use std::path::Path;
  5
  6use crate::db;
  7
  8#[derive(Deserialize)]
  9struct ImportTask {
 10    id: String,
 11    title: String,
 12    #[serde(default)]
 13    description: String,
 14    #[serde(rename = "type", default = "default_type")]
 15    task_type: String,
 16    #[serde(default = "default_priority")]
 17    priority: i32,
 18    #[serde(default = "default_status")]
 19    status: String,
 20    #[serde(default = "default_effort")]
 21    effort: i32,
 22    #[serde(default)]
 23    parent: String,
 24    created: String,
 25    updated: String,
 26    #[serde(default)]
 27    labels: Vec<String>,
 28    #[serde(default)]
 29    blockers: Vec<String>,
 30    #[serde(default)]
 31    logs: Vec<ImportLogEntry>,
 32}
 33
 34#[derive(Deserialize)]
 35struct ImportLogEntry {
 36    timestamp: String,
 37    body: String,
 38}
 39
 40fn default_type() -> String {
 41    "task".into()
 42}
 43fn default_priority() -> i32 {
 44    2
 45}
 46fn default_status() -> String {
 47    "open".into()
 48}
 49fn default_effort() -> i32 {
 50    2
 51}
 52
 53pub fn run(root: &Path, file: &str) -> Result<()> {
 54    let conn = db::open(root)?;
 55
 56    eprintln!("info: importing from {file}...");
 57
 58    let reader: Box<dyn BufRead> = if file == "-" {
 59        Box::new(std::io::stdin().lock())
 60    } else {
 61        Box::new(std::io::BufReader::new(std::fs::File::open(file)?))
 62    };
 63
 64    for line in reader.lines() {
 65        let line = line?;
 66        if line.trim().is_empty() {
 67            continue;
 68        }
 69
 70        let t: ImportTask = serde_json::from_str(&line)?;
 71
 72        conn.execute(
 73            "INSERT OR REPLACE INTO tasks
 74             (id, title, description, type, priority, status, effort, parent, created, updated)
 75             VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10)",
 76            rusqlite::params![
 77                t.id,
 78                t.title,
 79                t.description,
 80                t.task_type,
 81                t.priority,
 82                t.status,
 83                t.effort,
 84                t.parent,
 85                t.created,
 86                t.updated,
 87            ],
 88        )?;
 89
 90        // Replace labels.
 91        conn.execute("DELETE FROM labels WHERE task_id = ?1", [&t.id])?;
 92        for lbl in &t.labels {
 93            conn.execute(
 94                "INSERT INTO labels (task_id, label) VALUES (?1, ?2)",
 95                [&t.id, lbl],
 96            )?;
 97        }
 98
 99        // Replace blockers.
100        conn.execute("DELETE FROM blockers WHERE task_id = ?1", [&t.id])?;
101        for blk in &t.blockers {
102            conn.execute(
103                "INSERT INTO blockers (task_id, blocker_id) VALUES (?1, ?2)",
104                [&t.id, blk],
105            )?;
106        }
107
108        // Replace logs.
109        conn.execute("DELETE FROM task_logs WHERE task_id = ?1", [&t.id])?;
110        for log in &t.logs {
111            conn.execute(
112                "INSERT INTO task_logs (task_id, timestamp, body) VALUES (?1, ?2, ?3)",
113                rusqlite::params![&t.id, &log.timestamp, &log.body],
114            )?;
115        }
116    }
117
118    eprintln!("info: import complete");
119    Ok(())
120}