9.4 KiB · text History 8e00315

gg — command-line client

Speaks /api/mobile/v1, the same surface the Android app in ../android uses. The two are kept in step deliberately: a capability that exists on the phone exists here, and a field that changes shape breaks both at once rather than drifting apart.

cargo build --release   # target/release/gg
cargo test

Signing in

Mint a token on the web under Settings → API tokens with the Mobile app bundle ticked, then either paste the pairing URI the QR encodes:

gg login 'gitgud://pair?base=https://forge.example.com&token=ggp_…'

or give the two fields:

gg login --url https://forge.example.com        # prompts for the token, unechoed
gg login --url https://forge.example.com --token-stdin < token.txt

Credentials land in ~/.config/gitgud/config.toml, mode 0600. GITGUD_URL and GITGUD_TOKEN override the file; --url and --token override both.

Which repository

Commands that need one take --repo owner/name. Without it, the git remote of the current directory is used when it points at this forge — so inside a checkout, gg ci list just works. gg login --default-repo owner/name sets a fallback for everywhere else.

Commands

gg whoami                        who the token belongs to, and its scopes
gg repos [--sort pushed|name|ci] repositories this token can reach
gg repo [owner/name]             counts, permissions, latest run

gg ci list [--limit N]           recent workflow runs
gg ci show [N] [--steps]         one run: jobs, steps, artifacts
gg ci logs [N] [--job JOB]       a job's logs, grouped by step
gg ci rerun N [--job JOB]        re-queue a run, or one job of it
gg ci cancel N                   cancel a queued or running run
gg ci watch [N]                  poll until it settles; exits non-zero on failure
gg ci workflows                  workflows that can be run by hand, with their inputs
gg ci dispatch WORKFLOW [--ref REF] [-f NAME=VALUE …] [--watch]
                                 run one (workflow_dispatch); prints the new run

gg issues list [--state open|closed|all] [--search TEXT]
gg issue show N [--no-comments]
gg issue create --title TEXT [--body TEXT | --body-file PATH] [--label LABEL …]
                                 open one; prints the number and URL
gg issue comment N --body TEXT   (or --body-file PATH, or piped stdin)
gg issue close|reopen N

gg pulls list [--state open|closed|merged|all]
gg pull show N
gg pull create --title TEXT [--head BRANCH] [--base BRANCH] [--body TEXT | --body-file PATH]
                                 open one; --head defaults to the checked-out branch
                                 (pushed already), --base to the default branch;
                                 --head-repo OWNER/NAME takes the head from a fork.
                                 Prints the number and URL
gg pull edit N [--title TEXT] [--body TEXT | --body-file PATH]
gg pull close|reopen N
gg pull label N [--add LABEL …] [--remove LABEL …]
gg pull labels                   the labels a pull request here can be given
gg pull comment N --body TEXT
gg pull review N --state approve|request-changes|comment [--body TEXT]
gg pull merge N [--message TEXT] [--override-rules]
gg pull approve-runs N           let a fork's waiting workflow runs start
gg pull reject-runs N            cancel them instead

gg labels [list]                 the repository's labels, own and inherited
gg labels create NAME [--color '#RRGGBB'] [--description TEXT]
                                 repository admins only

gg wiki list | gg wiki show SLUG
gg packages list | gg package show FORMAT NAME | gg package usage FORMAT

gg badges [list]                 every badge the repository has, and what it says now
gg badges readme [KEY …] [--all] [--style split|dot|outline] [--corners round|square]
                 [--scale 1x|2x|2.5x] [--gg | --no-gg] [--theme auto|NAME]
                 [--format svg|png] [--html | --markdown]
                                 print a README block of badges
gg badges readme … --patch README.md [--check]
                                 write it into the file, or with --check exit
                                 non-zero if the file's block is out of date

gg dashboard [--refresh SECS]    interactive CI dashboard

gg audit [--category C] [--actor HANDLE] [--limit N] [--before ID]
                                 the repository's audit log: who changed what
gg audit --org ORG [--in NAME] …  an organization's, optionally about one repository

gg audit takes a token with the audit:read scope, which no pairing bundle includes — tick it by hand on the tokens page — and answers only for the logs its owner may read on the web (an org’s admins, a personal repository’s owner). It has no Android counterpart on purpose: reading a security log is a desk job, not a phone one, so it’s the one exception to keeping the two in step.

gg badges readme --patch keeps its block between two comments, and the opening one records what it was asked for:

<!-- gg:badges ci coverage release license style=dot gg -->
[![CI status](…)](…) [![coverage](…)](…) …
<!-- /gg:badges -->

So a second run needs no arguments: gg badges readme --patch README.md rebuilds the same badges with the same options, and anything passed overrides what was recorded. Without a block yet, it goes under the file’s first # heading, or at the top. Nothing outside the markers is touched, which makes it safe for an agent or a CI job to run, and --check lets CI fail a README that has drifted. The Markdown comes from the forge, the same the badge page’s README block builds.

--json on any command prints the server’s response verbatim instead of a table, which is the intended way to script against it. (gg ci dispatch --json prints the new run and doesn’t watch it.)

CI statuses

Statuses read as the Android app’s do. Besides queued, running, blocked, success, failure, cancelled and skipped:

mark status what
… waiting held by its concurrency group
⚑ needs approval a fork pull request’s run, waiting for someone with write access (gg pull approve-runs N)
⊘ superseded cancelled because a newer run or job in its concurrency group replaced it — dim, not red

gg ci show lists why each job is waiting, skipped or cancelled, a re-run job’s attempt, and says when a dynamic matrix is still a placeholder; such a job is replaced by its legs (new ids) when it expands, so gg ci logs on one that’s gone says so. gg ci watch keeps watching anything that hasn’t finished — including a run waiting for approval, which it points out once. gg pull show lists a fork’s runs waiting for approval.

Opening, editing, closing, reopening and labelling a pull request fire its pull_request workflows (opened, edited, closed, reopened, labeled, unlabeled) exactly as the web page does, and follow its rules: anyone who can read the repository may open one; its author or a repository admin may edit, close, reopen or label it. All of them need a token with pulls:write. Each prints the pull request as gg pull show --json would under --json; gg pull label with several labels prints the last reply.

gg ci dispatch takes inputs as strings, true/false for a boolean; the server checks them against what the workflow declares and the CLI prints its reason per input when it refuses them.

Dashboard

gg dashboard opens runs, jobs and logs over one repository.

j/k, ↑/↓   move          Enter  open the run / the job's logs
Tab        switch pane   l      logs for the selected job
r          refresh now   Esc    back a level
R          re-run        C      cancel
q          quit

Write actions are capitalized so neither is a neighbour of a navigation key — re-running someone else’s release because you reached for r is not a recoverable mistake.

Packaging

CI builds a .deb and an .rpm on every push to master and publishes both to this forge’s own registries — gg installs from the same place the server package does:

# Debian / Ubuntu (trixie, forky)
sudo apt install gg

# Fedora / RHEL / openSUSE
sudo dnf install gg

The workflow prints the repository lines to add first; see the cli job in .forgejo/workflows/build.yml.

Both packages come from metadata in Cargo.toml, and both build by hand — a packaging bug should be reproducible without pushing a commit:

cargo deb            # target/debian/gg_<version>_<arch>.deb
cargo generate-rpm   # target/generate-rpm/gg-<version>-<release>.<arch>.rpm

cli/Dockerfile is what CI drives. It compiles the crate twice, once on Debian trixie and once on EL9, because a Rust binary is linked against the glibc of whatever built it — one binary packaged for both families would install cleanly and then die with version GLIBC_2.41 not found. It also runs cargo fmt --check, cargo clippy -D warnings and cargo test on the way past, so a failing tree produces no package.

docker buildx build --target packages \
  --output type=local,dest=./cli-dist --file cli/Dockerfile .

The build context is the repository root, not cli/: the copyright file in both packages is the repo’s LICENSE.md.

Layout

path what
src/cli.rs the clap command tree
src/api/ HTTP client and the wire types, mirroring the server’s JSON module
src/commands/ one module per noun, plus repo inference and body input
src/output.rs colour, the CI status vocabulary, tables, relative times
src/tui/ the dashboard