.gitbay/wiki/Users.org
1133 lines · 56241 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* Deleting your account
370
371#+begin_src sh
372gitbay auth delete --confirm <username> # mails a link to your primary address
373gitbay auth delete --cancel # withdraw it before the link is opened
374#+end_src
375
376The same is at the bottom of =/settings=. Nothing changes until the
377mailed link is opened (it works for 24 hours, and needs a verified
378address). Opening it disables the account at once and deletes it seven
379days later: its repositories, snippets, keys, addresses and tokens go.
380Issues, merge requests, comments and reviews it wrote on other people's
381repositories stay, under the name =ghost=. Signing in during the seven
382days — on the web, or any command over SSH with a full-scope key —
383cancels; git-, read-, runner-scoped keys and API tokens are refused and
384cannot cancel. The only admin of an organization is refused until
385another admin exists or the organization is deleted. Export first
386(=gitbay auth export=) to keep a copy. The username is free again after
387the purge.
388
389* Organizations
390
391Orgs share the owner namespace with users and own repositories at
392=org/repo=. By default members get write on all org repos; org admins
393get repo admin, create repos under the org, and manage membership. An
394org's existence and member list are public; its teams are visible to
395members, and anyone else asking is refused rather than told the org
396does not exist.
397
398#+begin_src sh
399gitbay org create krz
400gitbay org members add krz alice [--role admin]
401gitbay org show krz
402gitbay org rename krz newname # clone URLs change
403gitbay org delete krz --yes # only when it owns no repositories
404#+end_src
405
406Large orgs scope access with teams: set what plain membership implies,
407then grant per-repo roles through named teams (org admins always keep
408admin; the default =write= keeps the simple model):
409
410#+begin_src sh
411gitbay org settings members-role krz none # write | read | none
412gitbay org team create krz core-devs
413gitbay org team add krz core-devs alice bob # org members only
414gitbay org team grant krz core-devs krz/gitbay write
415gitbay org team show krz core-devs # members + grants
416gitbay org team revoke / remove / delete ...
417#+end_src
418
419* Issues
420
421Anyone who can read a repository can file and comment. Closing/reopening
422is for the author or anyone with write; labels and assignees need write.
423
424#+begin_src sh
425gitbay issue create --title "it breaks" [--body "..." | --file -]
426 [--label bug]... [--milestone v1.0] [--assignee alice]... # these need write
427gitbay issue list [--state open|closed|all]
428gitbay issue show 4
429gitbay issue comment 4 --message "same here"
430gitbay issue react 4 +1 [--comment 12] [--remove] # the same on mr
431gitbay issue edit 4 --title "better title" [--body|--file -] # author or write
432gitbay issue close 4 / reopen 4
433gitbay issue label 4 --add bug --remove wontfix
434gitbay issue assign 4 --add alice
435gitbay issue milestone 4 v1.0 # or "none" to clear
436#+end_src
437
438A reaction is one of =+1 -1 laugh hooray confused heart rocket eyes=,
439given by name or as the emoji itself, on an issue or merge request or on
440one of its comments (=show= prints comment ids). Anyone who may comment
441may react; reacting twice or removing an absent reaction does nothing.
442=show= carries the counts and which are yours. A reaction sends no
443notification and appears in no activity feed.
444
445Inside a clone, the repository is inferred from the =origin= remote —
446that is why no =owner/name= appears above. Anywhere else, pass it as the
447first argument. Long text: =--body= inline, =--file -= from stdin, or
448neither on a terminal and =$EDITOR= opens.
449
450A wiki is the =.gitbay/wiki/= directory on the default branch — no
451separate repository, the same rendering pipeline as READMEs (markdown
452and org). Editing it is editing a file: a push, =repo commit-file=, or
453the web editor on a repository that permits server-authored commits (one
454requiring signed commits is push-only, since the server signs nothing):
455
456#+begin_src sh
457mkdir -p .gitbay/wiki
458# add Home.md (or .org), more pages, images; relative links between
459# pages just work on the web at /you/project/wiki
460git add .gitbay/wiki && git commit -m "wiki" && git push
461#+end_src
462
463Pages may sit in subfolders. A nested page is named by its path —
464=.gitbay/wiki/Guide/Install.md= is =/you/project/wiki/Guide/Install= and
465=wiki show you/project Guide/Install= — and the sidebar groups it under
466its folder. A relative link or image on a nested page resolves from the
467page's own folder first, then from the top of the wiki, so a link to
468=Home= still reaches the top-level page. SVG images render.
469
470Releases anchor notes and binary assets to a pushed tag (write access;
471assets stream over SSH, capped by the instance's =max_asset_bytes=).
472While a release exists its tag can be neither deleted nor moved, whether
473or not the tag is protected: delete the release first.
474
475#+begin_src sh
476gitbay release create v1.0 --title "First light" [--notes|--file -|$EDITOR]
477gitbay release asset add v1.0 tool-linux-amd64 < dist/tool-linux-amd64
478gitbay release asset get v1.0 tool-linux-amd64 > tool # or the web download link
479gitbay release list / show v1.0 / delete v1.0 --yes
480#+end_src
481
482The web shows them under the repository's =releases= tab with rendered
483notes, sha256 sums, and download links; writers add and remove assets
484there (a file upload, up to =max_asset_bytes=; removal asks for the file
485name). Feed readers subscribe at
486=/you/project/releases.atom=; commits are at =/you/project/log.atom=
487(the default branch) or =/you/project/log.atom/<ref>=, and an owner's
488activity on their public repositories at =/you/activity.atom=. The
489pages carry the discovery link.
490
491Commit messages act on issues when the commits land on the default
492branch (direct push, MR merge, =repo commit-file= or the web editor):
493=closes/fixes/resolves #4= closes the
494issue with a linking comment, and a bare =#4= leaves a reference
495comment. Each issue/commit pair acts once, ever. =Closes owner/name#N= closes an issue in another repository when
496you hold write there with a key whose scope reaches it; otherwise it
497stays a plain link. A deploy key closes nothing outside the repository
498it is bound to, but naming that repository by full path works as the
499bare form does. The comment left
500on the closed issue links the closing commit by its repository path, so
501closing a public repository's issue from a private one names the private
502repository there. A bare =owner/name#N= links and does nothing.
503
504Milestones group issues and MRs toward a release (write access to
505manage, attach with =issue milestone= / =mr milestone=; progress shows
506on the web at =/owner/name/milestones=, where writers create, close and
507reopen them):
508
509#+begin_src sh
510gitbay milestone create v1.0 --description "first release" --due 2027-01-01
511gitbay milestone list [--state open|closed|all]
512gitbay milestone close v1.0 / reopen v1.0
513#+end_src
514
515An org holds labels and milestones every repository under it sees
516beside its own. =issue label --add=, =mr label --add=, =issue
517milestone= and =mr milestone= resolve the org's row first; a repository
518cannot create a label or milestone with a name its org holds. Creating
519an org label or milestone whose name repositories under the org already
520use folds them in: their issues and merge requests move to the org's
521row. Org admins manage them, on the CLI or at =/<org>/-/labels= and
522=/<org>/-/milestones=; counts span the repositories you can read.
523
524#+begin_src sh
525gitbay org label set acme bug --color cf222e
526gitbay org label list acme / remove acme bug
527gitbay org milestone create acme v2 --due 2027-03-01
528gitbay org milestone list acme [--state open|closed|all]
529gitbay org milestone close acme v2 / reopen acme v2
530#+end_src
531
532On the web: =/acme/-/labels= and =/acme/-/milestones=, read-only.
533
534Issue templates: commit =.gitbay/issue-template.md= (and optional
535=issue-template-<name>.md= variants) to the default branch. =gitbay
536issue create= prefills =$EDITOR= with the default template, the web
537form prefills its textarea, and =gitbay issue templates= lists them.
538
539Lists narrow the same way on every surface: =issue list --label bug
540--assignee bob --author alice --milestone v1= (or =--milestone none=),
541=mr list --label bug --author bob --milestone v1=; the web's issue and
542merge request lists take the same names as query parameters, and each
543active filter shows with a link that drops it.
544
545A query spans repositories, and a saved one keeps it under a name:
546
547#+begin_src sh
548gitbay query save mine is:open assignee:@me
549gitbay query save triage 'repo:krz/*' is:issue is:open no:label
550gitbay query save v2 owner:krz label:bug 'label:needs review' milestone:v2
551gitbay query list / show mine / remove mine
552gitbay query run mine [--limit 20] [--cursor <c>] # issues and MRs
553gitbay issue list --query mine # only the issues
554gitbay mr list --q 'owner:krz is:open author:@me' # a query written out
555gitbay query pin mine # on the dashboard; unpin
556#+end_src
557
558The terms: =repo:owner/name=, =repo:owner/glob*= (=*= in the name
559only), =repo:*=, =owner:name=; =is:open=, =is:closed=, =is:merged=,
560=is:issue=, =is:mr=; =label:x= (repeat it: every label must be there),
561=no:label=; =milestone:x=, =no:milestone=; =assignee:user=,
562=author:user=, either as =@me= for whoever runs the query. Anything
563without a colon is text matched against title and body, as =search=
564does. Every term narrows, except that several =repo:= and =owner:=
565terms widen the scope to any of them; with none the query covers every
566repository you can read, and only those — someone else's private
567repository is neither a row nor part of a count. Merge requests have no
568assignees, so =assignee:= means issues. A term that does not parse
569exits 2 and names itself. =@me= is resolved when the query runs, and
570the saved text is the query in a canonical order.
571
572Rows come newest first, each naming its repository, paged as ={items,
573next}= with fifty to a page unless =--limit= says otherwise.
574An account keeps at most 50 saved queries, 10 of them pinned; past
575either, =query save= or =query pin= exits 2 until one is removed or
576unpinned. =dashboard --json= carries each pinned query's count and first five
577rows under =queries=; the web dashboard shows them, and
578=/you/-/queries= lists your saved queries with a page per query.
579
580Labels take a colour: =gitbay label set bug --color cf222e=; =label
581list= shows each with its colour and how many issues and merge requests
582carry it, and =label remove= takes one off all of them. =issue label
583--add= and =mr label --add= still create a colourless label on the fly.
584
585* Merge requests
586
587#+begin_src sh
588gitbay mr create --source feature --target main --title "add thing"
589gitbay mr create other/upstream --source you/fork:feature --target main --title "..."
590gitbay mr list / show 4 / diff 4
591gitbay mr checkout 4 # local branch mr/4 from the MR head
592gitbay mr review 4 --approve # or --request-changes / --comment
593gitbay mr label 4 --add bug --remove wontfix
594gitbay mr merge 4 [--strategy ff|merge|squash|rebase]
595gitbay mr merge 4 --when-ready # merge once the gates pass; --cancel dequeues
596gitbay mr close 4
597#+end_src
598
599A merge request closed without merging can name the one that carries
600its change forward: =mr close 4 --by 7= records it and both pages show
601it, and =mr edit 4 --superseded-by 7|none= sets or clears it
602afterwards, refused on anything but a closed merge request.
603
604The web list shows each request's combined check state and comment
605count alongside its title, so open work needing attention stands out
606without opening it.
607
608Semantics worth knowing:
609
610- the MR head lives in the *target* repository as
611 =refs/merge-requests/N/head= (fetchable by any reader), so an MR
612 survives deletion of its source branch or fork. It is never removed
613 on merge or close; after a history rewrite an instance admin can
614 drop it with =admin mr prune=.
615- force-pushing the source updates the MR and marks existing reviews
616 stale — unless the diff is the one they reviewed. A rebase onto a
617 target that moved on changes every sha and nothing about the change,
618 so fresh reviews follow it to the new head; a push that changes the
619 diff stales them.
620- default strategy: fast-forward when possible, else a merge commit.
621 Squash makes one commit authored by the MR author, committed by the
622 merger. Rebase replays a linear range preserving authors; it refuses
623 ranges containing merge commits, and when fast-forward is possible it
624 *is* one (original commits and signatures land untouched).
625- on =require_signed_commits= branches only fast-forwards of fully
626 verified commits merge; everything server-created is refused with
627 instructions to rebase locally. =gitbay mr rebase <n>= is those
628 instructions: in a clone of the target it replays the source branch
629 onto the target and force-pushes it, then =mr merge <n>= fast-forwards.
630 The replay is local, so the commits carry your signature and not the
631 server's — it holds no key. A branch living in a fork is refused, with
632 the repository to run it in; rebase it there.
633
634Repo admins can gate merges (=repo settings ...=): =require-approvals
635<n>= (fresh, non-author approvals; each reviewer's latest review is
636their stance, and a fresh request-changes blocks), =require-resolved=
637(no open review threads), =require-checks= (all statuses green), and
638=require-codeowners= (an approval from an owner of every owned changed
639file). Owners come from a =CODEOWNERS= file on the target branch, root
640or =.gitbay/=, gitignore-style patterns, last match wins. The toggle is
641the opt-in, so a repository can carry the file as documentation of who
642to ask without it gating merges; with it on and no file on the target
643branch, the merge is refused and says so. It does not wait on
644=require-approvals=.
645
646=mr show= carries a =gates= block with where the merge request stands
647against all of them — approvals counted and required, owners still
648outstanding per file, open threads, checks, and whether a fast-forward
649is possible — and the merge request page shows the same block. =mr
650merge= names every unmet gate at once rather than the first. =mr
651review= says when a verdict is advisory, which it is from anyone
652without write access: the gates do not count it.
653
654=mr merge <n> --when-ready= queues the merge and merges once the gates
655pass, or at once if they already do. It is tried again when a status
656is reported on the head, a review is submitted, a thread is resolved,
657the request is marked ready, and the source branch is pushed; a push
658by someone who can write to the target keeps it queued, and the new
659head has to pass on its own. The merge is made as the user who queued
660it, with their rights checked at that moment. Anything the queuer can
661fix (an unmet gate, a conflict, a branch behind a
662=require_signed_commits= target that needs a rebase) leaves it queued
663with the reason shown on =mr show= and the page.
664
665The queue is bound to the credential it was made with. It is dequeued,
666with the reason on the request's timeline, when:
667
668- the queuer loses write access or their account is disabled;
669- the SSH key or API token it was queued with is removed, revoked,
670 expires, or no longer has full scope (a merge queued from the web
671 rests on the account alone);
672- someone who cannot write to the target pushes to the source branch
673 or retargets the request;
674- the source branch is deleted, or the request is closed.
675
676An expiring key or token cannot queue a merge; use one without an
677expiry, or the web. =mr merge <n> --cancel= dequeues without closing.
678=merge= and =squash= are refused at queue time on a
679=require_signed_commits= repository.
680
681Those gates apply to =mr merge=. A direct push to a protected branch
682passes none of them until =require-mr on=: then an existing protected
683branch refuses every push, including =repo commit-file= and the web
684editor, and the server's merge is its only writer. Creating the branch
685is still a push, since there is nothing to open a merge request against
686yet.
687
688A merge request whose target is another open merge request's source
689branch is stacked on it: =mr create= says so, =mr show= carries
690=stacked_on= and =stacked=, and merging the lower one retargets the
691upper onto =main= with its reviews kept. See [[Stacked-MRs][Stacked
692merge requests]].
693
694Review threads anchor to diff lines:
695
696#+begin_src sh
697gitbay mr diff-comment 4 --path main.go --line 12 --message "use log here"
698gitbay mr diff-comment 4 --reply 7 --message "done" # join thread 7
699gitbay mr threads 4 # threads with staleness
700gitbay mr resolve 4 7 / unresolve 4 7
701#+end_src
702
703Threads render inline on the MR page. A force-push marks them stale
704(shown under "threads on earlier revisions") rather than guessing new
705anchors; =mr show= reports the unresolved count. Resolving is for the
706thread author, the MR author, or anyone with write.
707
708A thread can span lines: =--start-line 12 --line 13= anchors it to
709lines 12 and 13 of the new file. A fenced =suggestion= block in the
710comment proposes replacement lines for that range; an empty block
711proposes deleting it.
712
713#+begin_src sh
714gitbay mr diff-comment 4 --path main.go --start-line 12 --line 13 --file - < suggestion.md
715gitbay mr apply-suggestion 4 9 # commit thread 9's suggestion
716#+end_src
717
718where =suggestion.md= is
719
720#+begin_example
721one call does both
722```suggestion
723 log.Printf("starting %s", name)
724```
725#+end_example
726
727The page and =mr threads= show a suggestion as the lines it replaces
728and the lines it proposes; =mr threads --json= carries it as
729=suggestion= with the path, the line range, the commit and blob it was
730made against, the original and replacement text, =outdated= with a
731reason, and =apply= (=server= or =local=). A suggestion is outdated
732once the lines it replaces differ at the head from what it was made
733against, or the file is renamed or deleted; it cannot be applied then.
734
735Applying commits the replacement to the source branch as the applying
736user, with a message naming the merge request and thread, and resolves
737the thread if the applier could resolve it by hand (the thread author,
738the MR author, or a writer of the target); otherwise the thread stays
739open and the output says so. It is a push: only the source branch's
740writers can apply (for a merge request from a fork, the fork's
741writers), the branch's protection, =require-mr= and the owner's storage
742quota apply, and a queued merge treats it as a push by the applier. The web's Apply suggestion button and =mr
743apply-suggestion= commit it on the server. The server cannot sign, so
744where the source or target requires signed commits (=apply= is
745=local=) the CLI fetches the source branch into the clone it runs in,
746builds the commit without touching the working tree, signs it with
747=git commit-tree -S= under your git signing configuration, pushes it,
748and resolves the thread; the page shows that command.
749
750* CI builds
751
752A =.gitbay/ci.yml= in the repo runs jobs on every branch push:
753
754#+begin_src yaml
755jobs:
756 test:
757 steps:
758 - go test ./...
759#+end_src
760
761A config holds at most ten jobs of fifty steps each, each step under
7624096 bytes, in a file under 64 KiB; a longer step runs a script from the
763repository (=- sh ci/publish.sh=). A config past a cap, or otherwise
764unparseable, records a failed =ci/config= status on the commit and
765queues nothing, so look there when a push builds nothing.
766
767Each job becomes a build (=build list=, =build log=, the builds tab on
768the web) and a =ci/<job>= commit status, which =repo settings
769require-checks= can gate merges on. =repo settings
770require-contexts= names statuses the gate waits for until they report,
771and turns the gate on. =build list= takes =--ref=,
772=--status= and =--job= to narrow the listing, combinable; the builds
773tab reads the same flags from its =?ref=, =?status= and =?job= query
774parameters and groups the result into one row per commit. =build show=
775names a failed build's step and how long it ran, and =build log= takes
776=--step <n>|failed= and =--tail <lines>=. Steps run
777with =sh -c= on the instance's runner, stopping at the first failure;
778a broken config surfaces as a failed =ci/config= status. Environment:
779=GITBAY_REPO=,
780=GITBAY_SHA=, =GITBAY_REF=, =GITBAY_JOB=, =CI=true=, and =GITBAY_SSH=,
781the instance's ssh destination as the build reaches it: on the forge's
782own runner, =git@169.254.1.2=, pasta's address for the host, with
783=:port= when the instance's ssh is not on 22; from any other runner,
784the destination that runner polls (its =-remote=, such as
785=git@gitbay.org=). Use it as =ssh://$GITBAY_SSH/owner/name.git= or
786=ssh ssh://$GITBAY_SSH …=, which work in either form. A job that
787talks back to the instance — a release asset, a comment, a push to a
788pages branch — uses =$GITBAY_SSH= with a key it holds as a secret;
789the build's container has no key of its own. On the forge's own runner a
790build from a fork's merge request cannot reach the instance at all, and
791reaches the internet only over HTTPS, HTTP and DNS (CI page). Two things about that
792container: a secret with newlines (a private key) arrives intact, and
793=ssh= expands =~= from the passwd entry, =/root=, not from =$HOME=,
794which is the build home — so keep an ssh config in the workspace and
795pass it with =ssh -F= (and =GIT_SSH_COMMAND="ssh -F ..."= for git),
796which also works on a runner with no container, where writing to
797=~/.ssh= would edit that machine's own configuration.
798
799Secrets: =repo secret set <owner/name> <NAME>= reads the value from
800stdin (never argv) and injects it into the repo's builds as =$NAME=;
801=repo secret list= shows names only, and the value is never echoed
802back. Anyone with write access can read a secret from inside a build,
803so scope them accordingly.
804
805Schedules: a job with =schedule: "17 11,23 * * *"= (five-field cron,
806server-local time; lists, ranges, and steps supported) runs on its cron
807against the default branch instead of on push. A default-branch push
808registers or updates the schedule. A job with =tags: "v*"= runs when a
809matching tag is pushed — and only then; =schedule= and =tags= are
810mutually exclusive. =build trigger <owner/name> <job>= queues any job
811immediately, and =build cancel <owner/name> <n>= withdraws one, queued
812or running: a running build stops at the runner within seconds and the
813log says who cancelled it. Both need write access.
814
815What each push shape queues, with dedupe, path filters, schedules and
816the reaper together, is one table on [[CI][CI]].
817
818** Your own runner
819
820Builds run on runners attached to the repository. An instance need not
821offer any: install =gitbay-runner= on a machine of yours and attach it.
822
823#+begin_src sh
824brew install krz/tap/gitbay-runner # or a binary from the release
825gitbay-runner init -remote git@gitbay.org
826#+end_src
827
828=init= generates a key under =~/.config/gitbay-runner/=, writes
829=config.toml= beside it, and prints the public key with the command to
830attach it:
831
832#+begin_src sh
833gitbay repo runner add owner/name < ~/.config/gitbay-runner/id_ed25519.pub
834#+end_src
835
836or paste the key under Runners on the repository's settings page. Then
837=brew services start krz/tap/gitbay-runner=, or run =gitbay-runner= with
838no arguments; it reads the config file, and any flag overrides it.
839
840What it builds: every build for the repositories it is attached to,
841with the repository's secrets, and nothing else. Merge requests from
842forks are untrusted and wait unless the runner runs with =-untrusted=,
843which is only sensible with =-isolation podman -image <ref>= (see
844[[Admin][Admin]]). Attach one runner to several repositories by repeating
845=repo runner add=; run several runners on one account by running =init=
846on each machine. =repo runner list= shows each attached key, when it
847last polled and the build it holds; =repo runner remove <fingerprint>=
848detaches one (the key stays on your account; =keys remove= drops it).
849A runner key reaches only the runner protocol and read-only git, so a
850build step that reads it off disk cannot administer your account.
851
852* Large files (LFS)
853
854Standard Git LFS works over both transports with no setup beyond the
855usual =git lfs track=. SSH remotes authenticate through
856=git-lfs-authenticate= (deploy keys included: ro keys can download, rw
857keys upload); anonymous HTTPS clones of public repositories can fetch
858LFS objects with no credentials. Objects are verified against their
859sha256 on upload and capped at 512MB by default.
860
861* Pages
862
863On instances with =[pages] domain= set, a =pages= branch in any public
864repo is served as a static site: the repo named =pages= at
865=https://<you>.<domain>/=, every other repo at
866=https://<you>.<domain>/<repo>/=. Push HTML to publish; a CI job can
867build and push the branch for automatic deploys. Sites run on a
868separate origin — your scripts work, and the forge's cookies are out of
869reach.
870
871A repo can also serve its pages branch on a domain you own.
872=repo domain add <owner/name> <domain>= claims it and prints a DNS TXT
873challenge (=_gitbay-challenge.<domain>=); create the record, run
874=repo domain verify=, then point the domain's A/AAAA records at the
875instance (DNS-only if the domain sits behind a proxying provider — the
876instance issues its own certificates). Claims are exclusive per
877instance; unverified claims serve nothing and expire after 7 days.
878=repo domain list= reports pending/verified/expired.
879
880* Snippets
881
882A snippet is one or more named text files you own outside any
883repository, for a log or a fragment shared by URL. Create one from a
884file on stdin; the reply is the id and the URL:
885
886#+begin_src sh
887gitbay snippet create build.log --description "failing build" < build.log
888gitbay snippet file set <id> notes.txt < notes.txt # add or replace a file
889gitbay snippet file get <id> build.log > build.log
890gitbay snippet edit <id> --visibility public
891gitbay snippet list # yours
892gitbay snippet list <owner> # their public ones
893gitbay snippet delete <id>
894#+end_src
895
896Visibility is =public= (listed on your page), =unlisted= (anyone with
897the URL, listed nowhere; the default) or =private= (you alone; not
898found to everyone else). Files are text, valid UTF-8, each under the
899instance's =max_snippet_bytes=, at most 64 per snippet. A snippet keeps
900at least one file. There is no history: setting a file replaces it.
901
902On the web, =/<you>/-/snippets= lists yours, each snippet page renders
903its files with a raw link per file, and the same page creates, edits
904and deletes through the commands above.
905
906* Browser sessions
907
908=gitbay web login= mints a one-time URL; the session it opens ends
909after twelve hours without a request, and after seven days in any case.
910=gitbay web sessions list= shows each of yours by a short id with its
911creation, expiry and last use, and =gitbay web sessions revoke <id>=
912or =--all= ends them from the terminal, which is where a lost laptop is
913handled.
914
915Actions that create a credential or grant access (adding a key, token
916or email, org and repository roles, transfers, a repository's
917visibility) ask you to sign in again
918when your web sign-in is older than 15 minutes. The form shows a "Sign
919in again" link and the login returns to the page. Idle renewal does not
920extend this window.
921
922=web theme set light= or =dark= fixes the web UI's colour scheme for
923your account; =system=, the default, follows the browser's own
924preference. =web theme show= prints it. The account page has the same
925control under Appearance.
926
927=web diff set split= draws diffs side by side on the merge request,
928commit and compare pages; =unified=, the default, keeps one column.
929=web diff show= prints it, and the account page has the same control
930under Appearance. =?layout=split= or =?layout=unified= on a diff page
931overrides the setting for that request. In a narrow window the split
932layout stacks the old line over the new one, as a unified diff does.
933Each column's line numbers open a comment on their own side.
934
935* Notifications
936
937When the instance has SMTP configured, activity mails you as well as
938filing the inbox row: someone opens an issue or MR on your repository,
939comments where you are a participant (author, commenter, reviewer, or
940mentioned), reviews, closes, merges, or assigns you.
941=notifications settings mail off= keeps the inbox and stops that mail
942(login links are not activity and still arrive); the account page has
943the same switch.
944=notifications settings watch on= makes you a recipient of every issue
945and merge request on the repositories you can write to, as if you had
946run =repo watch= on each: it is consulted when a notice is delivered,
947so a grant or a revoke needs no watch row, and an explicit watch or
948mute on a repository still wins. Off by default; the account page has
949this switch too. Writing =@name= in an issue, merge request or
950comment files "mentioned you" in that account's inbox and makes them a
951participant of the thread, provided they can read the repository and
952have not muted it; watchers are not told about a mention addressed to
953someone else.
954You are never mailed about your own actions, and only verified primary
955addresses receive anything. Delivery retries on relay failure.
956
957=notifications settings reply on= lets you answer that mail: issue and
958merge request mail then carries a =Reply-To= address, and replying to
959it posts your reply as a comment on the thread, as you. Off by
960default; the account page has the switch when the instance reads
961replies, and turning it on is refused where it does not
962(=[mail.inbound]= off). A reply is posted only when it comes from one
963of your verified addresses, you can still comment on the thread, and
964the mail it answers is less than thirty days old. Quoted text (lines
965starting with =>=, the "On … wrote:" line and what follows it, the
966header block Outlook quotes) and everything after a =-- = signature
967line are removed, and the rest is stored as markdown. HTML-only mail is
968not accepted; send plain text or the usual plain-and-HTML pair. A
969refused reply gets no answer: nothing is mailed back. Each reply
970address names you, so do not forward notification mail you would not
971want answered from your own address.
972
973Push is the same activity again, delivered to a phone: the iOS app
974registers a device, and =notifications settings push off= silences it
975the way =mail off= silences mail, without deregistering anything.
976=notifications device list= shows what is registered (token shown
977truncated); =notifications device remove <id>= drops one by hand, from
978the CLI or the account page. A device is also dropped on its own the
979moment Apple reports the token dead, so an app deleted from a phone
980stops costing anything without you having to notice. Push only works
981when the instance operator has configured it: on an instance with
982=[push] enabled = false=, =notifications device add= refuses rather
983than registering a device nothing can deliver to.
984
985Push notifications carry the notice in full: a private repository's
986name and the issue or merge request number reach Apple and can appear
987on a lock screen, the same as any other text a phone shows in a
988notification. This is deliberate, not an oversight — weigh it against
989what you keep in a private repository before registering a device.
990
991* Web vocabulary
992
993Sign in / Log out, Search, sentence-case headings and buttons, product
994name lowercase.
995
996* Output rules
997
998=--json= is the contract; the plain output is for a person at a
999terminal, and follows these rules so every noun reads the same way.
1000
1001- A list command prints one row per item, tab-separated, no header.
1002 Columns run identifier, state, then description; a trailing column
1003 may carry a word (=due 2027-01-01=, =via team=). Piped, a timestamp in
1004 a row is RFC3339 to the second, UTC. Under =--json= nothing is
1005 touched.
1006- An empty list prints nothing on stdout and =nothing to list= on
1007 stderr.
1008- A mutation prints one line: verb, object, identifier
1009 (=created krz/gitbay#7=). A second line appears only for something
1010 to copy: a URL, a token shown once.
1011- A bad invocation prints =usage:= and the command's registered usage,
1012 the same text =help <noun>= shows. Exit 2.
1013- A refusal says who may and what to do instead: =only admins of acme
1014 can manage teams; ask one to add you=. Exit 4. A thing that does not
1015 exist, or that you may not know exists, is exit 3.
1016- stdout is the result; stderr is everything else: progress, a note
1017 that output was truncated, errors.
1018- Verbs: =create= and =delete= for things with their own identity
1019 (repository, issue, release, team), =add= and =remove= for attaching
1020 something to them (a key, a member, a label on an issue), =set= for
1021 a value, =revoke= for a credential, =show= and =list= for reads.
1022
1023** At a terminal
1024
1025The =gitbay= CLI sends a leading =--term=<cols>[,<option>]...= argument on
1026the SSH command line when stdout is a terminal (OpenSSH's multiplexed
1027sessions, which the CLI uses, do not forward a session's =SetEnv=).
1028The argument goes first: the server reads =--term=<v>= only as the
1029first argument, and ignores it over HTTP. =color= is the one option
1030the server acts on; it ignores options it does not know. The server
1031then prints:
1032
1033- lists padded and fitted to the width (the title or description
1034 column is cut with =…= first; a title or description column blank on
1035 most rows is held to a third of the width), with no trailing
1036 whitespace. A list has a dim header row only when a column is a
1037 number or a size, which would not say what it counts without one. The
1038 next page is a =Next page= line on stderr with the command that
1039 fetches it (piped output keeps a =next\t<cursor>= row instead);
1040- colour by meaning: green for open, success, approved, verified; magenta
1041 for merged; red for failures, =private=, and bad or untrusted
1042 signatures; dim for closed, pending, archived, unsigned; yellow only
1043 for what waits on you (an unverified address); references (=#12=,
1044 =krz/gitbay=, SHAs) dim. A marker piped output keeps as its own
1045 trailing cell (=[archived]=, =primary=) joins the state at a terminal:
1046 =public, archived=;
1047- every list and show drawn as a screen: a header block of =Label:=
1048 lines (labels dim), the description or notes, sections titled =Title
1049 (n)= in bold blue, and a legend under a rule of the commands that
1050 apply now, grouped and chosen from the item's state and your access.
1051 Rows have no header (unless a column is a number or a size) and no
1052 indent: a dim reference, a state glyph, the text, then dim facts
1053 joined by =·=. The glyphs are =✓= passed, =✗= failed or blocked, =◐=
1054 still going, =●= waiting on you (your review requested, an issue
1055 assigned to you, an unread notification, a pending account, an
1056 overdue milestone, the session's own key), =○= closed or draft. A
1057 section cut short ends with =+n more= and the command for the rest.
1058 Below 80 columns the legend's groups stack; a suggested command is
1059 never cut, so it may run past the width. =dashboard= opens with what
1060 waits on you; for admins a =Problems= line counts background
1061 failures, and =admin stats= has the detail. =repo commit= prints its
1062 screen, then the diff. Secrets never appear on a screen, and a
1063 webhook URL shows its scheme and host only. =wiki show= piped prints
1064 the page source verbatim;
1065- errors on stderr after a red =error:=, a mistyped flag with the flag
1066 it is closest to (=did you mean --state?=), and a usage line wrapped
1067 to the width between its bracketed groups, one alternative per line;
1068- help with flag descriptions, defaults, and examples; =gitbay --help=
1069 groups commands under WORK, REPOSITORIES, YOU and INSTANCE;
1070 =help --json= adds =flags= and =examples=.
1071
1072=NO_COLOR=, =TERM=dumb= and =--no-color= drop the colour. =show=,
1073=diff= and =log= at a terminal go through =$GITBAY_PAGER=, else
1074=$PAGER=, else =less= (with =LESS=FRX= when =LESS= is unset); an empty
1075=GITBAY_PAGER= turns paging off. Paging never applies to =--follow=,
1076=--json=, or piped output.
1077
1078Inside a clone of a repository on the instance, the CLI adds
1079=here=<owner/name>= to =--term=, so a command the output suggests leaves
1080that repository out, as the CLI would infer it. An instance that does
1081not know the option ignores it.
1082
1083=GITBAY_TERM=off= stops the CLI sending =--term= at all, for an
1084instance older than v1.36.0, which refuses the argument as an unknown
1085command.
1086
1087Two more options say what the terminal can show. =truecolor= (sent when
1088=COLORTERM= is =truecolor= or =24bit=) puts a dot in each label's own
1089colour beside its hex in =label list=. =links= (sent in iTerm2, WezTerm,
1090Ghostty, VS Code, kitty and VTE terminals, or when =GITBAY_LINKS=1=;
1091=GITBAY_LINKS=0= stops it) makes references in lists open their page,
1092as OSC 8 hyperlinks. A terminal outside that list would print the
1093escape's text, so it is not sent there by default. =GITBAY_TERM=basic=
1094sends only the width and colour, for an instance older than the release
1095that added these options, which turns any option it does not know into
1096plain output.
1097
1098Stock ssh without the CLI's multiplexing gets the plain output unless
1099it passes the same leading argument or sets the environment variable
1100sshd is told to accept. OpenSSH parses options after the host, so the
1101argument goes after =--=:
1102
1103#+begin_src sh
1104ssh git@gitbay.org -- --term=120,color issue list krz/gitbay
1105ssh -o SetEnv=GITBAY_TERM=120,color git@gitbay.org issue list krz/gitbay
1106#+end_src
1107
1108* Scripting
1109
1110Every read command takes =--json= and emits one envelope:
1111={"protocol_version": 1, "data": ...}=. stdout is data, stderr is
1112messages. Exit codes are stable: 0 ok, 1 failure, 2 usage, 3 not found,
11134 denied, 5 server/protocol error. Usage means the arguments were wrong;
1114a refusal — a name already taken, a state that does not allow the change
1115— is a failure. Nothing ever prompts; destructive commands take =--yes=.
1116
1117For HTTP automation see [[API]].
1118
1119* CLI setup
1120
1121#+begin_src sh
1122gitbay remote add myforge forge.example.org [--port n] [--user u] --default
1123gitbay remote list
1124gitbay init [name] [--private] # git init + repo create + origin, in one step
1125#+end_src
1126
1127Configuration lives at =~/.config/gitbay/config.toml=. The CLI shells
1128out to your real =ssh=, so =~/.ssh/config=, the agent, and hardware keys
1129all apply. It shares one connection per instance through a control
1130socket under =~/.ssh=: the first command in five minutes pays the
1131handshake and the rest ride it. =no_multiplex = true= on an instance in
1132the config turns that off. Man pages: =gitbay man --dir <dir>=; completions:
1133=gitbay completion bash|zsh|fish=.