A CLI-first git forge.

cli forge git self-hosted

https://gitbay.org

Commit ff6271a9d4

ff6271a9d4570cd46f169091637a9d2e40ad5c2a

parent: 59a6e6bf08

Verified · cmc ci/build: success

cmc <hello@cleberg.net> · 2026-08-28T04:42:51Z

cli: front-ends for every registry command, and a test that keeps it that way

The command tree in cmd/gitbay is hand-written, so a command could land
in the registry — reachable over raw SSH, the web and the API — while
`gitbay <cmd>` still answered "unknown command". explore, wiki, blame,
commit, commit-file, refs, download, settings visibility and audit were
all in that state.

TestEveryCommandIsReachable walks the tree and fails on any registered
command with no way to type it. Commands deliberately absent (the
runner's wire protocol, account import-bundle) are listed with reasons.
cmd/gitbay/coverage_test.go added +57
@@ -0,0 +1,57 @@
1package main
2
3import (
4 "strings"
5 "testing"
6
7 "github.com/spf13/cobra"
8
9 "gitbay.org/gitbay/internal/control"
10)
11
12// Commands with no CLI front-end on purpose. Everything else in the
13// registry must be reachable by typing it, or the CLI is not the
14// complete interface the design claims.
15var notInCLI = map[string]string{
16 "help": "cobra provides its own",
17 "runner next": "the CI runner's wire protocol, not for humans",
18 "runner done": "the CI runner's wire protocol, not for humans",
19 "runner log": "the CI runner's wire protocol, not for humans",
20 "account import-bundle": "the wire format behind `gitbay migrate`",
21}
22
23// TestEveryCommandIsReachable guards the invariant that adding a command
24// to the registry is enough. The tree is hand-written, so a command can
25// land server-side — reachable over raw SSH, the web and the API — while
26// `gitbay <cmd>` still says "unknown command". That happened to `explore`
27// and `wiki`.
28func TestEveryCommandIsReachable(t *testing.T) {
29 wired := map[string]bool{}
30 var walk func(*cobra.Command)
31 walk = func(c *cobra.Command) {
32 if p := c.Annotations[serverPath]; p != "" {
33 wired[p] = true
34 }
35 for _, sub := range c.Commands() {
36 walk(sub)
37 }
38 }
39 walk(newRoot())
40
41 for _, cmd := range control.Commands() {
42 path := strings.Join(cmd.Path, " ")
43 if wired[path] || notInCLI[path] != "" {
44 continue
45 }
46 t.Errorf("%q is in the registry but not in the CLI tree; "+
47 "add a pass() for it, or list it in notInCLI with a reason", path)
48 }
49
50 // A hand-built command (auth pgp add, repo settings visibility) is
51 // fine, but it must still name a real command.
52 for path := range wired {
53 if _, _, ok := control.Lookup(strings.Fields(path)); !ok {
54 t.Errorf("CLI dispatches %q, which the registry does not define", path)
55 }
56 }
57}
cmd/gitbay/main.go +34 −5
@@ -17,6 +17,15 @@ import (
1717 )
1818
1919 func main() {
20 if err := newRoot().Execute(); err != nil {
21 fmt.Fprintln(os.Stderr, "gitbay:", err)
22 os.Exit(protocol.ExitUsage)
23 }
24}
25
26// newRoot builds the command tree. Separate from main so the coverage
27// test can walk it.
28func newRoot() *cobra.Command {
2029 root := &cobra.Command{
2130 Use: "gitbay",
2231 Short: "CLI-first git forge client",
@@ -40,6 +49,12 @@ func main() {
4049 passOpts{server: []string{"dashboard"}}),
4150 pass("feed", "activity on repositories you can reach [--limit n] [--cursor c]",
4251 passOpts{server: []string{"feed"}}),
52 pass("explore", "public repositories on this instance [--limit n] [--cursor c]",
53 passOpts{server: []string{"explore"}}),
54 group("wiki", "a repository's wiki pages",
55 pass("list", "list pages: [<owner/name>]", passOpts{server: []string{"wiki", "list"}, needsRepo: true}),
56 pass("show", "print a page: [<owner/name>] [<page>]", passOpts{server: []string{"wiki", "show"}, needsRepo: true}),
57 ),
4358 repoCmd(),
4459 issueCmd(),
4560 milestoneCmd(),
@@ -57,15 +72,16 @@ func main() {
5772 initCmd(),
5873 pass("register", "create an account on the default instance: gitbay register --username <n> --email <a> | --invite <code>",
5974 passOpts{server: []string{"register"}}),
75 pass("audit", "instance audit log (admins): [--limit <n>]", passOpts{server: []string{"audit"}}),
6076 manCmd(root),
6177 )
62
63 if err := root.Execute(); err != nil {
64 fmt.Fprintln(os.Stderr, "gitbay:", err)
65 os.Exit(protocol.ExitUsage)
66 }
78 return root
6779 }
6880
81// serverPath is the annotation key holding a passthrough command's
82// server-side path, so the tree can be checked against the registry.
83const serverPath = "gitbay.server_path"
84
6985 // passOpts describes how one CLI command maps onto the server command.
7086 type passOpts struct {
7187 server []string // server-side command path
@@ -81,6 +97,7 @@ func pass(use, short string, o passOpts) *cobra.Command {
8197 return &cobra.Command{
8298 Use: use,
8399 Short: short,
100 Annotations: map[string]string{serverPath: strings.Join(o.server, " ")},
84101 DisableFlagParsing: true,
85102 RunE: func(cmd *cobra.Command, args []string) error {
86103 // cobra still owns `forge <cmd> --help`.
@@ -194,6 +211,7 @@ func authCmd() *cobra.Command {
194211 }
195212 pgpAdd := &cobra.Command{
196213 Use: "add", Short: "register an OpenPGP public key (armored, on stdin)",
214 Annotations: map[string]string{serverPath: "pgp add"},
197215 DisableFlagParsing: true,
198216 RunE: func(cmd *cobra.Command, args []string) error {
199217 t, err := resolveTarget()
@@ -245,6 +263,15 @@ func repoCmd() *cobra.Command {
245263 pass("grep", "search file contents: <query> [--ref <ref>]", passOpts{server: []string{"repo", "grep"}, needsRepo: true}),
246264 pass("tree", "list a directory: [<path>] [--ref <ref>]", passOpts{server: []string{"repo", "tree"}, needsRepo: true}),
247265 pass("cat", "read a file: <path> [--ref <ref>]", passOpts{server: []string{"repo", "cat"}, needsRepo: true}),
266 pass("blame", "attribute lines to commits: <path> [--ref <ref>] [--from <n>] [--to <n>]",
267 passOpts{server: []string{"repo", "blame"}, needsRepo: true}),
268 pass("commit", "show one commit with its patch: <sha>",
269 passOpts{server: []string{"repo", "commit"}, needsRepo: true}),
270 pass("commit-file", "write a file and commit it: <path> [--ref <ref>] [--message <m>] --file -",
271 passOpts{server: []string{"repo", "commit-file"}, needsRepo: true, stdinOK: true}),
272 pass("refs", "list branches and tags", passOpts{server: []string{"repo", "refs"}, needsRepo: true}),
273 pass("download", "write a tar.gz of a ref to stdout: [--ref <r>] > repo.tar.gz",
274 passOpts{server: []string{"repo", "download"}, needsRepo: true}),
248275 pass("pin", "pin a repository to your dashboard", passOpts{server: []string{"repo", "pin"}, needsRepo: true}),
249276 pass("unpin", "unpin a repository", passOpts{server: []string{"repo", "unpin"}, needsRepo: true}),
250277 pass("archive", "archive a repository (read-only)", passOpts{server: []string{"repo", "archive"}, needsRepo: true}),
@@ -293,6 +320,7 @@ func repoCmd() *cobra.Command {
293320 pass("require-approvals", "require N fresh approvals to merge: <n>", passOpts{server: []string{"repo", "settings", "require-approvals"}, needsRepo: true}),
294321 pass("require-resolved", "require threads resolved to merge: on|off", passOpts{server: []string{"repo", "settings", "require-resolved"}, needsRepo: true}),
295322 pass("require-checks", "gate merges on green statuses: ... on|off", passOpts{server: []string{"repo", "settings", "require-checks"}, needsRepo: true}),
323 pass("visibility", "set repository visibility: public|private", passOpts{server: []string{"repo", "settings", "visibility"}, needsRepo: true}),
296324 pass("require-signed", "require verified commit signatures: ... on|off", passOpts{server: []string{"repo", "settings", "require-signed"}, needsRepo: true}),
297325 pass("description", "set the repository description: <text>", passOpts{server: []string{"repo", "settings", "description"}, needsRepo: true}),
298326 pass("website", "set the repository website: <url> ('' clears)", passOpts{server: []string{"repo", "settings", "website"}, needsRepo: true}),
@@ -373,6 +401,7 @@ func importCmd() *cobra.Command {
373401 return &cobra.Command{
374402 Use: "import",
375403 Short: "server-side mirror of a foreign repo: gitbay repo import <owner/name> --from <url> [--private] [--token-stdin]",
404 Annotations: map[string]string{serverPath: "repo import"},
376405 DisableFlagParsing: true,
377406 RunE: func(cmd *cobra.Command, args []string) error {
378407 for _, a := range args {