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.

462 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", "list_projects"],
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", "list_projects"],
65 },
66 Mapping {
67 command: "repo fork",
68 ops: &["fork_repo"],
69 },
70 Mapping {
71 command: "repo forks",
72 ops: &["list_forks", "list_projects"],
73 },
74 Mapping {
75 command: "repo sync",
76 ops: &["sync_fork"],
77 },
78 Mapping {
79 command: "repo detach",
80 ops: &["detach_fork"],
81 },
82 Mapping {
83 command: "pr list",
84 ops: &["list_pull_requests"],
85 },
86 Mapping {
87 command: "pr view",
88 ops: &["get_pull_request", "list_pull_requests"],
89 },
90 Mapping {
91 command: "pr create",
92 ops: &[
93 "create_pull_request",
94 "get_repo",
95 "convert_pull_request_to_draft",
96 "upload_attachment",
97 ],
98 },
99 Mapping {
100 command: "pr checkout",
101 ops: &["get_pull_request"],
102 },
103 Mapping {
104 command: "pr diff",
105 ops: &["get_pull_request_changes"],
106 },
107 Mapping {
108 command: "pr checks",
109 ops: &[
110 "get_pull_request",
111 "list_check_runs_for_ref",
112 "get_combined_status",
113 ],
114 },
115 Mapping {
116 command: "pr review",
117 ops: &["review_pull_request", "add_comment"],
118 },
119 Mapping {
120 command: "pr merge",
121 ops: &[
122 "merge_pull_request",
123 "enable_pull_request_auto_merge",
124 "disable_pull_request_auto_merge",
125 ],
126 },
127 Mapping {
128 command: "pr update-branch",
129 ops: &["update_pull_request_branch"],
130 },
131 Mapping {
132 command: "pr ready",
133 ops: &["mark_pull_request_ready", "convert_pull_request_to_draft", "upload_attachment"],
134 },
135 Mapping {
136 command: "pr comment",
137 ops: &["add_comment", "upload_attachment"],
138 },
139 Mapping {
140 command: "pr close",
141 ops: &["close_pull_request"],
142 },
143 Mapping {
144 command: "pr reopen",
145 ops: &["reopen_pull_request"],
146 },
147 Mapping {
148 command: "run list",
149 ops: &["list_workflow_runs"],
150 },
151 Mapping {
152 command: "run view",
153 ops: &["get_workflow_run", "get_job_logs"],
154 },
155 Mapping {
156 command: "run watch",
157 ops: &["get_workflow_run"],
158 },
159 Mapping {
160 command: "run logs",
161 ops: &["get_workflow_run", "get_job_logs"],
162 },
163 Mapping {
164 command: "run rerun",
165 ops: &["rerun_workflow_run", "rerun_failed_jobs", "rerun_job"],
166 },
167 Mapping {
168 command: "run cancel",
169 ops: &["cancel_workflow_run"],
170 },
171 Mapping {
172 command: "workflow list",
173 ops: &["list_workflows"],
174 },
175 Mapping {
176 command: "workflow view",
177 ops: &["list_workflows", "get_workflow", "list_workflow_runs"],
178 },
179 Mapping {
180 command: "workflow run",
181 ops: &["list_workflows", "dispatch_workflow", "list_workflow_runs"],
182 },
183 Mapping {
184 command: "workflow enable",
185 ops: &["list_workflows", "enable_workflow"],
186 },
187 Mapping {
188 command: "workflow disable",
189 ops: &["list_workflows", "disable_workflow"],
190 },
191 Mapping {
192 command: "secret list",
193 ops: &["list_actions_secrets", "list_actions_secrets_for_workspace"],
194 },
195 Mapping {
196 command: "secret set",
197 ops: &["set_actions_secret", "set_actions_secret_for_workspace"],
198 },
199 Mapping {
200 command: "secret delete",
201 ops: &[
202 "delete_actions_secret",
203 "delete_actions_secret_for_workspace",
204 ],
205 },
206 Mapping {
207 command: "cache list",
208 ops: &["list_caches"],
209 },
210 Mapping {
211 command: "cache usage",
212 ops: &["get_cache_usage"],
213 },
214 Mapping {
215 command: "cache delete",
216 ops: &["delete_cache", "delete_caches_by_key"],
217 },
218 Mapping {
219 command: "deploy list",
220 ops: &["list_deployments"],
221 },
222 Mapping {
223 command: "deploy view",
224 ops: &["get_deployment"],
225 },
226 // These find the current repository's project as `logs` does.
227 Mapping {
228 command: "deploy settings",
229 ops: &["get_deployment_settings", "update_deployment_settings"],
230 },
231 Mapping {
232 command: "deploy enable",
233 ops: &["update_deployment_settings"],
234 },
235 Mapping {
236 command: "deploy disable",
237 ops: &["update_deployment_settings"],
238 },
239 Mapping {
240 command: "logs",
241 ops: &["list_logs", "list_errors", "list_projects", "get_repo"],
242 },
243 Mapping {
244 command: "status",
245 ops: &[
246 "search",
247 "list_notifications",
248 "get_pull_request",
249 "list_sessions",
250 "list_claims",
251 ],
252 },
253 Mapping { command: "session start", ops: &["start_session"] },
254 Mapping { command: "session heartbeat", ops: &["update_session"] },
255 Mapping { command: "session log", ops: &["record_session_entries"] },
256 Mapping { command: "session attach", ops: &["update_session"] },
257 Mapping { command: "session end", ops: &["end_session"] },
258 Mapping { command: "session list", ops: &["list_sessions", "list_workspace_sessions"] },
259 Mapping { command: "session view", ops: &["get_session"] },
260 Mapping { command: "session updates", ops: &["get_session"] },
261 Mapping { command: "session acknowledge", ops: &["record_session_entries"] },
262 Mapping { command: "session touch", ops: &["record_session_entries"] },
263 Mapping { command: "work list", ops: &["list_work_items"] },
264 Mapping { command: "work view", ops: &["get_work", "get_work_item"] },
265 Mapping { command: "work new", ops: &["open_work_item", "update_work_item", "upload_attachment", "upload_work_item_attachment"] },
266 Mapping { command: "work comment", ops: &["add_work_item_comment", "upload_work_item_attachment"] },
267 Mapping { command: "work edit", ops: &["update_work_item", "get_work_item"] },
268 Mapping { command: "work close", ops: &["close_work_item", "add_work_item_comment"] },
269 Mapping { command: "work reopen", ops: &["reopen_work_item", "add_work_item_comment"] },
270 Mapping { command: "work link", ops: &["link_work_item"] },
271 Mapping { command: "work unlink", ops: &["unlink_work_item"] },
272 Mapping { command: "work claim", ops: &["claim_work_item", "claim_new_work_item"] },
273 Mapping { command: "work release", ops: &["release_claim"] },
274 Mapping { command: "work handoff", ops: &["hand_off_work_item"] },
275 Mapping { command: "work status", ops: &["list_claims", "list_sessions"] },
276 Mapping { command: "work block", ops: &["get_work_item", "update_work_item"] },
277 Mapping { command: "work unblock", ops: &["get_work_item", "update_work_item"] },
278 Mapping { command: "work events", ops: &["list_events"] },
279 Mapping { command: "work plan", ops: &["get_plan", "plan_work_item"] },
280 Mapping { command: "work next", ops: &["next_work_item", "claim_work_item"] },
281 Mapping { command: "work dispatch", ops: &["start_agent_session", "next_work_item"] },
282 Mapping { command: "why", ops: &["why"] },
283 Mapping { command: "attach", ops: &["upload_attachment", "upload_work_item_attachment"] },
284 // Every operation, by name: the table generated from the OpenAPI document.
285 Mapping {
286 command: "api",
287 ops: &[],
288 },
289 Mapping {
290 command: "browse",
291 ops: &["resolve_ref"],
292 },
293 Mapping {
294 command: "alias set",
295 ops: &[],
296 },
297 Mapping {
298 command: "alias list",
299 ops: &[],
300 },
301 Mapping {
302 command: "alias delete",
303 ops: &[],
304 },
305 Mapping {
306 command: "completion",
307 ops: &[],
308 },
309 // The registry's own protocol, not the REST API.
310 Mapping {
311 command: "push",
312 ops: &[],
313 },
314 // The site's downloads, not the REST API.
315 Mapping {
316 command: "upgrade",
317 ops: &[],
318 },
319];
320
321/// The first command that calls each operation, for `g1t api --list`.
322pub fn by_operation() -> BTreeMap<&'static str, &'static str> {
323 let mut map = BTreeMap::new();
324 for m in COMMANDS {
325 for op in m.ops {
326 map.entry(*op).or_insert(m.command);
327 }
328 }
329 map
330}
331
332#[cfg(test)]
333mod tests {
334 use super::*;
335 use crate::cli::Cli;
336 use clap::CommandFactory;
337 use std::collections::BTreeSet;
338
339 fn leaves(command: &clap::Command, prefix: &str, out: &mut Vec<String>) {
340 let subs: Vec<&clap::Command> = command
341 .get_subcommands()
342 .filter(|c| c.get_name() != "help")
343 .collect();
344 if subs.is_empty() {
345 out.push(prefix.trim().to_owned());
346 }
347 for sub in subs {
348 leaves(sub, &format!("{prefix} {}", sub.get_name()), out);
349 }
350 }
351
352 #[test]
353 fn every_mapped_operation_exists() {
354 for m in COMMANDS {
355 for op in m.ops {
356 assert!(
357 crate::ops::find(op).is_some(),
358 "`g1t {}` calls {op}, which the API no longer has",
359 m.command
360 );
361 }
362 }
363 }
364
365 #[test]
366 fn every_command_is_mapped_and_nothing_else() {
367 let mut tree = Vec::new();
368 leaves(&Cli::command(), "", &mut tree);
369 let tree: BTreeSet<String> = tree.into_iter().collect();
370 let mapped: BTreeSet<String> = COMMANDS.iter().map(|m| m.command.to_owned()).collect();
371 let missing: Vec<&String> = tree.difference(&mapped).collect();
372 let extra: Vec<&String> = mapped.difference(&tree).collect();
373 assert!(
374 missing.is_empty(),
375 "commands with no entry in mapping.rs: {missing:?}"
376 );
377 assert!(
378 extra.is_empty(),
379 "mapping.rs lists commands that do not exist: {extra:?}"
380 );
381 }
382
383 /// The operations a source file names, as string literals.
384 fn named_in(source: &str) -> BTreeSet<&'static str> {
385 let source = source.split("#[cfg(test)]").next().unwrap_or(source);
386 crate::ops::all()
387 .map(|op| op.id)
388 .filter(|id| source.contains(&format!("\"{id}\"")))
389 .collect()
390 }
391
392 #[test]
393 fn the_table_matches_what_the_code_calls() {
394 let sources: &[(&str, &str)] = &[
395 ("auth", include_str!("commands/auth.rs")),
396 ("repo", include_str!("commands/repo.rs")),
397 ("pr", include_str!("commands/pr.rs")),
398 ("run", include_str!("commands/run.rs")),
399 ("workflow", include_str!("commands/workflow.rs")),
400 ("secret", include_str!("commands/secret.rs")),
401 ("cache", include_str!("commands/cache.rs")),
402 ("deploy", include_str!("commands/deploy.rs")),
403 ("logs", include_str!("commands/logs.rs")),
404 ("status", include_str!("commands/status.rs")),
405 ("session", include_str!("commands/session.rs")),
406 ("work", include_str!("commands/work.rs")),
407 ("why", include_str!("commands/why.rs")),
408 ("attach", include_str!("commands/attach.rs")),
409 ("browse", include_str!("commands/browse.rs")),
410 ];
411 // What context.rs calls on every command's behalf, and what
412 // attach.rs uploads for --attach.
413 let shared: BTreeSet<&str> = named_in(include_str!("context.rs"))
414 .union(&named_in(include_str!("commands/attach.rs")))
415 .copied()
416 .collect();
417 for (top, source) in sources {
418 let named = named_in(source);
419 let listed: BTreeSet<&str> = COMMANDS
420 .iter()
421 .filter(|m| m.command == *top || m.command.starts_with(&format!("{top} ")))
422 .flat_map(|m| m.ops.iter().copied())
423 .collect();
424 let unlisted: Vec<&&str> = named.difference(&listed).collect();
425 let unused: Vec<&&str> = listed
426 .difference(&named)
427 .filter(|op| !shared.contains(**op))
428 .collect();
429 assert!(
430 unlisted.is_empty(),
431 "commands/{top}.rs calls {unlisted:?}, which mapping.rs does not list"
432 );
433 assert!(
434 unused.is_empty(),
435 "mapping.rs lists {unused:?} for `{top}`, which commands/{top}.rs does not call"
436 );
437 }
438 }
439
440 /// `g1t api <operation>` can make a request for every operation: each
441 /// path parameter can be filled from a field.
442 #[test]
443 fn every_operation_is_reachable_through_api() {
444 let repo = || {
445 Ok(crate::repo::Repo {
446 host: "g1t.sh".into(),
447 owner: "acme".into(),
448 name: "web".into(),
449 })
450 };
451 for op in crate::ops::OPERATIONS {
452 let mut given = serde_json::Map::new();
453 for p in op.path_params() {
454 given.insert(p.name.to_owned(), serde_json::Value::String("x".into()));
455 }
456 let plan = crate::commands::api::plan_op(op, None, given, &repo)
457 .unwrap_or_else(|e| panic!("{}: {}", op.id, e.message));
458 assert!(!plan.path.contains('{'), "{}: {}", op.id, plan.path);
459 assert_eq!(plan.method, op.method);
460 }
461 }
462}