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
1,600 linesCodeBlameRaw
1//! The command line's shape: every command, flag and argument.
2
3use clap::{Args, Parser, Subcommand, ValueEnum};
4use std::path::PathBuf;
5
6pub const ABOUT: &str = "Work with g1t from the command line.";
7
8pub const AFTER_HELP: &str = "\
9Examples:
10 g1t auth login
11 g1t issue list --label bug
12 g1t pr create --fill
13 g1t pr checks --watch --fail-fast
14 g1t run logs --failed
15 g1t workflow run release.yml -f channel=stable
16 g1t api list_issues --jq '.[].title'
17
18Environment:
19 G1T_TOKEN A token to use instead of the stored one
20 G1T_HOST The host to talk to when no repository names one
21 G1T_REPO The repository, as OWNER/NAME, instead of the git remote
22 NO_COLOR Plain output on a terminal too
23
24Exit codes: 0 done, 1 failed, 2 used wrongly, 4 sign-in needed, 8 checks pending.
25Docs: https://docs.g1t.sh/guides/cli/";
26
27#[derive(Debug, Parser)]
28#[command(
29 name = "g1t",
30 version,
31 about = ABOUT,
32 after_help = AFTER_HELP,
33 propagate_version = true,
34 max_term_width = 100,
35 arg_required_else_help = true
36)]
37pub struct Cli {
38 #[command(flatten)]
39 pub global: Global,
40 #[command(subcommand)]
41 pub command: Command,
42}
43
44#[derive(Debug, Args, Clone, Default)]
45pub struct Global {
46 /// The repository, as [HOST/]OWNER/NAME, instead of the current directory's.
47 #[arg(
48 short = 'R',
49 long,
50 global = true,
51 value_name = "[HOST/]OWNER/NAME",
52 env = "G1T_REPO"
53 )]
54 pub repo: Option<String>,
55 /// The g1t host, such as g1t.sh or git.example.com.
56 #[arg(long, global = true, value_name = "HOST")]
57 pub hostname: Option<String>,
58 /// Print JSON: every field, or only the comma-separated FIELDS.
59 #[arg(
60 long,
61 global = true,
62 value_name = "FIELDS",
63 num_args = 0..=1,
64 default_missing_value = "",
65 help_heading = "Output"
66 )]
67 pub json: Option<String>,
68 /// Run a jq expression over the JSON and print what it gives.
69 #[arg(
70 long,
71 global = true,
72 value_name = "EXPRESSION",
73 help_heading = "Output"
74 )]
75 pub jq: Option<String>,
76}
77
78#[derive(Debug, Subcommand)]
79pub enum Command {
80 /// Sign in, sign out, and set git up to use g1t's credentials.
81 #[command(subcommand)]
82 Auth(AuthCommand),
83 /// Repositories: view, clone, create, list.
84 #[command(subcommand)]
85 Repo(RepoCommand),
86 /// Issues: list, view, create, comment, close, reopen, edit.
87 #[command(subcommand)]
88 Issue(IssueCommand),
89 /// Pull requests: list, view, create, check out, diff, checks, review, merge.
90 #[command(subcommand)]
91 Pr(PrCommand),
92 /// Workflow runs: list, view, watch, logs, rerun, cancel.
93 #[command(subcommand)]
94 Run(RunCommand),
95 /// Workflows in .g1t/workflows: list, view, run, enable, disable.
96 #[command(subcommand)]
97 Workflow(WorkflowCommand),
98 /// Secrets for workflows and deployments, a repository's or a workspace's.
99 #[command(subcommand)]
100 Secret(SecretCommand),
101 /// The repository's actions/cache entries: list, usage, delete.
102 #[command(subcommand)]
103 Cache(CacheCommand),
104 /// Deployments: list and view.
105 #[command(subcommand)]
106 Deploy(DeployCommand),
107 /// A project's logs and errors.
108 Logs(LogsArgs),
109 /// What needs you: your pull requests, reviews asked of you, your issues, failing checks, and your sessions and claims here.
110 Status(StatusArgs),
111 /// Sessions: one run of an agent's harness, recorded as it works.
112 #[command(subcommand)]
113 Session(SessionCommand),
114 /// Coordinate work on issues: context, claims, hand-offs, dependencies, events.
115 #[command(subcommand)]
116 Work(WorkCommand),
117 /// Why a file or line is as it is: its commit, pull request, issue, decisions and sessions.
118 Why(WhyArgs),
119 /// Call any API operation or path, with your token.
120 Api(ApiArgs),
121 /// Open the repository, an issue or a pull request in the browser.
122 Browse(BrowseArgs),
123 /// Shortcuts for commands you type often.
124 #[command(subcommand)]
125 Alias(AliasCommand),
126 /// Print a shell completion script.
127 Completion(CompletionArgs),
128 /// Push a local container image to g1t's registry, in chunks.
129 Push(PushArgs),
130}
131
132// --- auth -------------------------------------------------------------
133
134#[derive(Debug, Subcommand)]
135pub enum AuthCommand {
136 /// Sign in to a g1t host.
137 #[command(after_help = "\
138Without --with-token, g1t shows a code and a link; approve it in the browser, signed in.
139With --with-token, the token is read from stdin:
140 g1t auth login --with-token < token.txt")]
141 Login(LoginArgs),
142 /// Sign out of an account on a host.
143 Logout(AccountArgs),
144 /// Show which accounts are signed in, and whether their tokens work.
145 Status(AuthStatusArgs),
146 /// Print the token in use, for another program.
147 Token(AccountArgs),
148 /// Make another signed-in account on a host the active one.
149 Switch(AccountArgs),
150 /// Make git use g1t's credentials for pushing and fetching over HTTPS.
151 SetupGit(SetupGitArgs),
152 /// The git credential helper `setup-git` installs. Git runs it.
153 #[command(hide = true)]
154 GitCredential(GitCredentialArgs),
155}
156
157#[derive(Debug, Args)]
158pub struct LoginArgs {
159 /// Read a token from stdin instead of approving in the browser.
160 #[arg(long)]
161 pub with_token: bool,
162 /// Keep the token in the config file, not the system keychain.
163 #[arg(long)]
164 pub insecure_storage: bool,
165 /// Do not open the browser; just print the link.
166 #[arg(long)]
167 pub no_browser: bool,
168 /// Also set git up to use g1t's credentials (see `g1t auth setup-git`).
169 #[arg(long)]
170 pub git: bool,
171}
172
173#[derive(Debug, Args)]
174pub struct AccountArgs {
175 /// The account, when more than one is signed in on the host.
176 #[arg(short, long)]
177 pub user: Option<String>,
178}
179
180#[derive(Debug, Args)]
181pub struct AuthStatusArgs {
182 /// Print each token too.
183 #[arg(long)]
184 pub show_token: bool,
185}
186
187#[derive(Debug, Args)]
188pub struct SetupGitArgs {
189 /// Print the git config commands instead of running them.
190 #[arg(long)]
191 pub dry_run: bool,
192}
193
194#[derive(Debug, Args)]
195pub struct GitCredentialArgs {
196 /// get, store or erase, as git says.
197 pub operation: String,
198}
199
200// --- repo -------------------------------------------------------------
201
202#[derive(Debug, Subcommand)]
203pub enum RepoCommand {
204 /// Show a repository.
205 View(RepoViewArgs),
206 /// Clone a repository.
207 Clone(RepoCloneArgs),
208 /// Make a repository.
209 Create(RepoCreateArgs),
210 /// List repositories you can see.
211 List(RepoListArgs),
212}
213
214#[derive(Debug, Args)]
215pub struct RepoViewArgs {
216 /// OWNER/NAME; the current repository when left out.
217 pub repository: Option<String>,
218 /// Open it in the browser.
219 #[arg(short, long)]
220 pub web: bool,
221}
222
223#[derive(Debug, Args)]
224pub struct RepoCloneArgs {
225 /// OWNER/NAME, or NAME for one of your own.
226 pub repository: String,
227 /// Where to clone it.
228 pub directory: Option<String>,
229 /// Flags for git clone, after --.
230 #[arg(last = true)]
231 pub git_flags: Vec<String>,
232}
233
234#[derive(Debug, Args)]
235pub struct RepoCreateArgs {
236 /// NAME, or WORKSPACE/NAME.
237 pub name: Option<String>,
238 /// Anyone can see it. Repositories are private unless this is given.
239 #[arg(long, conflicts_with = "private")]
240 pub public: bool,
241 /// Only people with a role on it can see it (the default).
242 #[arg(long)]
243 pub private: bool,
244 #[arg(short, long)]
245 pub description: Option<String>,
246 /// Clone it once it is made.
247 #[arg(short, long)]
248 pub clone: bool,
249 /// Add it as a remote of this directory's repository and push it there.
250 #[arg(long, value_name = "DIR")]
251 pub source: Option<String>,
252 /// The remote's name, with --source.
253 #[arg(long, default_value = "origin")]
254 pub remote: String,
255}
256
257#[derive(Debug, Args)]
258pub struct RepoListArgs {
259 /// Only this workspace's.
260 pub workspace: Option<String>,
261 /// Only those whose name or description holds this.
262 #[arg(short, long)]
263 pub query: Option<String>,
264 #[arg(short = 'L', long, default_value_t = 20)]
265 pub limit: usize,
266}
267
268// --- issue ------------------------------------------------------------
269
270#[derive(Debug, Subcommand)]
271pub enum IssueCommand {
272 /// List issues.
273 List(IssueListArgs),
274 /// Show an issue, with its comments.
275 View(IssueViewArgs),
276 /// Open an issue.
277 Create(IssueCreateArgs),
278 /// Comment on an issue.
279 Comment(CommentArgs),
280 /// Close an issue.
281 Close(IssueCloseArgs),
282 /// Reopen an issue.
283 Reopen(ReopenArgs),
284 /// Change an issue's title, body, labels, assignees or milestone.
285 Edit(IssueEditArgs),
286}
287
288#[derive(Clone, Copy, Debug, PartialEq, Eq, ValueEnum)]
289pub enum IssueState {
290 Open,
291 Closed,
292 All,
293}
294
295#[derive(Debug, Args)]
296pub struct IssueListArgs {
297 #[arg(short, long, value_enum, default_value_t = IssueState::Open)]
298 pub state: IssueState,
299 /// Only issues with this label.
300 #[arg(short, long)]
301 pub label: Option<String>,
302 /// Only issues in the milestone of this number.
303 #[arg(short, long)]
304 pub milestone: Option<u64>,
305 /// Only issues assigned to this person (@me for you).
306 #[arg(short, long)]
307 pub assignee: Option<String>,
308 /// Only issues opened by this person (@me for you).
309 #[arg(short = 'A', long)]
310 pub author: Option<String>,
311 #[arg(short = 'L', long, default_value_t = 20)]
312 pub limit: usize,
313}
314
315#[derive(Debug, Args)]
316pub struct IssueViewArgs {
317 /// The issue's number.
318 pub number: u64,
319 /// Show its comments too.
320 #[arg(short, long)]
321 pub comments: bool,
322 /// Open it in the browser.
323 #[arg(short, long)]
324 pub web: bool,
325}
326
327#[derive(Debug, Args)]
328pub struct BodyArgs {
329 /// The text. Markdown.
330 #[arg(short, long)]
331 pub body: Option<String>,
332 /// Read the text from a file, or - for stdin.
333 #[arg(short = 'F', long, value_name = "FILE")]
334 pub body_file: Option<String>,
335}
336
337#[derive(Debug, Args)]
338pub struct IssueCreateArgs {
339 #[arg(short, long)]
340 pub title: Option<String>,
341 #[command(flatten)]
342 pub body: BodyArgs,
343 /// A label; repeat for more.
344 #[arg(short, long)]
345 pub label: Vec<String>,
346 /// Someone to assign; repeat for more (@me for you).
347 #[arg(short, long)]
348 pub assignee: Vec<String>,
349 /// A milestone's number.
350 #[arg(short, long)]
351 pub milestone: Option<u64>,
352 /// Open the new-issue page in the browser instead.
353 #[arg(short, long)]
354 pub web: bool,
355}
356
357#[derive(Debug, Args)]
358pub struct CommentArgs {
359 /// The number; for a pull request, the current branch's when left out.
360 pub number: Option<u64>,
361 #[command(flatten)]
362 pub body: BodyArgs,
363}
364
365#[derive(Clone, Copy, Debug, PartialEq, Eq, ValueEnum)]
366pub enum CloseReason {
367 Completed,
368 // Spelled as the API spells it.
369 #[value(name = "not_planned", alias = "not-planned")]
370 NotPlanned,
371}
372
373#[derive(Debug, Args)]
374pub struct IssueCloseArgs {
375 pub number: u64,
376 #[arg(short, long, value_enum)]
377 pub reason: Option<CloseReason>,
378 /// Leave a comment as it closes.
379 #[arg(short, long)]
380 pub comment: Option<String>,
381}
382
383#[derive(Debug, Args)]
384pub struct ReopenArgs {
385 pub number: u64,
386 /// Leave a comment as it reopens.
387 #[arg(short, long)]
388 pub comment: Option<String>,
389}
390
391#[derive(Debug, Args)]
392pub struct IssueEditArgs {
393 pub number: u64,
394 #[arg(short, long)]
395 pub title: Option<String>,
396 #[command(flatten)]
397 pub body: BodyArgs,
398 #[arg(long, value_name = "LABEL")]
399 pub add_label: Vec<String>,
400 #[arg(long, value_name = "LABEL")]
401 pub remove_label: Vec<String>,
402 #[arg(long, value_name = "LOGIN")]
403 pub add_assignee: Vec<String>,
404 #[arg(long, value_name = "LOGIN")]
405 pub remove_assignee: Vec<String>,
406 /// A milestone's number.
407 #[arg(short, long)]
408 pub milestone: Option<u64>,
409 /// Take it out of its milestone.
410 #[arg(long, conflicts_with = "milestone")]
411 pub remove_milestone: bool,
412}
413
414// --- pr ---------------------------------------------------------------
415
416#[derive(Debug, Subcommand)]
417pub enum PrCommand {
418 /// List pull requests.
419 List(PrListArgs),
420 /// Show a pull request.
421 View(PrViewArgs),
422 /// Open a pull request from a branch, or start one for an issue.
423 Create(PrCreateArgs),
424 /// Check a pull request's changes out locally.
425 Checkout(PrCheckoutArgs),
426 /// Show a pull request's changes as a diff.
427 Diff(PrDiffArgs),
428 /// Show a pull request's checks; --watch waits for them.
429 Checks(PrChecksArgs),
430 /// Approve a pull request, or ask for changes.
431 Review(PrReviewArgs),
432 /// Merge a pull request.
433 Merge(PrMergeArgs),
434 /// Mark a draft ready for review, or turn it back into a draft.
435 Ready(PrReadyArgs),
436 /// Comment on a pull request.
437 Comment(CommentArgs),
438 /// Close a pull request without merging it.
439 Close(PrSelect),
440 /// Reopen a closed pull request.
441 Reopen(PrSelect),
442}
443
444#[derive(Debug, Args)]
445pub struct PrSelect {
446 /// The number; the current branch's when left out.
447 pub number: Option<u64>,
448}
449
450#[derive(Clone, Copy, Debug, PartialEq, Eq, ValueEnum)]
451pub enum PrState {
452 Open,
453 Closed,
454 Merged,
455 All,
456}
457
458#[derive(Debug, Args)]
459pub struct PrListArgs {
460 #[arg(short, long, value_enum, default_value_t = PrState::Open)]
461 pub state: PrState,
462 /// Only those merging into this branch.
463 #[arg(short = 'B', long)]
464 pub base: Option<String>,
465 #[arg(short, long)]
466 pub label: Option<String>,
467 /// Only those opened by this person (@me for you).
468 #[arg(short = 'A', long)]
469 pub author: Option<String>,
470 /// Only drafts.
471 #[arg(short, long)]
472 pub draft: bool,
473 #[arg(short = 'L', long, default_value_t = 20)]
474 pub limit: usize,
475}
476
477#[derive(Debug, Args)]
478pub struct PrViewArgs {
479 /// The number; the current branch's when left out.
480 pub number: Option<u64>,
481 #[arg(short, long)]
482 pub comments: bool,
483 #[arg(short, long)]
484 pub web: bool,
485}
486
487#[derive(Debug, Args)]
488pub struct PrCreateArgs {
489 #[arg(short, long)]
490 pub title: Option<String>,
491 #[command(flatten)]
492 pub body: BodyArgs,
493 /// The branch to merge into; the default branch when left out.
494 #[arg(short = 'B', long)]
495 pub base: Option<String>,
496 /// The branch with the changes; the current one when left out.
497 #[arg(short = 'H', long)]
498 pub head: Option<String>,
499 /// The issue it is for. Without a branch, starts a draft in a working copy of its own.
500 #[arg(short, long)]
501 pub issue: Option<u64>,
502 /// Take the title and body from the branch's commits.
503 #[arg(short, long)]
504 pub fill: bool,
505 /// Open it as a draft.
506 #[arg(short, long)]
507 pub draft: bool,
508 /// Do not push the branch first.
509 #[arg(long)]
510 pub no_push: bool,
511}
512
513#[derive(Debug, Args)]
514pub struct PrCheckoutArgs {
515 pub number: u64,
516 /// The local branch's name; pr-NUMBER when left out.
517 #[arg(short, long)]
518 pub branch: Option<String>,
519 /// Reset the local branch to the pull request's head even if it has other commits.
520 #[arg(short, long)]
521 pub force: bool,
522}
523
524#[derive(Debug, Args)]
525pub struct PrDiffArgs {
526 pub number: Option<u64>,
527 /// Only the changed files' names.
528 #[arg(long)]
529 pub name_only: bool,
530 /// Color: auto, always or never.
531 #[arg(long, default_value = "auto")]
532 pub color: String,
533}
534
535#[derive(Debug, Args)]
536pub struct PrChecksArgs {
537 pub number: Option<u64>,
538 /// Wait until every check has finished.
539 #[arg(short, long)]
540 pub watch: bool,
541 /// With --watch, stop at the first failing check.
542 #[arg(long, requires = "watch")]
543 pub fail_fast: bool,
544 /// Seconds between looks, with --watch.
545 #[arg(short, long, default_value_t = 10)]
546 pub interval: u64,
547}
548
549#[derive(Debug, Args)]
550#[command(group(clap::ArgGroup::new("verdict").required(true).args(["approve", "request_changes", "comment"])))]
551pub struct PrReviewArgs {
552 pub number: Option<u64>,
553 #[arg(short, long)]
554 pub approve: bool,
555 #[arg(short, long)]
556 pub request_changes: bool,
557 /// Comment without approving or asking for changes.
558 #[arg(short, long)]
559 pub comment: bool,
560 #[command(flatten)]
561 pub body: BodyArgs,
562}
563
564#[derive(Debug, Args)]
565pub struct PrMergeArgs {
566 pub number: Option<u64>,
567 /// Leave the issue it is for open.
568 #[arg(long)]
569 pub keep_issue_open: bool,
570 /// Merge past failing required checks and rules (needs the Admin role).
571 #[arg(long)]
572 pub admin: bool,
573 /// Delete the local and remote branch afterwards.
574 #[arg(short, long)]
575 pub delete_branch: bool,
576}
577
578#[derive(Debug, Args)]
579pub struct PrReadyArgs {
580 pub number: Option<u64>,
581 /// What changed and why; it becomes the description. The current one when left out.
582 #[arg(short, long)]
583 pub summary: Option<String>,
584 /// Turn it back into a draft.
585 #[arg(long)]
586 pub undo: bool,
587}
588
589// --- run --------------------------------------------------------------
590
591#[derive(Debug, Subcommand)]
592pub enum RunCommand {
593 /// List workflow runs, newest first.
594 List(RunListArgs),
595 /// Show a run's jobs and steps.
596 View(RunViewArgs),
597 /// Follow a run until it finishes; exits 1 if it fails.
598 Watch(RunWatchArgs),
599 /// Print a run's logs; --failed prints only the failing steps' tails.
600 Logs(RunLogsArgs),
601 /// Run a run again: all of it, its failed jobs, or one job.
602 Rerun(RunRerunArgs),
603 /// Cancel a run.
604 Cancel(RunSelect),
605}
606
607#[derive(Debug, Args)]
608pub struct RunSelect {
609 /// The run's id (run_…) or number; the current branch's latest when left out.
610 pub run: Option<String>,
611}
612
613#[derive(Debug, Args)]
614pub struct RunListArgs {
615 /// Only this workflow's: its file name or id.
616 #[arg(short, long)]
617 pub workflow: Option<String>,
618 #[arg(short, long)]
619 pub branch: Option<String>,
620 /// Only runs for this event: push, pull_request, schedule...
621 #[arg(short, long)]
622 pub event: Option<String>,
623 /// Only runs of this commit.
624 #[arg(short, long)]
625 pub commit: Option<String>,
626 /// Only runs for this pull request.
627 #[arg(long)]
628 pub pr: Option<u64>,
629 #[arg(short = 'L', long, default_value_t = 20)]
630 pub limit: usize,
631}
632
633#[derive(Debug, Args)]
634pub struct RunViewArgs {
635 pub run: Option<String>,
636 /// Show this job's steps only.
637 #[arg(short, long)]
638 pub job: Option<String>,
639 /// Print the log too.
640 #[arg(long)]
641 pub log: bool,
642 /// Print the failing steps' logs too.
643 #[arg(long)]
644 pub log_failed: bool,
645 #[arg(short, long)]
646 pub web: bool,
647 /// Exit 1 when the run failed.
648 #[arg(long)]
649 pub exit_status: bool,
650}
651
652#[derive(Debug, Args)]
653pub struct RunWatchArgs {
654 pub run: Option<String>,
655 /// Seconds between looks.
656 #[arg(short, long, default_value_t = 3)]
657 pub interval: u64,
658}
659
660#[derive(Debug, Args)]
661pub struct RunLogsArgs {
662 pub run: Option<String>,
663 /// Only this job's (its id, key or name).
664 #[arg(short, long)]
665 pub job: Option<String>,
666 /// Only the failing steps, and only their last lines.
667 #[arg(short, long)]
668 pub failed: bool,
669 /// How many lines of each failing step to print, with --failed.
670 #[arg(short = 'n', long, default_value_t = 60)]
671 pub tail: usize,
672}
673
674#[derive(Debug, Args)]
675pub struct RunRerunArgs {
676 pub run: Option<String>,
677 /// Only the jobs that failed.
678 #[arg(long)]
679 pub failed: bool,
680 /// Only this job (its id).
681 #[arg(short, long, conflicts_with = "failed")]
682 pub job: Option<String>,
683 /// With debug logging.
684 #[arg(short, long)]
685 pub debug: bool,
686}
687
688// --- workflow ---------------------------------------------------------
689
690#[derive(Debug, Subcommand)]
691pub enum WorkflowCommand {
692 /// List the workflows in .g1t/workflows, with whether each is on.
693 List(WorkflowListArgs),
694 /// Show a workflow and its latest runs; --yaml prints its file.
695 View(WorkflowViewArgs),
696 /// Run a workflow that has `on: workflow_dispatch`, and print its run's address.
697 #[command(after_help = "\
698Inputs are given with -f (or -F, where @FILE reads a file), or as a JSON object on
699stdin with --json. At a terminal, with none given, g1t asks for each input.
700
701Examples:
702 g1t workflow run release.yml
703 g1t workflow run release.yml --ref v1.4.0 -f channel=stable -f dry_run=true
704 echo '{\"channel\":\"stable\"}' | g1t workflow run Release --json")]
705 Run(WorkflowRunArgs),
706 /// Turn a workflow on again.
707 Enable(WorkflowSelect),
708 /// Turn a workflow off: its file stays, and nothing starts it until it is turned on.
709 Disable(WorkflowSelect),
710}
711
712#[derive(Debug, Args)]
713pub struct WorkflowSelect {
714 /// Its file name (ci.yml), id (wfl_…) or name; asked for at a terminal when left out.
715 pub workflow: Option<String>,
716}
717
718#[derive(Debug, Args)]
719pub struct WorkflowListArgs {
720 /// Include the workflows that are turned off.
721 #[arg(short, long)]
722 pub all: bool,
723 #[arg(short = 'L', long, default_value_t = 50)]
724 pub limit: usize,
725}
726
727#[derive(Debug, Args)]
728pub struct WorkflowViewArgs {
729 /// Its file name (ci.yml), id (wfl_…) or name; asked for at a terminal when left out.
730 pub workflow: Option<String>,
731 /// Print the workflow's file.
732 #[arg(short, long)]
733 pub yaml: bool,
734 /// The branch or tag to read the file on, with --yaml; the default branch when left out.
735 #[arg(short, long = "ref", value_name = "REF", requires = "yaml")]
736 pub git_ref: Option<String>,
737 /// Open it in the browser.
738 #[arg(short, long, conflicts_with = "yaml")]
739 pub web: bool,
740}
741
742#[derive(Debug, Args)]
743pub struct WorkflowRunArgs {
744 /// Its file name (ci.yml), id (wfl_…) or name; asked for at a terminal when left out.
745 pub workflow: Option<String>,
746 /// The branch or tag to run it on; the default branch when left out.
747 #[arg(short, long = "ref", value_name = "REF")]
748 pub git_ref: Option<String>,
749 /// An input, key=value; repeat for more.
750 #[arg(short = 'f', long = "raw-field", value_name = "KEY=VALUE")]
751 pub raw_field: Vec<String>,
752 /// An input, key=value, where @FILE reads the value from a file (@- from stdin).
753 #[arg(short = 'F', long = "field", value_name = "KEY=VALUE")]
754 pub field: Vec<String>,
755}
756
757// --- secret -----------------------------------------------------------
758
759#[derive(Debug, Subcommand)]
760pub enum SecretCommand {
761 /// List secrets' names (never their values).
762 List(SecretScope),
763 /// Set a secret. The value is read from stdin when not given.
764 Set(SecretSetArgs),
765 /// Delete a secret.
766 Delete(SecretDeleteArgs),
767}
768
769#[derive(Debug, Args)]
770pub struct SecretScope {
771 /// A workspace's secrets instead of the repository's.
772 #[arg(short, long, value_name = "WORKSPACE")]
773 pub workspace: Option<String>,
774 /// Only the value for this environment (production, preview...).
775 #[arg(short, long, value_name = "ENVIRONMENT")]
776 pub env: Option<String>,
777}
778
779#[derive(Debug, Args)]
780pub struct SecretSetArgs {
781 /// The secret's name; uppercased.
782 pub name: String,
783 /// The value. Read from stdin when left out, so it stays out of your shell's history.
784 #[arg(short, long)]
785 pub body: Option<String>,
786 #[command(flatten)]
787 pub scope: SecretScope,
788 /// Who can read it: workflows, deployments, or both (repeat).
789 #[arg(long, value_name = "WHO")]
790 pub available_to: Vec<String>,
791 /// Broker it: jobs get a stand-in, and g1t fills the value in only on requests to this host. Repeat for more.
792 #[arg(long, value_name = "HOST")]
793 pub brokered_host: Vec<String>,
794 /// A note for whoever reads the list.
795 #[arg(long)]
796 pub note: Option<String>,
797}
798
799#[derive(Debug, Args)]
800pub struct SecretDeleteArgs {
801 pub name: String,
802 #[command(flatten)]
803 pub scope: SecretScope,
804}
805
806// --- cache ------------------------------------------------------------
807
808#[derive(Debug, Subcommand)]
809pub enum CacheCommand {
810 /// List the repository's cache entries, least recently used last.
811 List(CacheListArgs),
812 /// What the repository's cache entries hold, against its quota.
813 Usage,
814 /// Delete cache entries: one by its id, or every entry with a key.
815 Delete(CacheDeleteArgs),
816}
817
818#[derive(Clone, Copy, Debug, PartialEq, Eq, ValueEnum)]
819pub enum CacheSort {
820 /// When it was last restored or looked up.
821 LastAccessedAt,
822 CreatedAt,
823 SizeInBytes,
824}
825
826#[derive(Clone, Copy, Debug, PartialEq, Eq, ValueEnum)]
827pub enum SortOrder {
828 Asc,
829 Desc,
830}
831
832#[derive(Debug, Args)]
833pub struct CacheListArgs {
834 /// Only entries whose key starts with this.
835 #[arg(short, long)]
836 pub key: Option<String>,
837 /// Only entries saved by this ref's runs, e.g. refs/heads/main.
838 #[arg(short, long = "ref", value_name = "REF")]
839 pub git_ref: Option<String>,
840 #[arg(short = 'S', long, value_enum, default_value_t = CacheSort::LastAccessedAt)]
841 pub sort: CacheSort,
842 #[arg(short = 'O', long, value_enum, default_value_t = SortOrder::Desc)]
843 pub order: SortOrder,
844 #[arg(short = 'L', long, default_value_t = 30)]
845 pub limit: usize,
846}
847
848#[derive(Debug, Args)]
849pub struct CacheDeleteArgs {
850 /// An entry's id (a number, from `g1t cache list`), or a key: every entry with exactly that key.
851 pub entry: String,
852 /// With a key, only the entries this ref's runs saved.
853 #[arg(short, long = "ref", value_name = "REF")]
854 pub git_ref: Option<String>,
855}
856
857// --- deploy -----------------------------------------------------------
858
859#[derive(Debug, Subcommand)]
860pub enum DeployCommand {
861 /// List deployments, newest first.
862 List(DeployListArgs),
863 /// Show a deployment and its statuses.
864 View(DeployViewArgs),
865}
866
867#[derive(Debug, Args)]
868pub struct DeployListArgs {
869 #[arg(short, long)]
870 pub env: Option<String>,
871 /// queued, in_progress, success, failure, error or inactive.
872 #[arg(short, long)]
873 pub state: Option<String>,
874 /// api, actions or g1t_page.
875 #[arg(long)]
876 pub source: Option<String>,
877 #[arg(short = 'L', long, default_value_t = 20)]
878 pub limit: usize,
879}
880
881#[derive(Debug, Args)]
882pub struct DeployViewArgs {
883 pub id: String,
884 #[arg(short, long)]
885 pub web: bool,
886}
887
888// --- logs -------------------------------------------------------------
889
890#[derive(Debug, Args)]
891pub struct LogsArgs {
892 /// The project; the current repository's when left out.
893 #[arg(short, long)]
894 pub project: Option<String>,
895 /// Grouped errors instead of log lines.
896 #[arg(long)]
897 pub errors: bool,
898 /// Lines holding all of these words, or a "quoted phrase".
899 #[arg(short, long)]
900 pub query: Option<String>,
901 /// Levels: trace, debug, info, warn, error, fatal (comma-separated).
902 #[arg(short, long)]
903 pub level: Option<String>,
904 #[arg(short, long)]
905 pub env: Option<String>,
906 #[arg(long)]
907 pub service: Option<String>,
908 /// How far back: 15m, 2h, 1d, or a time like 2026-10-12T09:00:00Z.
909 #[arg(short, long)]
910 pub since: Option<String>,
911 /// With --errors: open, resolved or ignored.
912 #[arg(long)]
913 pub state: Option<String>,
914 /// Keep printing new lines as they come.
915 #[arg(short, long, conflicts_with = "errors")]
916 pub follow: bool,
917 #[arg(short = 'L', long, default_value_t = 50)]
918 pub limit: usize,
919}
920
921// --- session ----------------------------------------------------------
922
923#[derive(Clone, Copy, Debug, PartialEq, Eq, ValueEnum)]
924pub enum Harness {
925 // Spelled as the API spells it, which is what the plugin sends.
926 #[value(name = "claude_code", alias = "claude-code")]
927 ClaudeCode,
928 Codex,
929 Cursor,
930 Copilot,
931 Other,
932}
933
934impl Harness {
935 pub fn name(self) -> &'static str {
936 match self {
937 Harness::ClaudeCode => "claude_code",
938 Harness::Codex => "codex",
939 Harness::Cursor => "cursor",
940 Harness::Copilot => "copilot",
941 Harness::Other => "other",
942 }
943 }
944}
945
946#[derive(Debug, Subcommand)]
947pub enum SessionCommand {
948 /// Start a session and remember it for this working copy. Prints its id.
949 #[command(after_help = "\
950The id is kept in .git/g1t/session (each worktree has its own); G1T_SESSION overrides it.
951Starting again with the same --harness-session-id returns the same session.")]
952 Start(SessionStartArgs),
953 /// Say the current session is still working; renews its claims.
954 Heartbeat,
955 /// Record a note, prompt or message in the current session.
956 Log(SessionLogArgs),
957 /// Attach the current session to an issue, a pull request or a branch, or give it a title.
958 Attach(SessionAttachArgs),
959 /// End the current session.
960 End(SessionEndArgs),
961 /// List the repository's sessions.
962 List(SessionListArgs),
963 /// Show a session and what it recorded; the current one when left out.
964 View(SessionViewArgs),
965 /// Say what the current session touched: files, commits, branches, pull requests, issues.
966 #[command(after_help = "\nEach is kept once. A file another active session touched too (a sibling subagent's included)
967is an overlap.detected event for both. Pull requests and issues tie the session to them.")]
968 Touch(SessionTouchArgs),
969}
970
971#[derive(Debug, Args)]
972#[command(group(clap::ArgGroup::new("what").required(true).multiple(true).args(["file", "commit", "branch", "pull", "issue"])))]
973pub struct SessionTouchArgs {
974 /// A file it wrote, from the repository's root. Repeat for more.
975 #[arg(long)]
976 pub file: Vec<String>,
977 /// A commit it made.
978 #[arg(long)]
979 pub commit: Vec<String>,
980 /// A branch it pushed.
981 #[arg(long)]
982 pub branch: Vec<String>,
983 /// A pull request it opened.
984 #[arg(long)]
985 pub pull: Vec<u64>,
986 /// An issue it named.
987 #[arg(long)]
988 pub issue: Vec<u64>,
989}
990
991#[derive(Debug, Args)]
992pub struct SessionStartArgs {
993 /// The issue it works on.
994 #[arg(short, long)]
995 pub issue: Option<u64>,
996 /// The pull request it works on.
997 #[arg(short, long)]
998 pub pull: Option<u64>,
999 /// The harness running it.
1000 #[arg(long, value_enum, default_value_t = Harness::Other)]
1001 pub harness: Harness,
1002 /// The harness's own id for the run, which makes starting again return the same session.
1003 #[arg(long, value_name = "ID")]
1004 pub harness_session_id: Option<String>,
1005 #[arg(short, long)]
1006 pub title: Option<String>,
1007 /// The branch it works on; the current one when left out.
1008 #[arg(short, long)]
1009 pub branch: Option<String>,
1010 /// For a subagent: the session that started it (ses_…).
1011 #[arg(long, value_name = "SESSION")]
1012 pub parent: Option<String>,
1013 /// For a subagent: its kind (Explore, security-reviewer).
1014 #[arg(long)]
1015 pub agent_type: Option<String>,
1016 /// For a subagent: the task it was given, in a few words.
1017 #[arg(long)]
1018 pub task: Option<String>,
1019 /// For a subagent: the prompt its parent gave it.
1020 #[arg(long)]
1021 pub prompt: Option<String>,
1022 /// The model it runs on.
1023 #[arg(long)]
1024 pub model: Option<String>,
1025 /// Do not remember it as this working copy's session (a subagent's, kept by its harness).
1026 #[arg(long)]
1027 pub no_remember: bool,
1028}
1029
1030#[derive(Clone, Copy, Debug, PartialEq, Eq, ValueEnum)]
1031pub enum EntryKind {
1032 Note,
1033 Prompt,
1034 Message,
1035 // Spelled as the API spells it, which is what the plugin sends.
1036 #[value(name = "tool_call", alias = "tool-call")]
1037 ToolCall,
1038 /// A skill the session used: give its name.
1039 Skill,
1040}
1041
1042impl EntryKind {
1043 pub fn name(self) -> &'static str {
1044 match self {
1045 EntryKind::Note => "note",
1046 EntryKind::Prompt => "prompt",
1047 EntryKind::Message => "message",
1048 EntryKind::ToolCall => "tool_call",
1049 EntryKind::Skill => "skill",
1050 }
1051 }
1052}
1053
1054#[derive(Debug, Args)]
1055pub struct SessionLogArgs {
1056 /// What to record; - reads it from stdin.
1057 pub text: String,
1058 #[arg(short, long, value_enum, default_value_t = EntryKind::Note)]
1059 pub kind: EntryKind,
1060 /// The tool, for a tool_call.
1061 #[arg(long)]
1062 pub tool: Option<String>,
1063 /// A key that makes recording it again do nothing.
1064 #[arg(long)]
1065 pub key: Option<String>,
1066}
1067
1068#[derive(Debug, Args)]
1069#[command(group(clap::ArgGroup::new("what").required(true).multiple(true).args(["issue", "pull", "branch", "head_commit", "title"])))]
1070pub struct SessionAttachArgs {
1071 #[arg(short, long)]
1072 pub issue: Option<u64>,
1073 #[arg(short, long)]
1074 pub pull: Option<u64>,
1075 #[arg(short, long)]
1076 pub branch: Option<String>,
1077 /// The commit it pushed last.
1078 #[arg(long, value_name = "SHA")]
1079 pub head_commit: Option<String>,
1080 /// What it is for, in a few words; replaces its title.
1081 #[arg(short, long)]
1082 pub title: Option<String>,
1083}
1084
1085#[derive(Debug, Args)]
1086pub struct SessionEndArgs {
1087 /// Why it ended: done, abandoned, handed_off...
1088 #[arg(short, long)]
1089 pub reason: Option<String>,
1090 /// What it reported when it finished (a subagent's final message); - reads it from stdin.
1091 #[arg(long)]
1092 pub result: Option<String>,
1093}
1094
1095#[derive(Clone, Copy, Debug, PartialEq, Eq, ValueEnum)]
1096pub enum SessionState {
1097 Active,
1098 Idle,
1099 Ended,
1100}
1101
1102#[derive(Debug, Args)]
1103pub struct SessionListArgs {
1104 /// Only sessions in this state.
1105 #[arg(short, long, value_enum)]
1106 pub state: Option<SessionState>,
1107 #[arg(short, long)]
1108 pub issue: Option<u64>,
1109 #[arg(short, long)]
1110 pub pull: Option<u64>,
1111 #[arg(short = 'L', long, default_value_t = 20)]
1112 pub limit: usize,
1113 /// Only sessions that touched this file, or any under it when it ends in /.
1114 #[arg(long)]
1115 pub path: Option<String>,
1116 /// List subagents too, under the sessions that started them.
1117 #[arg(long)]
1118 pub subagents: bool,
1119}
1120
1121#[derive(Debug, Args)]
1122pub struct SessionViewArgs {
1123 /// The session's id (ses_…); the current one when left out.
1124 pub id: Option<String>,
1125}
1126
1127// --- work -------------------------------------------------------------
1128
1129#[derive(Debug, Subcommand)]
1130pub enum WorkCommand {
1131 /// Everything about an issue's work: claims, sessions, pull requests, dependencies, decisions.
1132 Context(WorkNumber),
1133 /// Claim an issue: say you are working on it. Others are told, never stopped.
1134 #[command(after_help = "\nWith --title instead of a number, a new issue is opened and claimed in one step, and the
1135current session is attached to it: for work that became a change others should know about.
1136Examples:
1137 g1t work claim 12 --note \"Rewriting the retry loop\"
1138 g1t work claim --title \"Preview builds time out on large repositories\"")]
1139 Claim(WorkClaimArgs),
1140 /// Release your claim on an issue.
1141 Release(WorkReleaseArgs),
1142 /// Hand an issue over to someone else, or to anyone.
1143 Handoff(WorkHandoffArgs),
1144 /// Active claims and sessions in the repository.
1145 List,
1146 /// Say an issue waits on others: it is blocked by them.
1147 Block(WorkBlockArgs),
1148 /// Say an issue no longer waits on others.
1149 Unblock(WorkBlockArgs),
1150 /// The repository's events; --follow keeps printing them as they happen.
1151 #[command(after_help = "\
1152Examples:
1153 g1t work events --follow --issue 12
1154 g1t work events --follow --type integration.failed --type overlap.detected --json
1155With --json, each event is one JSON object on a line of its own.")]
1156 Events(WorkEventsArgs),
1157 /// Start work on an issue in a session on your own Claude, or print the Claude Code command for it.
1158 #[command(after_help = "\nWith the workspace's Anthropic connection, a Claude Managed Agents session starts on that
1159account and is recorded on g1t; otherwise (or with --handoff) this prints a `claude` command
1160to run with g1t's plugin, whose session attaches to the issue as it starts.
1161Examples:
1162 g1t work dispatch 12
1163 g1t work dispatch 12 --mode plan --instruction \"Keep the old flag\"
1164 g1t work dispatch --title \"Add a healthcheck\" --handoff")]
1165 Dispatch(WorkDispatchArgs),
1166}
1167
1168#[derive(Clone, Copy, Debug, PartialEq, Eq, ValueEnum)]
1169pub enum DispatchMode {
1170 Plan,
1171 Implement,
1172 Review,
1173}
1174
1175impl DispatchMode {
1176 pub fn name(self) -> &'static str {
1177 match self {
1178 DispatchMode::Plan => "plan",
1179 DispatchMode::Implement => "implement",
1180 DispatchMode::Review => "review",
1181 }
1182 }
1183}
1184
1185#[derive(Debug, Args)]
1186#[command(group(clap::ArgGroup::new("which").required(true).args(["number", "title"])))]
1187pub struct WorkDispatchArgs {
1188 /// The issue's number.
1189 pub number: Option<u64>,
1190 /// Open a new issue with this title and work on it.
1191 #[arg(long, conflicts_with = "number")]
1192 pub title: Option<String>,
1193 /// The new issue's description.
1194 #[arg(long, requires = "title")]
1195 pub body: Option<String>,
1196 /// What to do beyond what the issue says.
1197 #[arg(short, long)]
1198 pub instruction: Option<String>,
1199 /// What kind of work.
1200 #[arg(short, long, value_enum)]
1201 pub mode: Option<DispatchMode>,
1202 /// Print the Claude Code command to run yourself; start nothing.
1203 #[arg(long, conflicts_with = "managed")]
1204 pub handoff: bool,
1205 /// Start it on the workspace's Anthropic account, or fail.
1206 #[arg(long)]
1207 pub managed: bool,
1208}
1209
1210#[derive(Debug, Args)]
1211pub struct WorkNumber {
1212 /// The issue's number.
1213 pub number: u64,
1214}
1215
1216#[derive(Debug, Args)]
1217#[command(group(clap::ArgGroup::new("which").required(true).args(["number", "title"])))]
1218pub struct WorkClaimArgs {
1219 /// The issue's number.
1220 pub number: Option<u64>,
1221 /// Open a new issue with this title and claim it; the current session is attached to it.
1222 #[arg(long, conflicts_with = "number")]
1223 pub title: Option<String>,
1224 /// The new issue's description.
1225 #[arg(long, requires = "title")]
1226 pub body: Option<String>,
1227 /// What you are doing about it.
1228 #[arg(short, long)]
1229 pub note: Option<String>,
1230 /// How long the claim lasts unless renewed: 30m, 2h (at most 4h).
1231 #[arg(long, value_name = "DURATION", value_parser = parse_ttl)]
1232 pub ttl: Option<u64>,
1233}
1234
1235#[derive(Debug, Args)]
1236pub struct WorkReleaseArgs {
1237 pub number: u64,
1238 /// Why: done, blocked, abandoned...
1239 #[arg(short, long)]
1240 pub reason: Option<String>,
1241}
1242
1243#[derive(Debug, Args)]
1244pub struct WorkHandoffArgs {
1245 pub number: u64,
1246 /// Who takes it over; anyone when left out.
1247 #[arg(long, value_name = "LOGIN")]
1248 pub to: Option<String>,
1249 /// Where it stands and what is left, for whoever takes it.
1250 #[arg(short, long)]
1251 pub note: String,
1252}
1253
1254#[derive(Debug, Args)]
1255pub struct WorkBlockArgs {
1256 /// The issue that waits.
1257 pub number: u64,
1258 /// The issue it waits on; repeat for more.
1259 #[arg(long = "by", value_name = "NUMBER", required = true)]
1260 pub by: Vec<u64>,
1261}
1262
1263#[derive(Debug, Args)]
1264pub struct WorkEventsArgs {
1265 /// Keep printing events as they happen, until stopped.
1266 #[arg(short, long)]
1267 pub follow: bool,
1268 /// Only events of this type; repeat for more.
1269 #[arg(short = 't', long = "type", value_name = "TYPE")]
1270 pub types: Vec<String>,
1271 /// Only events about this issue or pull request; repeat for more.
1272 #[arg(short, long = "issue", value_name = "NUMBER")]
1273 pub issues: Vec<u64>,
1274 /// Only events since then: 15m, 2h, 1d, or a time like 2026-10-12T09:00:00Z.
1275 #[arg(short, long)]
1276 pub since: Option<String>,
1277 /// Start after this event id, as a follow that stopped would resume.
1278 #[arg(long, value_name = "EVENT_ID")]
1279 pub after: Option<String>,
1280 /// How many past events to show first.
1281 #[arg(short = 'L', long, default_value_t = 20)]
1282 pub limit: usize,
1283}
1284
1285/// `30m`, `2h`, `90` (minutes) as minutes, from 1 minute to 4 hours.
1286pub fn parse_ttl(text: &str) -> Result<u64, String> {
1287 let text = text.trim();
1288 let (number, unit) = text.split_at(text.find(|c: char| !c.is_ascii_digit()).unwrap_or(text.len()));
1289 let wrong = || format!("`{text}` is not a duration: write it as 30m or 2h");
1290 let number: u64 = number.parse().map_err(|_| wrong())?;
1291 let minutes = match unit {
1292 "" | "m" | "min" => number,
1293 "h" => number * 60,
1294 _ => return Err(wrong()),
1295 };
1296 if minutes == 0 || minutes > 240 {
1297 return Err("a claim lasts from 1 minute to 4 hours".to_owned());
1298 }
1299 Ok(minutes)
1300}
1301
1302// --- why --------------------------------------------------------------
1303
1304#[derive(Debug, Args)]
1305pub struct WhyArgs {
1306 /// A file, or a line of it as PATH:LINE.
1307 pub target: String,
1308 /// The branch, tag or commit to read it at; the default branch when left out.
1309 #[arg(long = "ref", value_name = "REF")]
1310 pub reference: Option<String>,
1311}
1312
1313// --- status, api, browse ---------------------------------------------
1314
1315#[derive(Debug, Args)]
1316pub struct StatusArgs {}
1317
1318#[derive(Debug, Args)]
1319#[command(after_help = "\
1320ENDPOINT is an operation's name (list_issues) or a path (/repos/{owner}/{name}/issues).
1321{owner}, {name}, {repo} and {workspace} are filled in from the current repository,
1322and an operation's other path parameters from -f/-F fields of the same name.
1323
1324Examples:
1325 g1t api list_issues -f state=closed
1326 g1t api /user --jq .username
1327 g1t api create_issue -f title='Broken link' -f labels[]=docs
1328 g1t api list_workflow_runs --paginate --jq '.[].conclusion'
1329 g1t api --list issue")]
1330pub struct ApiArgs {
1331 /// An operation's name, or a path.
1332 pub endpoint: Option<String>,
1333 /// List the operations, or those matching this word.
1334 #[arg(long, value_name = "QUERY", num_args = 0..=1, default_missing_value = "")]
1335 pub list: Option<String>,
1336 /// The HTTP method: GET unless fields are given, then POST.
1337 #[arg(short = 'X', long)]
1338 pub method: Option<String>,
1339 /// A string field, key=value. key[]=value adds to a list.
1340 #[arg(short = 'f', long = "raw-field", value_name = "KEY=VALUE")]
1341 pub raw_field: Vec<String>,
1342 /// A typed field: true, false, null and numbers become JSON; @file reads a file.
1343 #[arg(short = 'F', long = "field", value_name = "KEY=VALUE")]
1344 pub field: Vec<String>,
1345 /// A request header, key:value.
1346 #[arg(short = 'H', long = "header", value_name = "KEY:VALUE")]
1347 pub header: Vec<String>,
1348 /// The request body, from a file or - for stdin.
1349 #[arg(long, value_name = "FILE")]
1350 pub input: Option<String>,
1351 /// Follow next pages until there are none.
1352 #[arg(long)]
1353 pub paginate: bool,
1354 /// With --paginate, put every page's items into one array.
1355 #[arg(long, requires = "paginate")]
1356 pub slurp: bool,
1357 /// Print the status line and headers too.
1358 #[arg(short, long)]
1359 pub include: bool,
1360 /// Print nothing on success.
1361 #[arg(long)]
1362 pub silent: bool,
1363}
1364
1365#[derive(Debug, Args)]
1366pub struct BrowseArgs {
1367 /// An issue or pull request number, a file path (path:line), or a commit.
1368 pub target: Option<String>,
1369 /// The branch to show files on.
1370 #[arg(short, long)]
1371 pub branch: Option<String>,
1372 #[arg(short, long)]
1373 pub settings: bool,
1374 #[arg(long)]
1375 pub issues: bool,
1376 #[arg(long)]
1377 pub pulls: bool,
1378 #[arg(long)]
1379 pub actions: bool,
1380 /// Print the address instead of opening it.
1381 #[arg(short, long)]
1382 pub no_browser: bool,
1383}
1384
1385// --- alias, completion, push -----------------------------------------
1386
1387#[derive(Debug, Subcommand)]
1388pub enum AliasCommand {
1389 /// Make an alias: g1t alias set co 'pr checkout'. Start it with ! to run it through the shell.
1390 Set(AliasSetArgs),
1391 /// List aliases.
1392 List,
1393 /// Delete an alias.
1394 Delete(AliasDeleteArgs),
1395}
1396
1397#[derive(Debug, Args)]
1398pub struct AliasSetArgs {
1399 pub name: String,
1400 /// What it expands to. $1, $2... take the arguments after it.
1401 pub expansion: String,
1402}
1403
1404#[derive(Debug, Args)]
1405pub struct AliasDeleteArgs {
1406 pub name: String,
1407}
1408
1409#[derive(Debug, Args)]
1410pub struct CompletionArgs {
1411 #[arg(value_enum)]
1412 pub shell: clap_complete::Shell,
1413}
1414
1415#[derive(Debug, Args)]
1416#[command(after_help = "\
1417The token is the first of: --token-stdin, G1T_TOKEN, the account `g1t auth login`
1418signed in for the registry's host, or what `docker login g1t.sh` stored.")]
1419pub struct PushArgs {
1420 /// The local image, [REGISTRY/]NAME[:TAG].
1421 pub image: Option<String>,
1422 /// Where to push, when the image's own name is not an address on g1t.sh. The tag defaults to latest.
1423 #[arg(long = "as", value_name = "ADDRESS")]
1424 pub target: Option<String>,
1425 /// How much of a layer each request carries: 5MB to 95MB.
1426 #[arg(long, value_name = "SIZE", default_value = "90MB", value_parser = parse_chunk)]
1427 pub chunk_size: u64,
1428 /// Read the g1t token from stdin.
1429 #[arg(long)]
1430 pub token_stdin: bool,
1431 /// Push a tarball `docker save` wrote, instead of asking docker for the image.
1432 #[arg(long, value_name = "FILE", conflicts_with = "image")]
1433 pub archive: Option<PathBuf>,
1434}
1435
1436fn parse_chunk(text: &str) -> Result<u64, String> {
1437 crate::chunk::check_chunk(crate::chunk::parse_size(text)?)
1438}
1439
1440#[cfg(test)]
1441mod tests {
1442 use super::Cli;
1443 use clap::Parser;
1444 use serde_json::Value;
1445
1446 /// Every command the Claude Code plugin's hooks run, from the contract
1447 /// their tests hold them to (plugins/claude-code/test/cli-contract.json),
1448 /// with every value each flag is given there, parses here.
1449 #[test]
1450 fn the_plugin_hooks_commands_parse() {
1451 let path = concat!(
1452 env!("CARGO_MANIFEST_DIR"),
1453 "/../../plugins/claude-code/test/cli-contract.json"
1454 );
1455 let contract: Value =
1456 serde_json::from_str(&std::fs::read_to_string(path).unwrap()).unwrap();
1457 let mut checked = 0;
1458 for command in contract["commands"].as_array().unwrap() {
1459 let words: Vec<String> = command["command"]
1460 .as_array()
1461 .unwrap()
1462 .iter()
1463 .map(|w| w.as_str().unwrap().to_owned())
1464 .collect();
1465 let flags = command["flags"].as_object().unwrap();
1466 // One argv for each value of each flag, with every other flag too.
1467 let mut variants: Vec<Vec<(String, Option<String>)>> = vec![vec![]];
1468 for (flag, kind) in flags {
1469 let values: Vec<Option<String>> = match kind {
1470 Value::Array(values) => values
1471 .iter()
1472 .map(|v| Some(v.as_str().unwrap().to_owned()))
1473 .collect(),
1474 Value::String(kind) => match kind.as_str() {
1475 // Spaced, and as --flag=VALUE, which the hooks use for
1476 // text that may start with a dash.
1477 "text" => vec![
1478 Some("a text, with spaces".into()),
1479 Some("=- a text that starts with a dash".into()),
1480 ],
1481 "number" => vec![Some("42".into())],
1482 "switch" | "json" => vec![None],
1483 other => panic!("{flag}: no type {other}"),
1484 },
1485 other => panic!("{flag}: {other}"),
1486 };
1487 let mut next = Vec::new();
1488 for variant in &variants {
1489 for value in &values {
1490 let mut v = variant.clone();
1491 v.push((flag.clone(), value.clone()));
1492 next.push(v);
1493 }
1494 }
1495 variants = next;
1496 }
1497 let dash_dash = command["dash_dash"].as_bool().unwrap_or(false);
1498 // A text positional after -- may start with a dash.
1499 let positionals: Vec<String> = command["positionals"]
1500 .as_array()
1501 .map(|p| {
1502 p.iter()
1503 .map(|k| match (k.as_str(), dash_dash) {
1504 (Some("number"), _) => "42".to_owned(),
1505 (_, true) => "-a text that starts with a dash".to_owned(),
1506 _ => "a_text".to_owned(),
1507 })
1508 .collect()
1509 })
1510 .unwrap_or_default();
1511 for variant in variants {
1512 let mut argv = vec!["g1t".to_owned()];
1513 argv.extend(words.iter().cloned());
1514 // --json last: with nothing after it, it takes no fields.
1515 let mut json = false;
1516 for (flag, value) in &variant {
1517 if flag == "--json" {
1518 json = true;
1519 continue;
1520 }
1521 match value {
1522 Some(v) if v.starts_with('=') => argv.push(format!("{flag}{v}")),
1523 _ => {
1524 argv.push(flag.clone());
1525 argv.extend(value.iter().cloned());
1526 }
1527 }
1528 }
1529 if dash_dash {
1530 argv.push("--".into());
1531 }
1532 argv.extend(positionals.iter().cloned());
1533 if json {
1534 argv.push("--json".into());
1535 }
1536 match Cli::try_parse_from(&argv) {
1537 Ok(_) => {}
1538 Err(e) if e.kind() == clap::error::ErrorKind::DisplayVersion => {}
1539 Err(e) => panic!("the plugin runs {argv:?}, which the CLI refuses: {e}"),
1540 }
1541 checked += 1;
1542 }
1543 }
1544 assert!(checked >= 10, "checked {checked}");
1545 // This CLI is at least the version the hooks ask for.
1546 let version = |v: &str| {
1547 v.split('.')
1548 .map(|n| n.parse::<u32>().unwrap())
1549 .collect::<Vec<_>>()
1550 };
1551 let minimum = contract["minimum_version"].as_str().unwrap();
1552 assert!(
1553 version(env!("CARGO_PKG_VERSION")) >= version(minimum),
1554 "the hooks need {minimum}"
1555 );
1556 }
1557
1558 #[test]
1559 fn entry_kinds_take_both_spellings() {
1560 for kind in ["tool_call", "tool-call"] {
1561 let cli = Cli::try_parse_from(["g1t", "session", "log", "--kind", kind, "x"]).unwrap();
1562 let super::Command::Session(super::SessionCommand::Log(args)) = cli.command else {
1563 panic!("not session log")
1564 };
1565 assert_eq!(args.kind.name(), "tool_call");
1566 }
1567 }
1568
1569 /// `work claim` takes an issue's number, or a title for a new one, never both.
1570 #[test]
1571 fn a_claim_names_an_issue_or_a_title_for_a_new_one() {
1572 let claim = |argv: &[&str]| {
1573 let mut all = vec!["g1t", "work", "claim"];
1574 all.extend_from_slice(argv);
1575 Cli::try_parse_from(all).map(|cli| match cli.command {
1576 super::Command::Work(super::WorkCommand::Claim(args)) => args,
1577 _ => panic!("not work claim"),
1578 })
1579 };
1580 let by_number = claim(&["12", "--note", "on it"]).unwrap();
1581 assert_eq!((by_number.number, by_number.title), (Some(12), None));
1582 let new = claim(&["--title", "Preview builds time out", "--body", "On repositories over 1 GB."]).unwrap();
1583 assert_eq!(new.number, None);
1584 assert_eq!(new.title.as_deref(), Some("Preview builds time out"));
1585 assert_eq!(new.body.as_deref(), Some("On repositories over 1 GB."));
1586 assert!(claim(&[]).is_err());
1587 assert!(claim(&["12", "--title", "x"]).is_err());
1588 // --body goes with --title: claim() refuses it with a number.
1589 }
1590
1591 #[test]
1592 fn a_session_takes_a_title_on_its_own() {
1593 let cli = Cli::try_parse_from(["g1t", "session", "attach", "--title", "Fix the preview builds"]).unwrap();
1594 let super::Command::Session(super::SessionCommand::Attach(args)) = cli.command else {
1595 panic!("not session attach")
1596 };
1597 assert_eq!(args.title.as_deref(), Some("Fix the preview builds"));
1598 assert!(Cli::try_parse_from(["g1t", "session", "attach"]).is_err());
1599 }
1600}