Pick any line to see why it is the way it is: the commit, the pull request and issue it came from, and what the agent was thinking.
| Project settings say who created the project, by name | 1 | //! Reading a page's worth of rows in one round trip. |
| 2 | //! | |
| 3 | //! A pull request's page used to cost about twenty queries one after | |
| 4 | //! another: the repository, the pull request, its issue, then where it | |
| The work, events, webhooks, actions, repos and packages services no longer run g1t's own agents. Work drops the 35 calls that queued issues for g1t, delegated them, messaged and reviewed through the agent, planned, captured memories automatically, rated confidence and capped agent spend, and its pull request lifecycle no longer revises or reviews: a pull request only advances to a merge by the repository's auto-merge setting. Merging a pull request that is behind now catches it up itself, through the repository's branch update, and lands it when the push arrives; when it can't be caught up cleanly the merge says to merge the base in and try again. Recording a session on a pull request, memory recall and remember, overlaps, the merge queue and required checks stay as they were. | 5 | //! stands (its progress, settings, statuses, verdicts and queue entry), |
| 6 | //! its comments and checks. Each is a round trip to the database, and from most places a | |
| Project settings say who created the project, by name | 7 | //! round trip is 20 to 40 ms. Here they go as one D1 batch, keyed by the |
| 8 | //! repository's id and the pull request's number, and the batch starts | |
| 9 | //! while the repos service is still deciding whether the viewer may see | |
| 10 | //! the repository ([`Work::repo_then`]). | |
| 11 | //! | |
| 12 | //! The helpers that answer those questions (`settings`, `statuses`, | |
| The work, events, webhooks, actions, repos and packages services no longer run g1t's own agents. Work drops the 35 calls that queued issues for g1t, delegated them, messaged and reviewed through the agent, planned, captured memories automatically, rated confidence and capped agent spend, and its pull request lifecycle no longer revises or reviews: a pull request only advances to a merge by the repository's auto-merge setting. Merging a pull request that is behind now catches it up itself, through the repository's branch update, and lands it when the push arrives; when it can't be caught up cleanly the merge says to merge the base in and try again. Recording a session on a pull request, memory recall and remember, overlaps, the merge queue and required checks stay as they were. | 13 | //! `approvals_gap` and the rest) look here first, so the |
| Project settings say who created the project, by name | 14 | //! code that decides a pull request's lifecycle is the same code whether |
| 15 | //! its rows were batched or read one at a time. Only reads are kept; a | |
| 16 | //! helper that writes and reads back (mergeability) reads the database. | |
| 17 | ||
| 18 | use std::cell::RefCell; | |
| 19 | use std::collections::HashMap; | |
| 20 | use std::future::Future; | |
| 21 | use std::rc::Rc; | |
| 22 | ||
| 23 | use g1t_contracts::repos::{Repo, RepoPath}; | |
| 24 | use g1t_contracts::{Outcome, Viewer}; | |
| 25 | use serde::de::DeserializeOwned; | |
| 26 | use worker::wasm_bindgen::JsValue; | |
| 27 | use worker::{D1PreparedStatement, D1Result, Result}; | |
| 28 | ||
| 29 | use crate::rows::PULL_COLUMNS; | |
| 30 | use crate::{ISSUE_COLUMNS, Work}; | |
| 31 | ||
| 32 | /// The pull request's id, from the batch's `?1` (repository) and `?2` (number). | |
| 33 | const PULL_ID: &str = "(SELECT id FROM pulls WHERE repo_id = ?1 AND number = ?2)"; | |
| 34 | ||
| 35 | /// Each statement of a pull request's batch, in order. | |
| 36 | #[derive(Clone, Copy)] | |
| 37 | pub(crate) enum Slot { | |
| 38 | Pull, | |
| 39 | Issue, | |
| 40 | Comments, | |
| 41 | Verdicts, | |
| 42 | Runs, | |
| 43 | Statuses, | |
| 44 | Settings, | |
| 45 | Others, | |
| 46 | Queued, | |
| Merge rulesets: branch and tag rules, agent-first, enforced on push and merge | 47 | Rulesets, |
| 48 | Adopted, | |
| Project settings say who created the project, by name | 49 | } |
| 50 | ||
| 51 | /// How many runs `latest_checks` and `earlier_checks` show together. | |
| 52 | pub(crate) const RECENT_RUNS: u32 = 11; | |
| 53 | ||
| 54 | /// One pull request's rows, as read for this request. | |
| 55 | pub(crate) struct Prefetched { | |
| 56 | pub(crate) pull_id: String, | |
| 57 | pub(crate) repo_id: String, | |
| Merge rulesets: branch and tag rules, agent-first, enforced on push and merge | 58 | /// The workspace its rulesets were read for, lowercase. |
| 59 | pub(crate) namespace: String, | |
| Project settings say who created the project, by name | 60 | /// Its head commit when read, which its statuses are for. |
| 61 | pub(crate) head: Option<String>, | |
| 62 | results: Vec<D1Result>, | |
| 63 | } | |
| 64 | ||
| 65 | impl Prefetched { | |
| 66 | /// A slot's rows, as the helper that reads them would have. | |
| 67 | pub(crate) fn rows<T: DeserializeOwned>(&self, slot: Slot) -> Result<Vec<T>> { | |
| 68 | match self.results.get(slot as usize) { | |
| 69 | Some(result) => result.results::<T>(), | |
| 70 | None => Ok(Vec::new()), | |
| 71 | } | |
| 72 | } | |
| 73 | ||
| 74 | /// A slot's first row. | |
| 75 | pub(crate) fn first<T: DeserializeOwned>(&self, slot: Slot) -> Result<Option<T>> { | |
| 76 | Ok(self.rows::<T>(slot)?.into_iter().next()) | |
| 77 | } | |
| 78 | } | |
| 79 | ||
| 80 | #[derive(serde::Deserialize)] | |
| 81 | struct IdRow { | |
| 82 | id: String, | |
| 83 | head_commit: Option<String>, | |
| 84 | } | |
| 85 | ||
| 86 | thread_local! { | |
| 87 | /// Repository ids by path, as the repos service last answered: a guess | |
| 88 | /// that lets a page's batch start before the answer comes. Never | |
| 89 | /// trusted: nothing read with a guess is used unless the repos service | |
| 90 | /// then says the viewer may see that very repository. | |
| 91 | static REPO_IDS: RefCell<HashMap<String, String>> = RefCell::new(HashMap::new()); | |
| 92 | } | |
| 93 | ||
| 94 | /// Guesses kept at most; the map is emptied past this. | |
| 95 | const MAX_GUESSES: usize = 1024; | |
| 96 | ||
| 97 | fn path_key(path: &RepoPath) -> String { | |
| 98 | format!("{}/{}", path.namespace.to_lowercase(), path.name.to_lowercase()) | |
| 99 | } | |
| 100 | ||
| 101 | fn guess(path: &RepoPath) -> Option<String> { | |
| 102 | REPO_IDS.with(|ids| ids.borrow().get(&path_key(path)).cloned()) | |
| 103 | } | |
| 104 | ||
| 105 | fn learn(path: &RepoPath, id: Option<&str>) { | |
| 106 | REPO_IDS.with(|ids| { | |
| 107 | let mut ids = ids.borrow_mut(); | |
| 108 | match id { | |
| 109 | Some(id) => { | |
| 110 | if ids.len() >= MAX_GUESSES { | |
| 111 | ids.clear(); | |
| 112 | } | |
| 113 | ids.insert(path_key(path), id.to_owned()); | |
| 114 | } | |
| 115 | None => { | |
| 116 | ids.remove(&path_key(path)); | |
| 117 | } | |
| 118 | } | |
| 119 | }); | |
| 120 | } | |
| 121 | ||
| 122 | impl Work { | |
| 123 | /// The repository at `path` if `viewer` may see it, and what `load` | |
| 124 | /// reads for it. When this isolate has seen the repository before, | |
| 125 | /// `load` starts with its id at once, beside the access check, instead | |
| 126 | /// of after it: one round trip instead of two. What it read is | |
| 127 | /// discarded unless the check passes for that same repository. | |
| 128 | pub(crate) async fn repo_then<T, F, Fut>(&self, path: &RepoPath, viewer: &Viewer, load: F) -> Result<Outcome<(Repo, T)>> | |
| 129 | where | |
| 130 | F: Fn(String) -> Fut, | |
| 131 | Fut: Future<Output = Result<T>>, | |
| 132 | { | |
| 133 | let guessed = guess(path); | |
| 134 | let (found, early) = match &guessed { | |
| 135 | Some(id) => { | |
| 136 | let (found, early) = futures_util::future::join(self.repo(path, viewer), load(id.clone())).await; | |
| 137 | (found?, Some(early)) | |
| 138 | } | |
| 139 | None => (self.repo(path, viewer).await?, None), | |
| 140 | }; | |
| 141 | let repo = match found { | |
| 142 | Outcome::Ok(repo) => repo, | |
| 143 | Outcome::Fail(failure) => { | |
| 144 | learn(path, None); | |
| 145 | return Ok(Outcome::Fail(failure)); | |
| 146 | } | |
| 147 | }; | |
| 148 | if let (Some(id), Some(Ok(value))) = (&guessed, early) | |
| 149 | && *id == repo.id | |
| 150 | { | |
| 151 | return Ok(Outcome::Ok((repo, value))); | |
| 152 | } | |
| 153 | learn(path, Some(&repo.id)); | |
| 154 | let value = load(repo.id.clone()).await?; | |
| 155 | Ok(Outcome::Ok((repo, value))) | |
| 156 | } | |
| 157 | ||
| 158 | fn statement(&self, sql: &str, binds: &[JsValue]) -> Result<D1PreparedStatement> { | |
| 159 | self.db.prepare(sql).bind(binds) | |
| 160 | } | |
| 161 | ||
| 162 | /// Everything a pull request's page and its lifecycle read, in one | |
| 163 | /// batch. `None` when there is no such pull request. | |
| Merge rulesets: branch and tag rules, agent-first, enforced on push and merge | 164 | pub(crate) async fn prefetch_pull(&self, repo_id: String, namespace: String, number: u32) -> Result<Option<Prefetched>> { |
| Project settings say who created the project, by name | 165 | let key = || -> [JsValue; 2] { [repo_id.as_str().into(), number.into()] }; |
| 166 | let with = |extra: JsValue| -> [JsValue; 3] { [repo_id.as_str().into(), number.into(), extra] }; | |
| 167 | let repo_only = || -> [JsValue; 1] { [repo_id.as_str().into()] }; | |
| 168 | let statements = vec![ | |
| 169 | // Slot::Pull: the row, with everything the lifecycle tracks on it. | |
| 170 | self.statement(&format!("SELECT {PULL_COLUMNS} FROM pulls WHERE repo_id = ?1 AND number = ?2"), &key())?, | |
| 171 | // Slot::Issue | |
| 172 | self.statement( | |
| 173 | &format!( | |
| 174 | "SELECT {ISSUE_COLUMNS} FROM issues | |
| 175 | WHERE repo_id = ?1 AND number = (SELECT issue_number FROM pulls WHERE repo_id = ?1 AND number = ?2)" | |
| 176 | ), | |
| 177 | &key(), | |
| 178 | )?, | |
| 179 | // Slot::Comments (lib.rs `comments`) | |
| 180 | self.statement("SELECT * FROM comments WHERE repo_id = ?1 AND number = ?2 ORDER BY id LIMIT 500", &key())?, | |
| 181 | // Slot::Verdicts: every verdict, oldest first, for the approval | |
| 182 | // rule, a person's request for changes and a person's approval. | |
| 183 | self.statement( | |
| 184 | "SELECT author_id, author_name, verdict, created_at FROM comments | |
| 185 | WHERE repo_id = ?1 AND number = ?2 AND verdict IS NOT NULL ORDER BY id", | |
| 186 | &key(), | |
| 187 | )?, | |
| 188 | // Slot::Runs: the latest and the ten before it (checks.rs). | |
| 189 | self.statement( | |
| 190 | &format!("SELECT * FROM check_runs WHERE pull_id = {PULL_ID} ORDER BY id DESC LIMIT ?3"), | |
| 191 | &with(RECENT_RUNS.into()), | |
| 192 | )?, | |
| 193 | // Slot::Statuses: on its head (statuses.rs `statuses`). | |
| 194 | self.statement( | |
| Merge checks: statuses and check runs on every commit | 195 | "SELECT context, state, description, target_url, updated_at, source, check_run_id FROM commit_statuses |
| Project settings say who created the project, by name | 196 | WHERE repo_id = ?1 AND sha = (SELECT head_commit FROM pulls WHERE repo_id = ?1 AND number = ?2) |
| 197 | ORDER BY context", | |
| 198 | &key(), | |
| 199 | )?, | |
| The work, events, webhooks, actions, repos and packages services no longer run g1t's own agents. Work drops the 35 calls that queued issues for g1t, delegated them, messaged and reviewed through the agent, planned, captured memories automatically, rated confidence and capped agent spend, and its pull request lifecycle no longer revises or reviews: a pull request only advances to a merge by the repository's auto-merge setting. Merging a pull request that is behind now catches it up itself, through the repository's branch update, and lands it when the push arrives; when it can't be caught up cleanly the merge says to merge the base in and try again. Recording a session on a pull request, memory recall and remember, overlaps, the merge queue and required checks stay as they were. | 200 | // Slot::Settings (settings.rs `settings`) |
| Project settings say who created the project, by name | 201 | self.statement("SELECT * FROM repo_settings WHERE repo_id = ?1", &repo_only())?, |
| 202 | // Slot::Others (reviews.rs `overlaps`) | |
| 203 | self.statement( | |
| 204 | "SELECT number, title, issue_number, files FROM pulls | |
| 205 | WHERE repo_id = ?1 AND number != ?2 AND status IN ('draft', 'open') | |
| 206 | ORDER BY number LIMIT 200", | |
| 207 | &key(), | |
| 208 | )?, | |
| 209 | // Slot::Queued (queue.rs `queued_entry`) | |
| 210 | self.statement( | |
| 211 | &format!( | |
| 212 | "SELECT * FROM queue_entries | |
| 213 | WHERE pull_id = {PULL_ID} AND state IN ('waiting', 'testing', 'passed') LIMIT 1" | |
| 214 | ), | |
| 215 | &key(), | |
| 216 | )?, | |
| Merge rulesets: branch and tag rules, agent-first, enforced on push and merge | 217 | // Slot::Rulesets and Slot::Adopted (rulesets.rs `rulesets_for`) |
| 218 | self.statement(crate::rulesets::prefetch_sql().0, &[repo_id.as_str().into(), namespace.to_lowercase().into()])?, | |
| 219 | self.statement(crate::rulesets::prefetch_sql().1, &repo_only())?, | |
| Project settings say who created the project, by name | 220 | ]; |
| 221 | let count = statements.len() as u32; | |
| 222 | let results = self.timing.db(count, self.db.batch(statements)).await?; | |
| 223 | let Some(row) = results | |
| 224 | .first() | |
| 225 | .map(|result| result.results::<IdRow>()) | |
| 226 | .transpose()? | |
| 227 | .and_then(|rows| rows.into_iter().next()) | |
| 228 | else { | |
| 229 | return Ok(None); | |
| 230 | }; | |
| 231 | Ok(Some(Prefetched { | |
| 232 | pull_id: row.id, | |
| 233 | repo_id, | |
| Merge rulesets: branch and tag rules, agent-first, enforced on push and merge | 234 | namespace: namespace.to_lowercase(), |
| Project settings say who created the project, by name | 235 | head: row.head_commit, |
| 236 | results, | |
| 237 | })) | |
| 238 | } | |
| 239 | ||
| 240 | /// The rows read for pull request `pull_id` in this request, if any. | |
| 241 | pub(crate) fn prefetched_pull(&self, pull_id: &str) -> Option<Rc<Prefetched>> { | |
| 242 | self.prefetched | |
| 243 | .borrow() | |
| 244 | .as_ref() | |
| 245 | .filter(|found| found.pull_id == pull_id) | |
| 246 | .cloned() | |
| 247 | } | |
| 248 | ||
| 249 | /// The rows read in this request for a pull request of `repo_id`. | |
| 250 | pub(crate) fn prefetched_repo(&self, repo_id: &str) -> Option<Rc<Prefetched>> { | |
| 251 | self.prefetched | |
| 252 | .borrow() | |
| 253 | .as_ref() | |
| 254 | .filter(|found| found.repo_id == repo_id) | |
| 255 | .cloned() | |
| 256 | } | |
| 257 | ||
| 258 | /// Keeps `found` for the rest of this request. | |
| 259 | pub(crate) fn keep_prefetched(&self, found: Option<Prefetched>) { | |
| 260 | *self.prefetched.borrow_mut() = found.map(Rc::new); | |
| 261 | } | |
| 262 | } | |
| 263 | ||
| 264 | #[cfg(test)] | |
| 265 | mod tests { | |
| 266 | use super::*; | |
| 267 | ||
| 268 | fn at(namespace: &str, name: &str) -> RepoPath { | |
| 269 | RepoPath { namespace: namespace.to_owned(), name: name.to_owned() } | |
| 270 | } | |
| 271 | ||
| 272 | #[test] | |
| 273 | fn guesses_ignore_case_and_are_forgotten_on_refusal() { | |
| 274 | learn(&at("Flagon-IO", "G1T"), Some("repo_1")); | |
| 275 | assert_eq!(guess(&at("flagon-io", "g1t")).as_deref(), Some("repo_1")); | |
| 276 | learn(&at("flagon-io", "g1t"), None); | |
| 277 | assert_eq!(guess(&at("flagon-io", "g1t")), None); | |
| 278 | } | |
| 279 | } |
This file's history is long; its oldest lines are credited to the oldest commit read.