#+title: gitbay user guide Everything here works from stock OpenSSH — replace =gitbay= with =ssh git@= in any command and it behaves identically. The CLI adds convenience (instance profiles, repo inference, =$EDITOR=), nothing more. =ssh git@ help= lists every command the server knows. * 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 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 remove SHA256:... #+end_src 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). 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. * 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 About text is markdown by default, or org-mode. It takes inline text or stdin, so it can live in a file you keep: #+begin_src sh gitbay profile set --about "I maintain a few small tools." gitbay profile set --file - --about-format org < about.org #+end_src 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" --about-format org --file - < krz.org gitbay org profile krz # no flags shows it #+end_src The web renders profiles but has no form for editing one, so the CLI is the only interface today. =profile set= is not =SSHOnly=, so the JSON API runs it 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 delete you/project --yes #+end_src =repo show= also reports your own state on the repository — =watch= (=watching= or =muted=) 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 settings protect you/project main # no force-push, no delete 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 GitHub issue and PR history (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 #+end_src 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. #+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. Wikis are companion repositories edited by push — no separate storage, no web editor, the same rendering pipeline as READMEs (markdown and org). Access mirrors the parent repo (read to view, write to push); the companion is created on your first push and follows the repo through transfer and delete: #+begin_src sh git clone ssh://git@/you/project.wiki.git # add Home.md (or .org), more pages, images; relative links between # pages just work on the web at /you/project/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=): #+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. Commit messages act on issues when the commits land on the default branch (direct push or MR merge): =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. Same repository only. 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 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 --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 carry it, and =label remove= takes one off every issue. =issue label --add= still creates 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 merge 4 [--strategy ff|merge|squash|rebase] gitbay mr close 4 #+end_src 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. - force-pushing the source updates the MR and marks existing reviews stale. - 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=. 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 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. 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=. 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. * 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. * 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. * Notifications When the instance has SMTP configured, activity mails you: someone opens an issue or MR on your repository, comments where you are a participant (author, commenter, reviewer), reviews, closes, or merges. You are never mailed about your own actions, and only verified primary addresses receive anything. Delivery retries on relay failure. * 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. 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=.