1use anyhow::{anyhow, bail, Result};
4use daggy::{Dag, NodeIndex};
5use petgraph::algo::{has_path_connecting, toposort};
6use petgraph::Direction;
7use std::collections::{HashMap, HashSet, VecDeque};
8
9use crate::error::Error;
10use crate::model::IssueHeading;
11
12pub struct DependencyGraph {
14 dag: Dag<String, ()>,
15 ids: HashMap<String, NodeIndex>,
16}
17
18impl DependencyGraph {
19 pub fn from_issues(issues: &[(String, IssueHeading)]) -> Result<Self> {
21 Self::from_headings(issues.iter().map(|(_, heading)| heading))
22 }
23
24 pub fn from_headings<'a, I>(issues: I) -> Result<Self>
26 where
27 I: IntoIterator<Item = &'a IssueHeading>,
28 {
29 let headings: Vec<&IssueHeading> = issues.into_iter().collect();
30 let mut graph = Self {
31 dag: Dag::new(),
32 ids: HashMap::with_capacity(headings.len()),
33 };
34 let mut seen_edges = HashSet::new();
35 for issue in &headings {
36 if graph.ids.contains_key(&issue.id) {
37 bail!("duplicate issue id {}", issue.id);
38 }
39 let node = graph.dag.add_node(issue.id.clone());
40 graph.ids.insert(issue.id.clone(), node);
41 }
42 for issue in &headings {
43 for blocker in blocker_ids(issue) {
44 let Some(&from) = graph.ids.get(blocker) else {
45 continue;
46 };
47 let to = graph.ids[&issue.id];
48 if from == to {
49 bail!("issue {} blocks itself", issue.id);
50 }
51 if !seen_edges.insert((from, to)) {
52 continue;
53 }
54 graph
55 .dag
56 .add_edge(from, to, ())
57 .map_err(|_| anyhow!("blocker cycle involving {} and {}", blocker, issue.id))?;
58 }
59 }
60 Ok(graph)
61 }
62
63 pub fn accepts_edge(&self, blocker: &str, issue: &str) -> std::result::Result<(), Error> {
65 let Some(&from) = self.ids.get(blocker) else {
66 return Ok(());
67 };
68 let Some(&to) = self.ids.get(issue) else {
69 return Ok(());
70 };
71 if from == to || has_path_connecting(self.dag.graph(), to, from, None) {
72 return Err(Error::BlockerCycle {
73 blocker: blocker.to_string(),
74 issue: issue.to_string(),
75 });
76 }
77 Ok(())
78 }
79
80 pub fn topological_ids(&self) -> Result<Vec<String>> {
82 let ids = toposort(self.dag.graph(), None)
83 .map_err(|cycle| anyhow!("dependency cycle at {}", self.dag.graph()[cycle.node_id()]))?
84 .into_iter()
85 .map(|node| self.dag.graph()[node].clone())
86 .collect::<Vec<_>>();
87 Ok(ids)
88 }
89
90 pub fn ancestors(
92 &self,
93 issue: &str,
94 depth: usize,
95 ) -> std::result::Result<Vec<(usize, String)>, Error> {
96 self.walk(issue, depth, Direction::Incoming)
97 }
98
99 pub fn descendants(
101 &self,
102 issue: &str,
103 depth: usize,
104 ) -> std::result::Result<Vec<(usize, String)>, Error> {
105 self.walk(issue, depth, Direction::Outgoing)
106 }
107
108 fn walk(
109 &self,
110 issue: &str,
111 depth: usize,
112 direction: Direction,
113 ) -> std::result::Result<Vec<(usize, String)>, Error> {
114 let Some(&root) = self.ids.get(issue) else {
115 return Err(Error::IssueNotFound {
116 id: issue.to_string(),
117 });
118 };
119 let mut seen = HashSet::from([root]);
120 let mut queue = VecDeque::from([(root, 0usize)]);
121 let mut result = Vec::new();
122 while let Some((node, distance)) = queue.pop_front() {
123 if distance == depth {
124 continue;
125 }
126 for neighbor in self.dag.graph().neighbors_directed(node, direction) {
127 if !seen.insert(neighbor) {
128 continue;
129 }
130 let next_distance = distance + 1;
131 result.push((next_distance, self.dag.graph()[neighbor].clone()));
132 queue.push_back((neighbor, next_distance));
133 }
134 }
135 result.sort();
136 Ok(result)
137 }
138}
139
140fn blocker_ids(issue: &IssueHeading) -> impl Iterator<Item = &str> {
141 issue
142 .properties
143 .get("BLOCKED_BY")
144 .into_iter()
145 .flat_map(|raw| raw.split(|c: char| c == ',' || c.is_whitespace()))
146 .map(str::trim)
147 .filter(|id| !id.is_empty())
148}