A CLI-first git forge.

cli forge git self-hosted

https://gitbay.org

Wiki: Users

Users

gitbay user guide

Everything here works from stock OpenSSH — replace gitbay with ssh git@<host> in any command and it behaves identically. The CLI adds convenience (instance profiles, repo inference, $EDITOR), nothing more. ssh git@<host> help lists every command the server knows.

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.org

A verification code arrives by mail. Until you run ssh git@<host> email verify <code>, 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.

gitbay auth keys list
gitbay auth keys add --scope git < ~/.ssh/ci_key.pub   # key on stdin
gitbay auth keys remove SHA256:...

Scopes: full (default; git plus every control command) or git (git transport only — right for CI and automation keys, which then cannot touch issues, settings, or your account).

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).

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.

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

About text is markdown by default, or org-mode. It takes inline text or stdin, so it can live in a file you keep:

gitbay profile set --about "I maintain a few small tools."
gitbay profile set --file - --about-format org < about.org

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" --about-format org --file - < krz.org
gitbay org profile krz            # no flags shows it

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

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

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 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

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):

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

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>

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.

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 -]
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

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:

git clone ssh://git@<host>/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

Releases anchor notes and binary assets to a pushed tag (write access; assets stream over SSH, capped by the instance's max_asset_bytes):

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.

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):

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

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.

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 merge 4 [--strategy ff|merge|squash|rebase]
gitbay mr close 4

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.

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). With approvals required, a CODEOWNERS file on the target branch (root or .gitbay/) additionally demands an approval from an owner of every owned changed file — gitignore-style patterns, last match wins.

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.

CI builds

A .gitbay/ci.yml in the repo runs jobs on every branch push:

jobs:
  test:
    steps:
      - go test ./...

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. 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 <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.

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.

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

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. Man pages: gitbay man --dir <dir>; completions: gitbay completion bash|zsh|fish.