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

g1t is the stored author of what it opens; the person who asked is requested_by and keeps the author's rights1//! 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};
g1t is the stored author of what it opens; the person who asked is requested_by and keeps the author's rights11use 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;
g1t is the stored author of what it opens; the person who asked is requested_by and keeps the author's rights21
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,
Every agent can have its own computer. A session that needs one wakes it: a home of its own on g1t cloud, one per agent and never shared, where it runs commands, reads and writes files and keeps what it made, with each session working in its own folder under a shared home; after ten idle minutes it sleeps, its home kept as a snapshot and restored when it wakes, and Reset wipes the home while memory and artifacts stay. Its shell and files are abilities with the usual choices, Alone, Alone when asked, Ask first or Never, offered only inside sessions and never to a chat reply; every command shows on the session with its output, and the agent's new Computer tab shows the state, the disk used of the five gigabytes included, the recent commands, and Wake, Put to sleep and Reset. Machine time counts only while it is awake, on the sandbox lines of the ledger that name the agent and who asked, held to the same spend caps as the session; the disk itself costs nothing in this version. The runner gained a long-lived supervisor that answers the computer's requests inside the container, and the runner service a computer per agent that keeps its snapshot in the agent homes bucket when one is attached, and says so when none is. The REST API and the agent tool can read a computer, wake it, put it to sleep and reset it. The agents, abilities, sessions, runners, billing and deploy guides say how it works and what an operator sets up; pinning a computer to your own runner, its browser and take-over come next.136 // Agents' computers travel in `snake_case` from the agents service.
137 Op::GetAgentComputer | Op::WakeAgentComputer | Op::SleepAgentComputer | Op::ResetAgentComputer | Op::ListAgentComputerCommands => return as_is,
g1t is the stored author of what it opens; the person who asked is requested_by and keeps the author's rights138 // Built by the API itself, in `snake_case`.
139 Op::ListSecurityAlerts => return through::<Vec<crate::alerts::SecurityAlert>>(op, as_is),
140 Op::DismissSecurityAlert | Op::ReopenSecurityAlert => {
141 return through::<crate::alerts::SecurityAlert>(op, as_is);
142 }
143 _ => {}
144 }
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar145 let mut sent = as_services_send(example);
146 // A pull request's code owners are `snake_case` inside it.
147 if op == Op::GetPullRequest
148 && let Some(code_owners) = example.get("code_owners")
149 {
150 sent["codeOwners"] = code_owners.clone();
151 }
g1t is the stored author of what it opens; the person who asked is requested_by and keeps the author's rights152 match op {
Merge branch 'worktree-agent-ad7c6d88d93adc817'153 Op::GetWorkspace | Op::CreateWorkspace | Op::UpdateWorkspace => through::<g1t_contracts::identity::Workspace>(op, sent),
g1t is the stored author of what it opens; the person who asked is requested_by and keeps the author's rights154 Op::ListRepos => through::<Vec<repos::Repo>>(op, sent),
155 Op::Search => through::<search::SearchResults>(op, sent),
156 Op::GetRepo
157 | Op::CreateRepo
158 | Op::UpdateRepo
159 | Op::TransferRepo
160 | Op::RenameRepo
161 | Op::RenameBranch
162 | Op::ArchiveRepo
163 | Op::UnarchiveRepo
164 | Op::SetRepoVisibility
165 | 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.166 Op::CreateBranch | Op::DeleteBranch => through::<repos::Branch>(op, sent),
g1t is the stored author of what it opens; the person who asked is requested_by and keeps the author's rights167 Op::DeleteRepo => through::<repos::DeletedRepo>(op, sent),
168 Op::ListDeletedRepos => through::<Vec<repos::DeletedRepo>>(op, sent),
169 Op::GetRepoSettings | Op::UpdateRepoSettings => through::<work::RepoSettings>(op, sent),
170 Op::ListCheckNames => through::<Vec<work::SeenCheck>>(op, sent),
171 Op::GetMergeQueue => through::<work::QueueView>(op, sent),
172 Op::ListIssues => through::<Vec<work::Issue>>(op, sent),
173 Op::CreateIssue | Op::UpdateIssue | Op::CloseIssue | Op::ReopenIssue => {
174 through::<work::Issue>(op, sent)
175 }
176 Op::GetIssue => through::<work::IssueDetail>(op, sent),
177 Op::Delegate => through::<work::Delegated>(op, sent),
178 Op::ListPullRequests => through::<Vec<work::Pull>>(op, sent),
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar179 Op::UpdatePullRequest => through::<work::Pull>(op, sent),
180 Op::ListLabels | Op::AddDefaultLabels | Op::ListIssueLabels => through::<Vec<work::Label>>(op, sent),
181 Op::CreateLabel | Op::UpdateLabel => through::<work::Label>(op, sent),
182 Op::ListMilestones => through::<Vec<work::Milestone>>(op, sent),
183 Op::CreateMilestone | Op::UpdateMilestone => through::<work::Milestone>(op, sent),
184 Op::GetMilestone => through::<work::MilestoneDetail>(op, sent),
g1t is the stored author of what it opens; the person who asked is requested_by and keeps the author's rights185 Op::GetPullRequest => through::<work::PullDetail>(op, sent),
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar186 Op::MarkPullRequestReady
187 | Op::ClosePullRequest
Merge Actions: cross-repo workflows and actions, release and deployment triggers, step timeouts188 | Op::ReopenPullRequest
189 | Op::ConvertPullRequestToDraft
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar190 | Op::MergePullRequest
191 | Op::AssignIssue
192 | Op::RequestReviewers
193 | Op::RemoveRequestedReviewers => {
g1t is the stored author of what it opens; the person who asked is requested_by and keeps the author's rights194 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::GetModelRoutes | Op::SetModelRoutes => through::<Vec<integrations::ModelRoute>>(op, sent),
212 Op::ListEvents => through::<Vec<g1t_contracts::events::Event>>(op, sent),
213 Op::ListEmails | Op::AddEmail | Op::RemoveEmail | Op::UpdateEmailSettings => {
214 through::<g1t_contracts::accounts::AccountEmails>(op, sent)
215 }
Merge email confirmation gate: a code and a link, nothing until confirmed (identity 0036)216 Op::ConfirmEmail => through::<g1t_contracts::accounts::EmailConfirmed>(op, sent),
g1t is the stored author of what it opens; the person who asked is requested_by and keeps the author's rights217 Op::ListInvites => through::<g1t_contracts::identity::InvitesOverview>(op, sent),
218 Op::CreateInvite | Op::RevokeInvite | Op::InviteMember | Op::RevokeWorkspaceInvite => {
219 through::<g1t_contracts::identity::Invite>(op, sent)
220 }
221 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)222 Op::ListInvitations => through::<Vec<g1t_contracts::identity::WorkspaceInvitation>>(op, sent),
API: notifications over REST and MCP, with notifications scopes223 Op::ListNotifications => through::<g1t_contracts::inbox::InboxPage>(op, sent),
224 Op::GetNotificationThread | Op::MarkThreadRead | Op::MarkThreadDone | Op::SaveThread | Op::SnoozeThread => {
225 through::<g1t_contracts::inbox::InboxThread>(op, sent)
226 }
227 Op::GetThreadSubscription | Op::SetThreadSubscription | Op::DeleteThreadSubscription => {
228 through::<g1t_contracts::inbox::ThreadSubscription>(op, sent)
229 }
Merge checks: statuses and check runs on every commit230 Op::Checks(ChecksOp::CreateCommitStatus) => through::<work::CommitStatus>(op, sent),
231 Op::Checks(ChecksOp::ListCommitStatuses) => through::<Vec<work::CommitStatus>>(op, sent),
232 Op::Checks(ChecksOp::GetCombinedStatus) => through::<checks::CombinedStatus>(op, sent),
233 Op::Checks(ChecksOp::CreateCheckRun | ChecksOp::UpdateCheckRun | ChecksOp::GetCheckRun) => {
234 through::<checks::CommitCheckRun>(op, sent)
235 }
236 Op::Checks(ChecksOp::ListCheckRunAnnotations) => through::<Vec<checks::CheckAnnotation>>(op, sent),
237 Op::Checks(ChecksOp::ListCheckRunsForRef) => through::<checks::CheckRunList>(op, sent),
238 Op::Checks(ChecksOp::ListCheckSuitesForRef) => through::<checks::CheckSuiteList>(op, sent),
239 Op::Checks(ChecksOp::GetCheckSuite) => through::<checks::CommitCheckSuite>(op, sent),
g1t is the stored author of what it opens; the person who asked is requested_by and keeps the author's rights240 _ => sent,
241 }
242}
243
244/// Every key of `example`, as paths, outside the maps passed through.
245fn paths(value: &Value, path: &str, out: &mut Vec<String>) {
246 match value {
247 Value::Object(fields) => {
248 for (key, value) in fields {
249 let here = format!("{path}.{key}");
250 out.push(here.clone());
251 let user_keyed = value.is_object()
252 && (USER_KEYED.contains(&key.as_str()) || key.starts_with("by_"));
253 if !user_keyed {
254 paths(value, &here, out);
255 }
256 }
257 }
258 Value::Array(items) => {
259 for item in items {
260 paths(item, &format!("{path}[]"), out);
261 }
262 }
263 _ => {}
264 }
265}
266
267#[test]
268fn no_route_answers_with_camel_case() {
269 let document = document();
270 let (mut checked, mut converted) = (0, 0);
271 for (path, methods) in document["paths"].as_object().unwrap() {
272 for (method, operation) in methods.as_object().unwrap() {
273 let example = &operation["responses"]["200"]["content"]["application/json"]["example"];
274 let tool = operation["x-operation"].as_str().unwrap_or_default();
275 let Some(op) = Op::by_name(tool) else {
276 // Device sign-in, which is written in `snake_case` by hand.
277 assert!(wire::camel_case_keys(example).is_empty(), "{method} {path}");
278 continue;
279 };
Cards you act on in chat; agents comment and review as themselves; names shown cleanly; commits on the calendar280 let mut sample = sample(op, example);
281 // Each person gets their chosen case on the way out (people.rs).
282 crate::people::fill(&mut sample, &std::collections::HashMap::new());
g1t is the stored author of what it opens; the person who asked is requested_by and keeps the author's rights283 converted += wire::camel_case_keys(&sample).len();
284 let sent = wire::snake_case(sample);
285 let leaked = wire::camel_case_keys(&sent);
286 assert!(leaked.is_empty(), "{method} {path} sends {leaked:?}");
287 // The reference shows what is sent: each of its names is one.
288 let (mut shown, mut real) = (Vec::new(), Vec::new());
289 paths(example, "", &mut shown);
290 paths(&sent, "", &mut real);
291 for name in shown {
292 assert!(real.contains(&name), "{method} {path}: the reference shows {name}, which is not sent");
293 }
294 checked += 1;
295 }
296 }
297 assert!(checked >= Op::ALL.len());
298 // The samples are in the services' spelling, so there was something to
299 // convert.
300 assert!(converted > 100, "{converted}");
301}
302
303#[test]
304fn every_route_has_a_sample() {
305 let document = document();
306 for route in crate::rest::ROUTES {
Merge Actions: cross-repo workflows and actions, release and deployment triggers, step timeouts307 if route.no_content() {
308 continue;
309 }
g1t is the stored author of what it opens; the person who asked is requested_by and keeps the author's rights310 let path = route
311 .path
312 .split('/')
313 .map(|segment| match segment.strip_prefix(':') {
314 Some(name) => format!("{{{name}}}"),
315 None => segment.to_owned(),
316 })
317 .collect::<Vec<_>>()
318 .join("/");
319 let example = &document["paths"][&path][route.method.to_lowercase()]["responses"]["200"]
320 ["content"]["application/json"]["example"];
321 assert!(!example.is_null(), "{} {path}", route.method);
322 }
323}
324
325#[test]
326fn errors_and_reports_are_snake_case() {
327 let failure = g1t_contracts::Failure {
328 code: g1t_contracts::FailureCode::NotFound,
329 message: "No such endpoint.".to_owned(),
330 };
331 assert!(wire::camel_case_keys(&wire::snake_case(json!({ "error": failure }))).is_empty());
332}
333
334#[test]
335fn a_job_spec_keeps_github_s_spelling() {
336 let spec = json!({
337 "job": "job_1",
338 "spec": { "runs-on": "ubuntu-latest", "timeoutMinutes": 5 },
339 "workflow": { "env": { "nodeEnv": "x" } },
340 "github": { "eventName": "push", "headRef": "" },
341 "event": { "pull_request": { "headSha": "x" } },
342 "contexts": { "inputs": { "dryRun": true }, "matrix": { "nodeVersion": 20 } },
343 "checkout": { "ref": "main" },
344 "timeoutMinutes": 30,
345 "masks": [],
346 });
347 let sent = wire::snake_case_keeping(spec.clone(), crate::JOB_SPEC_AS_GIVEN);
348 assert_eq!(sent["timeout_minutes"], 30);
349 assert!(sent.get("timeoutMinutes").is_none());
350 for kept in ["spec", "workflow", "github", "event", "contexts", "checkout"] {
351 assert_eq!(sent[kept], spec[kept], "{kept}");
352 }
353}

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