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

Download ZIP deflates sixteen files at a time, within a Worker's memory1//! Every response the REST routes give, run through the converter the API
2//! sends them with, checked for `camelCase` that would leak out.
3//!
4//! The samples are the reference's example responses, put back into the
5//! `camelCase` the services send (as serde's `rename_all` writes it) and,
6//! where an operation returns a contract type, decoded into that type and
7//! encoded again, so that every field the type has is sent, not only the
8//! ones an example shows.
9
Merge checks: statuses and check runs on every commit10use g1t_contracts::{access, actions, checks, codeowners, integrations, repos, rules, search, teams, webhooks, work};
Download ZIP deflates sixteen files at a time, within a Worker's memory11use g1t_kit::wire::{self, USER_KEYED};
12use serde::Serialize;
13use serde::de::DeserializeOwned;
14use serde_json::{Map, Value, json};
15
16use crate::openapi::document;
17use crate::operations::Op;
Merge checks: statuses and check runs on every commit18use crate::checks::ChecksOp;
Merge rulesets: branch and tag rules, agent-first, enforced on push and merge19use crate::rules::RulesOp;
Merge branch 'worktree-agent-a5a5de74d8863d554' into worktree-agent-a16631325aecf58ca20use crate::deploy_keys::DeployKeysOp;
Download ZIP deflates sixteen files at a time, within a Worker's memory21
22/// A key as `#[serde(rename_all = "camelCase")]` writes it.
23fn camel_key(key: &str) -> String {
24 let mut out = String::with_capacity(key.len());
25 let mut upper = false;
26 for c in key.chars() {
27 if c == '_' {
28 upper = true;
29 } else if upper {
30 out.extend(c.to_uppercase());
31 upper = false;
32 } else {
33 out.push(c);
34 }
35 }
36 out
37}
38
39/// A response as the services send it: `camelCase`, but for the maps the
40/// converter passes through, which are data.
41fn as_services_send(value: &Value) -> Value {
42 match value {
43 Value::Object(fields) => {
44 let mut out = Map::new();
45 for (key, value) in fields {
46 let user_keyed = value.is_object()
47 && (USER_KEYED.contains(&key.as_str()) || key.starts_with("by_"));
48 let value = if user_keyed { value.clone() } else { as_services_send(value) };
49 // A `by_…` map keeps its name in the converter's spelling.
50 let key = if key.starts_with("by_") { key.clone() } else { camel_key(key) };
51 out.insert(key, value);
52 }
53 Value::Object(out)
54 }
55 Value::Array(items) => Value::Array(items.iter().map(as_services_send).collect()),
56 other => other.clone(),
57 }
58}
59
60/// `value` decoded as `T` and encoded again, as the service would send it.
61fn through<T: DeserializeOwned + Serialize>(op: Op, value: Value) -> Value {
62 let decoded: T = serde_json::from_value(value)
63 .unwrap_or_else(|error| panic!("{}: the example is not a {}: {error}", op.name(), std::any::type_name::<T>()));
64 serde_json::to_value(decoded).unwrap()
65}
66
67/// What the service behind an operation sends, from its example.
68fn sample(op: Op, example: &Value) -> Value {
69 // Types serde already writes in `snake_case`: a person, and who has
70 // access. Sent as they are.
71 let as_is = example.clone();
72 match op {
73 Op::Whoami => return through::<g1t_contracts::User>(op, as_is),
74 Op::ListCollaborators => return through::<access::RepoAccess>(op, as_is),
75 Op::AddCollaborator => return through::<access::Added>(op, as_is),
76 Op::UpdateCollaborator => return through::<access::Collaborator>(op, as_is),
77 Op::GetCollaboratorPermission => return through::<access::PermissionInfo>(op, as_is),
78 Op::ListRepoInvitations | Op::ListMyRepoInvitations => {
79 return through::<Vec<access::RepoInvitation>>(op, as_is);
80 }
81 Op::RevokeRepoInvitation | Op::AcceptRepoInvitation | Op::DeclineRepoInvitation => {
82 return through::<access::RepoInvitation>(op, as_is);
83 }
84 Op::ListOutsideCollaborators => return through::<Vec<access::OutsideCollaborator>>(op, as_is),
Merge main (membership, two-factor, GitHub repo roles) into tokens85 // Members, also `snake_case`.
86 Op::ListMembers => return through::<Vec<g1t_contracts::identity::Member>>(op, as_is),
87 Op::UpdateMember => return through::<g1t_contracts::identity::Member>(op, as_is),
88 Op::RemoveMember | Op::TransferOwnership | Op::LeaveWorkspace => return through::<bool>(op, as_is),
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar89 // Teams and code owners, also `snake_case`.
90 Op::ListTeams | Op::ListChildTeams | Op::ListUserTeams => return through::<Vec<teams::Team>>(op, as_is),
91 Op::GetTeam | Op::CreateTeam | Op::UpdateTeam | Op::SetTeamReviewAssignment => {
92 return through::<teams::Team>(op, as_is);
93 }
94 Op::ListTeamMembers => return through::<Vec<teams::TeamMember>>(op, as_is),
95 Op::SetTeamMember => return through::<teams::TeamMember>(op, as_is),
96 Op::ListTeamRepos => return through::<Vec<teams::TeamRepo>>(op, as_is),
97 Op::SetTeamRepo => return through::<teams::TeamRepo>(op, as_is),
98 Op::DeleteTeam | Op::RemoveTeamMember | Op::RemoveTeamRepo => return through::<bool>(op, as_is),
99 Op::GetCodeownersErrors => return through::<codeowners::CodeOwnersReport>(op, as_is),
Merge rulesets: branch and tag rules, agent-first, enforced on push and merge100 // Rulesets travel in `snake_case` between services too.
101 Op::Rules(RulesOp::ListRepoRulesets | RulesOp::ListWorkspaceRulesets) => {
102 return through::<Vec<rules::Ruleset>>(op, as_is);
103 }
104 Op::Rules(
105 RulesOp::GetRepoRuleset
106 | RulesOp::CreateRepoRuleset
107 | RulesOp::UpdateRepoRuleset
108 | RulesOp::GetWorkspaceRuleset
109 | RulesOp::CreateWorkspaceRuleset
110 | RulesOp::UpdateWorkspaceRuleset,
111 ) => return through::<rules::Ruleset>(op, as_is),
112 Op::Rules(RulesOp::GetBranchRules) => return through::<rules::EffectiveRules>(op, as_is),
113 Op::Rules(RulesOp::ListRuleEvaluations | RulesOp::ListWorkspaceRuleEvaluations) => {
114 return through::<rules::EvaluationPage>(op, as_is);
115 }
116 // Built by the API itself.
117 Op::Rules(RulesOp::DeleteRepoRuleset | RulesOp::DeleteWorkspaceRuleset) => return as_is,
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb97118 // Deployments travel in `snake_case` between services too.
119 Op::Deployments(_) => return as_is,
Projects have Observability: their requests, logs, traces, metrics and errors, from apps on g1t.page with nothing to set up, and from software anywhere else over OpenTelemetry.120 // So does telemetry.
121 Op::Telemetry(_) => return as_is,
Actions: OIDC tokens, the toolkit's cache and artifact services, and artifacts in R2122 // Artifacts are shaped by the API itself, in `snake_case`.
123 Op::Artifacts(_) => return as_is,
Merge GitHub pull request #8 (claude/cool-newton-7nm8r9): Docker deployments on g1t.page behind a switch that stays off until Cloudflare's container APIs are checked live, workflows a workspace manages in its repositories, tarball and zipball downloads, refs/pull/* kept to g1t alone, framework detection for g1t.page builds, and a workflow's event payload matching GitHub's.124 // So are archive links.
125 Op::Archives(_) => return as_is,
Merge branch 'worktree-agent-a5a5de74d8863d554' into worktree-agent-a16631325aecf58ca126 // Deploy keys travel in `snake_case` from identity.
127 Op::DeployKeys(DeployKeysOp::ListDeployKeys) => return through::<Vec<g1t_contracts::deploy_keys::DeployKey>>(op, as_is),
128 Op::DeployKeys(DeployKeysOp::GetDeployKey | DeployKeysOp::CreateDeployKey) => {
129 return through::<g1t_contracts::deploy_keys::DeployKey>(op, as_is);
130 }
131 Op::DeployKeys(DeployKeysOp::DeleteDeployKey) => return through::<bool>(op, as_is),
Merge packages: roles, Actions access, source label, soft delete, API132 // Packages are shaped by the API itself, in `snake_case`.
133 Op::Packages(_) => return as_is,
Merge main into Artifacts Phase 2134 // Artifacts are shaped by the API itself, in `snake_case`.
135 Op::Folios(_) => return as_is,
Download ZIP deflates sixteen files at a time, within a Worker's memory136 // Built by the API itself, in `snake_case`.
137 Op::ListSecurityAlerts => return through::<Vec<crate::alerts::SecurityAlert>>(op, as_is),
138 Op::DismissSecurityAlert | Op::ReopenSecurityAlert => {
139 return through::<crate::alerts::SecurityAlert>(op, as_is);
140 }
141 _ => {}
142 }
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar143 let mut sent = as_services_send(example);
144 // A pull request's code owners are `snake_case` inside it.
145 if op == Op::GetPullRequest
146 && let Some(code_owners) = example.get("code_owners")
147 {
148 sent["codeOwners"] = code_owners.clone();
149 }
Download ZIP deflates sixteen files at a time, within a Worker's memory150 match op {
Merge branch 'worktree-agent-ad7c6d88d93adc817'151 Op::GetWorkspace | Op::CreateWorkspace | Op::UpdateWorkspace => through::<g1t_contracts::identity::Workspace>(op, sent),
Download ZIP deflates sixteen files at a time, within a Worker's memory152 Op::ListRepos => through::<Vec<repos::Repo>>(op, sent),
153 Op::Search => through::<search::SearchResults>(op, sent),
154 Op::GetRepo
155 | Op::CreateRepo
156 | Op::UpdateRepo
157 | Op::TransferRepo
158 | Op::RenameRepo
159 | Op::RenameBranch
160 | Op::ArchiveRepo
161 | Op::UnarchiveRepo
162 | Op::SetRepoVisibility
163 | Op::RestoreRepo => through::<repos::Repo>(op, sent),
The Branches page does things to branches now. It has tabs (Overview, Yours, Active, Stale, All, kept in the address as ?tab=) and search; Overview shows the default branch, then the first five of your branches, the active ones and the stale ones, each linking to the rest. Each branch is a row with its name in a code chip with a copy button, who made its latest commit and when, that commit's checks, behind and ahead counts with bars drawn against the largest shown, and its pull request (the open one, else the newest merged or closed) with its state; on a phone the columns fold under the name. Yours is branches whose latest commit is the viewer's; stale is no commit in 90 days.164 Op::CreateBranch | Op::DeleteBranch => through::<repos::Branch>(op, sent),
Download ZIP deflates sixteen files at a time, within a Worker's memory165 Op::DeleteRepo => through::<repos::DeletedRepo>(op, sent),
166 Op::ListDeletedRepos => through::<Vec<repos::DeletedRepo>>(op, sent),
167 Op::GetRepoSettings | Op::UpdateRepoSettings => through::<work::RepoSettings>(op, sent),
168 Op::ListCheckNames => through::<Vec<work::SeenCheck>>(op, sent),
169 Op::GetMergeQueue => through::<work::QueueView>(op, sent),
170 Op::ListIssues => through::<Vec<work::Issue>>(op, sent),
171 Op::CreateIssue | Op::UpdateIssue | Op::CloseIssue | Op::ReopenIssue => {
172 through::<work::Issue>(op, sent)
173 }
174 Op::GetIssue => through::<work::IssueDetail>(op, sent),
175 Op::ListPullRequests => through::<Vec<work::Pull>>(op, sent),
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar176 Op::UpdatePullRequest => through::<work::Pull>(op, sent),
177 Op::ListLabels | Op::AddDefaultLabels | Op::ListIssueLabels => through::<Vec<work::Label>>(op, sent),
178 Op::CreateLabel | Op::UpdateLabel => through::<work::Label>(op, sent),
179 Op::ListMilestones => through::<Vec<work::Milestone>>(op, sent),
180 Op::CreateMilestone | Op::UpdateMilestone => through::<work::Milestone>(op, sent),
181 Op::GetMilestone => through::<work::MilestoneDetail>(op, sent),
Download ZIP deflates sixteen files at a time, within a Worker's memory182 Op::GetPullRequest => through::<work::PullDetail>(op, sent),
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar183 Op::MarkPullRequestReady
184 | Op::ClosePullRequest
Merge Actions: cross-repo workflows and actions, release and deployment triggers, step timeouts185 | Op::ReopenPullRequest
186 | Op::ConvertPullRequestToDraft
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar187 | Op::MergePullRequest
188 | Op::RequestReviewers
189 | Op::RemoveRequestedReviewers => {
Download ZIP deflates sixteen files at a time, within a Worker's memory190 through::<work::Pull>(op, sent)
191 }
192 Op::ListWorkflows => through::<Vec<actions::Workflow>>(op, sent),
193 Op::ListWorkflowRuns => through::<Vec<actions::WorkflowRun>>(op, sent),
194 Op::GetWorkflowRun => through::<actions::RunDetail>(op, sent),
195 Op::GetJobLogs => through::<actions::JobLog>(op, sent),
196 Op::DispatchWorkflow | Op::CancelWorkflowRun | Op::RerunWorkflowRun => {
197 through::<actions::WorkflowRun>(op, sent)
198 }
199 Op::ListActionsSecrets | Op::ListActionsVariables => through::<Vec<actions::Setting>>(op, sent),
200 Op::ListRunners => through::<Vec<g1t_contracts::runners::Runner>>(op, sent),
201 Op::ListRunnerGroups => through::<Vec<g1t_contracts::runners::RunnerGroup>>(op, sent),
202 Op::CreateRunnerGroup | Op::UpdateRunnerGroup => through::<g1t_contracts::runners::RunnerGroup>(op, sent),
203 Op::GetRunnerSettings | Op::UpdateRunnerSettings => through::<g1t_contracts::runners::RunnerSettings>(op, sent),
204 Op::CreateRunnerRegistrationToken => through::<g1t_contracts::runners::RegistrationToken>(op, sent),
205 Op::ListWebhooks => through::<Vec<webhooks::Hook>>(op, sent),
206 Op::ListIntegrations => through::<Vec<integrations::Connection>>(op, sent),
207 Op::ListEvents => through::<Vec<g1t_contracts::events::Event>>(op, sent),
208 Op::ListEmails | Op::AddEmail | Op::RemoveEmail | Op::UpdateEmailSettings => {
209 through::<g1t_contracts::accounts::AccountEmails>(op, sent)
210 }
Merge email confirmation gate: a code and a link, nothing until confirmed (identity 0036)211 Op::ConfirmEmail => through::<g1t_contracts::accounts::EmailConfirmed>(op, sent),
Merge main into rm-agents: mirroring removal, open signups, free-tier limits, the g1t CLI and runner fixes, with the agent removal kept. The CLI's table of operations is regenerated from the API without the agent, customize and AI credit operations; no CLI command used them.212 Op::InviteMember | Op::RevokeWorkspaceInvite => {
Download ZIP deflates sixteen files at a time, within a Worker's memory213 through::<g1t_contracts::identity::Invite>(op, sent)
214 }
215 Op::ListWorkspaceInvites => through::<Vec<g1t_contracts::identity::Invite>>(op, sent),
Merge workspace invitations: nobody joins a workspace without saying yes, people are found by username, your own invites can bring someone in, and nobody is left without a workspace (identity 0040)216 Op::ListInvitations => through::<Vec<g1t_contracts::identity::WorkspaceInvitation>>(op, sent),
API: notifications over REST and MCP, with notifications scopes217 Op::ListNotifications => through::<g1t_contracts::inbox::InboxPage>(op, sent),
218 Op::GetNotificationThread | Op::MarkThreadRead | Op::MarkThreadDone | Op::SaveThread | Op::SnoozeThread => {
219 through::<g1t_contracts::inbox::InboxThread>(op, sent)
220 }
221 Op::GetThreadSubscription | Op::SetThreadSubscription | Op::DeleteThreadSubscription => {
222 through::<g1t_contracts::inbox::ThreadSubscription>(op, sent)
223 }
Merge checks: statuses and check runs on every commit224 Op::Checks(ChecksOp::CreateCommitStatus) => through::<work::CommitStatus>(op, sent),
225 Op::Checks(ChecksOp::ListCommitStatuses) => through::<Vec<work::CommitStatus>>(op, sent),
226 Op::Checks(ChecksOp::GetCombinedStatus) => through::<checks::CombinedStatus>(op, sent),
227 Op::Checks(ChecksOp::CreateCheckRun | ChecksOp::UpdateCheckRun | ChecksOp::GetCheckRun) => {
228 through::<checks::CommitCheckRun>(op, sent)
229 }
230 Op::Checks(ChecksOp::ListCheckRunAnnotations) => through::<Vec<checks::CheckAnnotation>>(op, sent),
231 Op::Checks(ChecksOp::ListCheckRunsForRef) => through::<checks::CheckRunList>(op, sent),
232 Op::Checks(ChecksOp::ListCheckSuitesForRef) => through::<checks::CheckSuiteList>(op, sent),
233 Op::Checks(ChecksOp::GetCheckSuite) => through::<checks::CommitCheckSuite>(op, sent),
Download ZIP deflates sixteen files at a time, within a Worker's memory234 _ => sent,
235 }
236}
237
238/// Every key of `example`, as paths, outside the maps passed through.
239fn paths(value: &Value, path: &str, out: &mut Vec<String>) {
240 match value {
241 Value::Object(fields) => {
242 for (key, value) in fields {
243 let here = format!("{path}.{key}");
244 out.push(here.clone());
245 let user_keyed = value.is_object()
246 && (USER_KEYED.contains(&key.as_str()) || key.starts_with("by_"));
247 if !user_keyed {
248 paths(value, &here, out);
249 }
250 }
251 }
252 Value::Array(items) => {
253 for item in items {
254 paths(item, &format!("{path}[]"), out);
255 }
256 }
257 _ => {}
258 }
259}
260
261#[test]
262fn no_route_answers_with_camel_case() {
263 let document = document();
264 let (mut checked, mut converted) = (0, 0);
265 for (path, methods) in document["paths"].as_object().unwrap() {
266 for (method, operation) in methods.as_object().unwrap() {
267 let example = &operation["responses"]["200"]["content"]["application/json"]["example"];
268 let tool = operation["x-operation"].as_str().unwrap_or_default();
269 let Some(op) = Op::by_name(tool) else {
270 // Device sign-in, which is written in `snake_case` by hand.
271 assert!(wire::camel_case_keys(example).is_empty(), "{method} {path}");
272 continue;
273 };
Cards you act on in chat; agents comment and review as themselves; names shown cleanly; commits on the calendar274 let mut sample = sample(op, example);
275 // Each person gets their chosen case on the way out (people.rs).
276 crate::people::fill(&mut sample, &std::collections::HashMap::new());
Download ZIP deflates sixteen files at a time, within a Worker's memory277 converted += wire::camel_case_keys(&sample).len();
278 let sent = wire::snake_case(sample);
279 let leaked = wire::camel_case_keys(&sent);
280 assert!(leaked.is_empty(), "{method} {path} sends {leaked:?}");
281 // The reference shows what is sent: each of its names is one.
282 let (mut shown, mut real) = (Vec::new(), Vec::new());
283 paths(example, "", &mut shown);
284 paths(&sent, "", &mut real);
285 for name in shown {
286 assert!(real.contains(&name), "{method} {path}: the reference shows {name}, which is not sent");
287 }
288 checked += 1;
289 }
290 }
291 assert!(checked >= Op::ALL.len());
292 // The samples are in the services' spelling, so there was something to
293 // convert.
294 assert!(converted > 100, "{converted}");
295}
296
297#[test]
298fn every_route_has_a_sample() {
299 let document = document();
300 for route in crate::rest::ROUTES {
Merge Actions: cross-repo workflows and actions, release and deployment triggers, step timeouts301 if route.no_content() {
302 continue;
303 }
Download ZIP deflates sixteen files at a time, within a Worker's memory304 let path = route
305 .path
306 .split('/')
307 .map(|segment| match segment.strip_prefix(':') {
308 Some(name) => format!("{{{name}}}"),
309 None => segment.to_owned(),
310 })
311 .collect::<Vec<_>>()
312 .join("/");
313 let example = &document["paths"][&path][route.method.to_lowercase()]["responses"]["200"]
314 ["content"]["application/json"]["example"];
315 assert!(!example.is_null(), "{} {path}", route.method);
316 }
317}
318
319#[test]
320fn errors_and_reports_are_snake_case() {
321 let failure = g1t_contracts::Failure {
322 code: g1t_contracts::FailureCode::NotFound,
323 message: "No such endpoint.".to_owned(),
324 };
325 assert!(wire::camel_case_keys(&wire::snake_case(json!({ "error": failure }))).is_empty());
326}
327
328#[test]
329fn a_job_spec_keeps_github_s_spelling() {
330 let spec = json!({
331 "job": "job_1",
332 "spec": { "runs-on": "ubuntu-latest", "timeoutMinutes": 5 },
333 "workflow": { "env": { "nodeEnv": "x" } },
334 "github": { "eventName": "push", "headRef": "" },
335 "event": { "pull_request": { "headSha": "x" } },
336 "contexts": { "inputs": { "dryRun": true }, "matrix": { "nodeVersion": 20 } },
337 "checkout": { "ref": "main" },
338 "timeoutMinutes": 30,
339 "masks": [],
340 });
341 let sent = wire::snake_case_keeping(spec.clone(), crate::JOB_SPEC_AS_GIVEN);
342 assert_eq!(sent["timeout_minutes"], 30);
343 assert!(sent.get("timeoutMinutes").is_none());
344 for kept in ["spec", "workflow", "github", "event", "contexts", "checkout"] {
345 assert_eq!(sent[kept], spec[kept], "{kept}");
346 }
347}

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