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.

285 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;
15use serde::Deserialize;
16use worker::Result;
17use worker::wasm_bindgen::JsValue;
18
19use super::rows::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 filed
28/// there under that old number, the workspace's work item, and the newest
29/// pull request of that number in the workspace's other repositories.
30pub(crate) const REF_SQL: &str = "SELECT
31 (SELECT 1 FROM pulls WHERE repo_id = ?1 AND number = ?3) AS pull_here,
32 (SELECT item_id FROM work_item_repos WHERE repo_id = ?1 AND legacy_number = ?3) AS legacy,
33 (SELECT id FROM work_items WHERE workspace = ?2 AND number = ?3) AS item,
34 (SELECT repo_id FROM pulls WHERE number = ?3 AND repo_id IS NOT ?1
35 AND repo_id IN (SELECT repo_id FROM repo_workspaces WHERE workspace = ?2)
36 ORDER BY created_at DESC LIMIT 1) AS pull_there";
37
38/// What [`REF_SQL`] found.
39#[derive(Clone, Debug, Default, Deserialize)]
40pub(crate) struct RefRow {
41 #[serde(default)]
42 pub pull_here: Option<u8>,
43 #[serde(default)]
44 pub legacy: Option<String>,
45 #[serde(default)]
46 pub item: Option<String>,
47 #[serde(default)]
48 pub pull_there: Option<String>,
49}
50
51/// What a reference names, by the order of `refs::lookup_order`.
52#[derive(Clone, Debug, PartialEq, Eq)]
53pub(crate) enum Named {
54 /// A pull request of the repository it was read in.
55 PullHere,
56 /// A work item, by id.
57 Item(String),
58 /// A pull request of another repository of the workspace, by id.
59 PullThere(String),
60}
61
62impl RefRow {
63 /// The first thing found, in the order references are read in: in a
64 /// repository (`in_repo`), or a workspace alone.
65 pub(crate) fn named(self, in_repo: bool) -> Option<Named> {
66 first_found(in_repo, |lookup| match lookup {
67 Lookup::PullInRepo => self.pull_here.map(|_| Named::PullHere),
68 Lookup::LegacyItem => self.legacy.clone().map(Named::Item),
69 Lookup::Item => self.item.clone().map(Named::Item),
70 Lookup::PullInWorkspace => self.pull_there.clone().map(Named::PullThere),
71 })
72 }
73}
74
75/// The work items a text names, by id and number, in the order named.
76#[derive(Clone, Debug, Default, PartialEq, Eq)]
77pub(crate) struct NamedItems {
78 pub closes: Vec<(String, u32)>,
79 pub parts: Vec<(String, u32)>,
80}
81
82impl NamedItems {
83 pub(crate) fn close_ids(&self) -> Vec<String> {
84 self.closes.iter().map(|(id, _)| id.clone()).collect()
85 }
86
87 pub(crate) fn part_ids(&self) -> Vec<String> {
88 self.parts.iter().map(|(id, _)| id.clone()).collect()
89 }
90
91 pub(crate) fn close_numbers(&self) -> Vec<u32> {
92 self.closes.iter().map(|(_, number)| *number).collect()
93 }
94
95 pub(crate) fn part_numbers(&self) -> Vec<u32> {
96 self.parts.iter().map(|(_, number)| *number).collect()
97 }
98}
99
100impl Work {
101 /// What [`REF_SQL`] finds for `#number` in `repo_id` (or none) of
102 /// `workspace`.
103 pub(super) async fn lookup_ref(&self, repo_id: Option<&str>, workspace: &str, number: u32) -> Result<RefRow> {
104 Ok(self
105 .db
106 .prepare(REF_SQL)
107 .bind(&[repo_id.map_or(JsValue::NULL, JsValue::from), workspace.to_lowercase().into(), number.into()])?
108 .first::<RefRow>(None)
109 .await?
110 .unwrap_or_default())
111 }
112
113 /// `resolve_ref`: what `#number` names for the viewer, read in `repo`
114 /// or in `workspace` (`refs`).
115 pub(crate) async fn resolve_ref(&self, a: ResolveRefArgs) -> Result<RefTarget> {
116 // Read in a repository the viewer may not read (a member without
117 // Code following an old link), `#N` still names the work items
118 // they may see; its pull requests stay out of reach.
119 let (repo, workspace) = match &a.repo {
120 Some(path) => match self.repo(path, &a.viewer).await? {
121 Outcome::Ok(repo) => {
122 let workspace = repo.namespace.to_lowercase();
123 (Some((repo.id.clone(), Some(path_of(&repo)))), workspace)
124 }
125 Outcome::Fail(_) => match self.recorded_repo(&format!("{}/{}", path.namespace, path.name)).await? {
126 Some((id, workspace)) => (Some((id, None)), workspace),
127 None => return Ok(RefTarget::None),
128 },
129 },
130 None => (None, a.workspace.trim().to_lowercase()),
131 };
132 if a.number == 0 || workspace.is_empty() {
133 return Ok(RefTarget::None);
134 }
135 // What the viewer may not see is passed over for what comes next.
136 let mut row = self.lookup_ref(repo.as_ref().map(|(id, _)| id.as_str()), &workspace, a.number).await?;
137 for _ in 0..4 {
138 let Some(named) = row.clone().named(repo.is_some()) else {
139 return Ok(RefTarget::None);
140 };
141 match named {
142 Named::PullHere => match repo.as_ref().and_then(|(_, path)| path.clone()) {
143 Some(repo) => return Ok(RefTarget::Pull { repo, number: a.number }),
144 None => row.pull_here = None,
145 },
146 Named::Item(id) => {
147 if let Some(held) = self.item_by_id(&id).await?
148 && held.may(a.viewer.as_ref(), Capability::Read)
149 {
150 return Ok(RefTarget::WorkItem { workspace: held.item.workspace, number: held.item.number, item_id: held.item.id });
151 }
152 if row.legacy.as_deref() == Some(id.as_str()) {
153 row.legacy = None;
154 } else {
155 row.item = None;
156 }
157 }
158 Named::PullThere(repo_id) => {
159 let readable: Vec<Repo> = g1t_kit::call(
160 &self.repos,
161 "readable",
162 &ReadableArgs { ids: vec![repo_id.clone()], viewer: a.viewer.clone() },
163 )
164 .await?;
165 return Ok(match readable.first() {
166 Some(other) => RefTarget::Pull { repo: path_of(other), number: a.number },
167 None => RefTarget::None,
168 });
169 }
170 }
171 }
172 Ok(RefTarget::None)
173 }
174
175 /// The id and workspace of the repository at `path` (`owner/name`), as
176 /// recorded, whoever is asking.
177 async fn recorded_repo(&self, path: &str) -> Result<Option<(String, String)>> {
178 #[derive(Deserialize)]
179 struct Recorded {
180 repo_id: String,
181 workspace: String,
182 }
183 Ok(self
184 .db
185 .prepare("SELECT repo_id, workspace FROM repo_workspaces WHERE lower(path) = lower(?)")
186 .bind(&[path.into()])?
187 .first::<Recorded>(None)
188 .await?
189 .map(|found| (found.repo_id, found.workspace)))
190 }
191
192 /// The work item `reference` names, read in `repo`: its id, when it is
193 /// one of the workspace's work items.
194 pub(super) async fn ref_item(&self, repo: &Repo, reference: &Ref) -> Result<Option<String>> {
195 let workspace = repo.namespace.to_lowercase();
196 let (repo_id, in_repo) = match &reference.scope {
197 RefScope::Here => (Some(repo.id.clone()), true),
198 RefScope::Workspace(named) => {
199 if *named != workspace {
200 return Ok(None);
201 }
202 (None, false)
203 }
204 RefScope::Repo(path) => {
205 if !path.namespace.eq_ignore_ascii_case(&workspace) {
206 return Ok(None);
207 }
208 let named = format!("{}/{}", path.namespace, path.name);
209 if named.eq_ignore_ascii_case(&path_of(repo)) {
210 (Some(repo.id.clone()), true)
211 } else {
212 let known = self
213 .db
214 .prepare("SELECT repo_id AS value FROM repo_workspaces WHERE lower(path) = lower(?)")
215 .bind(&[named.as_str().into()])?
216 .first::<ValueRow>(None)
217 .await?;
218 match known {
219 Some(row) => (Some(row.value), true),
220 None => (None, false),
221 }
222 }
223 }
224 };
225 let found = self.lookup_ref(repo_id.as_deref(), &workspace, reference.number).await?;
226 Ok(match found.named(in_repo) {
227 Some(Named::Item(id)) => Some(id),
228 _ => None,
229 })
230 }
231
232 /// The work items `text`, written in `repo`, closes and is part of,
233 /// with `own` (a pull request's own work item) closed first.
234 pub(super) async fn named_items(&self, repo: &Repo, own: Option<&str>, text: &str) -> Result<NamedItems> {
235 let named = refs::named(text);
236 let mut closes: Vec<String> = own.map(str::to_owned).into_iter().collect();
237 for reference in &named.closes {
238 if let Some(id) = self.ref_item(repo, reference).await?
239 && !closes.contains(&id)
240 {
241 closes.push(id);
242 }
243 }
244 let mut parts: Vec<String> = Vec::new();
245 for reference in &named.parts {
246 if let Some(id) = self.ref_item(repo, reference).await?
247 && !closes.contains(&id)
248 && !parts.contains(&id)
249 {
250 parts.push(id);
251 }
252 }
253 closes.truncate(MAX_CLOSES);
254 parts.truncate(MAX_CLOSES);
255 let mut all = closes.clone();
256 all.extend(parts.iter().cloned());
257 let held = self.items_by_ids(&all).await?;
258 let number = |id: &String| held.iter().find(|held| held.item.id == *id).map(|held| (id.clone(), held.item.number));
259 Ok(NamedItems { closes: closes.iter().filter_map(number).collect(), parts: parts.iter().filter_map(number).collect() })
260 }
261}
262
263#[cfg(test)]
264mod tests {
265 use super::*;
266
267 #[test]
268 fn a_reference_names_the_first_thing_found_in_order() {
269 let row = |pull_here: bool, legacy: Option<&str>, item: Option<&str>, there: Option<&str>| RefRow {
270 pull_here: pull_here.then_some(1),
271 legacy: legacy.map(str::to_owned),
272 item: item.map(str::to_owned),
273 pull_there: there.map(str::to_owned),
274 };
275 // In a repository: its own pull request, then its old issue, then
276 // the workspace's item, then another repository's pull request.
277 assert_eq!(row(true, Some("iss_1"), Some("wki_1"), None).named(true), Some(Named::PullHere));
278 assert_eq!(row(false, Some("iss_1"), Some("wki_1"), None).named(true), Some(Named::Item("iss_1".into())));
279 assert_eq!(row(false, None, Some("wki_1"), Some("rep_2")).named(true), Some(Named::Item("wki_1".into())));
280 assert_eq!(row(false, None, None, Some("rep_2")).named(true), Some(Named::PullThere("rep_2".into())));
281 // In a workspace alone: its item, then a pull request.
282 assert_eq!(row(false, None, Some("wki_1"), Some("rep_2")).named(false), Some(Named::Item("wki_1".into())));
283 assert_eq!(row(false, None, None, None).named(false), None);
284 }
285}