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
233 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.

API: notifications over REST and MCP, with notifications scopes1/**
2 * The pure parts of request timing and D1 read consistency for the site:
3 * the `g1t_d1` cookie, which session each service call asks for, which
4 * calls may write, and the `Server-Timing` header. perf.server.ts holds
The docs folder is gone, and what it held lives where people read it: how a self-hosted g1t runs and how to deploy g1t to Cloudflare are pages on docs.g1t.sh under Run g1t yourself, and speed, rate limits and operating g1t.sh are sections of CONTRIBUTING.md; code that cited a file in docs/ now points to the page or section that covers it, or says what it means itself, and applied migrations and the runner images are left as they were.5 * the per-request state; CONTRIBUTING.md, "Speed", says how to use it.
API: notifications over REST and MCP, with notifications scopes6 */
7
8/** The cookie that carries D1 bookmarks between a person's requests. */
9export const D1_COOKIE = "g1t_d1";
10
11/**
12 * How long after a write the services a request did not get a bookmark
13 * from read their primary. A write can reach a service the site did not
14 * call itself (work writing to repos, say), whose bookmark the site never
15 * sees; D1 replicas trail the primary by well under a second, so 30
16 * seconds covers that with room to spare.
17 */
18export const PRIMARY_WINDOW_SECONDS = 30;
19
20/** How long the bookmarks are kept: far longer than any replica trails. */
21export const D1_COOKIE_MAX_AGE = 300;
22
23/**
24 * The services whose RPCs open a D1 session (crates/kit/src/d1.rs and
25 * @g1t/contracts d1.ts), by the name the site gives their binding.
26 */
27export const SESSION_SERVICES = new Set(["identity", "repos", "work", "search", "billing", "projects", "deployments"]);
28
29/** What the site remembers from a person's last writes. */
30export type Bookmarks = {
31 /** Unix seconds of the last request that may have written, if recent. */
32 at: number | null;
33 /** The latest bookmark each service returned after it. */
34 services: Record<string, string>;
35};
36
37const BOOKMARK = /^[0-9A-Za-z-]{1,256}$/;
38const SERVICE = /^[a-z]{1,32}$/;
39
40/** Reads the `g1t_d1` cookie; anything malformed is dropped. */
41export function readBookmarks(cookieHeader: string | null): Bookmarks {
42 const empty: Bookmarks = { at: null, services: {} };
43 if (!cookieHeader) return empty;
44 const match = new RegExp(`(?:^|;\\s*)${D1_COOKIE}=([^;]*)`).exec(cookieHeader);
45 if (!match) return empty;
46 const found: Bookmarks = { at: null, services: {} };
47 for (const entry of match[1].split("~")) {
48 const colon = entry.indexOf(":");
49 if (colon < 1) continue;
50 const key = entry.slice(0, colon);
51 const value = entry.slice(colon + 1);
52 if (key === "at") {
53 const at = Number(value);
54 if (Number.isSafeInteger(at) && at > 0) found.at = at;
55 } else if (SERVICE.test(key) && SESSION_SERVICES.has(key) && BOOKMARK.test(value)) {
56 found.services[key] = value;
57 }
58 }
59 return found;
60}
61
62/** The `g1t_d1` cookie's value. */
63export function writeBookmarks(bookmarks: Bookmarks): string {
64 const entries = bookmarks.at ? [`at:${bookmarks.at}`] : [];
65 for (const [service, bookmark] of Object.entries(bookmarks.services).sort()) {
66 if (SESSION_SERVICES.has(service) && BOOKMARK.test(bookmark)) entries.push(`${service}:${bookmark}`);
67 }
68 return entries.join("~");
69}
70
71/** The `Set-Cookie` value for `bookmarks`. */
72export function bookmarkCookie(bookmarks: Bookmarks, secure: boolean): string {
73 return `${D1_COOKIE}=${writeBookmarks(bookmarks)}; Path=/; HttpOnly;${secure ? " Secure;" : ""} SameSite=Lax; Max-Age=${D1_COOKIE_MAX_AGE}`;
74}
75
76/**
77 * What a call to `service` sends as `x-d1-bookmark`, or null for none
78 * (that service reads its primary, as before sessions):
79 *
80 * - A request that writes (any method but GET and HEAD) starts every
81 * session on the primary, so what it checks before writing is current.
82 * - Within `PRIMARY_WINDOW_SECONDS` of the person's last write, the
83 * primary, for every service: that write may have reached a service
84 * through another one, whose bookmark the site never saw.
85 * - Otherwise the bookmark that service returned after that write: never
86 * older than what they did, however far a replica trails.
87 * - Otherwise the nearest copy.
88 */
89export function sessionFor(service: string, bookmarks: Bookmarks, writing: boolean, nowSeconds: number): string | null {
90 if (!SESSION_SERVICES.has(service)) return null;
91 if (writing) return "first-primary";
92 if (bookmarks.at && nowSeconds - bookmarks.at < PRIMARY_WINDOW_SECONDS) return "first-primary";
93 return bookmarks.services[service] ?? "first-unconstrained";
94}
95
96/**
97 * Methods that only read. Anything not listed is taken to write, so a new
98 * method errs towards a cookie and a primary read, never a stale page.
99 * `get`, `list`, `queue` and `pulls_for_repos` were missing: every project
100 * page called `get` (repos, projects), so every one set the cookie, was
101 * never kept in the public cache, and sent the next 30 s of the person's
Public pages cacheable again: a signed-out GET never sets g1t_d1; stars, about and public_links are reads102 * reads to the primary. `stars`, `about` and `public_links` (2026-10-08)
Merge project overview: one branch_drift call, spliced histories, cached tags, 6 repos calls instead of 25103 * did the same to every project page and Explore for 13 hours. `tags`,
104 * `commit_checks` and `shortcuts` (every overview), and the Files page's
105 * `last_commits`, `languages`, `contributors` and `license`, still made a
106 * signed-in view count as a write until 2026-10-08: the cookie, 30 s of
107 * primary reads, and no sidebar cache after every overview.
API: notifications over REST and MCP, with notifications scopes108 */
109const READS = new Set(
110 (
Merge branch 'worktree-agent-ac950b9c891220ad3'111 "account all_ids get list queue pulls_for_repos blame blob branches by_author by_repo contributions " +
112 "check_invite check_limit check_workspace_deletion check_workspace_rename collaborator_permission " +
113 "compare counts deleted deliveries domains entitlements explore features git_access has_feature " +
114 "invoices ledger limit limit_requests links log logs managed_pulls my_repo_invitations " +
115 "outside_collaborators overview path_by_id prices profile profile_workspaces public_namespaces " +
116 "read_session readable repo_access resolve resolve_branch resolve_path resolve_slug " +
117 "routes run runner_activity runner_groups runner_settings runners runs search usage_report settings " +
118 "statement statement_entries status status_by_id suggest tree usage usage_meters user_by_username " +
119 "user_for_session user_for_access_token usernames workflows workspace workspace_invites " +
120 "github_enabled stars about public_links branch_drift tags last_commits languages contributors " +
121 "license releases release stargazers starred commit_checks shortcuts spend usage_report templates " +
122 "people_directory team_members github_visible_installations last_visit " +
Home says what people did as well as what agents did. Since you were last here now has a People column and an Agents column: who pushed how many commits to which projects, pull requests opened, merged and reviewed, issues opened and closed, docs edited, messages sent and deploys that went out, each line a link to where those are listed, with the agents' acceptance (first time, after review, didn't finish) kept as a row of their column; the sentence under the heading sums it up honestly, and says when a part of g1t could not be read, or that the span was quiet. Landed counts merged pull requests, commits pushed straight to a default branch, production deploys that went live, releases and packages, newest first; Running now adds workflow runs. Behind it, the events service answers an activity digest over a span in one round trip from its existing indexes, every push now records how many commits it carried, and chat counts the messages sent in the conversations you can read. The Home guide defines every line, and says how this scales.123 "activity_digest activity"
API: notifications over REST and MCP, with notifications scopes124 ).split(" "),
125);
126
Public pages cacheable again: a signed-out GET never sets g1t_d1; stars, about and public_links are reads127/**
128 * Whether a response sets the `g1t_d1` cookie. A signed-out GET never
129 * does, whatever it called: nobody signed out can write anything their
130 * next page must read, and a cookie keeps the page out of the public
131 * cache. Signing in on a GET (GitHub) starts a session, so it does.
132 */
133export function setsBookmark(request: { writing: boolean; wrote: boolean; hasSession: boolean; signedIn: boolean }): boolean {
134 if (request.writing || request.signedIn) return true;
135 return request.wrote && request.hasSession;
136}
137
API: notifications over REST and MCP, with notifications scopes138/** Whether an RPC to `method` may write. */
139export function mayWrite(method: string): boolean {
140 if (READS.has(method)) return false;
141 return !(method.startsWith("get_") || method.startsWith("list_"));
142}
143
144/** The method name of an RPC URL (`https://service/rpc/<method>`). */
145export function rpcMethodOf(input: string): string {
146 const at = input.indexOf("/rpc/");
147 return at < 0 ? "" : input.slice(at + 5).split(/[?#]/)[0];
148}
149
150/** One service's calls during a request. */
151export type ServiceTiming = {
152 calls: number;
153 /** Summed wall time of its calls, from here. */
154 wallMs: number;
155 /** Summed time its own `server-timing: svc;dur` reported. */
156 serviceMs: number;
157 /**
158 * Of that, summed time it reported waiting on its database
159 * (`db;dur`, crates/kit/src/d1.rs `Timing`) and in how many round trips.
160 * Absent for services that do not time them.
161 */
162 dbMs?: number;
163 dbTrips?: number;
164};
165
166/** The `svc;dur=N` a service reports, or null. */
167export function serviceDuration(header: string | null): number | null {
168 if (!header) return null;
169 const match = /(?:^|,)\s*svc;dur=([0-9.]+)/.exec(header);
170 return match ? Number(match[1]) : null;
171}
172
173/**
174 * The `db;dur=N;desc="T round trips, …"` a service reports: its summed
175 * database time and round trips, or null.
176 */
177export function databaseTime(header: string | null): { ms: number; trips: number } | null {
178 if (!header) return null;
179 const match = /(?:^|,)\s*db;dur=([0-9.]+)(?:;desc="(\d+) round trips?)?/.exec(header);
180 return match ? { ms: Number(match[1]), trips: Number(match[2] ?? 0) } : null;
181}
182
183/** Total time covered by overlapping [start, end] intervals. */
184export function coveredMs(intervals: [number, number][]): number {
185 const sorted = [...intervals].sort((a, b) => a[0] - b[0]);
186 let total = 0;
187 let end = -Infinity;
188 let start = -Infinity;
189 for (const [from, to] of sorted) {
190 if (from > end) {
191 if (end > start) total += end - start;
192 start = from;
193 end = to;
194 } else if (to > end) {
195 end = to;
196 }
197 }
198 if (end > start) total += end - start;
199 return total;
200}
201
202/** A Server-Timing metric name: a token, so `/` and spaces become `.`. */
203export function metricName(name: string): string {
204 return name.replace(/^routes\//, "").replace(/[^A-Za-z0-9_.-]+/g, ".");
205}
206
207/**
208 * The `Server-Timing` header for a request: the whole, the loaders, the
209 * time spent waiting on services (overlap counted once), then each
210 * service. DevTools shows them in this order under Network → Timing.
211 */
212export function serverTiming(input: {
213 totalMs: number;
214 loaders: { id: string; ms: number; kind: "loader" | "action" }[];
215 rpcMs: number;
216 services: Record<string, ServiceTiming>;
217 sessions: string;
218}): string {
219 const parts = [`total;dur=${input.totalMs};desc="web to first byte"`];
220 for (const loader of input.loaders) {
221 parts.push(`${loader.kind}.${metricName(loader.id)};dur=${loader.ms}`);
222 }
223 const calls = Object.values(input.services).reduce((sum, timing) => sum + timing.calls, 0);
224 if (calls > 0) parts.push(`rpc;dur=${input.rpcMs};desc="${calls} service calls, overlap counted once"`);
225 const ranked = Object.entries(input.services).sort((a, b) => b[1].wallMs - a[1].wallMs);
226 for (const [name, timing] of ranked) {
227 const db = timing.dbTrips ? `, db ${timing.dbMs ?? 0}ms in ${timing.dbTrips} round trip${timing.dbTrips === 1 ? "" : "s"}` : "";
228 const inside = timing.serviceMs > 0 ? `, ${timing.serviceMs}ms inside${db}` : "";
229 parts.push(`${metricName(name)};dur=${timing.wallMs};desc="${timing.calls} call${timing.calls === 1 ? "" : "s"}${inside}"`);
230 }
231 if (input.sessions) parts.push(`d1;desc="${input.sessions}"`);
232 return parts.join(", ");
233}

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