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<String> = 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.as_str().to_string())
 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        eprintln!(
 77            "warning: removed blockers from {}",
 78            unblocked_ids.join(", ")
 79        );
 80    }
 81
 82    if json {
 83        let out = RmResult {
 84            requested_ids: ids.to_vec(),
 85            deleted_ids: deleted_ids
 86                .iter()
 87                .map(|id| id.as_str().to_string())
 88                .collect(),
 89            unblocked_ids,
 90        };
 91        println!("{}", serde_json::to_string(&out)?);
 92    } else {
 93        let c = crate::color::stdout_theme();
 94        for id in deleted_ids {
 95            println!("{}deleted{} {id}", c.green, c.reset);
 96        }
 97    }
 98
 99    Ok(())
100}
101
102fn collect_subtree(all: &[db::Task], root: &db::TaskId, out: &mut BTreeSet<db::TaskId>) {
103    if !out.insert(root.clone()) {
104        return;
105    }
106    for task in all {
107        if task.parent.as_ref() == Some(root) && task.deleted_at.is_none() {
108            collect_subtree(all, &task.id, out);
109        }
110    }
111}