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}