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