sara-tasks 2.7.5

Sara — folder-aware task manager
use std::collections::HashMap;
use std::io::Read;
use std::path::Path;

use anyhow::{Context, Result};
use rusqlite::Connection;
use uuid::Uuid;

use crate::domain::Task;
use crate::infrastructure::config::Config;
use crate::infrastructure::db;
use crate::infrastructure::util::portable::Bundle;

mod render;

pub fn run(
    conn: &mut Connection,
    cfg: &Config,
    source: Option<&str>,
    project_override: Option<&str>,
) -> Result<()> {
    let raw = read_source(source)?;
    let bundle = Bundle::decode(&raw)?;

    let tx = conn.transaction()?;
    let mut id_map: HashMap<Uuid, Uuid> = HashMap::with_capacity(bundle.tasks.len());

    for env in &bundle.tasks {
        let project = project_override.unwrap_or(&env.project).to_string();
        let mut task = Task::new(env.description.clone(), project);
        task.status = env.status.clone();
        task.priority = env.priority.clone();
        task.due = env.due;
        task.entry = env.entry;
        task.modified = chrono::Utc::now();
        task.tags = env.tags.clone();
        task.estimate_mins = env.estimate_mins;
        task.recur = env.recur.clone();

        db::insert_task(&tx, &mut task)?;
        id_map.insert(env.uuid, task.uuid);

        for a in &env.annotations {
            db::add_annotation_full(
                &tx,
                &task.uuid,
                &a.text,
                &a.kind,
                &a.author,
                a.target_kind.as_deref(),
                a.target_id.as_deref(),
                false,
            )?;
        }
        for c in &env.checklist {
            let step_id = db::add_step(
                &tx,
                &task.uuid,
                &c.text,
                c.intent.as_deref(),
                &c.kind,
                &c.source,
                c.verify_cmd.as_deref(),
            )?;
            if c.done {
                db::set_step_done(
                    &tx,
                    step_id,
                    true,
                    c.result.as_deref(),
                    c.done_commit.as_deref(),
                )?;
            }
        }
        for l in &env.links {
            db::add_link(&tx, &task.uuid, &l.url, l.label.as_deref())?;
        }
        if !env.files.is_empty() {
            let files: Vec<(String, String)> = env
                .files
                .iter()
                .map(|f| (f.path.clone(), f.source.clone()))
                .collect();
            db::set_task_files_sourced(&tx, &task.uuid, &files)?;
        }
    }

    for env in &bundle.tasks {
        let new_task = id_map[&env.uuid];
        for dep in &env.blocked_by {
            if let Some(new_dep) = id_map.get(dep) {
                db::add_dependency(&tx, &new_task, new_dep)?;
            }
        }
    }

    for new_uuid in id_map.values() {
        db::refresh_urgency(&tx, &cfg.urgency, new_uuid)?;
    }

    tx.commit()?;

    render::report(conn, &bundle, &id_map, project_override)?;
    Ok(())
}

fn read_source(source: Option<&str>) -> Result<String> {
    match source {
        None => {
            let mut buf = String::new();
            std::io::stdin()
                .read_to_string(&mut buf)
                .context("reading task blob from stdin")?;
            Ok(buf)
        }
        Some(s) => {
            let path = Path::new(s);
            if path.is_file() {
                std::fs::read_to_string(path)
                    .with_context(|| format!("reading task blob from {}", path.display()))
            } else {
                Ok(s.to_string())
            }
        }
    }
}

#[cfg(test)]
#[path = "../../../tests/unit/commands/import/mod.rs"]
mod tests;