Skip to content

Where people and agents ship software together. The open-source git platform for the whole job: issues, agents, checks and deploys to the edge.

210 linesCodeBlameRaw
1//! What a work item is blocked by: the work items of the same workspace
2//! that have to land first (`work_items.blocked_by`, a JSON array of ids;
3//! numbers in and out).
4//!
5//! `update_work_item` replaces the set, refusing a number that is not one
6//! of the workspace's work items, the item itself, or a set that would
7//! make a cycle. Each change is a `dependency.changed` event on the work
8//! item; what follows a blocker closing is coordination's
9//! (`coordination/dependencies.rs`).
10
11use std::collections::{HashMap, HashSet};
12
13use g1t_contracts::work::*;
14use g1t_contracts::{FailureCode, Outcome, User};
15use serde::Deserialize;
16use worker::Result;
17
18use super::reading::{held_rows, select};
19use super::rows::Held;
20use crate::Work;
21
22#[derive(Deserialize)]
23struct EdgeRow {
24 number: u32,
25 /// JSON array of the numbers it is blocked by.
26 blocked_by: Option<String>,
27}
28
29/// A work item's number.
30#[derive(Clone, Debug, Deserialize)]
31struct Numbered {
32 number: u32,
33}
34
35/// The numbers of those of `found` that `viewer` may see, and of those
36/// already in `kept` (what a work item is blocked by now).
37pub(crate) fn visible_numbers(found: &[Held], viewer: Option<&User>, kept: &[u32]) -> HashSet<u32> {
38 found
39 .iter()
40 .filter(|item| kept.contains(&item.item.number) || item.may(viewer, g1t_contracts::access::Capability::Read))
41 .map(|item| item.item.number)
42 .collect()
43}
44
45fn numbers(json: Option<&str>) -> Vec<u32> {
46 json.and_then(|json| serde_json::from_str(json).ok()).unwrap_or_default()
47}
48
49/// A workspace's (`?1`) work items that are blocked by something, each
50/// with what it is blocked by, by number.
51const EDGES: &str = "SELECT number,
52 (SELECT json_group_array(b.number) FROM json_each(work_items.blocked_by) j JOIN work_items b ON b.id = j.value
53 WHERE b.number IS NOT NULL) AS blocked_by
54 FROM work_items WHERE workspace = ?1 AND blocked_by <> '[]' AND number IS NOT NULL";
55
56impl Work {
57 /// `asked` as `held`'s new `blocked_by`, set by `actor`, or why it
58 /// cannot be. A work item `actor` may not see is refused as one that
59 /// is not there, unless `held` is blocked by it already.
60 pub(super) async fn checked_blocked_by(&self, held: &Held, actor: &User, asked: &[u32]) -> Result<Outcome<Vec<u32>>> {
61 let workspace = held.item.workspace.as_str();
62 let wanted = serde_json::to_string(asked)?;
63 let results = self
64 .db
65 .batch(vec![
66 self.db
67 .prepare(select("workspace = ?1 AND number IN (SELECT value FROM json_each(?2))"))
68 .bind(&[workspace.into(), wanted.into()])?,
69 self.db.prepare(EDGES).bind(&[workspace.into()])?,
70 ])
71 .await?;
72 let mut results = results.into_iter();
73 let found: Vec<Held> = match results.next() {
74 Some(result) => held_rows(&result)?,
75 None => Vec::new(),
76 };
77 let edges: Vec<EdgeRow> = match results.next() {
78 Some(result) => result.results()?,
79 None => Vec::new(),
80 };
81 let items = visible_numbers(&found, Some(actor), &held.item.blocked_by);
82 let number = held.item.number;
83 let graph: HashMap<u32, Vec<u32>> = edges.into_iter().map(|row| (row.number, numbers(row.blocked_by.as_deref()))).collect();
84 Ok(match check_blocked_by(number, asked, &items, &graph) {
85 Ok(set) => Outcome::Ok(set),
86 Err(message) => Outcome::fail(FailureCode::Invalid, message),
87 })
88 }
89
90 /// The `dependency.changed` that setting `held`'s `blocked_by` to
91 /// `after` makes, worked out before the change so that its updates are
92 /// written with it; none when nothing changes.
93 pub(super) async fn dependency_change(&self, held: &Held, after: &[u32], actor: &User) -> Result<Option<serde_json::Value>> {
94 let before = &held.item.blocked_by;
95 let added: Vec<u32> = after.iter().filter(|n| !before.contains(n)).copied().collect();
96 let removed: Vec<u32> = before.iter().filter(|n| !after.contains(n)).copied().collect();
97 if added.is_empty() && removed.is_empty() {
98 return Ok(None);
99 }
100 let remaining = self.still_open(&held.item.workspace, after, None).await?;
101 let change = DependencyChanged {
102 workspace: held.item.workspace.clone(),
103 number: held.item.number,
104 item: held.item.number,
105 repo_ids: held.repo_ids(),
106 blocked_by: after.to_vec(),
107 added,
108 removed,
109 cleared: Vec::new(),
110 remaining,
111 merged_pull: None,
112 };
113 Ok(Some(g1t_contracts::events::marked(change, Some(actor))))
114 }
115
116 /// Notes a change to `held`'s `blocked_by` (from `before`) in its
117 /// conversation.
118 pub(super) async fn blocked_by_changed(&self, held: &Held, before: &[u32], actor: &User) -> Result<()> {
119 let added: Vec<u32> = held.item.blocked_by.iter().filter(|n| !before.contains(n)).copied().collect();
120 let removed: Vec<u32> = before.iter().filter(|n| !held.item.blocked_by.contains(n)).copied().collect();
121 if added.is_empty() && removed.is_empty() {
122 return Ok(());
123 }
124 let list = |numbers: &[u32]| numbers.iter().map(|n| format!("#{n}")).collect::<Vec<_>>().join(", ");
125 if !added.is_empty() {
126 self.note_item(&held.item.id, actor, &format!("marked this as blocked by {}", list(&added))).await?;
127 }
128 if !removed.is_empty() {
129 self.note_item(&held.item.id, actor, &format!("marked this as no longer blocked by {}", list(&removed))).await?;
130 }
131 Ok(())
132 }
133
134 /// Those of `numbers` whose work item is still open, leaving out
135 /// `cleared` (a blocker that just landed).
136 pub(super) async fn still_open(&self, workspace: &str, numbers: &[u32], cleared: Option<u32>) -> Result<Vec<u32>> {
137 if numbers.is_empty() {
138 return Ok(Vec::new());
139 }
140 let open: Vec<Numbered> = self
141 .db
142 .prepare(
143 "SELECT number FROM work_items
144 WHERE workspace = ? AND state = 'open' AND number IN (SELECT value FROM json_each(?))",
145 )
146 .bind(&[workspace.into(), serde_json::to_string(numbers)?.into()])?
147 .all()
148 .await?
149 .results()?;
150 Ok(numbers
151 .iter()
152 .filter(|number| Some(**number) != cleared && open.iter().any(|row| row.number == **number))
153 .copied()
154 .collect())
155 }
156
157 /// The open work items `held` blocks.
158 pub(super) async fn dependents(&self, held: &Held) -> Result<Vec<Held>> {
159 held_rows(
160 &self
161 .db
162 .prepare(select(
163 "workspace = ? AND state = 'open'
164 AND EXISTS (SELECT 1 FROM json_each(work_items.blocked_by) WHERE json_each.value = ?)",
165 ))
166 .bind(&[held.item.workspace.as_str().into(), held.item.id.as_str().into()])?
167 .all()
168 .await?,
169 )
170 }
171
172 /// The work items of `workspace` that `numbers` names and `viewer` may
173 /// see, in no particular order.
174 pub(super) async fn visible_numbered(&self, workspace: &str, numbers: &[u32], viewer: Option<&User>) -> Result<Vec<Held>> {
175 if numbers.is_empty() {
176 return Ok(Vec::new());
177 }
178 let found = held_rows(
179 &self
180 .db
181 .prepare(select("workspace = ?1 AND number IN (SELECT value FROM json_each(?2))"))
182 .bind(&[workspace.into(), serde_json::to_string(numbers)?.into()])?
183 .all()
184 .await?,
185 )?;
186 Ok(found.into_iter().filter(|item| item.may(viewer, g1t_contracts::access::Capability::Read)).collect())
187 }
188
189 /// The work items that name `held` in their `blocked_by`, by number:
190 /// those `viewer` may see.
191 pub(super) async fn blocked_items(&self, held: &Held, viewer: Option<&User>) -> Result<Vec<ItemBrief>> {
192 let found = held_rows(
193 &self
194 .db
195 .prepare(select(
196 "workspace = ? AND number IS NOT NULL
197 AND EXISTS (SELECT 1 FROM json_each(work_items.blocked_by) WHERE json_each.value = ?)
198 ORDER BY number LIMIT 100",
199 ))
200 .bind(&[held.item.workspace.as_str().into(), held.item.id.as_str().into()])?
201 .all()
202 .await?,
203 )?;
204 Ok(found
205 .into_iter()
206 .filter(|item| item.may(viewer, g1t_contracts::access::Capability::Read))
207 .map(|item| ItemBrief { number: item.item.number, title: item.item.title, state: item.item.state })
208 .collect())
209 }
210}