rm.rs

  1use anyhow::{anyhow, bail, Result};
  2use serde::Serialize;
  3use std::collections::{BTreeSet, HashSet};
  4use std::path::Path;
  5
  6use crate::db;
  7
  8#[derive(Serialize)]
  9struct RmResult {
 10    requested_ids: Vec<String>,
 11    deleted_ids: Vec<String>,
 12    unblocked_ids: Vec<String>,
 13}
 14
 15pub fn run(root: &Path, ids: &[String], recursive: bool, force: bool, json: bool) -> Result<()> {
 16    let store = db::open(root)?;
 17    let all = store.list_tasks_unfiltered()?;
 18
 19    let mut to_delete = BTreeSet::new();
 20    for raw in ids {
 21        let id = db::resolve_task_id(&store, raw, false)?;
 22        if recursive {
 23            collect_subtree(&all, &id, &mut to_delete);
 24        } else {
 25            if all
 26                .iter()
 27                .any(|t| t.parent.as_ref() == Some(&id) && t.deleted_at.is_none())
 28            {
 29                bail!("task '{id}' has children; use --recursive to delete subtree");
 30            }
 31            to_delete.insert(id);
 32        }
 33    }
 34
 35    let deleted_ids: Vec<db::TaskId> = to_delete.into_iter().collect();
 36    let deleted_set: HashSet<String> = deleted_ids
 37        .iter()
 38        .map(|id| id.as_str().to_string())
 39        .collect();
 40
 41    let unblocked_ids: Vec<db::TaskId> = all
 42        .iter()
 43        .filter(|t| !deleted_set.contains(t.id.as_str()))
 44        .filter(|t| t.blockers.iter().any(|b| deleted_set.contains(b.as_str())))
 45        .map(|t| t.id.clone())
 46        .collect();
 47
 48    let ts = db::now_utc();
 49    store.apply_and_persist(|doc| {
 50        let tasks = doc.get_map("tasks");
 51
 52        for task_id in &deleted_ids {
 53            let task =
 54                db::get_task_map(&tasks, task_id)?.ok_or_else(|| anyhow!("task not found"))?;
 55            task.insert("deleted_at", ts.clone())?;
 56            task.insert("updated_at", ts.clone())?;
 57            task.insert("status", db::status_label(db::Status::Closed))?;
 58        }
 59
 60        for task in store.list_tasks_unfiltered()? {
 61            if deleted_set.contains(task.id.as_str()) {
 62                continue;
 63            }
 64            if let Some(task_map) = db::get_task_map(&tasks, &task.id)? {
 65                let blockers = db::get_or_create_child_map(&task_map, "blockers")?;
 66                for deleted in &deleted_ids {
 67                    blockers.delete(deleted.as_str())?;
 68                }
 69            }
 70        }
 71
 72        Ok(())
 73    })?;
 74
 75    if !force && !unblocked_ids.is_empty() {
 76        let short: Vec<String> = unblocked_ids.iter().map(ToString::to_string).collect();
 77        eprintln!("warning: removed blockers from {}", short.join(", "));
 78    }
 79
 80    if json {
 81        let out = RmResult {
 82            requested_ids: ids.to_vec(),
 83            deleted_ids: deleted_ids
 84                .iter()
 85                .map(|id| id.as_str().to_string())
 86                .collect(),
 87            unblocked_ids: unblocked_ids
 88                .iter()
 89                .map(|id| id.as_str().to_string())
 90                .collect(),
 91        };
 92        println!("{}", serde_json::to_string(&out)?);
 93    } else {
 94        let c = crate::color::stdout_theme();
 95        for id in deleted_ids {
 96            println!("{}deleted{} {id}", c.green, c.reset);
 97        }
 98    }
 99
100    Ok(())
101}
102
103fn collect_subtree(all: &[db::Task], root: &db::TaskId, out: &mut BTreeSet<db::TaskId>) {
104    if !out.insert(root.clone()) {
105        return;
106    }
107    for task in all {
108        if task.parent.as_ref() == Some(root) && task.deleted_at.is_none() {
109            collect_subtree(all, &task.id, out);
110        }
111    }
112}