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.

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