.gitbay/wiki/Users.org
1133 lines · 56241 bytes
gitbay user guide
- Installing the CLI
- Getting an account
- SSH keys
- Verified commits
- Profiles
- Repositories
- Deleting your account
- Organizations
- Issues
- Merge requests
- CI builds
- Large files (LFS)
- Pages
- Snippets
- Browser sessions
- Notifications
- Web vocabulary
- Output rules
- Scripting
- CLI setup
Everything here works from stock OpenSSH — replace gitbay with
ssh git@<host> in any command. The CLI adds convenience (instance
profiles, repo inference, $EDITOR), nothing more. Two things differ
over bare ssh:
- ssh joins its arguments with spaces and the server tokenizes the
result, so a value with a space in it needs a second layer of quotes:
--description "'a widget'", not--description "a widget", which arrives as two arguments. --helpis not understood.ssh git@<host> helplists every command the server knows, andhelp <noun>(help repo,help mr) prints the usage of each command under that noun.
Installing the CLI
go install gitbay.org/gitbay/cmd/gitbay@latest # any platform with Go
brew tap krz/tap https://gitbay.org/krz/homebrew-tap.git
brew install krz/tap/gitbay # Homebrew (macOS/Linux)
Or build from source: go build ./cmd/gitbay in a clone of
https://gitbay.org/krz/gitbay.git.
Getting an account
How you join depends on the instance's registration mode. On instances
with web accounts enabled, /register offers the same signup as a
browser form (paste your SSH public key); everything below works from
the terminal alone:
- closed
- an admin creates your account on the host and registers your first SSH key. Nothing for you to do but hand over your public key.
- invite
-
you receive a single-use code by email. With the SSH key you want to use:
ssh git@<host> register --username you --invite <code>Your account is active immediately; the invited address is your verified email.
- open
-
ssh git@<host> register --username you --email you@example.orgA verification code arrives by mail. Until you run
ssh git@<host> email verify <code>, the account is pending: you can runwhoamiand the email commands, and nothing else — no git, no repos.
SSH keys
New to SSH keys? SSH-keys makes one in two commands.
Your key is your identity; there are no passwords anywhere. The SSH
username is always git — the key alone determines who you are.
gitbay auth keys list
gitbay auth keys add --scope git < ~/.ssh/ci_key.pub # key on stdin
gitbay auth keys add --label laptop < ~/.ssh/id_ed25519.pub
gitbay auth keys add --scope git --ttl 90d < ~/.ssh/ci_key.pub
gitbay auth keys label SHA256:... "work laptop"
gitbay auth keys remove SHA256:...
A key's label is the comment on its authorized_keys line unless
--label gives one; keys label renames a key, and with no text
clears the name. Labels are one line of up to 64 bytes.
--ttl 90d (or any Go duration, 720h) makes a key stop
authenticating after that long; repo deploy-key add takes the same
flag. An expiring key cannot create credentials: tokens, keys, login
links. keys list shows when each key was last used and when it
expires, so a key nobody uses is easy to spot.
Removing a key closes every connection it opened, including the CLI's shared one; removing the key the current command runs on ends that command's connection too.
Scopes: full (default; git plus every control command), git (git
transport only — right for automation keys, which then cannot touch
issues, settings, or your account), or runner (the CI runner's
protocol plus read-only git, for the key a gitbay-runner host holds;
see Admin).
A key belongs to exactly one account instance-wide. Registering a key someone else already holds is refused without telling you whose it is.
Verified commits
The commit badge is driven by the author email and the signing key:
verified means the signature is valid, the key is registered to an
account, and the author email is a verified address on that account.
For OpenPGP signing (git's default):
gpg --armor --export you@example.org | gitbay auth pgp add
For SSH signing (git config gpg.format ssh): sign with any key
registered on your account; your verified addresses act as the principal
set. No separate registration step.
Add and verify additional addresses with email add <address> /
email verify <code> (requires the instance to have SMTP; otherwise an
admin can assert an address for you). email list shows them. email
remove <address> drops one, with two refusals: the primary stays until
email primary <address> names another verified address, and the last
verified address stays, since activation, login links and commit
identity all resolve through verified addresses. Removing a verified
address re-evaluates signature states, so a commit authored from it
shows as signed_email_mismatch afterwards. A removed address is free
for any account to claim.
The states you will see, in decreasing order of trust: verified,
signed_unknown_key (valid signature, key not registered here — register
it and history upgrades retroactively), signed_email_mismatch (real
key, author line claims someone else), signed_key_expired /
signed_key_revoked, bad_signature, unsigned. Server-created commits
(web edits, merge commits) are always unsigned — the server holds no
signing key on principle.
repo bookmark <owner/name> saves a repository to come back to and
repo unbookmark drops it; repo bookmarks lists yours, each with how
many people have bookmarked it. The web has the control on the
repository header, the count in the facts bar, and the list at
/bookmarks. Bookmarks are public and counted; pins are private and
drive the rail. Read access is all a bookmark needs.
A job's result is a property of the commit's tree, not its sha: a
rebase onto a base that touched nothing the branch did gives every
commit a new sha and the same tree, and a job that already passed for
that tree is not run again — the new commit gets the earlier result as
its ci/<job> status, naming the build it came from. Scheduled and tag
jobs are never reused this way; their trigger is the clock or the tag.
Profiles
A profile is what /{owner} shows: a one-line description, a website, a
set of links, long-form about text, the repositories you can see, org
membership, and a year of activity. Users and orgs have the same fields.
Repository rows carry the listing metadata too — topics, license,
default branch, last commit — so a client renders a profile listing the
way /{owner} does. The web dispatches this command rather than
assembling the page itself.
gitbay profile show # your own
gitbay profile show alice
gitbay profile set --description "builds small tools" --website https://alice.example
The about text is not a field. It is profile/README.md or
profile/README.org on the default branch of <owner>/.gitbay, a
repository you own like any other, written by a push or by
repo commit-file:
gitbay repo create alice/.gitbay
gitbay repo commit-file alice/.gitbay profile/README.org \
--ref main --file - < about.org
The extension picks the renderer; .md, .org and .markdown are
resolved in that order, so a README.md beside a README.org wins.
profile show reports the text as about, its format as
about_format, and the file it came from as about_path.
Access follows the repository: a private .gitbay keeps the about text
to you and the admins. A repository whose name starts with a dot is
infrastructure rather than a project, so it stays out of explore and
off the profile's repository list — repo list still shows it, and it
is reachable at its own URL.
Up to five links, each label|url or a bare url, http(s) only. Passing
--link replaces the whole set; a single empty one clears it:
gitbay profile set --link "Mastodon|https://fosstodon.example/@alice" \
--link https://alice.example/now
gitbay profile set --link "" # clear
Every field follows the same rule as the rest of the CLI: a flag you leave out is untouched, and ='' clears the one you name. Org profiles work the same way and need org admin:
gitbay org profile krz --description "software and experiments"
gitbay org profile krz # no flags shows it
An org's about text works the same way, in <org>/.gitbay.
The settings page edits description, website and links, and points at the
about file — with a button that creates <owner>/.gitbay and its first
README when you have none, so the file editor has a branch to open. The
JSON API runs profile set like any other write command.
Repositories
gitbay repo create you/project [--private]
gitbay repo clone you/project
gitbay repo list
gitbay repo show you/project
gitbay repo log you/project --limit 20 # commits with signature states
gitbay repo fork other/project [--name mine]
gitbay repo rename you/project tool # clone URLs change
gitbay repo delete you/project --yes
repo show also reports your own state on the repository — watch
(watching or muted; repo unwatch clears either) and bookmarked —
and fork_of when it is a
fork whose parent you can read, so a client draws a toggle rather than
two blind buttons. Absent means none.
On the web, the repository's settings page carries access, webhooks,
rename, transfer and delete, and /new imports from a remote. Delete and
transfer ask for the repository's path to be typed. Large imports belong
on the CLI, where progress is visible.
Pushing is SSH-only. Public repositories are anonymously readable over
HTTPS (and git:// where enabled); private repositories exist only over
SSH and answer "not found" to everyone without access.
Access and settings (owner or admin grant):
gitbay repo access grant you/project alice write # read | write | admin
gitbay repo access revoke you/project alice
gitbay repo access list you/project # everyone who can reach it, role, and via what
gitbay repo settings protect you/project main # no force-push, no delete
gitbay repo settings require-mr you/project on # protected branches: merge requests only
gitbay repo settings protect-tag you/project 'v*' # matching tags: created once, never moved or deleted
gitbay repo settings default-branch you/project trunk # HEAD, and what the web shows
gitbay repo settings require-signed you/project on # every commit must verify
gitbay repo settings git-daemon you/project on # expose over git://
gitbay repo topics add you/project cli forge # free-form tags, shown on the web
gitbay repo search forge # find repos by name/description/topic
gitbay repo grep you/project "some string" # literal git grep over the default branch
gitbay repo symbols you/project Dispatch # where a name is defined, from the symbol index
gitbay repo pin you/project # pin to your web dashboard
gitbay repo unpin you/project
gitbay repo archive you/project # read-only: pushes and issue/MR
gitbay repo unarchive you/project # writes refused, browsing intact
A push to the default branch queues a symbol index of its tree, built in
the background after the push completes; a tree that is already indexed
is not indexed again. repo symbols <owner/name> [--kind k] <query>
lists definitions whose name starts with the query (two characters
at least), exact matches
first, case-sensitive before case-insensitive, as name, kind, path and
line; --limit and --cursor page it, and without them it stops at
200 rows. A Go method is listed as Type.Method and also matches on
Method. Only the default branch is indexed: --ref accepts another
ref only when its tree is the indexed one. The index follows read
access: a repository you cannot read has no symbols either.
What is indexed:
| language | kinds |
|---|---|
| Go (parsed) | function, method, type, const, var |
| Swift | function, class, struct, enum, interface (protocol), type |
| Rust | function, struct, enum, interface (trait), type, module, const, macro |
| Python | function, method, class |
| JavaScript, TypeScript | function, class, interface, type, enum, const |
C and C++ headers (.h, .hpp) |
function (prototype), struct, enum, class, type, macro |
shell (.sh, .bash, .zsh) |
function |
| org, Markdown | section (a heading) |
Languages other than Go are matched a line at a time, so unusual
definition shapes are missed. Files over 1 MiB, anything under
vendor/ or node_modules/, *_gen.go, *.pb.go, Go files marked
Code generated ... DO NOT EDIT., and *.min.js are skipped, and a
name longer than 256 bytes is dropped. One run stops at 200,000
symbols, 32 MiB of names and paths, or two minutes, and publishes what
it found as a partial index that says which bound it reached.
A new index is written while the previous one stays in use, and
replaces it in one step. A run that cannot build an index records why
and leaves the previous index current; the same tree is tried again
once after an hour, or when a push changes the tree. A repository that
has not been pushed to since the index existed has none until its next
push to the default branch. admin symbols reindex <owner/name>
rebuilds one regardless.
On the web, a file viewed at the indexed tree (the default branch's
head, or any commit with the same tree) links each name the index
holds: to its definition when there is one, to the results page at
/{owner}/{repo}/symbols?q=<name> when there are several. The file's
own definitions are listed above its source.
Import from another forge — git data first, then optionally the issue and PR history from GitHub or any Forgejo instance such as Codeberg (issues keep state/labels/comments; PRs land as closed or merged MRs with their discussion; originals are attributed inline since foreign authors have no local account; re-running resumes where it stopped):
gitbay repo import you/mirror --from https://github.com/you/repo.git \
[--private] [--token-stdin] # token on stdin, never in the URL
gitbay repo import-issues you/mirror --from you/repo --token-stdin
gitbay repo import-issues you/mirror --from you/repo \
--api-base https://codeberg.org/api/v1 # Forgejo: the site's /api/v1
repo import fetches over http and https only, from an address that
passes the same check as a webhook target; a git:// URL is refused,
so use the repository's https URL.
Sourcehut has no API of that shape; repo import takes its git data
and the todo.sr.ht tracker is not read.
Moving between gitbay instances (no lock-in): run on the TARGET, with
your key registered on both sides. Profile, repos with settings,
issues, MRs, and comments replay with attribution; git data mirrors
client-side through your own key. Keys never transfer and emails
arrive unverified — trust is per-instance. Re-running resumes.
Push-blocking policies (require-signed, protected branches) are
deferred and printed for you to re-apply after the data lands.
gitbay auth export alone doubles as a user-level backup.
gitbay migrate --from old-instance.example [--from-port 22]
Mirroring keeps a foreign remote in sync during a gradual migration (repo admin; https remotes; the token is stored server-side for the recurring sync and never echoed back):
gitbay repo mirror add you/project https://github.com/you/project.git \
--direction push --token-stdin # propagate after every local push
gitbay repo mirror add you/copy https://github.com/them/theirs.git \
--direction pull # follow upstream; local pushes refused
gitbay repo mirror list # sync status and last error, per mirror
gitbay repo mirror sync / remove <id>
Markdown and org files render on the web when opened, the way a README
does on the repository page, with relative links resolved against the
file's directory; source in the file's action bar (or ?view=source)
shows the text instead. Other files show the text with highlighting.
TeX math renders as MathML wherever markdown or org renders: READMEs,
files, wiki pages, issue, merge request and release bodies, comments,
and their previews. Markdown takes $…$ inline and $$…$$ for display,
either inline or with the $$ lines on their own. A $ followed by a
space, or a closing $ preceded by a space or followed by a digit, is a
dollar sign, so $5 and $10 stays prose; \$ is always one. Org takes
$…$, $$…$$, \(…\), \[…\] and \begin{…}…\end{…}. Code spans
and blocks are left alone. The supported TeX is a subset (letters,
numbers, operators, scripts, \frac, \sqrt, Greek and common symbols,
\left=/\right=, accents, \text, the \math… fonts, matrices,
cases and aligned; the full list heads internal/texmath/texmath.go).
Anything outside it, including macros, colours and links, shows as
source, as does an expression over 8 KiB or nested more than 64 deep,
and so does all math in a document after its first 1000 expressions or
256 KiB of TeX. A $$ line opens display math only when a line ending
in $$ follows before a blank line. MathML typed as raw HTML is
stripped like any other markup the sanitizer does not allow. The CLI
shows the source.
Deleting your account
gitbay auth delete --confirm <username> # mails a link to your primary address
gitbay auth delete --cancel # withdraw it before the link is opened
The same is at the bottom of /settings. Nothing changes until the
mailed link is opened (it works for 24 hours, and needs a verified
address). Opening it disables the account at once and deletes it seven
days later: its repositories, snippets, keys, addresses and tokens go.
Issues, merge requests, comments and reviews it wrote on other people's
repositories stay, under the name ghost. Signing in during the seven
days — on the web, or any command over SSH with a full-scope key —
cancels; git-, read-, runner-scoped keys and API tokens are refused and
cannot cancel. The only admin of an organization is refused until
another admin exists or the organization is deleted. Export first
(gitbay auth export) to keep a copy. The username is free again after
the purge.
Organizations
Orgs share the owner namespace with users and own repositories at
org/repo. By default members get write on all org repos; org admins
get repo admin, create repos under the org, and manage membership. An
org's existence and member list are public; its teams are visible to
members, and anyone else asking is refused rather than told the org
does not exist.
gitbay org create krz
gitbay org members add krz alice [--role admin]
gitbay org show krz
gitbay org rename krz newname # clone URLs change
gitbay org delete krz --yes # only when it owns no repositories
Large orgs scope access with teams: set what plain membership implies,
then grant per-repo roles through named teams (org admins always keep
admin; the default write keeps the simple model):
gitbay org settings members-role krz none # write | read | none
gitbay org team create krz core-devs
gitbay org team add krz core-devs alice bob # org members only
gitbay org team grant krz core-devs krz/gitbay write
gitbay org team show krz core-devs # members + grants
gitbay org team revoke / remove / delete ...
Issues
Anyone who can read a repository can file and comment. Closing/reopening is for the author or anyone with write; labels and assignees need write.
gitbay issue create --title "it breaks" [--body "..." | --file -]
[--label bug]... [--milestone v1.0] [--assignee alice]... # these need write
gitbay issue list [--state open|closed|all]
gitbay issue show 4
gitbay issue comment 4 --message "same here"
gitbay issue react 4 +1 [--comment 12] [--remove] # the same on mr
gitbay issue edit 4 --title "better title" [--body|--file -] # author or write
gitbay issue close 4 / reopen 4
gitbay issue label 4 --add bug --remove wontfix
gitbay issue assign 4 --add alice
gitbay issue milestone 4 v1.0 # or "none" to clear
A reaction is one of +1 -1 laugh hooray confused heart rocket eyes,
given by name or as the emoji itself, on an issue or merge request or on
one of its comments (show prints comment ids). Anyone who may comment
may react; reacting twice or removing an absent reaction does nothing.
show carries the counts and which are yours. A reaction sends no
notification and appears in no activity feed.
Inside a clone, the repository is inferred from the origin remote —
that is why no owner/name appears above. Anywhere else, pass it as the
first argument. Long text: --body inline, --file - from stdin, or
neither on a terminal and $EDITOR opens.
A wiki is the .gitbay/wiki/ directory on the default branch — no
separate repository, the same rendering pipeline as READMEs (markdown
and org). Editing it is editing a file: a push, repo commit-file, or
the web editor on a repository that permits server-authored commits (one
requiring signed commits is push-only, since the server signs nothing):
mkdir -p .gitbay/wiki
# add Home.md (or .org), more pages, images; relative links between
# pages just work on the web at /you/project/wiki
git add .gitbay/wiki && git commit -m "wiki" && git push
Pages may sit in subfolders. A nested page is named by its path —
.gitbay/wiki/Guide/Install.md is /you/project/wiki/Guide/Install and
wiki show you/project Guide/Install — and the sidebar groups it under
its folder. A relative link or image on a nested page resolves from the
page's own folder first, then from the top of the wiki, so a link to
Home still reaches the top-level page. SVG images render.
Releases anchor notes and binary assets to a pushed tag (write access;
assets stream over SSH, capped by the instance's max_asset_bytes).
While a release exists its tag can be neither deleted nor moved, whether
or not the tag is protected: delete the release first.
gitbay release create v1.0 --title "First light" [--notes|--file -|$EDITOR]
gitbay release asset add v1.0 tool-linux-amd64 < dist/tool-linux-amd64
gitbay release asset get v1.0 tool-linux-amd64 > tool # or the web download link
gitbay release list / show v1.0 / delete v1.0 --yes
The web shows them under the repository's releases tab with rendered
notes, sha256 sums, and download links; writers add and remove assets
there (a file upload, up to max_asset_bytes; removal asks for the file
name). Feed readers subscribe at
/you/project/releases.atom; commits are at /you/project/log.atom
(the default branch) or /you/project/log.atom/<ref>, and an owner's
activity on their public repositories at /you/activity.atom. The
pages carry the discovery link.
Commit messages act on issues when the commits land on the default
branch (direct push, MR merge, repo commit-file or the web editor):
closes/fixes/resolves #4 closes the
issue with a linking comment, and a bare #4 leaves a reference
comment. Each issue/commit pair acts once, ever. Closes owner/name#N closes an issue in another repository when
you hold write there with a key whose scope reaches it; otherwise it
stays a plain link. A deploy key closes nothing outside the repository
it is bound to, but naming that repository by full path works as the
bare form does. The comment left
on the closed issue links the closing commit by its repository path, so
closing a public repository's issue from a private one names the private
repository there. A bare owner/name#N links and does nothing.
Milestones group issues and MRs toward a release (write access to
manage, attach with issue milestone / mr milestone; progress shows
on the web at /owner/name/milestones, where writers create, close and
reopen them):
gitbay milestone create v1.0 --description "first release" --due 2027-01-01
gitbay milestone list [--state open|closed|all]
gitbay milestone close v1.0 / reopen v1.0
An org holds labels and milestones every repository under it sees
beside its own. issue label --add, mr label --add, issue
milestone and mr milestone resolve the org's row first; a repository
cannot create a label or milestone with a name its org holds. Creating
an org label or milestone whose name repositories under the org already
use folds them in: their issues and merge requests move to the org's
row. Org admins manage them, on the CLI or at /<org>/-/labels and
/<org>/-/milestones; counts span the repositories you can read.
gitbay org label set acme bug --color cf222e
gitbay org label list acme / remove acme bug
gitbay org milestone create acme v2 --due 2027-03-01
gitbay org milestone list acme [--state open|closed|all]
gitbay org milestone close acme v2 / reopen acme v2
On the web: /acme/-/labels and /acme/-/milestones, read-only.
Issue templates: commit .gitbay/issue-template.md (and optional
issue-template-<name>.md variants) to the default branch. gitbay
issue create prefills $EDITOR with the default template, the web
form prefills its textarea, and gitbay issue templates lists them.
Lists narrow the same way on every surface: issue list --label bug
--assignee bob --author alice --milestone v1 (or --milestone none),
mr list --label bug --author bob --milestone v1; the web's issue and
merge request lists take the same names as query parameters, and each
active filter shows with a link that drops it.
A query spans repositories, and a saved one keeps it under a name:
gitbay query save mine is:open assignee:@me
gitbay query save triage 'repo:krz/*' is:issue is:open no:label
gitbay query save v2 owner:krz label:bug 'label:needs review' milestone:v2
gitbay query list / show mine / remove mine
gitbay query run mine [--limit 20] [--cursor <c>] # issues and MRs
gitbay issue list --query mine # only the issues
gitbay mr list --q 'owner:krz is:open author:@me' # a query written out
gitbay query pin mine # on the dashboard; unpin
The terms: repo:owner/name, repo:owner/glob* (* in the name
only), repo:*, owner:name; is:open, is:closed, is:merged,
is:issue, is:mr; label:x (repeat it: every label must be there),
no:label; milestone:x, no:milestone; assignee:user,
author:user, either as @me for whoever runs the query. Anything
without a colon is text matched against title and body, as search
does. Every term narrows, except that several repo: and owner:
terms widen the scope to any of them; with none the query covers every
repository you can read, and only those — someone else's private
repository is neither a row nor part of a count. Merge requests have no
assignees, so assignee: means issues. A term that does not parse
exits 2 and names itself. @me is resolved when the query runs, and
the saved text is the query in a canonical order.
Rows come newest first, each naming its repository, paged as {items,
next} with fifty to a page unless --limit says otherwise.
An account keeps at most 50 saved queries, 10 of them pinned; past
either, query save or query pin exits 2 until one is removed or
unpinned. dashboard --json carries each pinned query's count and first five
rows under queries; the web dashboard shows them, and
/you/-/queries lists your saved queries with a page per query.
Labels take a colour: gitbay label set bug --color cf222e; label
list shows each with its colour and how many issues and merge requests
carry it, and label remove takes one off all of them. issue label
--add and mr label --add still create a colourless label on the fly.
Merge requests
gitbay mr create --source feature --target main --title "add thing"
gitbay mr create other/upstream --source you/fork:feature --target main --title "..."
gitbay mr list / show 4 / diff 4
gitbay mr checkout 4 # local branch mr/4 from the MR head
gitbay mr review 4 --approve # or --request-changes / --comment
gitbay mr label 4 --add bug --remove wontfix
gitbay mr merge 4 [--strategy ff|merge|squash|rebase]
gitbay mr merge 4 --when-ready # merge once the gates pass; --cancel dequeues
gitbay mr close 4
A merge request closed without merging can name the one that carries
its change forward: mr close 4 --by 7 records it and both pages show
it, and mr edit 4 --superseded-by 7|none sets or clears it
afterwards, refused on anything but a closed merge request.
The web list shows each request's combined check state and comment count alongside its title, so open work needing attention stands out without opening it.
Semantics worth knowing:
- the MR head lives in the target repository as
refs/merge-requests/N/head(fetchable by any reader), so an MR survives deletion of its source branch or fork. It is never removed on merge or close; after a history rewrite an instance admin can drop it withadmin mr prune. - force-pushing the source updates the MR and marks existing reviews stale — unless the diff is the one they reviewed. A rebase onto a target that moved on changes every sha and nothing about the change, so fresh reviews follow it to the new head; a push that changes the diff stales them.
- default strategy: fast-forward when possible, else a merge commit. Squash makes one commit authored by the MR author, committed by the merger. Rebase replays a linear range preserving authors; it refuses ranges containing merge commits, and when fast-forward is possible it is one (original commits and signatures land untouched).
- on
require_signed_commitsbranches only fast-forwards of fully verified commits merge; everything server-created is refused with instructions to rebase locally.gitbay mr rebase <n>is those instructions: in a clone of the target it replays the source branch onto the target and force-pushes it, thenmr merge <n>fast-forwards. The replay is local, so the commits carry your signature and not the server's — it holds no key. A branch living in a fork is refused, with the repository to run it in; rebase it there.
Repo admins can gate merges (repo settings ...): require-approvals
<n> (fresh, non-author approvals; each reviewer's latest review is
their stance, and a fresh request-changes blocks), require-resolved
(no open review threads), require-checks (all statuses green), and
require-codeowners (an approval from an owner of every owned changed
file). Owners come from a CODEOWNERS file on the target branch, root
or .gitbay/, gitignore-style patterns, last match wins. The toggle is
the opt-in, so a repository can carry the file as documentation of who
to ask without it gating merges; with it on and no file on the target
branch, the merge is refused and says so. It does not wait on
require-approvals.
mr show carries a gates block with where the merge request stands
against all of them — approvals counted and required, owners still
outstanding per file, open threads, checks, and whether a fast-forward
is possible — and the merge request page shows the same block. mr
merge names every unmet gate at once rather than the first. mr
review says when a verdict is advisory, which it is from anyone
without write access: the gates do not count it.
mr merge <n> --when-ready queues the merge and merges once the gates
pass, or at once if they already do. It is tried again when a status
is reported on the head, a review is submitted, a thread is resolved,
the request is marked ready, and the source branch is pushed; a push
by someone who can write to the target keeps it queued, and the new
head has to pass on its own. The merge is made as the user who queued
it, with their rights checked at that moment. Anything the queuer can
fix (an unmet gate, a conflict, a branch behind a
require_signed_commits target that needs a rebase) leaves it queued
with the reason shown on mr show and the page.
The queue is bound to the credential it was made with. It is dequeued, with the reason on the request's timeline, when:
- the queuer loses write access or their account is disabled;
- the SSH key or API token it was queued with is removed, revoked, expires, or no longer has full scope (a merge queued from the web rests on the account alone);
- someone who cannot write to the target pushes to the source branch or retargets the request;
- the source branch is deleted, or the request is closed.
An expiring key or token cannot queue a merge; use one without an
expiry, or the web. mr merge <n> --cancel dequeues without closing.
merge and squash are refused at queue time on a
require_signed_commits repository.
Those gates apply to mr merge. A direct push to a protected branch
passes none of them until require-mr on: then an existing protected
branch refuses every push, including repo commit-file and the web
editor, and the server's merge is its only writer. Creating the branch
is still a push, since there is nothing to open a merge request against
yet.
A merge request whose target is another open merge request's source
branch is stacked on it: mr create says so, mr show carries
stacked_on and stacked, and merging the lower one retargets the
upper onto main with its reviews kept. See Stacked
merge requests.
Review threads anchor to diff lines:
gitbay mr diff-comment 4 --path main.go --line 12 --message "use log here"
gitbay mr diff-comment 4 --reply 7 --message "done" # join thread 7
gitbay mr threads 4 # threads with staleness
gitbay mr resolve 4 7 / unresolve 4 7
Threads render inline on the MR page. A force-push marks them stale
(shown under "threads on earlier revisions") rather than guessing new
anchors; mr show reports the unresolved count. Resolving is for the
thread author, the MR author, or anyone with write.
A thread can span lines: --start-line 12 --line 13 anchors it to
lines 12 and 13 of the new file. A fenced suggestion block in the
comment proposes replacement lines for that range; an empty block
proposes deleting it.
gitbay mr diff-comment 4 --path main.go --start-line 12 --line 13 --file - < suggestion.md
gitbay mr apply-suggestion 4 9 # commit thread 9's suggestion
where suggestion.md is
one call does both
```suggestion
log.Printf("starting %s", name)
```
The page and mr threads show a suggestion as the lines it replaces
and the lines it proposes; mr threads --json carries it as
suggestion with the path, the line range, the commit and blob it was
made against, the original and replacement text, outdated with a
reason, and apply (server or local). A suggestion is outdated
once the lines it replaces differ at the head from what it was made
against, or the file is renamed or deleted; it cannot be applied then.
Applying commits the replacement to the source branch as the applying
user, with a message naming the merge request and thread, and resolves
the thread if the applier could resolve it by hand (the thread author,
the MR author, or a writer of the target); otherwise the thread stays
open and the output says so. It is a push: only the source branch's
writers can apply (for a merge request from a fork, the fork's
writers), the branch's protection, require-mr and the owner's storage
quota apply, and a queued merge treats it as a push by the applier. The web's Apply suggestion button and mr
apply-suggestion commit it on the server. The server cannot sign, so
where the source or target requires signed commits (apply is
local) the CLI fetches the source branch into the clone it runs in,
builds the commit without touching the working tree, signs it with
git commit-tree -S under your git signing configuration, pushes it,
and resolves the thread; the page shows that command.
CI builds
A .gitbay/ci.yml in the repo runs jobs on every branch push:
jobs:
test:
steps:
- go test ./...
A config holds at most ten jobs of fifty steps each, each step under
4096 bytes, in a file under 64 KiB; a longer step runs a script from the
repository (- sh ci/publish.sh). A config past a cap, or otherwise
unparseable, records a failed ci/config status on the commit and
queues nothing, so look there when a push builds nothing.
Each job becomes a build (build list, build log, the builds tab on
the web) and a ci/<job> commit status, which repo settings
require-checks can gate merges on. repo settings
require-contexts names statuses the gate waits for until they report,
and turns the gate on. build list takes --ref,
--status and --job to narrow the listing, combinable; the builds
tab reads the same flags from its ?ref, ?status and ?job query
parameters and groups the result into one row per commit. build show
names a failed build's step and how long it ran, and build log takes
--step <n>|failed and --tail <lines>. Steps run
with sh -c on the instance's runner, stopping at the first failure;
a broken config surfaces as a failed ci/config status. Environment:
GITBAY_REPO,
GITBAY_SHA, GITBAY_REF, GITBAY_JOB, CI=true, and GITBAY_SSH,
the instance's ssh destination as the build reaches it: on the forge's
own runner, git@169.254.1.2, pasta's address for the host, with
:port when the instance's ssh is not on 22; from any other runner,
the destination that runner polls (its -remote, such as
git@gitbay.org). Use it as ssh://$GITBAY_SSH/owner/name.git or
ssh ssh://$GITBAY_SSH …, which work in either form. A job that
talks back to the instance — a release asset, a comment, a push to a
pages branch — uses $GITBAY_SSH with a key it holds as a secret;
the build's container has no key of its own. On the forge's own runner a
build from a fork's merge request cannot reach the instance at all, and
reaches the internet only over HTTPS, HTTP and DNS (CI page). Two things about that
container: a secret with newlines (a private key) arrives intact, and
ssh expands ~ from the passwd entry, /root, not from $HOME,
which is the build home — so keep an ssh config in the workspace and
pass it with ssh -F (and GIT_SSH_COMMAND"ssh -F …"= for git),
which also works on a runner with no container, where writing to
~/.ssh would edit that machine's own configuration.
Secrets: repo secret set <owner/name> <NAME> reads the value from
stdin (never argv) and injects it into the repo's builds as $NAME;
repo secret list shows names only, and the value is never echoed
back. Anyone with write access can read a secret from inside a build,
so scope them accordingly.
Schedules: a job with schedule: "17 11,23 * * *" (five-field cron,
server-local time; lists, ranges, and steps supported) runs on its cron
against the default branch instead of on push. A default-branch push
registers or updates the schedule. A job with tags: "v*" runs when a
matching tag is pushed — and only then; schedule and tags are
mutually exclusive. build trigger <owner/name> <job> queues any job
immediately, and build cancel <owner/name> <n> withdraws one, queued
or running: a running build stops at the runner within seconds and the
log says who cancelled it. Both need write access.
What each push shape queues, with dedupe, path filters, schedules and the reaper together, is one table on CI.
Your own runner
Builds run on runners attached to the repository. An instance need not
offer any: install gitbay-runner on a machine of yours and attach it.
brew install krz/tap/gitbay-runner # or a binary from the release
gitbay-runner init -remote git@gitbay.org
init generates a key under ~/.config/gitbay-runner/, writes
config.toml beside it, and prints the public key with the command to
attach it:
gitbay repo runner add owner/name < ~/.config/gitbay-runner/id_ed25519.pub
or paste the key under Runners on the repository's settings page. Then
brew services start krz/tap/gitbay-runner, or run gitbay-runner with
no arguments; it reads the config file, and any flag overrides it.
What it builds: every build for the repositories it is attached to,
with the repository's secrets, and nothing else. Merge requests from
forks are untrusted and wait unless the runner runs with -untrusted,
which is only sensible with -isolation podman -image <ref> (see
Admin). Attach one runner to several repositories by repeating
repo runner add; run several runners on one account by running init
on each machine. repo runner list shows each attached key, when it
last polled and the build it holds; repo runner remove <fingerprint>
detaches one (the key stays on your account; keys remove drops it).
A runner key reaches only the runner protocol and read-only git, so a
build step that reads it off disk cannot administer your account.
Large files (LFS)
Standard Git LFS works over both transports with no setup beyond the
usual git lfs track. SSH remotes authenticate through
git-lfs-authenticate (deploy keys included: ro keys can download, rw
keys upload); anonymous HTTPS clones of public repositories can fetch
LFS objects with no credentials. Objects are verified against their
sha256 on upload and capped at 512MB by default.
Pages
On instances with [pages] domain set, a pages branch in any public
repo is served as a static site: the repo named pages at
https://<you>.<domain>/, every other repo at
https://<you>.<domain>/<repo>/. Push HTML to publish; a CI job can
build and push the branch for automatic deploys. Sites run on a
separate origin — your scripts work, and the forge's cookies are out of
reach.
A repo can also serve its pages branch on a domain you own.
repo domain add <owner/name> <domain> claims it and prints a DNS TXT
challenge (_gitbay-challenge.<domain>); create the record, run
repo domain verify, then point the domain's A/AAAA records at the
instance (DNS-only if the domain sits behind a proxying provider — the
instance issues its own certificates). Claims are exclusive per
instance; unverified claims serve nothing and expire after 7 days.
repo domain list reports pending/verified/expired.
Snippets
A snippet is one or more named text files you own outside any repository, for a log or a fragment shared by URL. Create one from a file on stdin; the reply is the id and the URL:
gitbay snippet create build.log --description "failing build" < build.log
gitbay snippet file set <id> notes.txt < notes.txt # add or replace a file
gitbay snippet file get <id> build.log > build.log
gitbay snippet edit <id> --visibility public
gitbay snippet list # yours
gitbay snippet list <owner> # their public ones
gitbay snippet delete <id>
Visibility is public (listed on your page), unlisted (anyone with
the URL, listed nowhere; the default) or private (you alone; not
found to everyone else). Files are text, valid UTF-8, each under the
instance's max_snippet_bytes, at most 64 per snippet. A snippet keeps
at least one file. There is no history: setting a file replaces it.
On the web, /<you>/-/snippets lists yours, each snippet page renders
its files with a raw link per file, and the same page creates, edits
and deletes through the commands above.
Browser sessions
gitbay web login mints a one-time URL; the session it opens ends
after twelve hours without a request, and after seven days in any case.
gitbay web sessions list shows each of yours by a short id with its
creation, expiry and last use, and gitbay web sessions revoke <id>
or --all ends them from the terminal, which is where a lost laptop is
handled.
Actions that create a credential or grant access (adding a key, token or email, org and repository roles, transfers, a repository's visibility) ask you to sign in again when your web sign-in is older than 15 minutes. The form shows a "Sign in again" link and the login returns to the page. Idle renewal does not extend this window.
web theme set light or dark fixes the web UI's colour scheme for
your account; system, the default, follows the browser's own
preference. web theme show prints it. The account page has the same
control under Appearance.
web diff set split draws diffs side by side on the merge request,
commit and compare pages; unified, the default, keeps one column.
web diff show prints it, and the account page has the same control
under Appearance. ?layout=split or ?layout=unified on a diff page
overrides the setting for that request. In a narrow window the split
layout stacks the old line over the new one, as a unified diff does.
Each column's line numbers open a comment on their own side.
Notifications
When the instance has SMTP configured, activity mails you as well as
filing the inbox row: someone opens an issue or MR on your repository,
comments where you are a participant (author, commenter, reviewer, or
mentioned), reviews, closes, merges, or assigns you.
notifications settings mail off keeps the inbox and stops that mail
(login links are not activity and still arrive); the account page has
the same switch.
notifications settings watch on makes you a recipient of every issue
and merge request on the repositories you can write to, as if you had
run repo watch on each: it is consulted when a notice is delivered,
so a grant or a revoke needs no watch row, and an explicit watch or
mute on a repository still wins. Off by default; the account page has
this switch too. Writing @name in an issue, merge request or
comment files "mentioned you" in that account's inbox and makes them a
participant of the thread, provided they can read the repository and
have not muted it; watchers are not told about a mention addressed to
someone else.
You are never mailed about your own actions, and only verified primary
addresses receive anything. Delivery retries on relay failure.
notifications settings reply on lets you answer that mail: issue and
merge request mail then carries a Reply-To address, and replying to
it posts your reply as a comment on the thread, as you. Off by
default; the account page has the switch when the instance reads
replies, and turning it on is refused where it does not
([mail.inbound] off). A reply is posted only when it comes from one
of your verified addresses, you can still comment on the thread, and
the mail it answers is less than thirty days old. Quoted text (lines
starting with >, the "On … wrote:" line and what follows it, the
header block Outlook quotes) and everything after a =– = signature
line are removed, and the rest is stored as markdown. HTML-only mail is
not accepted; send plain text or the usual plain-and-HTML pair. A
refused reply gets no answer: nothing is mailed back. Each reply
address names you, so do not forward notification mail you would not
want answered from your own address.
Push is the same activity again, delivered to a phone: the iOS app
registers a device, and notifications settings push off silences it
the way mail off silences mail, without deregistering anything.
notifications device list shows what is registered (token shown
truncated); notifications device remove <id> drops one by hand, from
the CLI or the account page. A device is also dropped on its own the
moment Apple reports the token dead, so an app deleted from a phone
stops costing anything without you having to notice. Push only works
when the instance operator has configured it: on an instance with
[push] enabled = false, notifications device add refuses rather
than registering a device nothing can deliver to.
Push notifications carry the notice in full: a private repository's name and the issue or merge request number reach Apple and can appear on a lock screen, the same as any other text a phone shows in a notification. This is deliberate, not an oversight — weigh it against what you keep in a private repository before registering a device.
Web vocabulary
Sign in / Log out, Search, sentence-case headings and buttons, product name lowercase.
Output rules
--json is the contract; the plain output is for a person at a
terminal, and follows these rules so every noun reads the same way.
- A list command prints one row per item, tab-separated, no header.
Columns run identifier, state, then description; a trailing column
may carry a word (
due 2027-01-01,via team). Piped, a timestamp in a row is RFC3339 to the second, UTC. Under--jsonnothing is touched. - An empty list prints nothing on stdout and
nothing to liston stderr. - A mutation prints one line: verb, object, identifier
(
created krz/gitbay#7). A second line appears only for something to copy: a URL, a token shown once. - A bad invocation prints
usage:and the command's registered usage, the same texthelp <noun>shows. Exit 2. - A refusal says who may and what to do instead:
only admins of acme can manage teams; ask one to add you. Exit 4. A thing that does not exist, or that you may not know exists, is exit 3. - stdout is the result; stderr is everything else: progress, a note that output was truncated, errors.
- Verbs:
createanddeletefor things with their own identity (repository, issue, release, team),addandremovefor attaching something to them (a key, a member, a label on an issue),setfor a value,revokefor a credential,showandlistfor reads.
At a terminal
The gitbay CLI sends a leading --term=<cols>[,<option>]... argument on
the SSH command line when stdout is a terminal (OpenSSH's multiplexed
sessions, which the CLI uses, do not forward a session's SetEnv).
The argument goes first: the server reads --term=<v> only as the
first argument, and ignores it over HTTP. color is the one option
the server acts on; it ignores options it does not know. The server
then prints:
- lists padded and fitted to the width (the title or description
column is cut with
…first; a title or description column blank on most rows is held to a third of the width), with no trailing whitespace. A list has a dim header row only when a column is a number or a size, which would not say what it counts without one. The next page is aNext pageline on stderr with the command that fetches it (piped output keeps anext\t<cursor>row instead); - colour by meaning: green for open, success, approved, verified; magenta
for merged; red for failures,
private, and bad or untrusted signatures; dim for closed, pending, archived, unsigned; yellow only for what waits on you (an unverified address); references (#12,krz/gitbay, SHAs) dim. A marker piped output keeps as its own trailing cell ([archived],primary) joins the state at a terminal:public, archived; - every list and show drawn as a screen: a header block of
Label:lines (labels dim), the description or notes, sections titledTitle (n)in bold blue, and a legend under a rule of the commands that apply now, grouped and chosen from the item's state and your access. Rows have no header (unless a column is a number or a size) and no indent: a dim reference, a state glyph, the text, then dim facts joined by·. The glyphs are✓passed,✗failed or blocked,◐still going,●waiting on you (your review requested, an issue assigned to you, an unread notification, a pending account, an overdue milestone, the session's own key),○closed or draft. A section cut short ends with+n moreand the command for the rest. Below 80 columns the legend's groups stack; a suggested command is never cut, so it may run past the width.dashboardopens with what waits on you; for admins aProblemsline counts background failures, andadmin statshas the detail.repo commitprints its screen, then the diff. Secrets never appear on a screen, and a webhook URL shows its scheme and host only.wiki showpiped prints the page source verbatim; - errors on stderr after a red
error:, a mistyped flag with the flag it is closest to (did you mean --state?), and a usage line wrapped to the width between its bracketed groups, one alternative per line; - help with flag descriptions, defaults, and examples;
gitbay --helpgroups commands under WORK, REPOSITORIES, YOU and INSTANCE;help --jsonaddsflagsandexamples.
NO_COLOR, TERM=dumb and --no-color drop the colour. show,
diff and log at a terminal go through $GITBAY_PAGER, else
$PAGER, else less (with LESS=FRX when LESS is unset); an empty
GITBAY_PAGER turns paging off. Paging never applies to --follow,
--json, or piped output.
Inside a clone of a repository on the instance, the CLI adds
here=<owner/name> to --term, so a command the output suggests leaves
that repository out, as the CLI would infer it. An instance that does
not know the option ignores it.
GITBAY_TERM=off stops the CLI sending --term at all, for an
instance older than v1.36.0, which refuses the argument as an unknown
command.
Two more options say what the terminal can show. truecolor (sent when
COLORTERM is truecolor or 24bit) puts a dot in each label's own
colour beside its hex in label list. links (sent in iTerm2, WezTerm,
Ghostty, VS Code, kitty and VTE terminals, or when GITBAY_LINKS=1;
GITBAY_LINKS=0 stops it) makes references in lists open their page,
as OSC 8 hyperlinks. A terminal outside that list would print the
escape's text, so it is not sent there by default. GITBAY_TERM=basic
sends only the width and colour, for an instance older than the release
that added these options, which turns any option it does not know into
plain output.
Stock ssh without the CLI's multiplexing gets the plain output unless
it passes the same leading argument or sets the environment variable
sshd is told to accept. OpenSSH parses options after the host, so the
argument goes after --:
ssh git@gitbay.org -- --term=120,color issue list krz/gitbay
ssh -o SetEnv=GITBAY_TERM=120,color git@gitbay.org issue list krz/gitbay
Scripting
Every read command takes --json and emits one envelope:
{"protocol_version": 1, "data": ...}. stdout is data, stderr is
messages. Exit codes are stable: 0 ok, 1 failure, 2 usage, 3 not found,
4 denied, 5 server/protocol error. Usage means the arguments were wrong;
a refusal — a name already taken, a state that does not allow the change
— is a failure. Nothing ever prompts; destructive commands take --yes.
For HTTP automation see API.
CLI setup
gitbay remote add myforge forge.example.org [--port n] [--user u] --default
gitbay remote list
gitbay init [name] [--private] # git init + repo create + origin, in one step
Configuration lives at ~/.config/gitbay/config.toml. The CLI shells
out to your real ssh, so ~/.ssh/config, the agent, and hardware keys
all apply. It shares one connection per instance through a control
socket under ~/.ssh: the first command in five minutes pays the
handshake and the rest ride it. no_multiplex = true on an instance in
the config turns that off. Man pages: gitbay man --dir <dir>; completions:
gitbay completion bash|zsh|fish.