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.

350 linesCodeBlameRaw
1//! What `#N` names (`g1t_contracts::work::refs`): read in a repository or
2//! a workspace alone, and the work items a text closes and is part of.
3//!
4//! A closing keyword followed by a reference (`Fixes #12`, `closes: #3`,
5//! `Resolves flagon-io/g1t#40`) names a work item to close; `Part of #12`
6//! or `Refs #12` one it is part of. Each is read in the repository the
7//! pull request or commit is in, and only work items of that repository's
8//! workspace count: another workspace's are never closed from here.
9
10use g1t_contracts::access::Capability;
11use g1t_contracts::repos::{ReadableArgs, Repo};
12use g1t_contracts::work::refs::{self, Lookup, Ref, RefScope, first_found};
13use g1t_contracts::work::*;
14use g1t_contracts::{Outcome, User};
15use serde::Deserialize;
16use worker::Result;
17use worker::wasm_bindgen::JsValue;
18
19use super::rows::{Held, path_of};
20use crate::Work;
21use crate::rows::ValueRow;
22
23/// The most work items one pull request or commit closes this way.
24pub(crate) const MAX_CLOSES: usize = refs::MAX_REFS;
25
26/// What `#N` could name, read in repository `?1` (NULL for none) of
27/// workspace `?2`: a pull request of the repository, a work item of the
28/// workspace filed there under that old number (a repository moved in from
29/// another workspace brings old numbers that name that workspace's work
30/// items, never this one's), the workspace's work item, and a pull request
31/// of that number in the workspace's other repositories. Only a number
32/// above the workspace's floor (`work_counters.floor`, the highest any of
33/// its repositories used on its own) is the workspace's alone; one at or
34/// below it may be any of its repositories' old pull requests, and names
35/// none of them from elsewhere.
36pub(crate) const REF_SQL: &str = "SELECT
37 (SELECT 1 FROM pulls WHERE repo_id = ?1 AND number = ?3) AS pull_here,
38 (SELECT item_id FROM work_item_repos WHERE repo_id = ?1 AND legacy_number = ?3
39 AND item_id IN (SELECT id FROM work_items WHERE workspace = ?2 OR workspace IS NULL)) AS legacy,
40 (SELECT id FROM work_items WHERE workspace = ?2 AND number = ?3) AS item,
41 (SELECT repo_id FROM pulls WHERE number = ?3 AND repo_id IS NOT ?1
42 AND repo_id IN (SELECT repo_id FROM repo_workspaces WHERE workspace = ?2)
43 AND number > COALESCE((SELECT floor FROM work_counters WHERE workspace = ?2), 0)
44 ORDER BY created_at DESC LIMIT 1) AS pull_there";
45
46/// What [`REF_SQL`] found.
47#[derive(Clone, Debug, Default, Deserialize)]
48pub(crate) struct RefRow {
49 #[serde(default)]
50 pub pull_here: Option<u8>,
51 #[serde(default)]
52 pub legacy: Option<String>,
53 #[serde(default)]
54 pub item: Option<String>,
55 #[serde(default)]
56 pub pull_there: Option<String>,
57}
58
59/// What a reference names, by the order of `refs::lookup_order`.
60#[derive(Clone, Debug, PartialEq, Eq)]
61pub(crate) enum Named {
62 /// A pull request of the repository it was read in.
63 PullHere,
64 /// A work item, by id.
65 Item(String),
66 /// A pull request of another repository of the workspace, by id.
67 PullThere(String),
68}
69
70impl RefRow {
71 /// The first thing found, in the order references are read in: in a
72 /// repository (`in_repo`), or a workspace alone.
73 pub(crate) fn named(self, in_repo: bool) -> Option<Named> {
74 first_found(in_repo, |lookup| match lookup {
75 Lookup::PullInRepo => self.pull_here.map(|_| Named::PullHere),
76 Lookup::LegacyItem => self.legacy.clone().map(Named::Item),
77 Lookup::Item => self.item.clone().map(Named::Item),
78 Lookup::PullInWorkspace => self.pull_there.clone().map(Named::PullThere),
79 })
80 }
81}
82
83/// The work items a text names, by id and number, in the order named.
84#[derive(Clone, Debug, Default, PartialEq, Eq)]
85pub(crate) struct NamedItems {
86 pub closes: Vec<(String, u32)>,
87 pub parts: Vec<(String, u32)>,
88}
89
90impl NamedItems {
91 pub(crate) fn close_ids(&self) -> Vec<String> {
92 self.closes.iter().map(|(id, _)| id.clone()).collect()
93 }
94
95 pub(crate) fn part_ids(&self) -> Vec<String> {
96 self.parts.iter().map(|(id, _)| id.clone()).collect()
97 }
98
99 pub(crate) fn close_numbers(&self) -> Vec<u32> {
100 self.closes.iter().map(|(_, number)| *number).collect()
101 }
102
103 pub(crate) fn part_numbers(&self) -> Vec<u32> {
104 self.parts.iter().map(|(_, number)| *number).collect()
105 }
106}
107
108impl Work {
109 /// What [`REF_SQL`] finds for `#number` in `repo_id` (or none) of
110 /// `workspace`.
111 pub(super) async fn lookup_ref(&self, repo_id: Option<&str>, workspace: &str, number: u32) -> Result<RefRow> {
112 Ok(self
113 .db
114 .prepare(REF_SQL)
115 .bind(&[repo_id.map_or(JsValue::NULL, JsValue::from), workspace.to_lowercase().into(), number.into()])?
116 .first::<RefRow>(None)
117 .await?
118 .unwrap_or_default())
119 }
120
121 /// `resolve_ref`: what `#number` names for the viewer, read in `repo`
122 /// or in `workspace` (`refs`).
123 pub(crate) async fn resolve_ref(&self, a: ResolveRefArgs) -> Result<RefTarget> {
124 // Read in a repository the viewer may not read, `#N` names
125 // nothing, exactly as in one that does not exist: anything else
126 // would tell them a private repository is there.
127 let (repo, workspace) = match &a.repo {
128 Some(path) => match read_in(self.repo(path, &a.viewer).await?) {
129 Some(repo) => {
130 let workspace = repo.namespace.to_lowercase();
131 (Some((repo.id.clone(), Some(path_of(&repo)))), workspace)
132 }
133 None => return Ok(RefTarget::None),
134 },
135 None => (None, a.workspace.trim().to_lowercase()),
136 };
137 if a.number == 0 || workspace.is_empty() {
138 return Ok(RefTarget::None);
139 }
140 // What the viewer may not see is passed over for what comes next.
141 let mut row = self.lookup_ref(repo.as_ref().map(|(id, _)| id.as_str()), &workspace, a.number).await?;
142 for _ in 0..4 {
143 let Some(named) = row.clone().named(repo.is_some()) else {
144 return Ok(RefTarget::None);
145 };
146 match named {
147 Named::PullHere => match repo.as_ref().and_then(|(_, path)| path.clone()) {
148 Some(repo) => return Ok(RefTarget::Pull { repo, number: a.number }),
149 None => row.pull_here = None,
150 },
151 Named::Item(id) => {
152 if let Some(held) = self.item_by_id(&id).await?
153 && held.may(a.viewer.as_ref(), Capability::Read)
154 {
155 return Ok(RefTarget::WorkItem { workspace: held.item.workspace, number: held.item.number, item_id: held.item.id });
156 }
157 if row.legacy.as_deref() == Some(id.as_str()) {
158 row.legacy = None;
159 } else {
160 row.item = None;
161 }
162 }
163 Named::PullThere(repo_id) => {
164 let readable: Vec<Repo> = g1t_kit::call(
165 &self.repos,
166 "readable",
167 &ReadableArgs { ids: vec![repo_id.clone()], viewer: a.viewer.clone() },
168 )
169 .await?;
170 return Ok(match readable.first() {
171 Some(other) => RefTarget::Pull { repo: path_of(other), number: a.number },
172 None => RefTarget::None,
173 });
174 }
175 }
176 }
177 Ok(RefTarget::None)
178 }
179
180 /// The work item `reference` names, read in `repo`: its id, when it is
181 /// one of the workspace's work items.
182 pub(super) async fn ref_item(&self, repo: &Repo, reference: &Ref) -> Result<Option<String>> {
183 let workspace = repo.namespace.to_lowercase();
184 let (repo_id, in_repo) = match &reference.scope {
185 RefScope::Here => (Some(repo.id.clone()), true),
186 RefScope::Workspace(named) => {
187 if *named != workspace {
188 return Ok(None);
189 }
190 (None, false)
191 }
192 RefScope::Repo(path) => {
193 if !path.namespace.eq_ignore_ascii_case(&workspace) {
194 return Ok(None);
195 }
196 let named = format!("{}/{}", path.namespace, path.name);
197 if named.eq_ignore_ascii_case(&path_of(repo)) {
198 (Some(repo.id.clone()), true)
199 } else {
200 let known = self
201 .db
202 .prepare("SELECT repo_id AS value FROM repo_workspaces WHERE lower(path) = lower(?)")
203 .bind(&[named.as_str().into()])?
204 .first::<ValueRow>(None)
205 .await?;
206 // A repository not known names nothing, as resolve_ref
207 // reads it: never the workspace's work item of that number.
208 match known {
209 Some(row) => (Some(row.value), true),
210 None => return Ok(None),
211 }
212 }
213 }
214 };
215 let found = self.lookup_ref(repo_id.as_deref(), &workspace, reference.number).await?;
216 Ok(match found.named(in_repo) {
217 Some(Named::Item(id)) => Some(id),
218 _ => None,
219 })
220 }
221
222 /// The work items `text`, written in `repo`, closes and is part of,
223 /// with `own` (a pull request's own work item) closed first. Each of
224 /// `who` (whoever wrote it, merges it or pushed it) must be able to
225 /// see what it names ([`admits`]); what one of them may not is left
226 /// unresolved, as a reference to nothing.
227 pub(super) async fn named_items(&self, repo: &Repo, own: Option<&str>, text: &str, who: &[&User]) -> Result<NamedItems> {
228 let named = refs::named(text);
229 let mut closes: Vec<String> = Vec::new();
230 for reference in &named.closes {
231 if let Some(id) = self.ref_item(repo, reference).await?
232 && !closes.contains(&id)
233 {
234 closes.push(id);
235 }
236 }
237 let mut parts: Vec<String> = Vec::new();
238 for reference in &named.parts {
239 if let Some(id) = self.ref_item(repo, reference).await?
240 && !closes.contains(&id)
241 && own != Some(id.as_str())
242 && !parts.contains(&id)
243 {
244 parts.push(id);
245 }
246 }
247 let mut all: Vec<String> = own.map(str::to_owned).into_iter().collect();
248 all.extend(closes.iter().take(MAX_CLOSES * 2).cloned());
249 all.extend(parts.iter().take(MAX_CLOSES * 2).cloned());
250 let held = self.items_by_ids(&all).await?;
251 let found = |id: &String| held.iter().find(|held| held.item.id == *id);
252 // Its own work item first, whoever may see it: the pull request
253 // was made for it.
254 let mut closing: Vec<(String, u32)> = own
255 .map(str::to_owned)
256 .into_iter()
257 .filter_map(|id| found(&id).map(|held| (id, held.item.number)))
258 .collect();
259 for id in &closes {
260 if let Some(held) = found(id).filter(|held| admits(held, who, true))
261 && !closing.iter().any(|(known, _)| known == id)
262 {
263 closing.push((id.clone(), held.item.number));
264 }
265 }
266 let mut parted: Vec<(String, u32)> = Vec::new();
267 for id in &parts {
268 if let Some(held) = found(id).filter(|held| admits(held, who, false)) {
269 parted.push((id.clone(), held.item.number));
270 }
271 }
272 closing.truncate(MAX_CLOSES);
273 parted.truncate(MAX_CLOSES);
274 Ok(NamedItems { closes: closing, parts: parted })
275 }
276}
277
278/// The repository `#N` is read in, as the viewer found it: none when they
279/// may not read it, exactly as when there is no such repository.
280fn read_in<T>(found: Outcome<T>) -> Option<T> {
281 match found {
282 Outcome::Ok(repo) => Some(repo),
283 Outcome::Fail(_) => None,
284 }
285}
286
287/// Whether everyone in `who` may have `held` closed (`closing`: they may
288/// triage it) or linked as something they are part of (they may see it).
289pub(crate) fn admits(held: &Held, who: &[&User], closing: bool) -> bool {
290 let capability = if closing { Capability::Triage } else { Capability::Read };
291 who.iter().all(|user| held.may(Some(user), capability))
292}
293
294#[cfg(test)]
295mod tests {
296 use super::*;
297
298 #[test]
299 fn a_repository_the_viewer_may_not_read_is_read_as_none() {
300 assert_eq!(read_in(Outcome::<u8>::fail(g1t_contracts::FailureCode::NotFound, "Repository not found.")), None);
301 assert_eq!(read_in(Outcome::Ok(1)), Some(1));
302 }
303
304 #[test]
305 fn only_what_everyone_involved_may_see_is_closed_or_linked() {
306 use crate::items::rows_for_test::{held_for_test, person_for_test};
307 let member = person_for_test("usr_2", Some("acme"));
308 // An outside collaborator with Write on public acme/web.
309 let mut outsider = person_for_test("usr_9", None);
310 outsider.grants.push(g1t_contracts::access::RepoGrant {
311 repo_id: "rep_web".into(),
312 workspace: "acme".into(),
313 role: g1t_contracts::access::RepoRole::Write,
314 team: None,
315 });
316 let filed_in_web = held_for_test(1, &[("rep_web", "origin", "acme", false)], &[]);
317 let members_only = held_for_test(2, &[("rep_web", "target", "acme", false)], &[]);
318 let private = held_for_test(3, &[("rep_secret", "origin", "acme", true)], &[]);
319 assert!(admits(&filed_in_web, &[&outsider], true));
320 assert!(!admits(&members_only, &[&outsider], true));
321 assert!(!admits(&members_only, &[&outsider], false));
322 assert!(!admits(&private, &[&outsider], true));
323 assert!(admits(&private, &[&member], true));
324 // Merged by a member, written by the outsider: what both may.
325 assert!(!admits(&private, &[&member, &outsider], true));
326 // Someone who may only read a public work item may not close it.
327 let reader = person_for_test("usr_8", None);
328 assert!(admits(&filed_in_web, &[&reader], false));
329 assert!(!admits(&filed_in_web, &[&reader], true));
330 }
331
332 #[test]
333 fn a_reference_names_the_first_thing_found_in_order() {
334 let row = |pull_here: bool, legacy: Option<&str>, item: Option<&str>, there: Option<&str>| RefRow {
335 pull_here: pull_here.then_some(1),
336 legacy: legacy.map(str::to_owned),
337 item: item.map(str::to_owned),
338 pull_there: there.map(str::to_owned),
339 };
340 // In a repository: its own pull request, then its old issue, then
341 // the workspace's item, then another repository's pull request.
342 assert_eq!(row(true, Some("iss_1"), Some("wki_1"), None).named(true), Some(Named::PullHere));
343 assert_eq!(row(false, Some("iss_1"), Some("wki_1"), None).named(true), Some(Named::Item("iss_1".into())));
344 assert_eq!(row(false, None, Some("wki_1"), Some("rep_2")).named(true), Some(Named::Item("wki_1".into())));
345 assert_eq!(row(false, None, None, Some("rep_2")).named(true), Some(Named::PullThere("rep_2".into())));
346 // In a workspace alone: its item, then a pull request.
347 assert_eq!(row(false, None, Some("wki_1"), Some("rep_2")).named(false), Some(Named::Item("wki_1".into())));
348 assert_eq!(row(false, None, None, None).named(false), None);
349 }
350}