cli: front-ends for every registry command !103

merged merged by cmc on 2026-08-28 04:44 UTC · krz/gitbay:cli-coverage into main

2 files changed, +91 −5

Layout: unified · split

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
1919func 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.
7086type 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 {