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.

g1t.sh
279 linesCodeBlameRaw

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.

Files pages say Files in the trail and keep the description in About only1//! 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
Files pages say Files in the trail and keep the description in About only7//! 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
Files pages say Files in the trail and keep the description in About only14//! 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
18use std::cell::RefCell;
19use std::collections::HashMap;
20use std::future::Future;
21use std::rc::Rc;
22
23use g1t_contracts::repos::{Repo, RepoPath};
24use g1t_contracts::{Outcome, Viewer};
25use serde::de::DeserializeOwned;
26use worker::wasm_bindgen::JsValue;
27use worker::{D1PreparedStatement, D1Result, Result};
28
29use crate::rows::PULL_COLUMNS;
30use crate::{ISSUE_COLUMNS, Work};
31
32/// The pull request's id, from the batch's `?1` (repository) and `?2` (number).
33const 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)]
37pub(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 merge47 Rulesets,
48 Adopted,
Files pages say Files in the trail and keep the description in About only49}
50
51/// How many runs `latest_checks` and `earlier_checks` show together.
52pub(crate) const RECENT_RUNS: u32 = 11;
53
54/// One pull request's rows, as read for this request.
55pub(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 merge58 /// The workspace its rulesets were read for, lowercase.
59 pub(crate) namespace: String,
Files pages say Files in the trail and keep the description in About only60 /// Its head commit when read, which its statuses are for.
61 pub(crate) head: Option<String>,
62 results: Vec<D1Result>,
63}
64
65impl 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)]
81struct IdRow {
82 id: String,
83 head_commit: Option<String>,
84}
85
86thread_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.
95const MAX_GUESSES: usize = 1024;
96
97fn path_key(path: &RepoPath) -> String {
98 format!("{}/{}", path.namespace.to_lowercase(), path.name.to_lowercase())
99}
100
101fn guess(path: &RepoPath) -> Option<String> {
102 REPO_IDS.with(|ids| ids.borrow().get(&path_key(path)).cloned())
103}
104
105fn 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
122impl 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 merge164 pub(crate) async fn prefetch_pull(&self, repo_id: String, namespace: String, number: u32) -> Result<Option<Prefetched>> {
Files pages say Files in the trail and keep the description in About only165 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 commit195 "SELECT context, state, description, target_url, updated_at, source, check_run_id FROM commit_statuses
Files pages say Files in the trail and keep the description in About only196 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`)
Files pages say Files in the trail and keep the description in About only201 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 merge217 // 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())?,
Files pages say Files in the trail and keep the description in About only220 ];
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 merge234 namespace: namespace.to_lowercase(),
Files pages say Files in the trail and keep the description in About only235 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)]
265mod 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.