Skip to content
361 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.

Compare diffs from where the two branches last agreed, as a pull request would1//! 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};
Compare diffs from where the two branches last agreed, as a pull request would11use 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;
Merge branch 'worktree-agent-a6ec0a89d7fe7a9ae'21use crate::coordination::CoordinationOp;
Compare diffs from where the two branches last agreed, as a pull request would22
23/// A key as `#[serde(rename_all = "camelCase")]` writes it.
24fn camel_key(key: &str) -> String {
25 let mut out = String::with_capacity(key.len());
26 let mut upper = false;
27 for c in key.chars() {
28 if c == '_' {
29 upper = true;
30 } else if upper {
31 out.extend(c.to_uppercase());
32 upper = false;
33 } else {
34 out.push(c);
35 }
36 }
37 out
38}
39
40/// A response as the services send it: `camelCase`, but for the maps the
41/// converter passes through, which are data.
42fn as_services_send(value: &Value) -> Value {
43 match value {
44 Value::Object(fields) => {
45 let mut out = Map::new();
46 for (key, value) in fields {
47 let user_keyed = value.is_object()
48 && (USER_KEYED.contains(&key.as_str()) || key.starts_with("by_"));
49 let value = if user_keyed { value.clone() } else { as_services_send(value) };
50 // A `by_…` map keeps its name in the converter's spelling.
51 let key = if key.starts_with("by_") { key.clone() } else { camel_key(key) };
52 out.insert(key, value);
53 }
54 Value::Object(out)
55 }
56 Value::Array(items) => Value::Array(items.iter().map(as_services_send).collect()),
57 other => other.clone(),
58 }
59}
60
61/// `value` decoded as `T` and encoded again, as the service would send it.
62fn through<T: DeserializeOwned + Serialize>(op: Op, value: Value) -> Value {
63 let decoded: T = serde_json::from_value(value)
64 .unwrap_or_else(|error| panic!("{}: the example is not a {}: {error}", op.name(), std::any::type_name::<T>()));
65 serde_json::to_value(decoded).unwrap()
66}
67
68/// What the service behind an operation sends, from its example.
69fn sample(op: Op, example: &Value) -> Value {
70 // Types serde already writes in `snake_case`: a person, and who has
71 // access. Sent as they are.
72 let as_is = example.clone();
73 match op {
74 Op::Whoami => return through::<g1t_contracts::User>(op, as_is),
75 Op::ListCollaborators => return through::<access::RepoAccess>(op, as_is),
76 Op::AddCollaborator => return through::<access::Added>(op, as_is),
77 Op::UpdateCollaborator => return through::<access::Collaborator>(op, as_is),
78 Op::GetCollaboratorPermission => return through::<access::PermissionInfo>(op, as_is),
79 Op::ListRepoInvitations | Op::ListMyRepoInvitations => {
80 return through::<Vec<access::RepoInvitation>>(op, as_is);
81 }
82 Op::RevokeRepoInvitation | Op::AcceptRepoInvitation | Op::DeclineRepoInvitation => {
83 return through::<access::RepoInvitation>(op, as_is);
84 }
85 Op::ListOutsideCollaborators => return through::<Vec<access::OutsideCollaborator>>(op, as_is),
Merge main (membership, two-factor, GitHub repo roles) into tokens86 // Members, also `snake_case`.
87 Op::ListMembers => return through::<Vec<g1t_contracts::identity::Member>>(op, as_is),
88 Op::UpdateMember => return through::<g1t_contracts::identity::Member>(op, as_is),
89 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 bar90 // Teams and code owners, also `snake_case`.
91 Op::ListTeams | Op::ListChildTeams | Op::ListUserTeams => return through::<Vec<teams::Team>>(op, as_is),
92 Op::GetTeam | Op::CreateTeam | Op::UpdateTeam | Op::SetTeamReviewAssignment => {
93 return through::<teams::Team>(op, as_is);
94 }
95 Op::ListTeamMembers => return through::<Vec<teams::TeamMember>>(op, as_is),
96 Op::SetTeamMember => return through::<teams::TeamMember>(op, as_is),
97 Op::ListTeamRepos => return through::<Vec<teams::TeamRepo>>(op, as_is),
98 Op::SetTeamRepo => return through::<teams::TeamRepo>(op, as_is),
99 Op::DeleteTeam | Op::RemoveTeamMember | Op::RemoveTeamRepo => return through::<bool>(op, as_is),
100 Op::GetCodeownersErrors => return through::<codeowners::CodeOwnersReport>(op, as_is),
Merge rulesets: branch and tag rules, agent-first, enforced on push and merge101 // Rulesets travel in `snake_case` between services too.
102 Op::Rules(RulesOp::ListRepoRulesets | RulesOp::ListWorkspaceRulesets) => {
103 return through::<Vec<rules::Ruleset>>(op, as_is);
104 }
105 Op::Rules(
106 RulesOp::GetRepoRuleset
107 | RulesOp::CreateRepoRuleset
108 | RulesOp::UpdateRepoRuleset
109 | RulesOp::GetWorkspaceRuleset
110 | RulesOp::CreateWorkspaceRuleset
111 | RulesOp::UpdateWorkspaceRuleset,
112 ) => return through::<rules::Ruleset>(op, as_is),
113 Op::Rules(RulesOp::GetBranchRules) => return through::<rules::EffectiveRules>(op, as_is),
114 Op::Rules(RulesOp::ListRuleEvaluations | RulesOp::ListWorkspaceRuleEvaluations) => {
115 return through::<rules::EvaluationPage>(op, as_is);
116 }
117 // Built by the API itself.
118 Op::Rules(RulesOp::DeleteRepoRuleset | RulesOp::DeleteWorkspaceRuleset) => return as_is,
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb97119 // Deployments travel in `snake_case` between services too.
120 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.121 // So does telemetry.
122 Op::Telemetry(_) => return as_is,
Actions: OIDC tokens, the toolkit's cache and artifact services, and artifacts in R2123 // Artifacts are shaped by the API itself, in `snake_case`.
124 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.125 // So are archive links.
126 Op::Archives(_) => return as_is,
Merge branch 'worktree-agent-a5a5de74d8863d554' into worktree-agent-a16631325aecf58ca127 // Deploy keys travel in `snake_case` from identity.
128 Op::DeployKeys(DeployKeysOp::ListDeployKeys) => return through::<Vec<g1t_contracts::deploy_keys::DeployKey>>(op, as_is),
129 Op::DeployKeys(DeployKeysOp::GetDeployKey | DeployKeysOp::CreateDeployKey) => {
130 return through::<g1t_contracts::deploy_keys::DeployKey>(op, as_is);
131 }
132 Op::DeployKeys(DeployKeysOp::DeleteDeployKey) => return through::<bool>(op, as_is),
Merge packages: roles, Actions access, source label, soft delete, API133 // Packages are shaped by the API itself, in `snake_case`.
134 Op::Packages(_) => return as_is,
Merge main into Artifacts Phase 2135 // Artifacts are shaped by the API itself, in `snake_case`.
136 Op::Folios(_) => return as_is,
Compare diffs from where the two branches last agreed, as a pull request would137 // Built by the API itself, in `snake_case`.
138 Op::ListSecurityAlerts => return through::<Vec<crate::alerts::SecurityAlert>>(op, as_is),
139 Op::DismissSecurityAlert | Op::ReopenSecurityAlert => {
140 return through::<crate::alerts::SecurityAlert>(op, as_is);
141 }
142 _ => {}
143 }
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar144 let mut sent = as_services_send(example);
145 // A pull request's code owners are `snake_case` inside it.
146 if op == Op::GetPullRequest
147 && let Some(code_owners) = example.get("code_owners")
148 {
149 sent["codeOwners"] = code_owners.clone();
150 }
Compare diffs from where the two branches last agreed, as a pull request would151 match op {
Merge branch 'worktree-agent-ad7c6d88d93adc817'152 Op::GetWorkspace | Op::CreateWorkspace | Op::UpdateWorkspace => through::<g1t_contracts::identity::Workspace>(op, sent),
Compare diffs from where the two branches last agreed, as a pull request would153 Op::ListRepos => through::<Vec<repos::Repo>>(op, sent),
154 Op::Search => through::<search::SearchResults>(op, sent),
155 Op::GetRepo
156 | Op::CreateRepo
157 | Op::UpdateRepo
158 | Op::TransferRepo
159 | Op::RenameRepo
160 | Op::RenameBranch
161 | Op::ArchiveRepo
162 | Op::UnarchiveRepo
163 | Op::SetRepoVisibility
164 | 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.165 Op::CreateBranch | Op::DeleteBranch => through::<repos::Branch>(op, sent),
Compare diffs from where the two branches last agreed, as a pull request would166 Op::DeleteRepo => through::<repos::DeletedRepo>(op, sent),
167 Op::ListDeletedRepos => through::<Vec<repos::DeletedRepo>>(op, sent),
168 Op::GetRepoSettings | Op::UpdateRepoSettings => through::<work::RepoSettings>(op, sent),
169 Op::ListCheckNames => through::<Vec<work::SeenCheck>>(op, sent),
170 Op::GetMergeQueue => through::<work::QueueView>(op, sent),
Merge branch 'worktree-agent-ab8033ab973f8d99a'171 Op::ListDecisions => through::<Vec<g1t_contracts::decisions::Decision>>(op, sent),
172 Op::RecordDecision => through::<g1t_contracts::decisions::Decision>(op, sent),
173 Op::Why => through::<g1t_contracts::decisions::Why>(op, sent),
Compare diffs from where the two branches last agreed, as a pull request would174 Op::ListIssues => through::<Vec<work::Issue>>(op, sent),
175 Op::CreateIssue | Op::UpdateIssue | Op::CloseIssue | Op::ReopenIssue => {
176 through::<work::Issue>(op, sent)
177 }
178 Op::GetIssue => through::<work::IssueDetail>(op, sent),
179 Op::ListPullRequests => through::<Vec<work::Pull>>(op, sent),
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar180 Op::UpdatePullRequest => through::<work::Pull>(op, sent),
181 Op::ListLabels | Op::AddDefaultLabels | Op::ListIssueLabels => through::<Vec<work::Label>>(op, sent),
182 Op::CreateLabel | Op::UpdateLabel => through::<work::Label>(op, sent),
183 Op::ListMilestones => through::<Vec<work::Milestone>>(op, sent),
184 Op::CreateMilestone | Op::UpdateMilestone => through::<work::Milestone>(op, sent),
185 Op::GetMilestone => through::<work::MilestoneDetail>(op, sent),
Compare diffs from where the two branches last agreed, as a pull request would186 Op::GetPullRequest => through::<work::PullDetail>(op, sent),
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar187 Op::MarkPullRequestReady
188 | Op::ClosePullRequest
Merge Actions: cross-repo workflows and actions, release and deployment triggers, step timeouts189 | Op::ReopenPullRequest
190 | Op::ConvertPullRequestToDraft
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar191 | Op::MergePullRequest
192 | Op::RequestReviewers
193 | Op::RemoveRequestedReviewers => {
Compare diffs from where the two branches last agreed, as a pull request would194 through::<work::Pull>(op, sent)
195 }
196 Op::ListWorkflows => through::<Vec<actions::Workflow>>(op, sent),
197 Op::ListWorkflowRuns => through::<Vec<actions::WorkflowRun>>(op, sent),
198 Op::GetWorkflowRun => through::<actions::RunDetail>(op, sent),
199 Op::GetJobLogs => through::<actions::JobLog>(op, sent),
200 Op::DispatchWorkflow | Op::CancelWorkflowRun | Op::RerunWorkflowRun => {
201 through::<actions::WorkflowRun>(op, sent)
202 }
203 Op::ListActionsSecrets | Op::ListActionsVariables => through::<Vec<actions::Setting>>(op, sent),
204 Op::ListRunners => through::<Vec<g1t_contracts::runners::Runner>>(op, sent),
205 Op::ListRunnerGroups => through::<Vec<g1t_contracts::runners::RunnerGroup>>(op, sent),
206 Op::CreateRunnerGroup | Op::UpdateRunnerGroup => through::<g1t_contracts::runners::RunnerGroup>(op, sent),
207 Op::GetRunnerSettings | Op::UpdateRunnerSettings => through::<g1t_contracts::runners::RunnerSettings>(op, sent),
208 Op::CreateRunnerRegistrationToken => through::<g1t_contracts::runners::RegistrationToken>(op, sent),
209 Op::ListWebhooks => through::<Vec<webhooks::Hook>>(op, sent),
210 Op::ListIntegrations => through::<Vec<integrations::Connection>>(op, sent),
211 Op::ListEvents => through::<Vec<g1t_contracts::events::Event>>(op, sent),
212 Op::ListEmails | Op::AddEmail | Op::RemoveEmail | Op::UpdateEmailSettings => {
213 through::<g1t_contracts::accounts::AccountEmails>(op, sent)
214 }
Merge email confirmation gate: a code and a link, nothing until confirmed (identity 0036)215 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.216 Op::InviteMember | Op::RevokeWorkspaceInvite => {
Compare diffs from where the two branches last agreed, as a pull request would217 through::<g1t_contracts::identity::Invite>(op, sent)
218 }
219 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)220 Op::ListInvitations => through::<Vec<g1t_contracts::identity::WorkspaceInvitation>>(op, sent),
API: notifications over REST and MCP, with notifications scopes221 Op::ListNotifications => through::<g1t_contracts::inbox::InboxPage>(op, sent),
222 Op::GetNotificationThread | Op::MarkThreadRead | Op::MarkThreadDone | Op::SaveThread | Op::SnoozeThread => {
223 through::<g1t_contracts::inbox::InboxThread>(op, sent)
224 }
225 Op::GetThreadSubscription | Op::SetThreadSubscription | Op::DeleteThreadSubscription => {
226 through::<g1t_contracts::inbox::ThreadSubscription>(op, sent)
227 }
Merge checks: statuses and check runs on every commit228 Op::Checks(ChecksOp::CreateCommitStatus) => through::<work::CommitStatus>(op, sent),
229 Op::Checks(ChecksOp::ListCommitStatuses) => through::<Vec<work::CommitStatus>>(op, sent),
230 Op::Checks(ChecksOp::GetCombinedStatus) => through::<checks::CombinedStatus>(op, sent),
231 Op::Checks(ChecksOp::CreateCheckRun | ChecksOp::UpdateCheckRun | ChecksOp::GetCheckRun) => {
232 through::<checks::CommitCheckRun>(op, sent)
233 }
234 Op::Checks(ChecksOp::ListCheckRunAnnotations) => through::<Vec<checks::CheckAnnotation>>(op, sent),
235 Op::Checks(ChecksOp::ListCheckRunsForRef) => through::<checks::CheckRunList>(op, sent),
236 Op::Checks(ChecksOp::ListCheckSuitesForRef) => through::<checks::CheckSuiteList>(op, sent),
237 Op::Checks(ChecksOp::GetCheckSuite) => through::<checks::CommitCheckSuite>(op, sent),
Merge branch 'worktree-agent-a6ec0a89d7fe7a9ae'238 Op::Coordination(CoordinationOp::StartSession | CoordinationOp::UpdateSession | CoordinationOp::EndSession) => {
239 through::<work::Session>(op, sent)
240 }
241 Op::Coordination(CoordinationOp::GetSession) => through::<work::SessionDetail>(op, sent),
242 Op::Coordination(CoordinationOp::ListSessions) => through::<Vec<work::Session>>(op, sent),
243 Op::Coordination(CoordinationOp::RecordSessionEntries) => through::<work::Recorded>(op, sent),
244 Op::Coordination(CoordinationOp::ClaimIssue) => through::<work::Claimed>(op, sent),
245 Op::Coordination(CoordinationOp::ReleaseClaim | CoordinationOp::HandOffIssue) => through::<work::Claim>(op, sent),
246 Op::Coordination(CoordinationOp::ListClaims) => through::<Vec<work::Claim>>(op, sent),
247 Op::Coordination(CoordinationOp::GetWork) => through::<work::WorkContext>(op, sent),
Compare diffs from where the two branches last agreed, as a pull request would248 _ => sent,
249 }
250}
251
252/// Every key of `example`, as paths, outside the maps passed through.
253fn paths(value: &Value, path: &str, out: &mut Vec<String>) {
254 match value {
255 Value::Object(fields) => {
256 for (key, value) in fields {
257 let here = format!("{path}.{key}");
258 out.push(here.clone());
259 let user_keyed = value.is_object()
260 && (USER_KEYED.contains(&key.as_str()) || key.starts_with("by_"));
261 if !user_keyed {
262 paths(value, &here, out);
263 }
264 }
265 }
266 Value::Array(items) => {
267 for item in items {
268 paths(item, &format!("{path}[]"), out);
269 }
270 }
271 _ => {}
272 }
273}
274
275#[test]
276fn no_route_answers_with_camel_case() {
277 let document = document();
278 let (mut checked, mut converted) = (0, 0);
279 for (path, methods) in document["paths"].as_object().unwrap() {
280 for (method, operation) in methods.as_object().unwrap() {
281 let example = &operation["responses"]["200"]["content"]["application/json"]["example"];
282 let tool = operation["x-operation"].as_str().unwrap_or_default();
283 let Some(op) = Op::by_name(tool) else {
284 // Device sign-in, which is written in `snake_case` by hand.
285 assert!(wire::camel_case_keys(example).is_empty(), "{method} {path}");
286 continue;
287 };
Cards you act on in chat; agents comment and review as themselves; names shown cleanly; commits on the calendar288 let mut sample = sample(op, example);
289 // Each person gets their chosen case on the way out (people.rs).
290 crate::people::fill(&mut sample, &std::collections::HashMap::new());
Compare diffs from where the two branches last agreed, as a pull request would291 converted += wire::camel_case_keys(&sample).len();
292 let sent = wire::snake_case(sample);
293 let leaked = wire::camel_case_keys(&sent);
294 assert!(leaked.is_empty(), "{method} {path} sends {leaked:?}");
295 // The reference shows what is sent: each of its names is one.
296 let (mut shown, mut real) = (Vec::new(), Vec::new());
297 paths(example, "", &mut shown);
298 paths(&sent, "", &mut real);
299 for name in shown {
300 assert!(real.contains(&name), "{method} {path}: the reference shows {name}, which is not sent");
301 }
302 checked += 1;
303 }
304 }
305 assert!(checked >= Op::ALL.len());
306 // The samples are in the services' spelling, so there was something to
307 // convert.
308 assert!(converted > 100, "{converted}");
309}
310
311#[test]
312fn every_route_has_a_sample() {
313 let document = document();
314 for route in crate::rest::ROUTES {
Merge Actions: cross-repo workflows and actions, release and deployment triggers, step timeouts315 if route.no_content() {
316 continue;
317 }
Compare diffs from where the two branches last agreed, as a pull request would318 let path = route
319 .path
320 .split('/')
321 .map(|segment| match segment.strip_prefix(':') {
322 Some(name) => format!("{{{name}}}"),
323 None => segment.to_owned(),
324 })
325 .collect::<Vec<_>>()
326 .join("/");
327 let example = &document["paths"][&path][route.method.to_lowercase()]["responses"]["200"]
328 ["content"]["application/json"]["example"];
329 assert!(!example.is_null(), "{} {path}", route.method);
330 }
331}
332
333#[test]
334fn errors_and_reports_are_snake_case() {
335 let failure = g1t_contracts::Failure {
336 code: g1t_contracts::FailureCode::NotFound,
337 message: "No such endpoint.".to_owned(),
338 };
339 assert!(wire::camel_case_keys(&wire::snake_case(json!({ "error": failure }))).is_empty());
340}
341
342#[test]
343fn a_job_spec_keeps_github_s_spelling() {
344 let spec = json!({
345 "job": "job_1",
346 "spec": { "runs-on": "ubuntu-latest", "timeoutMinutes": 5 },
347 "workflow": { "env": { "nodeEnv": "x" } },
348 "github": { "eventName": "push", "headRef": "" },
349 "event": { "pull_request": { "headSha": "x" } },
350 "contexts": { "inputs": { "dryRun": true }, "matrix": { "nodeVersion": 20 } },
351 "checkout": { "ref": "main" },
352 "timeoutMinutes": 30,
353 "masks": [],
354 });
355 let sent = wire::snake_case_keeping(spec.clone(), crate::JOB_SPEC_AS_GIVEN);
356 assert_eq!(sent["timeout_minutes"], 30);
357 assert!(sent.get("timeoutMinutes").is_none());
358 for kept in ["spec", "workflow", "github", "event", "contexts", "checkout"] {
359 assert_eq!(sent[kept], spec[kept], "{kept}");
360 }
361}

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