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
429 linesCodeBlameRaw
1//! Which API operations each command calls.
2//!
3//! Every operation is reachable as `g1t api <operation>`, from the table
4//! generated out of the API's OpenAPI document (`ops_table.rs`). The
5//! commands written by hand for the common paths are listed here with the
6//! operations they call, and the tests below hold the three together:
7//!
8//! - every operation named here is in the generated table, so a command
9//! whose operation the API drops or renames fails the build's tests;
10//! - every command in the command tree is listed here, and nothing else;
11//! - the operations each command's source names are exactly the ones
12//! listed for it, so this table cannot fall behind the code.
13
14use std::collections::BTreeMap;
15
16pub struct Mapping {
17 pub command: &'static str,
18 pub ops: &'static [&'static str],
19}
20
21pub static COMMANDS: &[Mapping] = &[
22 Mapping {
23 command: "auth login",
24 ops: &["device_code", "device_token", "whoami"],
25 },
26 Mapping {
27 command: "auth logout",
28 ops: &[],
29 },
30 Mapping {
31 command: "auth status",
32 ops: &["whoami"],
33 },
34 Mapping {
35 command: "auth token",
36 ops: &[],
37 },
38 Mapping {
39 command: "auth switch",
40 ops: &[],
41 },
42 Mapping {
43 command: "auth setup-git",
44 ops: &[],
45 },
46 Mapping {
47 command: "auth git-credential",
48 ops: &["whoami"],
49 },
50 Mapping {
51 command: "repo view",
52 ops: &["get_repo"],
53 },
54 Mapping {
55 command: "repo clone",
56 ops: &["whoami"],
57 },
58 Mapping {
59 command: "repo create",
60 ops: &["create_repo"],
61 },
62 Mapping {
63 command: "repo list",
64 ops: &["list_repos"],
65 },
66 Mapping {
67 command: "issue list",
68 ops: &["list_issues"],
69 },
70 Mapping {
71 command: "issue view",
72 ops: &["get_issue"],
73 },
74 Mapping {
75 command: "issue create",
76 ops: &["create_issue", "update_issue"],
77 },
78 Mapping {
79 command: "issue comment",
80 ops: &["add_comment"],
81 },
82 Mapping {
83 command: "issue close",
84 ops: &["close_issue", "add_comment"],
85 },
86 Mapping {
87 command: "issue reopen",
88 ops: &["reopen_issue", "add_comment"],
89 },
90 Mapping {
91 command: "issue edit",
92 ops: &["update_issue", "get_issue"],
93 },
94 Mapping {
95 command: "pr list",
96 ops: &["list_pull_requests"],
97 },
98 Mapping {
99 command: "pr view",
100 ops: &["get_pull_request", "list_pull_requests"],
101 },
102 Mapping {
103 command: "pr create",
104 ops: &[
105 "create_pull_request",
106 "get_repo",
107 "convert_pull_request_to_draft",
108 ],
109 },
110 Mapping {
111 command: "pr checkout",
112 ops: &["get_pull_request"],
113 },
114 Mapping {
115 command: "pr diff",
116 ops: &["get_pull_request_changes"],
117 },
118 Mapping {
119 command: "pr checks",
120 ops: &[
121 "get_pull_request",
122 "list_check_runs_for_ref",
123 "get_combined_status",
124 ],
125 },
126 Mapping {
127 command: "pr review",
128 ops: &["review_pull_request", "add_comment"],
129 },
130 Mapping {
131 command: "pr merge",
132 ops: &["merge_pull_request", "delete_branch"],
133 },
134 Mapping {
135 command: "pr ready",
136 ops: &["mark_pull_request_ready", "convert_pull_request_to_draft"],
137 },
138 Mapping {
139 command: "pr comment",
140 ops: &["add_comment"],
141 },
142 Mapping {
143 command: "pr close",
144 ops: &["close_pull_request"],
145 },
146 Mapping {
147 command: "pr reopen",
148 ops: &["reopen_pull_request"],
149 },
150 Mapping {
151 command: "run list",
152 ops: &["list_workflow_runs"],
153 },
154 Mapping {
155 command: "run view",
156 ops: &["get_workflow_run", "get_job_logs"],
157 },
158 Mapping {
159 command: "run watch",
160 ops: &["get_workflow_run"],
161 },
162 Mapping {
163 command: "run logs",
164 ops: &["get_workflow_run", "get_job_logs"],
165 },
166 Mapping {
167 command: "run rerun",
168 ops: &["rerun_workflow_run", "rerun_failed_jobs", "rerun_job"],
169 },
170 Mapping {
171 command: "run cancel",
172 ops: &["cancel_workflow_run"],
173 },
174 Mapping {
175 command: "workflow list",
176 ops: &["list_workflows"],
177 },
178 Mapping {
179 command: "workflow view",
180 ops: &["list_workflows", "get_workflow", "list_workflow_runs"],
181 },
182 Mapping {
183 command: "workflow run",
184 ops: &["list_workflows", "dispatch_workflow", "list_workflow_runs"],
185 },
186 Mapping {
187 command: "workflow enable",
188 ops: &["list_workflows", "enable_workflow"],
189 },
190 Mapping {
191 command: "workflow disable",
192 ops: &["list_workflows", "disable_workflow"],
193 },
194 Mapping {
195 command: "secret list",
196 ops: &["list_actions_secrets", "list_actions_secrets_for_workspace"],
197 },
198 Mapping {
199 command: "secret set",
200 ops: &["set_actions_secret", "set_actions_secret_for_workspace"],
201 },
202 Mapping {
203 command: "secret delete",
204 ops: &[
205 "delete_actions_secret",
206 "delete_actions_secret_for_workspace",
207 ],
208 },
209 Mapping {
210 command: "cache list",
211 ops: &["list_caches"],
212 },
213 Mapping {
214 command: "cache usage",
215 ops: &["get_cache_usage"],
216 },
217 Mapping {
218 command: "cache delete",
219 ops: &["delete_cache", "delete_caches_by_key"],
220 },
221 Mapping {
222 command: "deploy list",
223 ops: &["list_deployments"],
224 },
225 Mapping {
226 command: "deploy view",
227 ops: &["get_deployment"],
228 },
229 Mapping {
230 command: "logs",
231 ops: &["list_logs", "list_errors", "list_projects", "get_repo"],
232 },
233 Mapping {
234 command: "status",
235 ops: &[
236 "search",
237 "list_notifications",
238 "get_pull_request",
239 "list_sessions",
240 "list_claims",
241 ],
242 },
243 Mapping { command: "session start", ops: &["start_session"] },
244 Mapping { command: "session heartbeat", ops: &["update_session"] },
245 Mapping { command: "session log", ops: &["record_session_entries"] },
246 Mapping { command: "session attach", ops: &["update_session"] },
247 Mapping { command: "session end", ops: &["end_session"] },
248 Mapping { command: "session list", ops: &["list_sessions"] },
249 Mapping { command: "session view", ops: &["get_session"] },
250 Mapping { command: "session touch", ops: &["record_session_entries"] },
251 Mapping { command: "work context", ops: &["get_work"] },
252 Mapping { command: "work claim", ops: &["claim_issue"] },
253 Mapping { command: "work release", ops: &["release_claim"] },
254 Mapping { command: "work handoff", ops: &["hand_off_issue"] },
255 Mapping { command: "work list", ops: &["list_claims", "list_sessions"] },
256 Mapping { command: "work block", ops: &["get_issue", "update_issue"] },
257 Mapping { command: "work unblock", ops: &["get_issue", "update_issue"] },
258 Mapping { command: "work events", ops: &["list_events"] },
259 Mapping { command: "work dispatch", ops: &["start_agent_session"] },
260 Mapping { command: "why", ops: &["why"] },
261 // Every operation, by name: the table generated from the OpenAPI document.
262 Mapping {
263 command: "api",
264 ops: &[],
265 },
266 Mapping {
267 command: "browse",
268 ops: &[],
269 },
270 Mapping {
271 command: "alias set",
272 ops: &[],
273 },
274 Mapping {
275 command: "alias list",
276 ops: &[],
277 },
278 Mapping {
279 command: "alias delete",
280 ops: &[],
281 },
282 Mapping {
283 command: "completion",
284 ops: &[],
285 },
286 // The registry's own protocol, not the REST API.
287 Mapping {
288 command: "push",
289 ops: &[],
290 },
291];
292
293/// The first command that calls each operation, for `g1t api --list`.
294pub fn by_operation() -> BTreeMap<&'static str, &'static str> {
295 let mut map = BTreeMap::new();
296 for m in COMMANDS {
297 for op in m.ops {
298 map.entry(*op).or_insert(m.command);
299 }
300 }
301 map
302}
303
304#[cfg(test)]
305mod tests {
306 use super::*;
307 use crate::cli::Cli;
308 use clap::CommandFactory;
309 use std::collections::BTreeSet;
310
311 fn leaves(command: &clap::Command, prefix: &str, out: &mut Vec<String>) {
312 let subs: Vec<&clap::Command> = command
313 .get_subcommands()
314 .filter(|c| c.get_name() != "help")
315 .collect();
316 if subs.is_empty() {
317 out.push(prefix.trim().to_owned());
318 }
319 for sub in subs {
320 leaves(sub, &format!("{prefix} {}", sub.get_name()), out);
321 }
322 }
323
324 #[test]
325 fn every_mapped_operation_exists() {
326 for m in COMMANDS {
327 for op in m.ops {
328 assert!(
329 crate::ops::find(op).is_some(),
330 "`g1t {}` calls {op}, which the API no longer has",
331 m.command
332 );
333 }
334 }
335 }
336
337 #[test]
338 fn every_command_is_mapped_and_nothing_else() {
339 let mut tree = Vec::new();
340 leaves(&Cli::command(), "", &mut tree);
341 let tree: BTreeSet<String> = tree.into_iter().collect();
342 let mapped: BTreeSet<String> = COMMANDS.iter().map(|m| m.command.to_owned()).collect();
343 let missing: Vec<&String> = tree.difference(&mapped).collect();
344 let extra: Vec<&String> = mapped.difference(&tree).collect();
345 assert!(
346 missing.is_empty(),
347 "commands with no entry in mapping.rs: {missing:?}"
348 );
349 assert!(
350 extra.is_empty(),
351 "mapping.rs lists commands that do not exist: {extra:?}"
352 );
353 }
354
355 /// The operations a source file names, as string literals.
356 fn named_in(source: &str) -> BTreeSet<&'static str> {
357 let source = source.split("#[cfg(test)]").next().unwrap_or(source);
358 crate::ops::all()
359 .map(|op| op.id)
360 .filter(|id| source.contains(&format!("\"{id}\"")))
361 .collect()
362 }
363
364 #[test]
365 fn the_table_matches_what_the_code_calls() {
366 let sources: &[(&str, &str)] = &[
367 ("auth", include_str!("commands/auth.rs")),
368 ("repo", include_str!("commands/repo.rs")),
369 ("issue", include_str!("commands/issue.rs")),
370 ("pr", include_str!("commands/pr.rs")),
371 ("run", include_str!("commands/run.rs")),
372 ("workflow", include_str!("commands/workflow.rs")),
373 ("secret", include_str!("commands/secret.rs")),
374 ("cache", include_str!("commands/cache.rs")),
375 ("deploy", include_str!("commands/deploy.rs")),
376 ("logs", include_str!("commands/logs.rs")),
377 ("status", include_str!("commands/status.rs")),
378 ("session", include_str!("commands/session.rs")),
379 ("work", include_str!("commands/work.rs")),
380 ("why", include_str!("commands/why.rs")),
381 ];
382 // What context.rs calls on every command's behalf.
383 let shared: BTreeSet<&str> = named_in(include_str!("context.rs"));
384 for (top, source) in sources {
385 let named = named_in(source);
386 let listed: BTreeSet<&str> = COMMANDS
387 .iter()
388 .filter(|m| m.command == *top || m.command.starts_with(&format!("{top} ")))
389 .flat_map(|m| m.ops.iter().copied())
390 .collect();
391 let unlisted: Vec<&&str> = named.difference(&listed).collect();
392 let unused: Vec<&&str> = listed
393 .difference(&named)
394 .filter(|op| !shared.contains(**op))
395 .collect();
396 assert!(
397 unlisted.is_empty(),
398 "commands/{top}.rs calls {unlisted:?}, which mapping.rs does not list"
399 );
400 assert!(
401 unused.is_empty(),
402 "mapping.rs lists {unused:?} for `{top}`, which commands/{top}.rs does not call"
403 );
404 }
405 }
406
407 /// `g1t api <operation>` can make a request for every operation: each
408 /// path parameter can be filled from a field.
409 #[test]
410 fn every_operation_is_reachable_through_api() {
411 let repo = || {
412 Ok(crate::repo::Repo {
413 host: "g1t.sh".into(),
414 owner: "acme".into(),
415 name: "web".into(),
416 })
417 };
418 for op in crate::ops::OPERATIONS {
419 let mut given = serde_json::Map::new();
420 for p in op.path_params() {
421 given.insert(p.name.to_owned(), serde_json::Value::String("x".into()));
422 }
423 let plan = crate::commands::api::plan_op(op, None, given, &repo)
424 .unwrap_or_else(|e| panic!("{}: {}", op.id, e.message));
425 assert!(!plan.path.contains('{'), "{}: {}", op.id, plan.path);
426 assert_eq!(plan.method, op.method);
427 }
428 }
429}