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
300 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//! Repositories that are archived, deleted or purged, and branches
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.2//! renamed: what issues, pull requests and runs do about each.
Files pages say Files in the trail and keep the description in About only3//!
4//! An archived repository is read-only: its issues and pull requests are
Merge branch 'mirroring' into artifacts-mode5//! locked and nothing new starts on it. So is a mirror standing by (see
6//! `g1t_contracts::mirrors`): its work happens where it is mirrored from,
7//! until someone takes over on g1t. A deleted one looks missing (repos
Files pages say Files in the trail and keep the description in About only8//! hides it) and keeps its rows for a restore. A purged one is gone, and
9//! every row kept for it goes with it.
10
11use g1t_contracts::events::{BranchRenamed, Event, RepoArchived, RepoDeleted};
12use g1t_contracts::repos::{Repo, RepoStatus, StatusByIdArgs, archived_message};
13use g1t_contracts::{FailureCode, Outcome};
14use g1t_contracts::time::rfc3339;
15use g1t_kit::now_ms;
16use serde::{Deserialize, Serialize};
17use worker::Result;
18
19use crate::Work;
20
21/// `Ok` when `repo` may be changed; refused, saying why, when it is
Merge branch 'mirroring' into artifacts-mode22/// archived or a mirror that is not taken over.
Files pages say Files in the trail and keep the description in About only23pub(crate) fn writable(repo: &Repo) -> Outcome<()> {
Merge branch 'mirroring' into artifacts-mode24 match repo.read_only_reason() {
25 Some(reason) => Outcome::fail(FailureCode::Forbidden, reason),
26 None => Outcome::Ok(()),
27 }
28}
29
30/// `Ok` unless `repo` is archived. For what stays g1t's own on a mirror:
31/// its settings and rules, and the statuses and checks reported on its
32/// commits (CI failover reports them).
33pub(crate) fn not_archived(repo: &Repo) -> Outcome<()> {
Files pages say Files in the trail and keep the description in About only34 if repo.archived() {
35 Outcome::fail(FailureCode::Forbidden, archived_message(&repo.namespace, &repo.name))
36 } else {
37 Outcome::Ok(())
38 }
39}
40
41/// `repo` as found, unless it is archived: then refused, as [`writable`]
42/// says. For the steps g1t starts by itself, which stop on a refusal.
43pub(crate) fn unless_archived(repo: Outcome<Repo>) -> Outcome<Repo> {
44 match repo {
45 Outcome::Ok(repo) => match writable(&repo) {
46 Outcome::Ok(()) => Outcome::Ok(repo),
47 Outcome::Fail(failure) => Outcome::Fail(failure),
48 },
49 failed => failed,
50 }
51}
52
53/// `repo.purged`: every row kept for the repository, `?1` its id. Rows
54/// kept by pull request go first, while the pull requests still name them.
55pub(crate) const PURGED: &[&str] = &[
56 "DELETE FROM session_entries WHERE pull_id IN (SELECT id FROM pulls WHERE repo_id = ?1)",
57 "DELETE FROM check_runs WHERE pull_id IN (SELECT id FROM pulls WHERE repo_id = ?1)",
58 "DELETE FROM review_runs WHERE pull_id IN (SELECT id FROM pulls WHERE repo_id = ?1)",
59 "DELETE FROM agent_messages WHERE repo_id = ?1 OR pull_id IN (SELECT id FROM pulls WHERE repo_id = ?1)",
60 "DELETE FROM queue_entries WHERE repo_id = ?1",
61 "DELETE FROM agent_mentions WHERE repo_id = ?1",
62 "DELETE FROM agent_rules WHERE repo_id = ?1",
63 "DELETE FROM pull_confidence WHERE repo_id = ?1",
64 "DELETE FROM run_confidence WHERE repo_id = ?1",
65 "DELETE FROM confidence_rules WHERE repo_id = ?1",
66 "DELETE FROM agent_runs WHERE repo_id = ?1",
67 "DELETE FROM commit_statuses WHERE repo_id = ?1",
Merge checks: statuses and check runs on every commit68 "DELETE FROM commit_check_annotations WHERE run_id IN (SELECT id FROM commit_check_runs WHERE repo_id = ?1)",
69 "DELETE FROM commit_check_runs WHERE repo_id = ?1",
70 "DELETE FROM commit_check_suites WHERE repo_id = ?1",
Files pages say Files in the trail and keep the description in About only71 "DELETE FROM plans WHERE repo_id = ?1",
72 "DELETE FROM comments WHERE repo_id = ?1",
73 "DELETE FROM pulls WHERE repo_id = ?1",
74 "DELETE FROM issues WHERE repo_id = ?1",
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar75 "DELETE FROM labels WHERE repo_id = ?1",
76 "DELETE FROM milestones WHERE repo_id = ?1",
Files pages say Files in the trail and keep the description in About only77 "DELETE FROM counters WHERE repo_id = ?1",
78 "DELETE FROM repo_settings WHERE repo_id = ?1",
79 "DELETE FROM memories WHERE scope = 'project' AND scope_key = ?1",
80 "DELETE FROM guardrails WHERE scope <> 'workspace' AND scope_key = ?1",
81];
82
83/// What a run stopped because its repository was archived or deleted says
84/// as its last step; [`Work::runs_in_repo`] finds such runs by it.
85const STOPPED_STEP: &str = "Stopped: the repository was archived or deleted.";
86
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.87/// `repo.deleted` and `repo.archived`: the repository's runs that
Files pages say Files in the trail and keep the description in About only88/// are queued or running stop, `?1` the step they end on, `?2` the time,
89/// `?3` its id.
90const STOP_RUNS: &str = "UPDATE agent_runs SET status = 'stopped', step = ?1, finished_at = ?2, updated_at = ?2
91 WHERE repo_id = ?3 AND status IN ('queued', 'running')";
92
93/// `runs_in_repo` (internal, for the runner): the agent runs of a
94/// repository whose sandboxes should be stopped: those queued or running,
95/// and those work stopped in the last hour because the repository was
96/// archived or deleted. Takes `{ "repoId" }`; returns `[RepoRun]`.
97#[derive(Deserialize)]
98#[serde(rename_all = "camelCase")]
99pub(crate) struct RunsInRepoArgs {
100 pub repo_id: String,
101}
102
103#[derive(Debug, Deserialize, Serialize)]
104#[serde(rename_all = "camelCase")]
105pub(crate) struct RepoRun {
106 pub run_id: String,
107 pub sandbox: String,
108 pub kind: String,
109 pub status: String,
110 pub pull_id: Option<String>,
111}
112
113/// The repository, by id and path, that `event` says was deleted or
114/// archived (not unarchived): its runs stop.
115fn stopping(event: &Event) -> Option<(String, String, String)> {
116 let data = event.data.clone();
117 match event.kind.as_str() {
118 "repo.deleted" => serde_json::from_value::<RepoDeleted>(data).ok().map(|e| (e.repo_id, e.namespace, e.name)),
119 "repo.archived" => serde_json::from_value::<RepoArchived>(data)
120 .ok()
121 .filter(|e| e.archived)
122 .map(|e| (e.repo_id, e.namespace, e.name)),
123 _ => None,
124 }
125}
126
127/// `branch.renamed`: open pull requests from `?2` in repository `?1`
128/// come from `?3` now. A fork carries its change on its own default
129/// branch, so only branches of the repository itself are named.
130const BRANCH_RENAMED: &str = "UPDATE pulls SET source_branch = ?3
131 WHERE repo_id = ?1 AND source_branch = ?2 AND fork_repo_id IS NULL
132 AND status IN ('draft', 'open')";
133
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar134/// `branch.renamed`: open pull requests into `?2` merge into `?3` now.
135/// Those into the default branch name none, and follow it as they are.
136const BASE_RENAMED: &str = "UPDATE pulls SET base_branch = ?3
137 WHERE repo_id = ?1 AND base_branch = ?2 AND status IN ('draft', 'open')";
138
Files pages say Files in the trail and keep the description in About only139impl Work {
140 /// Whether work may start on the repository: neither archived nor
141 /// deleted. When repos cannot say, it is taken as active, and the
142 /// repos calls that follow (which hide deleted repositories) decide.
143 pub(crate) async fn repo_active(&self, repo_id: &str) -> Result<bool> {
144 let status: Result<RepoStatus> = g1t_kit::call(
145 &self.repos,
146 "status_by_id",
147 &StatusByIdArgs {
148 id: repo_id.to_owned(),
149 },
150 )
151 .await;
152 Ok(match status {
153 Ok(status) => status.active(),
154 Err(error) => {
155 worker::console_error!("status_by_id {repo_id}: {error}");
156 true
157 }
158 })
159 }
160
161 /// See [`RunsInRepoArgs`].
162 pub(crate) async fn runs_in_repo(&self, a: RunsInRepoArgs) -> Result<Vec<RepoRun>> {
163 let hour_ago = rfc3339(now_ms().saturating_sub(60 * 60 * 1000));
164 self.db
165 .prepare(
166 "SELECT id AS run_id, sandbox, kind, status, pull_id FROM agent_runs
167 WHERE repo_id = ?1 AND (status IN ('queued', 'running')
168 OR (status = 'stopped' AND step = ?2 AND finished_at >= ?3))
169 ORDER BY created_at LIMIT 200",
170 )
171 .bind(&[a.repo_id.as_str().into(), STOPPED_STEP.into(), hour_ago.as_str().into()])?
172 .all()
173 .await?
174 .results::<RepoRun>()
175 }
176
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.177 /// `repo.deleted` and `repo.archived`: its runs stop (the runner stops
178 /// their sandboxes, see [`RunsInRepoArgs`]). Rows are kept, for a restore.
179 async fn stop_runs_in(&self, repo_id: &str) -> Result<()> {
Files pages say Files in the trail and keep the description in About only180 let now = rfc3339(now_ms());
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.181 self.db
Files pages say Files in the trail and keep the description in About only182 .prepare(STOP_RUNS)
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.183 .bind(&[STOPPED_STEP.into(), now.as_str().into(), repo_id.into()])?
184 .run()
185 .await?;
Files pages say Files in the trail and keep the description in About only186 Ok(())
187 }
188
189 /// `repo.purged`, `repo.deleted`, `repo.archived` and `branch.renamed`.
190 /// Says whether `event` was one this handles completely.
191 pub(crate) async fn on_retired(&self, event: &Event) -> Result<bool> {
192 if g1t_kit::lifecycle::on_purged(&self.db, event, PURGED).await? {
193 return Ok(true);
194 }
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.195 if let Some((repo_id, _, _)) = stopping(event) {
196 self.stop_runs_in(&repo_id).await?;
Files pages say Files in the trail and keep the description in About only197 return Ok(true);
198 }
199 if event.kind != "branch.renamed" {
200 return Ok(false);
201 }
202 let Ok(renamed) = serde_json::from_value::<BranchRenamed>(event.data.clone()) else {
203 worker::console_error!("branch.renamed {} could not be read", event.id);
204 return Ok(true);
205 };
206 if renamed.from == renamed.to {
207 return Ok(true);
208 }
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar209 let names = [
210 renamed.repo_id.as_str().into(),
211 renamed.from.as_str().into(),
212 renamed.to.as_str().into(),
213 ];
Files pages say Files in the trail and keep the description in About only214 self.db
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar215 .batch(vec![
216 self.db.prepare(BRANCH_RENAMED).bind(&names)?,
217 self.db.prepare(BASE_RENAMED).bind(&names)?,
218 ])
Files pages say Files in the trail and keep the description in About only219 .await?;
220 Ok(true)
221 }
222}
223
224#[cfg(test)]
225mod tests {
226 use super::*;
227
228 fn repo(archived_at: Option<&str>) -> Repo {
229 Repo {
230 id: "rep_1".into(),
231 namespace: "acme".into(),
232 name: "web".into(),
233 description: None,
234 is_private: false,
235 owner_id: "usr_1".into(),
236 default_branch: "main".into(),
237 fork_of: None,
238 protected: false,
239 created_at: "2026-10-05T00:00:00Z".into(),
240 topics: Vec::new(),
241 website: None,
242 archived_at: archived_at.map(str::to_owned),
Merge branch 'mirroring' into artifacts-mode243 mirror: None,
Files pages say Files in the trail and keep the description in About only244 }
245 }
246
247 #[test]
248 fn an_archived_repository_refuses_changes() {
249 assert!(matches!(writable(&repo(None)), Outcome::Ok(())));
250 match writable(&repo(Some("2026-10-05T00:00:00Z"))) {
251 Outcome::Fail(failure) => {
252 assert_eq!(failure.code, FailureCode::Forbidden);
253 assert!(failure.message.contains("acme/web is archived"));
254 }
255 Outcome::Ok(()) => panic!("an archived repository was writable"),
256 }
257 }
258
259 #[test]
260 fn purging_takes_only_the_repository_id() {
261 for sql in PURGED {
262 assert_eq!(g1t_kit::transfer::parameters(sql), 1, "{sql}");
263 }
264 // Rows kept by pull request go before the pull requests do.
265 let pulls = PURGED.iter().position(|sql| sql.starts_with("DELETE FROM pulls")).unwrap();
266 for table in ["session_entries", "check_runs", "review_runs", "agent_messages"] {
267 let at = PURGED.iter().position(|sql| sql.contains(&format!("FROM {table} "))).unwrap();
268 assert!(at < pulls, "{table}");
269 }
270 }
271
272 fn event(kind: &str, data: serde_json::Value) -> Event {
273 Event {
274 id: "evt_1".into(),
275 kind: kind.into(),
276 source: "repos".into(),
277 time: "2026-10-05T00:00:00Z".into(),
278 repo_id: Some("rep_1".into()),
279 actor: None,
280 data,
281 }
282 }
283
284 #[test]
285 fn deleting_or_archiving_stops_runs_and_unarchiving_does_not() {
286 let named = Some(("rep_1".to_owned(), "acme".to_owned(), "web".to_owned()));
287 let data = serde_json::json!({ "repoId": "rep_1", "namespace": "acme", "name": "web", "archived": true });
288 assert_eq!(stopping(&event("repo.deleted", data.clone())), named);
289 assert_eq!(stopping(&event("repo.archived", data)), named);
290 let unarchived = serde_json::json!({ "repoId": "rep_1", "namespace": "acme", "name": "web", "archived": false });
291 assert_eq!(stopping(&event("repo.unarchived", unarchived.clone())), None);
292 assert_eq!(stopping(&event("repo.archived", unarchived)), None);
293 assert_eq!(STOP_RUNS.matches('?').count(), 4);
294 }
295
296 #[test]
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.297 fn renamed_branches_bind_three_names() {
Files pages say Files in the trail and keep the description in About only298 assert_eq!(g1t_kit::transfer::parameters(BRANCH_RENAMED), 3);
299 }
300}

This file's history is long; its oldest lines are credited to the oldest commit read.