.gitbay/wiki/Users.org

v1.41.0
gitbay/.gitbay/wiki/Users.org rendered · source · history · blame · raw

1104 lines · 54677 bytes

   1#+title: gitbay user guide
   2
   3Everything here works from stock OpenSSH — replace =gitbay= with
   4=ssh git@<host>= in any command. The CLI adds convenience (instance
   5profiles, repo inference, =$EDITOR=), nothing more. Two things differ
   6over bare ssh:
   7
   8- ssh joins its arguments with spaces and the server tokenizes the
   9  result, so a value with a space in it needs a second layer of quotes:
  10  =--description "'a widget'"=, not =--description "a widget"=, which
  11  arrives as two arguments.
  12- =--help= is not understood. =ssh git@<host> help= lists every command
  13  the server knows, and =help <noun>= (=help repo=, =help mr=) prints
  14  the usage of each command under that noun.
  15
  16* Installing the CLI
  17
  18#+begin_src sh
  19go install gitbay.org/gitbay/cmd/gitbay@latest   # any platform with Go
  20
  21brew tap krz/tap https://gitbay.org/krz/homebrew-tap.git
  22brew install krz/tap/gitbay                      # Homebrew (macOS/Linux)
  23#+end_src
  24
  25Or build from source: =go build ./cmd/gitbay= in a clone of
  26=https://gitbay.org/krz/gitbay.git=.
  27
  28* Getting an account
  29
  30How you join depends on the instance's registration mode. On instances
  31with web accounts enabled, =/register= offers the same signup as a
  32browser form (paste your SSH public key); everything below works from
  33the terminal alone:
  34
  35- closed :: an admin creates your account on the host and registers your
  36  first SSH key. Nothing for you to do but hand over your public key.
  37- invite :: you receive a single-use code by email. With the SSH key you
  38  want to use:
  39  #+begin_src sh
  40  ssh git@<host> register --username you --invite <code>
  41  #+end_src
  42  Your account is active immediately; the invited address is your
  43  verified email.
  44- open ::
  45  #+begin_src sh
  46  ssh git@<host> register --username you --email you@example.org
  47  #+end_src
  48  A verification code arrives by mail. Until you run
  49  =ssh git@<host> email verify <code>=, the account is pending: you can
  50  run =whoami= and the email commands, and nothing else — no git, no
  51  repos.
  52
  53* SSH keys
  54
  55New to SSH keys? [[SSH-keys]] makes one in two commands.
  56
  57Your key is your identity; there are no passwords anywhere. The SSH
  58username is always =git= — the key alone determines who you are.
  59
  60#+begin_src sh
  61gitbay auth keys list
  62gitbay auth keys add --scope git < ~/.ssh/ci_key.pub   # key on stdin
  63gitbay auth keys add --label laptop < ~/.ssh/id_ed25519.pub
  64gitbay auth keys add --scope git --ttl 90d < ~/.ssh/ci_key.pub
  65gitbay auth keys label SHA256:... "work laptop"
  66gitbay auth keys remove SHA256:...
  67#+end_src
  68
  69A key's label is the comment on its =authorized_keys= line unless
  70=--label= gives one; =keys label= renames a key, and with no text
  71clears the name. Labels are one line of up to 64 bytes.
  72
  73=--ttl 90d= (or any Go duration, =720h=) makes a key stop
  74authenticating after that long; =repo deploy-key add= takes the same
  75flag. An expiring key cannot create credentials: tokens, keys, login
  76links. =keys list= shows when each key was last used and when it
  77expires, so a key nobody uses is easy to spot.
  78
  79Removing a key closes every connection it opened, including the CLI's
  80shared one; removing the key the current command runs on ends that
  81command's connection too.
  82
  83Scopes: =full= (default; git plus every control command), =git= (git
  84transport only — right for automation keys, which then cannot touch
  85issues, settings, or your account), or =runner= (the CI runner's
  86protocol plus read-only git, for the key a =gitbay-runner= host holds;
  87see [[Admin]]).
  88
  89A key belongs to exactly one account instance-wide. Registering a key
  90someone else already holds is refused without telling you whose it is.
  91
  92* Verified commits
  93
  94The commit badge is driven by the *author* email and the signing key:
  95=verified= means the signature is valid, the key is registered to an
  96account, and the author email is a verified address on that account.
  97
  98For OpenPGP signing (git's default):
  99#+begin_src sh
 100gpg --armor --export you@example.org | gitbay auth pgp add
 101#+end_src
 102
 103For SSH signing (=git config gpg.format ssh=): sign with any key
 104registered on your account; your verified addresses act as the principal
 105set. No separate registration step.
 106
 107Add and verify additional addresses with =email add <address>= /
 108=email verify <code>= (requires the instance to have SMTP; otherwise an
 109admin can assert an address for you). =email list= shows them. =email
 110remove <address>= drops one, with two refusals: the primary stays until
 111=email primary <address>= names another verified address, and the last
 112verified address stays, since activation, login links and commit
 113identity all resolve through verified addresses. Removing a verified
 114address re-evaluates signature states, so a commit authored from it
 115shows as =signed_email_mismatch= afterwards. A removed address is free
 116for any account to claim.
 117
 118The states you will see, in decreasing order of trust: =verified=,
 119=signed_unknown_key= (valid signature, key not registered here — register
 120it and history upgrades retroactively), =signed_email_mismatch= (real
 121key, author line claims someone else), =signed_key_expired= /
 122=signed_key_revoked=, =bad_signature=, =unsigned=. Server-created commits
 123(web edits, merge commits) are always =unsigned= — the server holds no
 124signing key on principle.
 125
 126=repo bookmark <owner/name>= saves a repository to come back to and
 127=repo unbookmark= drops it; =repo bookmarks= lists yours, each with how
 128many people have bookmarked it. The web has the control on the
 129repository header, the count in the facts bar, and the list at
 130=/bookmarks=. Bookmarks are public and counted; pins are private and
 131drive the rail. Read access is all a bookmark needs.
 132
 133A job's result is a property of the commit's *tree*, not its sha: a
 134rebase onto a base that touched nothing the branch did gives every
 135commit a new sha and the same tree, and a job that already passed for
 136that tree is not run again — the new commit gets the earlier result as
 137its =ci/<job>= status, naming the build it came from. Scheduled and tag
 138jobs are never reused this way; their trigger is the clock or the tag.
 139
 140* Profiles
 141
 142A profile is what =/{owner}= shows: a one-line description, a website, a
 143set of links, long-form about text, the repositories you can see, org
 144membership, and a year of activity. Users and orgs have the same fields.
 145Repository rows carry the listing metadata too — topics, license,
 146default branch, last commit — so a client renders a profile listing the
 147way =/{owner}= does. The web dispatches this command rather than
 148assembling the page itself.
 149
 150#+begin_src sh
 151gitbay profile show               # your own
 152gitbay profile show alice
 153gitbay profile set --description "builds small tools" --website https://alice.example
 154#+end_src
 155
 156The about text is not a field. It is =profile/README.md= or
 157=profile/README.org= on the default branch of =<owner>/.gitbay=, a
 158repository you own like any other, written by a push or by
 159=repo commit-file=:
 160
 161#+begin_src sh
 162gitbay repo create alice/.gitbay
 163gitbay repo commit-file alice/.gitbay profile/README.org \
 164       --ref main --file - < about.org
 165#+end_src
 166
 167The extension picks the renderer; =.md=, =.org= and =.markdown= are
 168resolved in that order, so a =README.md= beside a =README.org= wins.
 169=profile show= reports the text as =about=, its format as
 170=about_format=, and the file it came from as =about_path=.
 171
 172Access follows the repository: a private =.gitbay= keeps the about text
 173to you and the admins. A repository whose name starts with a dot is
 174infrastructure rather than a project, so it stays out of =explore= and
 175off the profile's repository list — =repo list= still shows it, and it
 176is reachable at its own URL.
 177
 178Up to five links, each =label|url= or a bare url, http(s) only. Passing
 179=--link= replaces the whole set; a single empty one clears it:
 180
 181#+begin_src sh
 182gitbay profile set --link "Mastodon|https://fosstodon.example/@alice" \
 183                   --link https://alice.example/now
 184gitbay profile set --link ""      # clear
 185#+end_src
 186
 187Every field follows the same rule as the rest of the CLI: a flag you
 188leave out is untouched, and ='' clears the one you name. Org profiles
 189work the same way and need org admin:
 190
 191#+begin_src sh
 192gitbay org profile krz --description "software and experiments"
 193gitbay org profile krz            # no flags shows it
 194#+end_src
 195
 196An org's about text works the same way, in =<org>/.gitbay=.
 197
 198The settings page edits description, website and links, and points at the
 199about file — with a button that creates =<owner>/.gitbay= and its first
 200README when you have none, so the file editor has a branch to open. The
 201JSON API runs =profile set= like any other write command.
 202
 203* Repositories
 204
 205#+begin_src sh
 206gitbay repo create you/project [--private]
 207gitbay repo clone you/project
 208gitbay repo list
 209gitbay repo show you/project
 210gitbay repo log you/project --limit 20      # commits with signature states
 211gitbay repo fork other/project [--name mine]
 212gitbay repo rename you/project tool          # clone URLs change
 213gitbay repo delete you/project --yes
 214#+end_src
 215
 216=repo show= also reports your own state on the repository — =watch=
 217(=watching= or =muted=; =repo unwatch= clears either) and =bookmarked= —
 218and =fork_of= when it is a
 219fork whose parent you can read, so a client draws a toggle rather than
 220two blind buttons. Absent means none.
 221
 222On the web, the repository's settings page carries access, webhooks,
 223rename, transfer and delete, and =/new= imports from a remote. Delete and
 224transfer ask for the repository's path to be typed. Large imports belong
 225on the CLI, where progress is visible.
 226
 227Pushing is SSH-only. Public repositories are anonymously readable over
 228HTTPS (and =git://= where enabled); private repositories exist only over
 229SSH and answer "not found" to everyone without access.
 230
 231Access and settings (owner or =admin= grant):
 232#+begin_src sh
 233gitbay repo access grant you/project alice write    # read | write | admin
 234gitbay repo access revoke you/project alice
 235gitbay repo access list you/project                 # everyone who can reach it, role, and via what
 236gitbay repo settings protect you/project main       # no force-push, no delete
 237gitbay repo settings require-mr you/project on      # protected branches: merge requests only
 238gitbay repo settings protect-tag you/project 'v*'   # matching tags: created once, never moved or deleted
 239gitbay repo settings default-branch you/project trunk # HEAD, and what the web shows
 240gitbay repo settings require-signed you/project on  # every commit must verify
 241gitbay repo settings git-daemon you/project on      # expose over git://
 242gitbay repo topics add you/project cli forge        # free-form tags, shown on the web
 243gitbay repo search forge                            # find repos by name/description/topic
 244gitbay repo grep you/project "some string"          # literal git grep over the default branch
 245gitbay repo symbols you/project Dispatch            # where a name is defined, from the symbol index
 246gitbay repo pin you/project                         # pin to your web dashboard
 247gitbay repo unpin you/project
 248gitbay repo archive you/project                     # read-only: pushes and issue/MR
 249gitbay repo unarchive you/project                   #   writes refused, browsing intact
 250#+end_src
 251
 252A push to the default branch queues a symbol index of its tree, built in
 253the background after the push completes; a tree that is already indexed
 254is not indexed again. =repo symbols <owner/name> [--kind k] <query>=
 255lists definitions whose name starts with the query (two characters
 256at least), exact matches
 257first, case-sensitive before case-insensitive, as name, kind, path and
 258line; =--limit= and =--cursor= page it, and without them it stops at
 259200 rows. A Go method is listed as =Type.Method= and also matches on
 260=Method=. Only the default branch is indexed: =--ref= accepts another
 261ref only when its tree is the indexed one. The index follows read
 262access: a repository you cannot read has no symbols either.
 263
 264What is indexed:
 265
 266| language                       | kinds                                                     |
 267|--------------------------------+-----------------------------------------------------------|
 268| Go (parsed)                    | function, method, type, const, var                        |
 269| Swift                          | function, class, struct, enum, interface (protocol), type |
 270| Rust                           | function, struct, enum, interface (trait), type, module, const, macro |
 271| Python                         | function, method, class                                   |
 272| JavaScript, TypeScript         | function, class, interface, type, enum, const             |
 273| C and C++ headers (=.h=, =.hpp=) | function (prototype), struct, enum, class, type, macro  |
 274| shell (=.sh=, =.bash=, =.zsh=) | function                                                  |
 275| org, Markdown                  | section (a heading)                                       |
 276
 277Languages other than Go are matched a line at a time, so unusual
 278definition shapes are missed. Files over 1 MiB, anything under
 279=vendor/= or =node_modules/=, =*_gen.go=, =*.pb.go=, Go files marked
 280=Code generated ... DO NOT EDIT.=, and =*.min.js= are skipped, and a
 281name longer than 256 bytes is dropped. One run stops at 200,000
 282symbols, 32 MiB of names and paths, or two minutes, and publishes what
 283it found as a partial index that says which bound it reached.
 284
 285A new index is written while the previous one stays in use, and
 286replaces it in one step. A run that cannot build an index records why
 287and leaves the previous index current; the same tree is tried again
 288once after an hour, or when a push changes the tree. A repository that
 289has not been pushed to since the index existed has none until its next
 290push to the default branch. =admin symbols reindex <owner/name>=
 291rebuilds one regardless.
 292
 293On the web, a file viewed at the indexed tree (the default branch's
 294head, or any commit with the same tree) links each name the index
 295holds: to its definition when there is one, to the results page at
 296=/{owner}/{repo}/symbols?q=<name>= when there are several. The file's
 297own definitions are listed above its source.
 298
 299Import from another forge — git data first, then optionally the issue
 300and PR history from GitHub or any Forgejo instance such as Codeberg
 301(issues keep state/labels/comments; PRs land as closed or merged MRs
 302with their discussion; originals are attributed inline since foreign
 303authors have no local account; re-running resumes where it stopped):
 304#+begin_src sh
 305gitbay repo import you/mirror --from https://github.com/you/repo.git \
 306    [--private] [--token-stdin]        # token on stdin, never in the URL
 307gitbay repo import-issues you/mirror --from you/repo --token-stdin
 308gitbay repo import-issues you/mirror --from you/repo \
 309    --api-base https://codeberg.org/api/v1    # Forgejo: the site's /api/v1
 310#+end_src
 311
 312=repo import= fetches over http and https only, from an address that
 313passes the same check as a webhook target; a =git://= URL is refused,
 314so use the repository's https URL.
 315
 316Sourcehut has no API of that shape; =repo import= takes its git data
 317and the todo.sr.ht tracker is not read.
 318
 319Moving between gitbay instances (no lock-in): run on the TARGET, with
 320your key registered on both sides. Profile, repos with settings,
 321issues, MRs, and comments replay with attribution; git data mirrors
 322client-side through your own key. Keys never transfer and emails
 323arrive unverified — trust is per-instance. Re-running resumes.
 324Push-blocking policies (require-signed, protected branches) are
 325deferred and printed for you to re-apply after the data lands.
 326=gitbay auth export= alone doubles as a user-level backup.
 327
 328#+begin_src sh
 329gitbay migrate --from old-instance.example [--from-port 22]
 330#+end_src
 331
 332Mirroring keeps a foreign remote in sync during a gradual migration
 333(repo admin; https remotes; the token is stored server-side for the
 334recurring sync and never echoed back):
 335
 336#+begin_src sh
 337gitbay repo mirror add you/project https://github.com/you/project.git \
 338    --direction push --token-stdin    # propagate after every local push
 339gitbay repo mirror add you/copy https://github.com/them/theirs.git \
 340    --direction pull                  # follow upstream; local pushes refused
 341gitbay repo mirror list               # sync status and last error, per mirror
 342gitbay repo mirror sync / remove <id>
 343#+end_src
 344
 345Markdown and org files render on the web when opened, the way a README
 346does on the repository page, with relative links resolved against the
 347file's directory; =source= in the file's action bar (or =?view=source=)
 348shows the text instead. Other files show the text with highlighting.
 349
 350TeX math renders as MathML wherever markdown or org renders: READMEs,
 351files, wiki pages, issue, merge request and release bodies, comments,
 352and their previews. Markdown takes =$…$= inline and =$$…$$= for display,
 353either inline or with the =$$= lines on their own. A =$= followed by a
 354space, or a closing =$= preceded by a space or followed by a digit, is a
 355dollar sign, so =$5 and $10= stays prose; =\$= is always one. Org takes
 356=$…$=, =$$…$$=, =\(…\)=, =\[…\]= and =\begin{…}…\end{…}=. Code spans
 357and blocks are left alone. The supported TeX is a subset (letters,
 358numbers, operators, scripts, =\frac=, =\sqrt=, Greek and common symbols,
 359=\left=/=\right=, accents, =\text=, the =\math…= fonts, matrices,
 360=cases= and =aligned=; the full list heads =internal/texmath/texmath.go=).
 361Anything outside it, including macros, colours and links, shows as
 362source, as does an expression over 8 KiB or nested more than 64 deep,
 363and so does all math in a document after its first 1000 expressions or
 364256 KiB of TeX. A =$$= line opens display math only when a line ending
 365in =$$= follows before a blank line. MathML typed as raw HTML is
 366stripped like any other markup the sanitizer does not allow. The CLI
 367shows the source.
 368
 369* Organizations
 370
 371Orgs share the owner namespace with users and own repositories at
 372=org/repo=. By default members get write on all org repos; org admins
 373get repo admin, create repos under the org, and manage membership. An
 374org's existence and member list are public; its teams are visible to
 375members, and anyone else asking is refused rather than told the org
 376does not exist.
 377
 378#+begin_src sh
 379gitbay org create krz
 380gitbay org members add krz alice [--role admin]
 381gitbay org show krz
 382gitbay org rename krz newname     # clone URLs change
 383gitbay org delete krz --yes       # only when it owns no repositories
 384#+end_src
 385
 386Large orgs scope access with teams: set what plain membership implies,
 387then grant per-repo roles through named teams (org admins always keep
 388admin; the default =write= keeps the simple model):
 389
 390#+begin_src sh
 391gitbay org settings members-role krz none   # write | read | none
 392gitbay org team create krz core-devs
 393gitbay org team add krz core-devs alice bob # org members only
 394gitbay org team grant krz core-devs krz/gitbay write
 395gitbay org team show krz core-devs          # members + grants
 396gitbay org team revoke / remove / delete ...
 397#+end_src
 398
 399* Issues
 400
 401Anyone who can read a repository can file and comment. Closing/reopening
 402is for the author or anyone with write; labels and assignees need write.
 403
 404#+begin_src sh
 405gitbay issue create --title "it breaks" [--body "..." | --file -]
 406    [--label bug]... [--milestone v1.0] [--assignee alice]...  # these need write
 407gitbay issue list [--state open|closed|all]
 408gitbay issue show 4
 409gitbay issue comment 4 --message "same here"
 410gitbay issue react 4 +1 [--comment 12] [--remove]  # the same on mr
 411gitbay issue edit 4 --title "better title" [--body|--file -]  # author or write
 412gitbay issue close 4 / reopen 4
 413gitbay issue label 4 --add bug --remove wontfix
 414gitbay issue assign 4 --add alice
 415gitbay issue milestone 4 v1.0                 # or "none" to clear
 416#+end_src
 417
 418A reaction is one of =+1 -1 laugh hooray confused heart rocket eyes=,
 419given by name or as the emoji itself, on an issue or merge request or on
 420one of its comments (=show= prints comment ids). Anyone who may comment
 421may react; reacting twice or removing an absent reaction does nothing.
 422=show= carries the counts and which are yours. A reaction sends no
 423notification and appears in no activity feed.
 424
 425Inside a clone, the repository is inferred from the =origin= remote —
 426that is why no =owner/name= appears above. Anywhere else, pass it as the
 427first argument. Long text: =--body= inline, =--file -= from stdin, or
 428neither on a terminal and =$EDITOR= opens.
 429
 430A wiki is the =.gitbay/wiki/= directory on the default branch — no
 431separate repository, the same rendering pipeline as READMEs (markdown
 432and org). Editing it is editing a file: a push, =repo commit-file=, or
 433the web editor on a repository that permits server-authored commits (one
 434requiring signed commits is push-only, since the server signs nothing):
 435
 436#+begin_src sh
 437mkdir -p .gitbay/wiki
 438# add Home.md (or .org), more pages, images; relative links between
 439# pages just work on the web at /you/project/wiki
 440git add .gitbay/wiki && git commit -m "wiki" && git push
 441#+end_src
 442
 443Pages may sit in subfolders. A nested page is named by its path —
 444=.gitbay/wiki/Guide/Install.md= is =/you/project/wiki/Guide/Install= and
 445=wiki show you/project Guide/Install= — and the sidebar groups it under
 446its folder. A relative link or image on a nested page resolves from the
 447page's own folder first, then from the top of the wiki, so a link to
 448=Home= still reaches the top-level page. SVG images render.
 449
 450Releases anchor notes and binary assets to a pushed tag (write access;
 451assets stream over SSH, capped by the instance's =max_asset_bytes=).
 452While a release exists its tag can be neither deleted nor moved, whether
 453or not the tag is protected: delete the release first.
 454
 455#+begin_src sh
 456gitbay release create v1.0 --title "First light" [--notes|--file -|$EDITOR]
 457gitbay release asset add v1.0 tool-linux-amd64 < dist/tool-linux-amd64
 458gitbay release asset get v1.0 tool-linux-amd64 > tool   # or the web download link
 459gitbay release list / show v1.0 / delete v1.0 --yes
 460#+end_src
 461
 462The web shows them under the repository's =releases= tab with rendered
 463notes, sha256 sums, and download links; writers add and remove assets
 464there (a file upload, up to =max_asset_bytes=; removal asks for the file
 465name). Feed readers subscribe at
 466=/you/project/releases.atom=; commits are at =/you/project/log.atom=
 467(the default branch) or =/you/project/log.atom/<ref>=, and an owner's
 468activity on their public repositories at =/you/activity.atom=. The
 469pages carry the discovery link.
 470
 471Commit messages act on issues when the commits land on the default
 472branch (direct push, MR merge, =repo commit-file= or the web editor):
 473=closes/fixes/resolves #4= closes the
 474issue with a linking comment, and a bare =#4= leaves a reference
 475comment. Each issue/commit pair acts once, ever. =Closes owner/name#N= closes an issue in another repository when
 476you hold write there with a key whose scope reaches it; otherwise it
 477stays a plain link. A deploy key closes nothing outside the repository
 478it is bound to, but naming that repository by full path works as the
 479bare form does. The comment left
 480on the closed issue links the closing commit by its repository path, so
 481closing a public repository's issue from a private one names the private
 482repository there. A bare =owner/name#N= links and does nothing.
 483
 484Milestones group issues and MRs toward a release (write access to
 485manage, attach with =issue milestone= / =mr milestone=; progress shows
 486on the web at =/owner/name/milestones=, where writers create, close and
 487reopen them):
 488
 489#+begin_src sh
 490gitbay milestone create v1.0 --description "first release" --due 2027-01-01
 491gitbay milestone list [--state open|closed|all]
 492gitbay milestone close v1.0 / reopen v1.0
 493#+end_src
 494
 495An org holds labels and milestones every repository under it sees
 496beside its own. =issue label --add=, =mr label --add=, =issue
 497milestone= and =mr milestone= resolve the org's row first; a repository
 498cannot create a label or milestone with a name its org holds. Creating
 499an org label or milestone whose name repositories under the org already
 500use folds them in: their issues and merge requests move to the org's
 501row. Org admins manage them, on the CLI or at =/<org>/-/labels= and
 502=/<org>/-/milestones=; counts span the repositories you can read.
 503
 504#+begin_src sh
 505gitbay org label set acme bug --color cf222e
 506gitbay org label list acme / remove acme bug
 507gitbay org milestone create acme v2 --due 2027-03-01
 508gitbay org milestone list acme [--state open|closed|all]
 509gitbay org milestone close acme v2 / reopen acme v2
 510#+end_src
 511
 512On the web: =/acme/-/labels= and =/acme/-/milestones=, read-only.
 513
 514Issue templates: commit =.gitbay/issue-template.md= (and optional
 515=issue-template-<name>.md= variants) to the default branch. =gitbay
 516issue create= prefills =$EDITOR= with the default template, the web
 517form prefills its textarea, and =gitbay issue templates= lists them.
 518
 519Lists narrow the same way on every surface: =issue list --label bug
 520--assignee bob --author alice --milestone v1= (or =--milestone none=),
 521=mr list --label bug --author bob --milestone v1=; the web's issue and
 522merge request lists take the same names as query parameters, and each
 523active filter shows with a link that drops it.
 524
 525A query spans repositories, and a saved one keeps it under a name:
 526
 527#+begin_src sh
 528gitbay query save mine is:open assignee:@me
 529gitbay query save triage 'repo:krz/*' is:issue is:open no:label
 530gitbay query save v2 owner:krz label:bug 'label:needs review' milestone:v2
 531gitbay query list / show mine / remove mine
 532gitbay query run mine [--limit 20] [--cursor <c>]   # issues and MRs
 533gitbay issue list --query mine                     # only the issues
 534gitbay mr list --q 'owner:krz is:open author:@me'  # a query written out
 535gitbay query pin mine                              # on the dashboard; unpin
 536#+end_src
 537
 538The terms: =repo:owner/name=, =repo:owner/glob*= (=*= in the name
 539only), =repo:*=, =owner:name=; =is:open=, =is:closed=, =is:merged=,
 540=is:issue=, =is:mr=; =label:x= (repeat it: every label must be there),
 541=no:label=; =milestone:x=, =no:milestone=; =assignee:user=,
 542=author:user=, either as =@me= for whoever runs the query. Anything
 543without a colon is text matched against title and body, as =search=
 544does. Every term narrows, except that several =repo:= and =owner:=
 545terms widen the scope to any of them; with none the query covers every
 546repository you can read, and only those — someone else's private
 547repository is neither a row nor part of a count. Merge requests have no
 548assignees, so =assignee:= means issues. A term that does not parse
 549exits 2 and names itself. =@me= is resolved when the query runs, and
 550the saved text is the query in a canonical order.
 551
 552Rows come newest first, each naming its repository, paged as ={items,
 553next}= with fifty to a page unless =--limit= says otherwise.
 554An account keeps at most 50 saved queries, 10 of them pinned; past
 555either, =query save= or =query pin= exits 2 until one is removed or
 556unpinned. =dashboard --json= carries each pinned query's count and first five
 557rows under =queries=; the web dashboard shows them, and
 558=/you/-/queries= lists your saved queries with a page per query.
 559
 560Labels take a colour: =gitbay label set bug --color cf222e=; =label
 561list= shows each with its colour and how many issues and merge requests
 562carry it, and =label remove= takes one off all of them. =issue label
 563--add= and =mr label --add= still create a colourless label on the fly.
 564
 565* Merge requests
 566
 567#+begin_src sh
 568gitbay mr create --source feature --target main --title "add thing"
 569gitbay mr create other/upstream --source you/fork:feature --target main --title "..."
 570gitbay mr list / show 4 / diff 4
 571gitbay mr checkout 4              # local branch mr/4 from the MR head
 572gitbay mr review 4 --approve      # or --request-changes / --comment
 573gitbay mr label 4 --add bug --remove wontfix
 574gitbay mr merge 4 [--strategy ff|merge|squash|rebase]
 575gitbay mr merge 4 --when-ready      # merge once the gates pass; --cancel dequeues
 576gitbay mr close 4
 577#+end_src
 578
 579A merge request closed without merging can name the one that carries
 580its change forward: =mr close 4 --by 7= records it and both pages show
 581it, and =mr edit 4 --superseded-by 7|none= sets or clears it
 582afterwards, refused on anything but a closed merge request.
 583
 584The web list shows each request's combined check state and comment
 585count alongside its title, so open work needing attention stands out
 586without opening it.
 587
 588Semantics worth knowing:
 589
 590- the MR head lives in the *target* repository as
 591  =refs/merge-requests/N/head= (fetchable by any reader), so an MR
 592  survives deletion of its source branch or fork. It is never removed
 593  on merge or close; after a history rewrite an instance admin can
 594  drop it with =admin mr prune=.
 595- force-pushing the source updates the MR and marks existing reviews
 596  stale — unless the diff is the one they reviewed. A rebase onto a
 597  target that moved on changes every sha and nothing about the change,
 598  so fresh reviews follow it to the new head; a push that changes the
 599  diff stales them.
 600- default strategy: fast-forward when possible, else a merge commit.
 601  Squash makes one commit authored by the MR author, committed by the
 602  merger. Rebase replays a linear range preserving authors; it refuses
 603  ranges containing merge commits, and when fast-forward is possible it
 604  *is* one (original commits and signatures land untouched).
 605- on =require_signed_commits= branches only fast-forwards of fully
 606  verified commits merge; everything server-created is refused with
 607  instructions to rebase locally. =gitbay mr rebase <n>= is those
 608  instructions: in a clone of the target it replays the source branch
 609  onto the target and force-pushes it, then =mr merge <n>= fast-forwards.
 610  The replay is local, so the commits carry your signature and not the
 611  server's — it holds no key. A branch living in a fork is refused, with
 612  the repository to run it in; rebase it there.
 613
 614Repo admins can gate merges (=repo settings ...=): =require-approvals
 615<n>= (fresh, non-author approvals; each reviewer's latest review is
 616their stance, and a fresh request-changes blocks), =require-resolved=
 617(no open review threads), =require-checks= (all statuses green), and
 618=require-codeowners= (an approval from an owner of every owned changed
 619file). Owners come from a =CODEOWNERS= file on the target branch, root
 620or =.gitbay/=, gitignore-style patterns, last match wins. The toggle is
 621the opt-in, so a repository can carry the file as documentation of who
 622to ask without it gating merges; with it on and no file on the target
 623branch, the merge is refused and says so. It does not wait on
 624=require-approvals=.
 625
 626=mr show= carries a =gates= block with where the merge request stands
 627against all of them — approvals counted and required, owners still
 628outstanding per file, open threads, checks, and whether a fast-forward
 629is possible — and the merge request page shows the same block. =mr
 630merge= names every unmet gate at once rather than the first. =mr
 631review= says when a verdict is advisory, which it is from anyone
 632without write access: the gates do not count it.
 633
 634=mr merge <n> --when-ready= queues the merge and merges once the gates
 635pass, or at once if they already do. It is tried again when a status
 636is reported on the head, a review is submitted, a thread is resolved,
 637the request is marked ready, and the source branch is pushed; a push
 638by someone who can write to the target keeps it queued, and the new
 639head has to pass on its own. The merge is made as the user who queued
 640it, with their rights checked at that moment. Anything the queuer can
 641fix (an unmet gate, a conflict, a branch behind a
 642=require_signed_commits= target that needs a rebase) leaves it queued
 643with the reason shown on =mr show= and the page.
 644
 645The queue is bound to the credential it was made with. It is dequeued,
 646with the reason on the request's timeline, when:
 647
 648- the queuer loses write access or their account is disabled;
 649- the SSH key or API token it was queued with is removed, revoked,
 650  expires, or no longer has full scope (a merge queued from the web
 651  rests on the account alone);
 652- someone who cannot write to the target pushes to the source branch
 653  or retargets the request;
 654- the source branch is deleted, or the request is closed.
 655
 656An expiring key or token cannot queue a merge; use one without an
 657expiry, or the web. =mr merge <n> --cancel= dequeues without closing.
 658=merge= and =squash= are refused at queue time on a
 659=require_signed_commits= repository.
 660
 661Those gates apply to =mr merge=. A direct push to a protected branch
 662passes none of them until =require-mr on=: then an existing protected
 663branch refuses every push, including =repo commit-file= and the web
 664editor, and the server's merge is its only writer. Creating the branch
 665is still a push, since there is nothing to open a merge request against
 666yet.
 667
 668A merge request whose target is another open merge request's source
 669branch is stacked on it: =mr create= says so, =mr show= carries
 670=stacked_on= and =stacked=, and merging the lower one retargets the
 671upper onto =main= with its reviews kept. See [[Stacked-MRs][Stacked
 672merge requests]].
 673
 674Review threads anchor to diff lines:
 675
 676#+begin_src sh
 677gitbay mr diff-comment 4 --path main.go --line 12 --message "use log here"
 678gitbay mr diff-comment 4 --reply 7 --message "done"     # join thread 7
 679gitbay mr threads 4                                     # threads with staleness
 680gitbay mr resolve 4 7 / unresolve 4 7
 681#+end_src
 682
 683Threads render inline on the MR page. A force-push marks them stale
 684(shown under "threads on earlier revisions") rather than guessing new
 685anchors; =mr show= reports the unresolved count. Resolving is for the
 686thread author, the MR author, or anyone with write.
 687
 688A thread can span lines: =--start-line 12 --line 13= anchors it to
 689lines 12 and 13 of the new file. A fenced =suggestion= block in the
 690comment proposes replacement lines for that range; an empty block
 691proposes deleting it.
 692
 693#+begin_src sh
 694gitbay mr diff-comment 4 --path main.go --start-line 12 --line 13 --file - < suggestion.md
 695gitbay mr apply-suggestion 4 9       # commit thread 9's suggestion
 696#+end_src
 697
 698where =suggestion.md= is
 699
 700#+begin_example
 701one call does both
 702```suggestion
 703	log.Printf("starting %s", name)
 704```
 705#+end_example
 706
 707The page and =mr threads= show a suggestion as the lines it replaces
 708and the lines it proposes; =mr threads --json= carries it as
 709=suggestion= with the path, the line range, the commit and blob it was
 710made against, the original and replacement text, =outdated= with a
 711reason, and =apply= (=server= or =local=). A suggestion is outdated
 712once the lines it replaces differ at the head from what it was made
 713against, or the file is renamed or deleted; it cannot be applied then.
 714
 715Applying commits the replacement to the source branch as the applying
 716user, with a message naming the merge request and thread, and resolves
 717the thread if the applier could resolve it by hand (the thread author,
 718the MR author, or a writer of the target); otherwise the thread stays
 719open and the output says so. It is a push: only the source branch's
 720writers can apply (for a merge request from a fork, the fork's
 721writers), the branch's protection, =require-mr= and the owner's storage
 722quota apply, and a queued merge treats it as a push by the applier. The web's Apply suggestion button and =mr
 723apply-suggestion= commit it on the server. The server cannot sign, so
 724where the source or target requires signed commits (=apply= is
 725=local=) the CLI fetches the source branch into the clone it runs in,
 726builds the commit without touching the working tree, signs it with
 727=git commit-tree -S= under your git signing configuration, pushes it,
 728and resolves the thread; the page shows that command.
 729
 730* CI builds
 731
 732A =.gitbay/ci.yml= in the repo runs jobs on every branch push:
 733
 734#+begin_src yaml
 735jobs:
 736  test:
 737    steps:
 738      - go test ./...
 739#+end_src
 740
 741A config holds at most ten jobs of fifty steps each, each step under
 7424096 bytes, in a file under 64 KiB; a longer step runs a script from the
 743repository (=- sh ci/publish.sh=). A config past a cap, or otherwise
 744unparseable, records a failed =ci/config= status on the commit and
 745queues nothing, so look there when a push builds nothing.
 746
 747Each job becomes a build (=build list=, =build log=, the builds tab on
 748the web) and a =ci/<job>= commit status, which =repo settings
 749require-checks= can gate merges on. =repo settings
 750require-contexts= names statuses the gate waits for until they report,
 751and turns the gate on. =build list= takes =--ref=,
 752=--status= and =--job= to narrow the listing, combinable; the builds
 753tab reads the same flags from its =?ref=, =?status= and =?job= query
 754parameters and groups the result into one row per commit. =build show=
 755names a failed build's step and how long it ran, and =build log= takes
 756=--step <n>|failed= and =--tail <lines>=. Steps run
 757with =sh -c= on the instance's runner, stopping at the first failure;
 758a broken config surfaces as a failed =ci/config= status. Environment:
 759=GITBAY_REPO=,
 760=GITBAY_SHA=, =GITBAY_REF=, =GITBAY_JOB=, =CI=true=, and =GITBAY_SSH=,
 761the instance's ssh destination as the build reaches it: on the forge's
 762own runner, =git@169.254.1.2=, pasta's address for the host, with
 763=:port= when the instance's ssh is not on 22; from any other runner,
 764the destination that runner polls (its =-remote=, such as
 765=git@gitbay.org=). Use it as =ssh://$GITBAY_SSH/owner/name.git= or
 766=ssh ssh://$GITBAY_SSH …=, which work in either form. A job that
 767talks back to the instance — a release asset, a comment, a push to a
 768pages branch — uses =$GITBAY_SSH= with a key it holds as a secret;
 769the build's container has no key of its own. On the forge's own runner a
 770build from a fork's merge request cannot reach the instance at all, and
 771reaches the internet only over HTTPS, HTTP and DNS (CI page). Two things about that
 772container: a secret with newlines (a private key) arrives intact, and
 773=ssh= expands =~= from the passwd entry, =/root=, not from =$HOME=,
 774which is the build home — so keep an ssh config in the workspace and
 775pass it with =ssh -F= (and =GIT_SSH_COMMAND="ssh -F ..."= for git),
 776which also works on a runner with no container, where writing to
 777=~/.ssh= would edit that machine's own configuration.
 778
 779Secrets: =repo secret set <owner/name> <NAME>= reads the value from
 780stdin (never argv) and injects it into the repo's builds as =$NAME=;
 781=repo secret list= shows names only, and the value is never echoed
 782back. Anyone with write access can read a secret from inside a build,
 783so scope them accordingly.
 784
 785Schedules: a job with =schedule: "17 11,23 * * *"= (five-field cron,
 786server-local time; lists, ranges, and steps supported) runs on its cron
 787against the default branch instead of on push. A default-branch push
 788registers or updates the schedule. A job with =tags: "v*"= runs when a
 789matching tag is pushed — and only then; =schedule= and =tags= are
 790mutually exclusive. =build trigger <owner/name> <job>= queues any job
 791immediately, and =build cancel <owner/name> <n>= withdraws one, queued
 792or running: a running build stops at the runner within seconds and the
 793log says who cancelled it. Both need write access.
 794
 795What each push shape queues, with dedupe, path filters, schedules and
 796the reaper together, is one table on [[CI][CI]].
 797
 798** Your own runner
 799
 800Builds run on runners attached to the repository. An instance need not
 801offer any: install =gitbay-runner= on a machine of yours and attach it.
 802
 803#+begin_src sh
 804brew install krz/tap/gitbay-runner        # or a binary from the release
 805gitbay-runner init -remote git@gitbay.org
 806#+end_src
 807
 808=init= generates a key under =~/.config/gitbay-runner/=, writes
 809=config.toml= beside it, and prints the public key with the command to
 810attach it:
 811
 812#+begin_src sh
 813gitbay repo runner add owner/name < ~/.config/gitbay-runner/id_ed25519.pub
 814#+end_src
 815
 816or paste the key under Runners on the repository's settings page. Then
 817=brew services start krz/tap/gitbay-runner=, or run =gitbay-runner= with
 818no arguments; it reads the config file, and any flag overrides it.
 819
 820What it builds: every build for the repositories it is attached to,
 821with the repository's secrets, and nothing else. Merge requests from
 822forks are untrusted and wait unless the runner runs with =-untrusted=,
 823which is only sensible with =-isolation podman -image <ref>= (see
 824[[Admin][Admin]]). Attach one runner to several repositories by repeating
 825=repo runner add=; run several runners on one account by running =init=
 826on each machine. =repo runner list= shows each attached key, when it
 827last polled and the build it holds; =repo runner remove <fingerprint>=
 828detaches one (the key stays on your account; =keys remove= drops it).
 829A runner key reaches only the runner protocol and read-only git, so a
 830build step that reads it off disk cannot administer your account.
 831
 832* Large files (LFS)
 833
 834Standard Git LFS works over both transports with no setup beyond the
 835usual =git lfs track=. SSH remotes authenticate through
 836=git-lfs-authenticate= (deploy keys included: ro keys can download, rw
 837keys upload); anonymous HTTPS clones of public repositories can fetch
 838LFS objects with no credentials. Objects are verified against their
 839sha256 on upload and capped at 512MB by default.
 840
 841* Pages
 842
 843On instances with =[pages] domain= set, a =pages= branch in any public
 844repo is served as a static site: the repo named =pages= at
 845=https://<you>.<domain>/=, every other repo at
 846=https://<you>.<domain>/<repo>/=. Push HTML to publish; a CI job can
 847build and push the branch for automatic deploys. Sites run on a
 848separate origin — your scripts work, and the forge's cookies are out of
 849reach.
 850
 851A repo can also serve its pages branch on a domain you own.
 852=repo domain add <owner/name> <domain>= claims it and prints a DNS TXT
 853challenge (=_gitbay-challenge.<domain>=); create the record, run
 854=repo domain verify=, then point the domain's A/AAAA records at the
 855instance (DNS-only if the domain sits behind a proxying provider — the
 856instance issues its own certificates). Claims are exclusive per
 857instance; unverified claims serve nothing and expire after 7 days.
 858=repo domain list= reports pending/verified/expired.
 859
 860* Snippets
 861
 862A snippet is one or more named text files you own outside any
 863repository, for a log or a fragment shared by URL. Create one from a
 864file on stdin; the reply is the id and the URL:
 865
 866#+begin_src sh
 867gitbay snippet create build.log --description "failing build" < build.log
 868gitbay snippet file set <id> notes.txt < notes.txt   # add or replace a file
 869gitbay snippet file get <id> build.log > build.log
 870gitbay snippet edit <id> --visibility public
 871gitbay snippet list                                  # yours
 872gitbay snippet list <owner>                          # their public ones
 873gitbay snippet delete <id>
 874#+end_src
 875
 876Visibility is =public= (listed on your page), =unlisted= (anyone with
 877the URL, listed nowhere; the default) or =private= (you alone; not
 878found to everyone else). Files are text, valid UTF-8, each under the
 879instance's =max_snippet_bytes=, at most 64 per snippet. A snippet keeps
 880at least one file. There is no history: setting a file replaces it.
 881
 882On the web, =/<you>/-/snippets= lists yours, each snippet page renders
 883its files with a raw link per file, and the same page creates, edits
 884and deletes through the commands above.
 885
 886* Browser sessions
 887
 888=gitbay web login= mints a one-time URL; the session it opens ends
 889after twelve hours without a request, and after seven days in any case.
 890=gitbay web sessions list= shows each of yours by a short id with its
 891creation, expiry and last use, and =gitbay web sessions revoke <id>=
 892or =--all= ends them from the terminal, which is where a lost laptop is
 893handled.
 894
 895Actions that create a credential or grant access (adding a key, token
 896or email, org and repository roles, transfers, a repository's
 897visibility) ask you to sign in again
 898when your web sign-in is older than 15 minutes. The form shows a "Sign
 899in again" link and the login returns to the page. Idle renewal does not
 900extend this window.
 901
 902=web theme set light= or =dark= fixes the web UI's colour scheme for
 903your account; =system=, the default, follows the browser's own
 904preference. =web theme show= prints it. The account page has the same
 905control under Appearance.
 906
 907=web diff set split= draws diffs side by side on the merge request,
 908commit and compare pages; =unified=, the default, keeps one column.
 909=web diff show= prints it, and the account page has the same control
 910under Appearance. =?layout=split= or =?layout=unified= on a diff page
 911overrides the setting for that request. In a narrow window the split
 912layout stacks the old line over the new one, as a unified diff does.
 913Each column's line numbers open a comment on their own side.
 914
 915* Notifications
 916
 917When the instance has SMTP configured, activity mails you as well as
 918filing the inbox row: someone opens an issue or MR on your repository,
 919comments where you are a participant (author, commenter, reviewer, or
 920mentioned), reviews, closes, merges, or assigns you.
 921=notifications settings mail off= keeps the inbox and stops that mail
 922(login links are not activity and still arrive); the account page has
 923the same switch.
 924=notifications settings watch on= makes you a recipient of every issue
 925and merge request on the repositories you can write to, as if you had
 926run =repo watch= on each: it is consulted when a notice is delivered,
 927so a grant or a revoke needs no watch row, and an explicit watch or
 928mute on a repository still wins. Off by default; the account page has
 929this switch too. Writing =@name= in an issue, merge request or
 930comment files "mentioned you" in that account's inbox and makes them a
 931participant of the thread, provided they can read the repository and
 932have not muted it; watchers are not told about a mention addressed to
 933someone else.
 934You are never mailed about your own actions, and only verified primary
 935addresses receive anything. Delivery retries on relay failure.
 936
 937=notifications settings reply on= lets you answer that mail: issue and
 938merge request mail then carries a =Reply-To= address, and replying to
 939it posts your reply as a comment on the thread, as you. Off by
 940default; the account page has the switch when the instance reads
 941replies, and turning it on is refused where it does not
 942(=[mail.inbound]= off). A reply is posted only when it comes from one
 943of your verified addresses, you can still comment on the thread, and
 944the mail it answers is less than thirty days old. Quoted text (lines
 945starting with =>=, the "On … wrote:" line and what follows it, the
 946header block Outlook quotes) and everything after a =-- = signature
 947line are removed, and the rest is stored as markdown. HTML-only mail is
 948not accepted; send plain text or the usual plain-and-HTML pair. A
 949refused reply gets no answer: nothing is mailed back. Each reply
 950address names you, so do not forward notification mail you would not
 951want answered from your own address.
 952
 953Push is the same activity again, delivered to a phone: the iOS app
 954registers a device, and =notifications settings push off= silences it
 955the way =mail off= silences mail, without deregistering anything.
 956=notifications device list= shows what is registered (token shown
 957truncated); =notifications device remove <id>= drops one by hand, from
 958the CLI or the account page. A device is also dropped on its own the
 959moment Apple reports the token dead, so an app deleted from a phone
 960stops costing anything without you having to notice. Push only works
 961when the instance operator has configured it: on an instance with
 962=[push] enabled = false=, =notifications device add= refuses rather
 963than registering a device nothing can deliver to.
 964
 965Push notifications carry the notice in full: a private repository's
 966name and the issue or merge request number reach Apple and can appear
 967on a lock screen, the same as any other text a phone shows in a
 968notification. This is deliberate, not an oversight — weigh it against
 969what you keep in a private repository before registering a device.
 970
 971* Web vocabulary
 972
 973Sign in / Log out, Search, sentence-case headings and buttons, product
 974name lowercase.
 975
 976* Output rules
 977
 978=--json= is the contract; the plain output is for a person at a
 979terminal, and follows these rules so every noun reads the same way.
 980
 981- A list command prints one row per item, tab-separated, no header.
 982  Columns run identifier, state, then description; a trailing column
 983  may carry a word (=due 2027-01-01=, =via team=). Piped, a timestamp in
 984  a row is RFC3339 to the second, UTC. Under =--json= nothing is
 985  touched.
 986- An empty list prints nothing on stdout and =nothing to list= on
 987  stderr.
 988- A mutation prints one line: verb, object, identifier
 989  (=created krz/gitbay#7=). A second line appears only for something
 990  to copy: a URL, a token shown once.
 991- A bad invocation prints =usage:= and the command's registered usage,
 992  the same text =help <noun>= shows. Exit 2.
 993- A refusal says who may and what to do instead: =only admins of acme
 994  can manage teams; ask one to add you=. Exit 4. A thing that does not
 995  exist, or that you may not know exists, is exit 3.
 996- stdout is the result; stderr is everything else: progress, a note
 997  that output was truncated, errors.
 998- Verbs: =create= and =delete= for things with their own identity
 999  (repository, issue, release, team), =add= and =remove= for attaching
1000  something to them (a key, a member, a label on an issue), =set= for
1001  a value, =revoke= for a credential, =show= and =list= for reads.
1002
1003** At a terminal
1004
1005The =gitbay= CLI sends a leading =--term=<cols>[,<option>]...= argument on
1006the SSH command line when stdout is a terminal (OpenSSH's multiplexed
1007sessions, which the CLI uses, do not forward a session's =SetEnv=).
1008The argument goes first: the server reads =--term=<v>= only as the
1009first argument, and ignores it over HTTP. =color= is the one option
1010the server acts on; it ignores options it does not know. The server
1011then prints:
1012
1013- lists under a header, padded, fitted to the width (the title or
1014  description column is cut with =…= first; a title or description
1015  column blank on most rows is held to a third of the width), with no
1016  trailing whitespace, and the next page as a command on stderr (piped
1017  output keeps a =next\t<cursor>= row instead);
1018- colour by meaning: green for open, success, approved, verified; magenta
1019  for merged; red for failures, =private=, and bad or untrusted
1020  signatures; dim for closed, pending, archived, unsigned; yellow only
1021  for what waits on you (an unverified address); references (=#12=,
1022  =krz/gitbay=, SHAs) in cyan. A marker piped output keeps as its own
1023  trailing cell (=[archived]=, =primary=) joins the state at a terminal:
1024  =public, archived=;
1025- lists that say what to do next: =mr list= with checks, review state
1026  and age, =issue list= with labels, assignee, comments and age, a
1027  =dashboard= that opens with what needs you; a column empty on every
1028  row is left out. These columns are terminal-only; piped rows keep
1029  their own;
1030- diffs coloured (file headers bold, hunks cyan, added green, removed
1031  red), and =build log= with its step lines bold, the failed one red;
1032  both made safe like any user text, the log's own colour codes dropped;
1033- ages as =2h ago= and =in 5h=, sizes as =3.0 MiB=, durations as
1034  =53m43s=, where piped output keeps RFC3339, bytes and seconds;
1035- =show= views with a title line, aligned fields, the body rendered
1036  from markdown or org, one line per event, and comments under a rule;
1037  a sub-table (labels, revisions, and the like) carries a section label
1038  line in both terminal and piped output; timestamps as
1039  =2026-09-23 23:26 UTC=. =wiki show= piped prints the page source
1040  verbatim; the rendered page is terminal-only;
1041- errors on stderr after a red =error:=, a mistyped flag with the flag
1042  it is closest to (=did you mean --state?=), and a usage line wrapped
1043  to the width between its bracketed groups, one alternative per line;
1044- help with flag descriptions, defaults, and examples; =gitbay --help=
1045  groups commands under WORK, REPOSITORIES, YOU and INSTANCE;
1046  =help --json= adds =flags= and =examples=.
1047
1048=NO_COLOR=, =TERM=dumb= and =--no-color= drop the colour. =show=,
1049=diff= and =log= at a terminal go through =$GITBAY_PAGER=, else
1050=$PAGER=, else =less= (with =LESS=FRX= when =LESS= is unset); an empty
1051=GITBAY_PAGER= turns paging off. Paging never applies to =--follow=,
1052=--json=, or piped output.
1053
1054=GITBAY_TERM=off= stops the CLI sending =--term= at all, for an
1055instance older than v1.36.0, which refuses the argument as an unknown
1056command.
1057
1058Two more options say what the terminal can show. =truecolor= (sent when
1059=COLORTERM= is =truecolor= or =24bit=) puts a dot in each label's own
1060colour beside its hex in =label list=. =links= (sent in iTerm2, WezTerm,
1061Ghostty, VS Code, kitty and VTE terminals, or when =GITBAY_LINKS=1=;
1062=GITBAY_LINKS=0= stops it) makes references in lists open their page,
1063as OSC 8 hyperlinks. A terminal outside that list would print the
1064escape's text, so it is not sent there by default. =GITBAY_TERM=basic=
1065sends only the width and colour, for an instance older than the release
1066that added these options, which turns any option it does not know into
1067plain output.
1068
1069Stock ssh without the CLI's multiplexing gets the plain output unless
1070it passes the same leading argument or sets the environment variable
1071sshd is told to accept. OpenSSH parses options after the host, so the
1072argument goes after =--=:
1073
1074#+begin_src sh
1075ssh git@gitbay.org -- --term=120,color issue list krz/gitbay
1076ssh -o SetEnv=GITBAY_TERM=120,color git@gitbay.org issue list krz/gitbay
1077#+end_src
1078
1079* Scripting
1080
1081Every read command takes =--json= and emits one envelope:
1082={"protocol_version": 1, "data": ...}=. stdout is data, stderr is
1083messages. Exit codes are stable: 0 ok, 1 failure, 2 usage, 3 not found,
10844 denied, 5 server/protocol error. Usage means the arguments were wrong;
1085a refusal — a name already taken, a state that does not allow the change
1086— is a failure. Nothing ever prompts; destructive commands take =--yes=.
1087
1088For HTTP automation see [[API]].
1089
1090* CLI setup
1091
1092#+begin_src sh
1093gitbay remote add myforge forge.example.org [--port n] [--user u] --default
1094gitbay remote list
1095gitbay init [name] [--private]   # git init + repo create + origin, in one step
1096#+end_src
1097
1098Configuration lives at =~/.config/gitbay/config.toml=. The CLI shells
1099out to your real =ssh=, so =~/.ssh/config=, the agent, and hardware keys
1100all apply. It shares one connection per instance through a control
1101socket under =~/.ssh=: the first command in five minutes pays the
1102handshake and the rest ride it. =no_multiplex = true= on an instance in
1103the config turns that off. Man pages: =gitbay man --dir <dir>=; completions:
1104=gitbay completion bash|zsh|fish=.