| 1 | //! Work items (`g1t_contracts::work::items`): any tracked work of a |
| 2 | //! workspace, linked to the repositories it concerns |
| 3 | //! (migrations/0049_work_items.sql). |
| 4 | //! |
| 5 | //! A work item is numbered in its workspace's sequence (`work_counters`, |
| 6 | //! numbering.rs), which new pull requests share, and links repositories in |
| 7 | //! `work_item_repos` (links.rs): its origin, its targets and those it |
| 8 | //! touched. Who may see one is `access::item_role`: the workspace's |
| 9 | //! members, and anyone who can read its origin. What `#N` names is |
| 10 | //! refs.rs; what landing one does, landing.rs. Labels and milestones are |
| 11 | //! the workspace's, and pull requests carry them too (labels.rs, |
| 12 | //! milestones.rs). Moving repositories' issues in is migration.rs. |
| 13 | //! |
| 14 | //! This module is the boundary. Its RPC methods (`open_work_item` and the |
| 15 | //! other `*_work_item` methods, labels, milestones, `resolve_ref`, |
| 16 | //! `closed_by_commit`, `legacy_item_map`, `migrate_work_items`) are the |
| 17 | //! service's; the rest of the crate (pull requests, merging, pushes, |
| 18 | //! coordination) reaches work items only through the functions below, and |
| 19 | //! reads them in its own queries only through the fragments in [`sql`]. A |
| 20 | //! later split of work items into a service of their own turns each |
| 21 | //! function into an RPC method and each fragment into a copy the caller |
| 22 | //! keeps from `work_item.*` events. |
| 23 | //! |
| 24 | //! Work items call out the other way at a few points, which a split would |
| 25 | //! turn into events: what a close means for claims and blocked work |
| 26 | //! (`Work::item_resolved`), the session a close was made in |
| 27 | //! (`Work::caller_session`, `Work::trailer_session`), and repositories and |
| 28 | //! their settings. `repo_workspaces`, where each repository is, is kept |
| 29 | //! here and read by anyone. |
| 30 | |
| 31 | use g1t_contracts::access::Capability; |
| 32 | use g1t_contracts::repos::Repo; |
| 33 | use g1t_contracts::work::refs::Ref; |
| 34 | use g1t_contracts::{Outcome, User, Viewer}; |
| 35 | use worker::Result; |
| 36 | |
| 37 | use crate::Work; |
| 38 | |
| 39 | /// Unwraps an `Outcome`, returning its failure from the enclosing method. |
| 40 | macro_rules! check { |
| 41 | ($outcome:expr) => { |
| 42 | match $outcome { |
| 43 | Outcome::Ok(value) => value, |
| 44 | Outcome::Fail(failure) => return Ok(Outcome::Fail(failure)), |
| 45 | } |
| 46 | }; |
| 47 | } |
| 48 | |
| 49 | mod changes; |
| 50 | mod dependencies; |
| 51 | mod labels; |
| 52 | mod landing; |
| 53 | mod links; |
| 54 | mod migration; |
| 55 | mod milestones; |
| 56 | mod numbering; |
| 57 | mod reading; |
| 58 | mod refs; |
| 59 | mod rows; |
| 60 | pub(crate) mod sql; |
| 61 | #[cfg(test)] |
| 62 | mod tests; |
| 63 | |
| 64 | pub(crate) use labels::Labeled; |
| 65 | pub(crate) use landing::{Landing, PastLanding}; |
| 66 | pub(crate) use migration::{DELETED, RENAMED, TRANSFERRED}; |
| 67 | pub(crate) use reading::{held_rows, select}; |
| 68 | pub(crate) use refs::{MAX_CLOSES, Named, NamedItems}; |
| 69 | pub(crate) use rows::{Held, path_of, repo_readable, workspace_of}; |
| 70 | |
| 71 | /// Work items and people made up for other modules' tests. |
| 72 | #[cfg(test)] |
| 73 | pub(crate) mod rows_for_test { |
| 74 | pub(crate) use super::rows::{held_for_test, person_for_test}; |
| 75 | } |
| 76 | |
| 77 | // --- Reading --------------------------------------------------------------- |
| 78 | |
| 79 | /// The work item numbered `number` in `workspace`, whoever is asking. |
| 80 | pub(crate) async fn by_number(work: &Work, workspace: &str, number: u32) -> Result<Option<Held>> { |
| 81 | work.item(workspace, number).await |
| 82 | } |
| 83 | |
| 84 | /// A work item by id, whoever is asking. |
| 85 | pub(crate) async fn by_id(work: &Work, id: &str) -> Result<Option<Held>> { |
| 86 | work.item_by_id(id).await |
| 87 | } |
| 88 | |
| 89 | /// The work items of these ids, in no particular order. |
| 90 | pub(crate) async fn by_ids(work: &Work, ids: &[String]) -> Result<Vec<Held>> { |
| 91 | work.items_by_ids(ids).await |
| 92 | } |
| 93 | |
| 94 | /// A work item, if `viewer` may do `capability` on it; not found when they |
| 95 | /// may not see it. |
| 96 | pub(crate) async fn for_viewer(work: &Work, viewer: &Viewer, workspace: &str, number: u32, capability: Capability) -> Result<Outcome<Held>> { |
| 97 | work.item_for(viewer, workspace, number, capability).await |
| 98 | } |
| 99 | |
| 100 | /// A pull request's own work item, if it has one. |
| 101 | pub(crate) async fn for_pull(work: &Work, pull_id: &str) -> Result<Option<Held>> { |
| 102 | match work.pull_item_id(pull_id).await? { |
| 103 | Some(id) => work.item_by_id(&id).await, |
| 104 | None => Ok(None), |
| 105 | } |
| 106 | } |
| 107 | |
| 108 | // --- Numbers and repositories ---------------------------------------------- |
| 109 | |
| 110 | /// The next number of `workspace`'s sequence, for a pull request of |
| 111 | /// `repo_id` or (without one) a work item. |
| 112 | pub(crate) async fn next_number(work: &Work, workspace: &str, repo_id: Option<&str>) -> Result<u32> { |
| 113 | work.next_number(workspace, repo_id).await |
| 114 | } |
| 115 | |
| 116 | /// Records which workspace `repo` is in, its path and whether it is |
| 117 | /// private, as repos just said. |
| 118 | pub(crate) async fn remember_repo(work: &Work, repo: &Repo) -> Result<()> { |
| 119 | work.remember_repo(repo).await |
| 120 | } |
| 121 | |
| 122 | /// The workspace repository `repo_id` is in. |
| 123 | pub(crate) async fn repo_workspace(work: &Work, repo_id: &str) -> Result<Option<String>> { |
| 124 | work.repo_workspace(repo_id).await |
| 125 | } |
| 126 | |
| 127 | /// `repo.created`: where the repository is, and for a workspace's first, |
| 128 | /// the default labels. |
| 129 | pub(crate) async fn repo_created(work: &Work, data: &serde_json::Value) -> Result<()> { |
| 130 | work.repo_created(data).await |
| 131 | } |
| 132 | |
| 133 | /// `repo.restored`: where the repository is again, and what it kept for |
| 134 | /// itself, placed. |
| 135 | pub(crate) async fn repo_restored(work: &Work, data: &serde_json::Value) -> Result<()> { |
| 136 | work.repo_restored(data).await |
| 137 | } |
| 138 | |
| 139 | /// `repo.visibility_changed`: whether its work items are public. |
| 140 | pub(crate) async fn repo_visibility(work: &Work, data: &serde_json::Value) -> Result<()> { |
| 141 | work.repo_visibility(data).await |
| 142 | } |
| 143 | |
| 144 | // --- References and landing ------------------------------------------------ |
| 145 | |
| 146 | /// What `#number` names, read in repository `repo_id` of `workspace`. |
| 147 | pub(crate) async fn resolve(work: &Work, repo_id: &str, workspace: &str, number: u32) -> Result<Option<Named>> { |
| 148 | Ok(work.lookup_ref(Some(repo_id), workspace, number).await?.named(true)) |
| 149 | } |
| 150 | |
| 151 | /// The work item `reference` names, read in `repo`, by id: one of its |
| 152 | /// workspace's, or none. |
| 153 | pub(crate) async fn referenced(work: &Work, repo: &Repo, reference: &Ref) -> Result<Option<String>> { |
| 154 | work.ref_item(repo, reference).await |
| 155 | } |
| 156 | |
| 157 | /// The work items `text`, written in `repo`, closes and is part of, with |
| 158 | /// `own` (a pull request's own work item) closed first: those each of |
| 159 | /// `who` may close, or see (refs.rs `admits`). |
| 160 | pub(crate) async fn named(work: &Work, repo: &Repo, own: Option<&str>, text: &str, who: &[&User]) -> Result<NamedItems> { |
| 161 | work.named_items(repo, own, text, who).await |
| 162 | } |
| 163 | |
| 164 | /// Whether each of `who` may have `held` closed by naming it (`closing`), |
| 165 | /// or linked as something a change is part of. |
| 166 | pub(crate) fn admits(held: &Held, who: &[&User], closing: bool) -> bool { |
| 167 | refs::admits(held, who, closing) |
| 168 | } |
| 169 | |
| 170 | /// Lands `closes` (work items, by id) in `repo` by `landing` and links |
| 171 | /// `parts` (landing.rs). Returns the work items it closed. |
| 172 | #[allow(clippy::too_many_arguments)] |
| 173 | pub(crate) async fn land( |
| 174 | work: &Work, |
| 175 | repo: &Repo, |
| 176 | landing: &Landing, |
| 177 | actor: &User, |
| 178 | closes: &[String], |
| 179 | parts: &[String], |
| 180 | session: Option<&str>, |
| 181 | agent: bool, |
| 182 | ) -> Result<Vec<Held>> { |
| 183 | work.land(repo, landing, actor, closes, parts, session, agent).await |
| 184 | } |
| 185 | |
| 186 | /// Links `repo` to `held` as touched (it has a pull request for it), |
| 187 | /// unless it is linked already. |
| 188 | pub(crate) async fn touch(work: &Work, held: &Held, repo: &Repo, actor: &User) -> Result<()> { |
| 189 | work.touch_item(held, repo, actor).await |
| 190 | } |
| 191 | |
| 192 | /// Where the other work items of `held`'s workspace landed, newest first. |
| 193 | pub(crate) async fn landed_before(work: &Work, held: &Held, limit: u32) -> Result<Vec<PastLanding>> { |
| 194 | work.landed_before(held, limit).await |
| 195 | } |
| 196 | |
| 197 | // --- Its conversation ------------------------------------------------------ |
| 198 | |
| 199 | /// Notes something that happened to a work item, in its conversation. |
| 200 | pub(crate) async fn note<'a>(work: &Work, item_id: &str, author: impl Into<crate::rows::By<'a>>, text: &str) -> Result<()> { |
| 201 | work.note_item(item_id, author, text).await |
| 202 | } |
| 203 | |
| 204 | /// A statement that marks a work item updated at `at`, for a caller's |
| 205 | /// batch (a comment on it). |
| 206 | pub(crate) fn stamp(work: &Work, item_id: &str, at: &str) -> Result<worker::D1PreparedStatement> { |
| 207 | work.db.prepare("UPDATE work_items SET updated_at = ? WHERE id = ?").bind(&[at.into(), item_id.into()]) |
| 208 | } |
| 209 | |
| 210 | // --- Labels and milestones ------------------------------------------------- |
| 211 | |
| 212 | /// Sets the labels of a work item or pull request to `wanted`. |
| 213 | pub(crate) async fn relabel(work: &Work, actor: &User, labeled: &Labeled, wanted: &[String]) -> Result<Outcome<Vec<String>>> { |
| 214 | work.relabel(actor, labeled, wanted).await |
| 215 | } |
| 216 | |
| 217 | /// Puts a work item or pull request in milestone `number` (0: none). |
| 218 | pub(crate) async fn set_milestone(work: &Work, actor: &User, labeled: &Labeled, number: u32) -> Result<Outcome<()>> { |
| 219 | work.set_milestone(actor, labeled, number).await |
| 220 | } |
| 221 | |
| 222 | // --- What blocks what ------------------------------------------------------ |
| 223 | |
| 224 | /// The open work items `held` blocks. |
| 225 | pub(crate) async fn dependents(work: &Work, held: &Held) -> Result<Vec<Held>> { |
| 226 | work.dependents(held).await |
| 227 | } |
| 228 | |
| 229 | /// Those of `numbers` whose work item in `workspace` is still open, |
| 230 | /// leaving out `cleared`. |
| 231 | pub(crate) async fn still_open(work: &Work, workspace: &str, numbers: &[u32], cleared: Option<u32>) -> Result<Vec<u32>> { |
| 232 | work.still_open(workspace, numbers, cleared).await |
| 233 | } |
| 234 | |
| 235 | /// Those of `ids` (repositories) that `viewer` may read, as recorded in |
| 236 | /// `repo_workspaces`. |
| 237 | pub(crate) async fn readable_repo_ids(work: &Work, viewer: Option<&User>, ids: &[String]) -> Result<std::collections::HashSet<String>> { |
| 238 | work.readable_repo_ids(viewer, ids).await |
| 239 | } |
| 240 | |
| 241 | /// The work items of `workspace` that `numbers` names and `viewer` may |
| 242 | /// see, in no particular order. |
| 243 | pub(crate) async fn visible_numbered(work: &Work, workspace: &str, numbers: &[u32], viewer: Option<&User>) -> Result<Vec<Held>> { |
| 244 | work.visible_numbered(workspace, numbers, viewer).await |
| 245 | } |
| 246 | |
| 247 | /// The work items that name `held` in their `blocked_by`, those `viewer` |
| 248 | /// may see. |
| 249 | pub(crate) async fn blocked_items(work: &Work, held: &Held, viewer: Option<&User>) -> Result<Vec<g1t_contracts::work::ItemBrief>> { |
| 250 | work.blocked_items(held, viewer).await |
| 251 | } |