dep.rs

  1use anyhow::{anyhow, bail, Result};
  2use std::collections::{HashMap, HashSet, VecDeque};
  3use std::path::Path;
  4
  5use crate::cli::DepAction;
  6use crate::db;
  7
  8pub fn run(root: &Path, action: &DepAction, json: bool) -> Result<()> {
  9    let store = db::open(root)?;
 10
 11    match action {
 12        DepAction::Add { child, parent } => {
 13            let child_id = db::resolve_task_id(&store, child, false)?;
 14            let parent_id = db::resolve_task_id(&store, parent, false)?;
 15            if child_id == parent_id {
 16                bail!("adding dependency would create a cycle");
 17            }
 18            if would_cycle(&store, &child_id, &parent_id)? {
 19                bail!("adding dependency would create a cycle");
 20            }
 21            let ts = db::now_utc();
 22            store.apply_and_persist(|doc| {
 23                let tasks = doc.get_map("tasks");
 24                let child_task = db::get_task_map(&tasks, &child_id)?
 25                    .ok_or_else(|| anyhow!("task not found"))?;
 26                let blockers = db::get_or_create_child_map(&child_task, "blockers")?;
 27                blockers.insert(parent_id.as_str(), true)?;
 28                child_task.insert("updated_at", ts.clone())?;
 29                Ok(())
 30            })?;
 31            if json {
 32                println!(
 33                    "{}",
 34                    serde_json::json!({"child": child_id, "blocker": parent_id})
 35                );
 36            } else {
 37                let c = crate::color::stdout_theme();
 38                println!(
 39                    "{}{child_id}{} blocked by {}{parent_id}{}",
 40                    c.green, c.reset, c.yellow, c.reset
 41                );
 42            }
 43        }
 44        DepAction::Rm { child, parent } => {
 45            let child_id = db::resolve_task_id(&store, child, false)?;
 46            let parent_id = db::resolve_task_id(&store, parent, true)?;
 47            let ts = db::now_utc();
 48            store.apply_and_persist(|doc| {
 49                let tasks = doc.get_map("tasks");
 50                let child_task = db::get_task_map(&tasks, &child_id)?
 51                    .ok_or_else(|| anyhow!("task not found"))?;
 52                let blockers = db::get_or_create_child_map(&child_task, "blockers")?;
 53                blockers.delete(parent_id.as_str())?;
 54                child_task.insert("updated_at", ts.clone())?;
 55                Ok(())
 56            })?;
 57            if !json {
 58                let c = crate::color::stdout_theme();
 59                println!(
 60                    "{}{child_id}{} no longer blocked by {}{parent_id}{}",
 61                    c.green, c.reset, c.yellow, c.reset
 62                );
 63            }
 64        }
 65        DepAction::Tree { id } => {
 66            let root_id = db::resolve_task_id(&store, id, true)?;
 67            println!("{}", root_id);
 68            let mut children: Vec<_> = store
 69                .list_tasks_unfiltered()?
 70                .into_iter()
 71                .filter(|t| t.parent.as_ref() == Some(&root_id))
 72                .map(|t| t.id)
 73                .collect();
 74            children.sort_by(|a, b| a.as_str().cmp(b.as_str()));
 75            for child in children {
 76                println!("  {child}");
 77            }
 78        }
 79    }
 80
 81    Ok(())
 82}
 83
 84fn would_cycle(store: &db::Store, child: &db::TaskId, parent: &db::TaskId) -> Result<bool> {
 85    let tasks = store.list_tasks_unfiltered()?;
 86    let mut graph: HashMap<String, HashSet<String>> = HashMap::new();
 87    for task in tasks {
 88        for blocker in task.blockers {
 89            graph
 90                .entry(task.id.as_str().to_string())
 91                .or_default()
 92                .insert(blocker.as_str().to_string());
 93        }
 94    }
 95    graph
 96        .entry(child.as_str().to_string())
 97        .or_default()
 98        .insert(parent.as_str().to_string());
 99
100    let mut seen = HashSet::new();
101    let mut queue = VecDeque::from([parent.as_str().to_string()]);
102    while let Some(node) = queue.pop_front() {
103        if node == child.as_str() {
104            return Ok(true);
105        }
106        if !seen.insert(node.clone()) {
107            continue;
108        }
109        if let Some(nexts) = graph.get(&node) {
110            queue.extend(nexts.iter().cloned());
111        }
112    }
113
114    Ok(false)
115}