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 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 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.
--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 |