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
406 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: "secret list",
176 ops: &["list_actions_secrets", "list_actions_secrets_for_workspace"],
177 },
178 Mapping {
179 command: "secret set",
180 ops: &["set_actions_secret", "set_actions_secret_for_workspace"],
181 },
182 Mapping {
183 command: "secret delete",
184 ops: &[
185 "delete_actions_secret",
186 "delete_actions_secret_for_workspace",
187 ],
188 },
189 Mapping {
190 command: "cache list",
191 ops: &["list_caches"],
192 },
193 Mapping {
194 command: "cache usage",
195 ops: &["get_cache_usage"],
196 },
197 Mapping {
198 command: "cache delete",
199 ops: &["delete_cache", "delete_caches_by_key"],
200 },
201 Mapping {
202 command: "deploy list",
203 ops: &["list_deployments"],
204 },
205 Mapping {
206 command: "deploy view",
207 ops: &["get_deployment"],
208 },
209 Mapping {
210 command: "logs",
211 ops: &["list_logs", "list_errors", "list_projects", "get_repo"],
212 },
213 Mapping {
214 command: "status",
215 ops: &[
216 "search",
217 "list_notifications",
218 "get_pull_request",
219 "list_sessions",
220 "list_claims",
221 ],
222 },
223 Mapping { command: "session start", ops: &["start_session"] },
224 Mapping { command: "session heartbeat", ops: &["update_session"] },
225 Mapping { command: "session log", ops: &["record_session_entries"] },
226 Mapping { command: "session attach", ops: &["update_session"] },
227 Mapping { command: "session end", ops: &["end_session"] },
228 Mapping { command: "session list", ops: &["list_sessions"] },
229 Mapping { command: "session view", ops: &["get_session"] },
230 Mapping { command: "work context", ops: &["get_work"] },
231 Mapping { command: "work claim", ops: &["claim_issue"] },
232 Mapping { command: "work release", ops: &["release_claim"] },
233 Mapping { command: "work handoff", ops: &["hand_off_issue"] },
234 Mapping { command: "work list", ops: &["list_claims", "list_sessions"] },
235 Mapping { command: "work block", ops: &["get_issue", "update_issue"] },
236 Mapping { command: "work unblock", ops: &["get_issue", "update_issue"] },
237 Mapping { command: "work events", ops: &["list_events"] },
238 Mapping { command: "why", ops: &["why"] },
239 // Every operation, by name: the table generated from the OpenAPI document.
240 Mapping {
241 command: "api",
242 ops: &[],
243 },
244 Mapping {
245 command: "browse",
246 ops: &[],
247 },
248 Mapping {
249 command: "alias set",
250 ops: &[],
251 },
252 Mapping {
253 command: "alias list",
254 ops: &[],
255 },
256 Mapping {
257 command: "alias delete",
258 ops: &[],
259 },
260 Mapping {
261 command: "completion",
262 ops: &[],
263 },
264 // The registry's own protocol, not the REST API.
265 Mapping {
266 command: "push",
267 ops: &[],
268 },
269];
270
271/// The first command that calls each operation, for `g1t api --list`.
272pub fn by_operation() -> BTreeMap<&'static str, &'static str> {
273 let mut map = BTreeMap::new();
274 for m in COMMANDS {
275 for op in m.ops {
276 map.entry(*op).or_insert(m.command);
277 }
278 }
279 map
280}
281
282#[cfg(test)]
283mod tests {
284 use super::*;
285 use crate::cli::Cli;
286 use clap::CommandFactory;
287 use std::collections::BTreeSet;
288
289 fn leaves(command: &clap::Command, prefix: &str, out: &mut Vec<String>) {
290 let subs: Vec<&clap::Command> = command
291 .get_subcommands()
292 .filter(|c| c.get_name() != "help")
293 .collect();
294 if subs.is_empty() {
295 out.push(prefix.trim().to_owned());
296 }
297 for sub in subs {
298 leaves(sub, &format!("{prefix} {}", sub.get_name()), out);
299 }
300 }
301
302 #[test]
303 fn every_mapped_operation_exists() {
304 for m in COMMANDS {
305 for op in m.ops {
306 assert!(
307 crate::ops::find(op).is_some(),
308 "`g1t {}` calls {op}, which the API no longer has",
309 m.command
310 );
311 }
312 }
313 }
314
315 #[test]
316 fn every_command_is_mapped_and_nothing_else() {
317 let mut tree = Vec::new();
318 leaves(&Cli::command(), "", &mut tree);
319 let tree: BTreeSet<String> = tree.into_iter().collect();
320 let mapped: BTreeSet<String> = COMMANDS.iter().map(|m| m.command.to_owned()).collect();
321 let missing: Vec<&String> = tree.difference(&mapped).collect();
322 let extra: Vec<&String> = mapped.difference(&tree).collect();
323 assert!(
324 missing.is_empty(),
325 "commands with no entry in mapping.rs: {missing:?}"
326 );
327 assert!(
328 extra.is_empty(),
329 "mapping.rs lists commands that do not exist: {extra:?}"
330 );
331 }
332
333 /// The operations a source file names, as string literals.
334 fn named_in(source: &str) -> BTreeSet<&'static str> {
335 let source = source.split("#[cfg(test)]").next().unwrap_or(source);
336 crate::ops::all()
337 .map(|op| op.id)
338 .filter(|id| source.contains(&format!("\"{id}\"")))
339 .collect()
340 }
341
342 #[test]
343 fn the_table_matches_what_the_code_calls() {
344 let sources: &[(&str, &str)] = &[
345 ("auth", include_str!("commands/auth.rs")),
346 ("repo", include_str!("commands/repo.rs")),
347 ("issue", include_str!("commands/issue.rs")),
348 ("pr", include_str!("commands/pr.rs")),
349 ("run", include_str!("commands/run.rs")),
350 ("secret", include_str!("commands/secret.rs")),
351 ("cache", include_str!("commands/cache.rs")),
352 ("deploy", include_str!("commands/deploy.rs")),
353 ("logs", include_str!("commands/logs.rs")),
354 ("status", include_str!("commands/status.rs")),
355 ("session", include_str!("commands/session.rs")),
356 ("work", include_str!("commands/work.rs")),
357 ("why", include_str!("commands/why.rs")),
358 ];
359 // What context.rs calls on every command's behalf.
360 let shared: BTreeSet<&str> = named_in(include_str!("context.rs"));
361 for (top, source) in sources {
362 let named = named_in(source);
363 let listed: BTreeSet<&str> = COMMANDS
364 .iter()
365 .filter(|m| m.command == *top || m.command.starts_with(&format!("{top} ")))
366 .flat_map(|m| m.ops.iter().copied())
367 .collect();
368 let unlisted: Vec<&&str> = named.difference(&listed).collect();
369 let unused: Vec<&&str> = listed
370 .difference(&named)
371 .filter(|op| !shared.contains(**op))
372 .collect();
373 assert!(
374 unlisted.is_empty(),
375 "commands/{top}.rs calls {unlisted:?}, which mapping.rs does not list"
376 );
377 assert!(
378 unused.is_empty(),
379 "mapping.rs lists {unused:?} for `{top}`, which commands/{top}.rs does not call"
380 );
381 }
382 }
383
384 /// `g1t api <operation>` can make a request for every operation: each
385 /// path parameter can be filled from a field.
386 #[test]
387 fn every_operation_is_reachable_through_api() {
388 let repo = || {
389 Ok(crate::repo::Repo {
390 host: "g1t.sh".into(),
391 owner: "acme".into(),
392 name: "web".into(),
393 })
394 };
395 for op in crate::ops::OPERATIONS {
396 let mut given = serde_json::Map::new();
397 for p in op.path_params() {
398 given.insert(p.name.to_owned(), serde_json::Value::String("x".into()));
399 }
400 let plan = crate::commands::api::plan_op(op, None, given, &repo)
401 .unwrap_or_else(|e| panic!("{}: {}", op.id, e.message));
402 assert!(!plan.path.contains('{'), "{}: {}", op.id, plan.path);
403 assert_eq!(plan.method, op.method);
404 }
405 }
406}