#+title: gitbay user guide Everything here works from stock OpenSSH — replace =gitbay= with =ssh git@= 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. - =--help= is not understood. =ssh git@ help= lists every command the server knows, and =help = (=help repo=, =help mr=) prints the usage of each command under that noun. * Installing the CLI #+begin_src sh 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) #+end_src 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: #+begin_src sh ssh git@ register --username you --invite #+end_src Your account is active immediately; the invited address is your verified email. - open :: #+begin_src sh ssh git@ register --username you --email you@example.org #+end_src A verification code arrives by mail. Until you run =ssh git@ email verify =, the account is pending: you can run =whoami= and 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. #+begin_src sh 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 label SHA256:... "work laptop" gitbay auth keys remove SHA256:... #+end_src 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. 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): #+begin_src sh gpg --armor --export you@example.org | gitbay auth pgp add #+end_src 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
= / =email verify = (requires the instance to have SMTP; otherwise an admin can assert an address for you). =email list= shows them. =email remove
= drops one, with two refusals: the primary stays until =email primary
= 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 = 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/= 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. #+begin_src sh gitbay profile show # your own gitbay profile show alice gitbay profile set --description "builds small tools" --website https://alice.example #+end_src The about text is not a field. It is =profile/README.md= or =profile/README.org= on the default branch of =/.gitbay=, a repository you own like any other, written by a push or by =repo commit-file=: #+begin_src sh gitbay repo create alice/.gitbay gitbay repo commit-file alice/.gitbay profile/README.org \ --ref main --file - < about.org #+end_src 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: #+begin_src sh gitbay profile set --link "Mastodon|https://fosstodon.example/@alice" \ --link https://alice.example/now gitbay profile set --link "" # clear #+end_src 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: #+begin_src sh gitbay org profile krz --description "software and experiments" gitbay org profile krz # no flags shows it #+end_src An org's about text works the same way, in =/.gitbay=. The settings page edits description, website and links, and points at the about file — with a button that creates =/.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 #+begin_src sh 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 #+end_src =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. 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): #+begin_src sh 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 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 #+end_src 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): #+begin_src sh 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 #+end_src 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. #+begin_src sh gitbay migrate --from old-instance.example [--from-port 22] #+end_src 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): #+begin_src sh 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 #+end_src 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. * 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. #+begin_src sh 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 #+end_src 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): #+begin_src sh 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 ... #+end_src * 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. #+begin_src sh gitbay issue create --title "it breaks" [--body "..." | --file -] gitbay issue list [--state open|closed|all] gitbay issue show 4 gitbay issue comment 4 --message "same here" 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 #+end_src 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): #+begin_src sh 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 #+end_src 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. #+begin_src sh 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 #+end_src The web shows them under the repository's =releases= tab with rendered notes, sha256 sums, and download links. Feed readers subscribe at =/you/project/releases.atom=; commits are at =/you/project/log.atom= (the default branch) or =/you/project/log.atom/=, 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=): #+begin_src sh 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 #+end_src 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; counts span the repositories you can read. #+begin_src sh 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 #+end_src On the web: =/acme/-/labels= and =/acme/-/milestones=, read-only. Issue templates: commit =.gitbay/issue-template.md= (and optional =issue-template-.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. 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 #+begin_src sh 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 close 4 #+end_src 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 with =admin 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_commits= branches only fast-forwards of fully verified commits merge; everything server-created is refused with instructions to rebase locally. =gitbay mr rebase = is those instructions: in a clone of the target it replays the source branch onto the target and force-pushes it, then =mr merge = 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 = (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. 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-MRs][Stacked merge requests]]. Review threads anchor to diff lines: #+begin_src sh 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 #+end_src 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. * CI builds A =.gitbay/ci.yml= in the repo runs jobs on every branch push: #+begin_src yaml jobs: test: steps: - go test ./... #+end_src 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/= commit status, which =repo settings require-checks= can gate merges 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. 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 (=git@gitbay.org= from a runner elsewhere; inside a container on the server's own runner the host is at a private address the runner fills in). 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. 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 = 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 = queues any job immediately, and =build cancel = 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][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. #+begin_src sh brew install krz/tap/gitbay-runner # or a binary from the release gitbay-runner init -remote git@gitbay.org #+end_src =init= generates a key under =~/.config/gitbay-runner/=, writes =config.toml= beside it, and prints the public key with the command to attach it: #+begin_src sh gitbay repo runner add owner/name < ~/.config/gitbay-runner/id_ed25519.pub #+end_src 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 = (see [[Admin][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 = 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://./=, every other repo at =https://.//=. 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 = claims it and prints a DNS TXT challenge (=_gitbay-challenge.=); 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: #+begin_src sh gitbay snippet create build.log --description "failing build" < build.log gitbay snippet file set notes.txt < notes.txt # add or replace a file gitbay snippet file get build.log > build.log gitbay snippet edit --visibility public gitbay snippet list # yours gitbay snippet list # their public ones gitbay snippet delete #+end_src 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, =//-/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 lasts seven days. =gitbay web sessions list= shows each of yours by a short id with its creation and expiry, and =gitbay web sessions revoke = or =--all= ends them from the terminal, which is where a lost laptop is handled. =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. * 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, or merges. =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. * 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=). The =gitbay= CLI pads the tabs into aligned columns when stdout is a terminal and leaves them as tabs when piped, so =cut -f= sees the same bytes stock ssh prints. Under =--json= nothing is touched. - An empty list prints nothing on stdout and =nothing to list= on 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 text =help = 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: =create= and =delete= for things with their own identity (repository, issue, release, team), =add= and =remove= for attaching something to them (a key, a member, a label on an issue), =set= for a value, =revoke= for a credential, =show= and =list= for reads. * 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 #+begin_src sh 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 #+end_src 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 =; completions: =gitbay completion bash|zsh|fish=.