Skip to content
265 linesCodeBlameRaw
1//! What an issue is blocked by: the issues of the same repository that have
2//! to land first (`issues.blocked_by`, a JSON array of numbers).
3//!
4//! `update_issue` replaces the set, refusing an issue that is not one of
5//! the repository's, the issue itself, or a set that would make a cycle.
6//! Each change, and each blocker that closes or has its pull request
7//! merged, is a `dependency.changed` event on the issue it blocks.
8
9use std::collections::{HashMap, HashSet};
10
11use g1t_contracts::work::*;
12use g1t_contracts::{FailureCode, Outcome, User};
13use serde::Deserialize;
14use worker::Result;
15use worker::wasm_bindgen::JsValue;
16
17use super::{CLAIM_COLUMNS, ClaimRow};
18use crate::Work;
19
20#[derive(Deserialize)]
21struct EdgeRow {
22 number: u32,
23 blocked_by: String,
24}
25
26#[derive(Deserialize)]
27struct BriefRow {
28 number: u32,
29 title: String,
30 state: State,
31}
32
33#[derive(Deserialize)]
34struct PullBriefRow {
35 number: u32,
36 title: String,
37 status: PullStatus,
38 issue_number: u32,
39}
40
41fn numbers(json: &str) -> Vec<u32> {
42 serde_json::from_str(json).unwrap_or_default()
43}
44
45impl Work {
46 /// `asked` as `number`'s new `blocked_by`, or why it cannot be.
47 pub(crate) async fn checked_blocked_by(&self, repo_id: &str, number: u32, asked: &[u32]) -> Result<Outcome<Vec<u32>>> {
48 let wanted = serde_json::to_string(asked)?;
49 let results = self
50 .db
51 .batch(vec![
52 self.db
53 .prepare(
54 "SELECT number, title, state FROM issues
55 WHERE repo_id = ? AND number IN (SELECT value FROM json_each(?))",
56 )
57 .bind(&[repo_id.into(), wanted.into()])?,
58 self.db
59 .prepare("SELECT number, blocked_by FROM issues WHERE repo_id = ? AND blocked_by <> '[]'")
60 .bind(&[repo_id.into()])?,
61 ])
62 .await?;
63 let mut results = results.into_iter();
64 let found: Vec<BriefRow> = match results.next() {
65 Some(result) => result.results()?,
66 None => Vec::new(),
67 };
68 let edges: Vec<EdgeRow> = match results.next() {
69 Some(result) => result.results()?,
70 None => Vec::new(),
71 };
72 let issues: HashSet<u32> = found.iter().map(|row| row.number).collect();
73 let graph: HashMap<u32, Vec<u32>> = edges.into_iter().map(|row| (row.number, numbers(&row.blocked_by))).collect();
74 Ok(match check_blocked_by(number, asked, &issues, &graph) {
75 Ok(set) => Outcome::Ok(set),
76 Err(message) => Outcome::fail(FailureCode::Invalid, message),
77 })
78 }
79
80 /// Announces a change to `issue`'s `blocked_by` (from `before`), and
81 /// notes it in the issue's conversation.
82 pub(crate) async fn blocked_by_changed(&self, issue: &Issue, before: &[u32], actor: &User) -> Result<()> {
83 let added: Vec<u32> = issue.blocked_by.iter().filter(|n| !before.contains(n)).copied().collect();
84 let removed: Vec<u32> = before.iter().filter(|n| !issue.blocked_by.contains(n)).copied().collect();
85 if added.is_empty() && removed.is_empty() {
86 return Ok(());
87 }
88 let remaining = self.still_open(&issue.repo_id, &issue.blocked_by, None).await?;
89 self.publish(
90 "dependency.changed",
91 &issue.repo_id,
92 actor,
93 DependencyChanged {
94 repo_id: issue.repo_id.clone(),
95 number: issue.number,
96 issue: issue.number,
97 blocked_by: issue.blocked_by.clone(),
98 added: added.clone(),
99 removed: removed.clone(),
100 cleared: Vec::new(),
101 remaining,
102 merged_pull: None,
103 },
104 )
105 .await?;
106 let list = |numbers: &[u32]| numbers.iter().map(|n| format!("#{n}")).collect::<Vec<_>>().join(", ");
107 let who = (actor.id.as_str(), actor.username.as_str());
108 if !added.is_empty() {
109 self.note(&issue.repo_id, issue.number, who, &format!("marked this as blocked by {}", list(&added))).await?;
110 }
111 if !removed.is_empty() {
112 self.note(&issue.repo_id, issue.number, who, &format!("marked this as no longer blocked by {}", list(&removed)))
113 .await?;
114 }
115 Ok(())
116 }
117
118 /// Those of `numbers` whose issue is still open, leaving out `cleared`
119 /// (a blocker whose pull request just merged).
120 async fn still_open(&self, repo_id: &str, numbers: &[u32], cleared: Option<u32>) -> Result<Vec<u32>> {
121 if numbers.is_empty() {
122 return Ok(Vec::new());
123 }
124 let open: Vec<BriefRow> = self
125 .db
126 .prepare(
127 "SELECT number, title, state FROM issues
128 WHERE repo_id = ? AND state = 'open' AND number IN (SELECT value FROM json_each(?))",
129 )
130 .bind(&[repo_id.into(), serde_json::to_string(numbers)?.into()])?
131 .all()
132 .await?
133 .results()?;
134 Ok(numbers
135 .iter()
136 .filter(|number| Some(**number) != cleared && open.iter().any(|row| row.number == **number))
137 .copied()
138 .collect())
139 }
140
141 /// An issue closed (`closed`) or had a pull request merged for it
142 /// (`merged_pull`): its claims end if it closed, and each open issue it
143 /// blocks hears that it cleared. Never fails what caused it: a problem
144 /// is logged.
145 pub(crate) async fn issue_resolved(&self, repo_id: &str, number: u32, actor: &User, closed: bool, merged_pull: Option<u32>) {
146 let resolved = async {
147 if closed {
148 self.release_claims_on_close(repo_id, number, Some(actor)).await?;
149 }
150 let dependents: Vec<EdgeRow> = self
151 .db
152 .prepare(
153 "SELECT number, blocked_by FROM issues
154 WHERE repo_id = ? AND state = 'open'
155 AND EXISTS (SELECT 1 FROM json_each(issues.blocked_by) WHERE json_each.value = ?)",
156 )
157 .bind(&[repo_id.into(), number.into()])?
158 .all()
159 .await?
160 .results()?;
161 for dependent in dependents {
162 let blocked_by = numbers(&dependent.blocked_by);
163 let remaining = self.still_open(repo_id, &blocked_by, Some(number)).await?;
164 self.publish(
165 "dependency.changed",
166 repo_id,
167 actor,
168 DependencyChanged {
169 repo_id: repo_id.to_owned(),
170 number: dependent.number,
171 issue: dependent.number,
172 blocked_by,
173 cleared: vec![number],
174 remaining,
175 merged_pull,
176 ..DependencyChanged::default()
177 },
178 )
179 .await?;
180 }
181 Ok::<(), worker::Error>(())
182 };
183 if let Err(error) = resolved.await {
184 worker::console_error!("#{number} resolved, but its dependents were not told: {error}");
185 }
186 }
187
188 /// The issues `numbers` names, each with its active claims and its pull
189 /// requests, in the order given.
190 pub(crate) async fn blockers(&self, repo_id: &str, numbers: &[u32]) -> Result<Vec<Blocker>> {
191 if numbers.is_empty() {
192 return Ok(Vec::new());
193 }
194 let list = serde_json::to_string(numbers)?;
195 let bind = |sql: String| -> Result<worker::D1PreparedStatement> {
196 self.db.prepare(sql).bind(&[repo_id.into(), JsValue::from(list.as_str())])
197 };
198 let results = self
199 .db
200 .batch(vec![
201 bind("SELECT number, title, state FROM issues
202 WHERE repo_id = ? AND number IN (SELECT value FROM json_each(?))"
203 .to_owned())?,
204 bind(format!(
205 "SELECT {CLAIM_COLUMNS} FROM claims
206 WHERE repo_id = ? AND state = 'active' AND issue IN (SELECT value FROM json_each(?))
207 ORDER BY created_at, id"
208 ))?,
209 bind("SELECT number, title, status, issue_number FROM pulls
210 WHERE repo_id = ? AND issue_number IN (SELECT value FROM json_each(?))
211 ORDER BY number"
212 .to_owned())?,
213 ])
214 .await?;
215 let mut results = results.into_iter();
216 let mut next = || results.next();
217 let issues: Vec<BriefRow> = match next() {
218 Some(result) => result.results()?,
219 None => Vec::new(),
220 };
221 let claims: Vec<Claim> = match next() {
222 Some(result) => result.results::<ClaimRow>()?.into_iter().map(Claim::from).collect(),
223 None => Vec::new(),
224 };
225 let pulls: Vec<PullBriefRow> = match next() {
226 Some(result) => result.results()?,
227 None => Vec::new(),
228 };
229 Ok(numbers
230 .iter()
231 .filter_map(|number| {
232 let issue = issues.iter().find(|issue| issue.number == *number)?;
233 Some(Blocker {
234 number: issue.number,
235 title: issue.title.clone(),
236 state: issue.state,
237 claims: claims.iter().filter(|claim| claim.issue == *number).cloned().collect(),
238 pulls: pulls
239 .iter()
240 .filter(|pull| pull.issue_number == *number)
241 .map(|pull| PullBrief { number: pull.number, title: pull.title.clone(), status: pull.status })
242 .collect(),
243 })
244 })
245 .collect())
246 }
247
248 /// The issues that name `number` in their `blocked_by`, by number.
249 pub(crate) async fn blocked_issues(&self, repo_id: &str, number: u32) -> Result<Vec<IssueBrief>> {
250 Ok(self
251 .db
252 .prepare(
253 "SELECT number, title, state FROM issues
254 WHERE repo_id = ? AND EXISTS (SELECT 1 FROM json_each(issues.blocked_by) WHERE json_each.value = ?)
255 ORDER BY number LIMIT 100",
256 )
257 .bind(&[repo_id.into(), number.into()])?
258 .all()
259 .await?
260 .results::<BriefRow>()?
261 .into_iter()
262 .map(|row| IssueBrief { number: row.number, title: row.title, state: row.state })
263 .collect())
264 }
265}