help: filter by prefix, and document every command's arguments !129

merged merged by cmc on 2026-08-31 05:16 UTC · krz/gitbay:help-discoverability into main

36 files changed, +468 −160

Layout: unified · split

cmd/gitbay/coverage_test.go +1 −1
@@ -13,7 +13,7 @@ import (
1313// registry must be reachable by typing it, or the CLI is not the
1414// complete interface the design claims.
1515var notInCLI = map[string]string{
16 "help": "cobra provides its own",
16 "help": "cobra owns `gitbay help`; the registry's is reached by `<cmd> --help`",
1717 "runner next": "the CI runner's wire protocol, not for humans",
1818 "runner done": "the CI runner's wire protocol, not for humans",
1919 "runner log": "the CI runner's wire protocol, not for humans",
cmd/gitbay/main.go +14 −2
@@ -119,10 +119,12 @@ func pass(use, short string, o passOpts) *cobra.Command {
119119 },
120120 DisableFlagParsing: true,
121121 RunE: func(cmd *cobra.Command, args []string) error {
122 // cobra still owns `forge <cmd> --help`.
122 // The registry is the only place flags are written down, so
123 // --help asks the server rather than reprinting the one-line
124 // summary cobra holds.
123125 for _, a := range args {
124126 if a == "--help" || a == "-h" {
125 return cmd.Help()
127 os.Exit(runServerHelp(o))
126128 }
127129 }
128130 os.Exit(runPass(o, args))
@@ -131,6 +133,16 @@ func pass(use, short string, o passOpts) *cobra.Command {
131133 }
132134}
133135
136// runServerHelp prints the registry's usage for one command.
137func runServerHelp(o passOpts) int {
138 t, err := resolveTarget()
139 if err != nil {
140 fmt.Fprintln(os.Stderr, "gitbay:", err)
141 return protocol.ExitFailure
142 }
143 return runSSH(t, append([]string{"help"}, o.server...), strings.NewReader(""))
144}
145
134146func runPass(o passOpts, args []string) int {
135147 t, err := resolveTarget()
136148 if err != nil {
e2e/api_test.go +35 −3
@@ -7,6 +7,8 @@ import (
77 "io"
88 "net/http"
99 "net/url"
10 "os"
11 "path/filepath"
1012 "strings"
1113 "testing"
1214 "time"
@@ -96,11 +98,41 @@ func TestJSONAPI(t *testing.T) {
9698 t.Fatalf("unknown command: %d", status)
9799 }
98100
99 // Raw-output commands (no envelope) are wrapped.
100 status, body = inst.apiCall(t, token, []string{"help"}, "")
101 if status != 200 || !strings.Contains(body["output"].(string), "repo create") {
101 // Raw-output commands (no envelope) are wrapped. Reading a file is the
102 // cheapest raw output, so give the repo one commit to read from.
103 work := t.TempDir()
104 aliceEnv := inst.gitEnv(aliceKey)
105 mustGit(t, work, aliceEnv, "clone", inst.sshURL("alice/proj"), "proj")
106 dir := filepath.Join(work, "proj")
107 if err := os.WriteFile(filepath.Join(dir, "README"), []byte("plain text, not json\n"), 0o644); err != nil {
108 t.Fatal(err)
109 }
110 mustGit(t, dir, aliceEnv, "checkout", "-q", "-b", "main")
111 mustGit(t, dir, aliceEnv, "add", "README")
112 mustGit(t, dir, aliceEnv, "commit", "-q", "-m", "init")
113 mustGit(t, dir, aliceEnv, "push", "-q", "origin", "main")
114
115 // A tarball is not an envelope, so it comes back under "output".
116 status, body = inst.apiCall(t, token, []string{"repo", "download", "alice/proj"}, "")
117 raw, isRaw := body["output"].(string)
118 if status != 200 || !isRaw || raw == "" {
119 t.Fatalf("repo download via API: %d %v", status, body)
120 }
121
122 // help answers with the registry as data, so a consumer can read one
123 // command's arguments without scraping the whole listing.
124 status, body = inst.apiCall(t, token, []string{"help", "issue", "create"}, "")
125 if status != 200 {
102126 t.Fatalf("help via API: %d %v", status, body)
103127 }
128 rows, ok := body["data"].([]any)
129 if !ok || len(rows) != 1 {
130 t.Fatalf("help issue create: %v", body)
131 }
132 row := rows[0].(map[string]any)
133 if row["path"] != "issue create" || !strings.Contains(row["usage"].(string), "--title") {
134 t.Fatalf("help issue create row: %v", row)
135 }
104136
105137 // Git transport is refused by name.
106138 if status, _ = inst.apiCall(t, token, []string{"git-upload-pack", "alice/proj"}, ""); status != 400 {
internal/control/audit.go +2 −1
@@ -10,7 +10,8 @@ import (
1010
1111func init() {
1212 register(Command{Path: []string{"audit"},
13 Summary: "instance audit log (admins): audit [--limit <n>]", ReadOnly: true, SSHOnly: true, Run: runAudit})
13 Summary: "instance audit log (admins)",
14 Usage: "audit [--limit <n>]", ReadOnly: true, SSHOnly: true, Run: runAudit})
1415}
1516
1617func runAudit(c *Ctx, args []string) int {
internal/control/build.go +22 −11
@@ -18,37 +18,48 @@ import (
1818
1919func init() {
2020 register(Command{Path: []string{"build", "list"},
21 Summary: "list recent builds: build list <owner/name>", ReadOnly: true, Run: runBuildList})
21 Summary: "list recent builds",
22 Usage: "build list <owner/name>", ReadOnly: true, Run: runBuildList})
2223 register(Command{Path: []string{"build", "show"},
23 Summary: "show one build: build show <owner/name> <n>", ReadOnly: true, Run: runBuildShow})
24 Summary: "show one build",
25 Usage: "build show <owner/name> <n>", ReadOnly: true, Run: runBuildShow})
2426 register(Command{Path: []string{"build", "log"},
25 Summary: "print a build's log: build log <owner/name> <n>", ReadOnly: true, Run: runBuildLog})
27 Summary: "print a build's log",
28 Usage: "build log <owner/name> <n>", ReadOnly: true, Run: runBuildLog})
2629
2730 register(Command{Path: []string{"build", "jobs"},
28 Summary: "list the jobs a trigger can name: build jobs <owner/name>", ReadOnly: true, Run: runBuildJobs})
31 Summary: "list the jobs a trigger can name",
32 Usage: "build jobs <owner/name>", ReadOnly: true, Run: runBuildJobs})
2933
3034 register(Command{Path: []string{"build", "trigger"},
31 Summary: "queue a job now (scheduled or not): build trigger <owner/name> <job>", Run: runBuildTrigger})
35 Summary: "queue a job now (scheduled or not)",
36 Usage: "build trigger <owner/name> <job>", Run: runBuildTrigger})
3237 // Secrets: set over stdin, listed by name only, injected into the
3338 // repo's builds as environment variables. Same discipline as mirror
3439 // tokens — the value never appears in argv, logs, or output.
3540 register(Command{Path: []string{"repo", "secret", "set"},
36 Summary: "set a build secret: repo secret set <owner/name> <NAME> (value on stdin)",
41 Summary: "set a build secret",
42 Usage: "repo secret set <owner/name> <NAME> (value on stdin)",
3743 ReadsStdin: true, SSHOnly: true, Run: runSecretSet})
3844 register(Command{Path: []string{"repo", "secret", "remove"},
39 Summary: "remove a build secret: repo secret remove <owner/name> <NAME>", Run: runSecretRemove})
45 Summary: "remove a build secret",
46 Usage: "repo secret remove <owner/name> <NAME>", Run: runSecretRemove})
4047 register(Command{Path: []string{"repo", "secret", "list"},
41 Summary: "list build secret names: repo secret list <owner/name>", ReadOnly: true, Run: runSecretList})
48 Summary: "list build secret names",
49 Usage: "repo secret list <owner/name>", ReadOnly: true, Run: runSecretList})
4250
4351 // Runner commands: the claim/report loop for gitbay-runner. Admin-only —
4452 // a runner executes arbitrary repo code, so handing out jobs is the
4553 // instance operator's call.
4654 register(Command{Path: []string{"runner", "next"},
47 Summary: "claim the oldest pending build (runner protocol)", SSHOnly: true, Run: runRunnerNext})
55 Summary: "claim the oldest pending build (runner protocol)",
56 Usage: "runner next", SSHOnly: true, Run: runRunnerNext})
4857 register(Command{Path: []string{"runner", "log"},
49 Summary: "append a build's log from stdin: runner log <build-id>", SSHOnly: true, ReadsStdin: true, Run: runRunnerLog})
58 Summary: "append a build's log from stdin",
59 Usage: "runner log <build-id>", SSHOnly: true, ReadsStdin: true, Run: runRunnerLog})
5060 register(Command{Path: []string{"runner", "done"},
51 Summary: "finish a build: runner done <build-id> success|failure", SSHOnly: true, Run: runRunnerDone})
61 Summary: "finish a build",
62 Usage: "runner done <build-id> success|failure", SSHOnly: true, Run: runRunnerDone})
5263}
5364
5465type buildOut struct {
internal/control/commitfile.go +3 −2
@@ -12,8 +12,9 @@ import (
1212
1313func init() {
1414 register(Command{
15 Path: []string{"repo", "commit-file"},
16 Summary: "write a file and commit it: repo commit-file <owner/name> <path> " +
15 Path: []string{"repo", "commit-file"},
16 Summary: "write a file and commit it",
17 Usage: "repo commit-file <owner/name> <path> " +
1718 "--ref <branch> [--message <m>] [--file -]",
1819 ReadsStdin: true,
1920 Run: runCommitFile,
internal/control/control.go +42 −6
@@ -9,6 +9,7 @@ import (
99 "io"
1010 "reflect"
1111 "slices"
12 "strings"
1213
1314 "gitbay.org/gitbay/internal/config"
1415 "gitbay.org/gitbay/internal/protocol"
@@ -36,8 +37,12 @@ type Ctx struct {
3637}
3738
3839type Command struct {
39 Path []string // e.g. ["keys", "add"]
40 Path []string // e.g. ["keys", "add"]
41 // Summary is one line of prose: what the command does, no argument
42 // syntax. Usage is the argument syntax, opening with the command path.
43 // help renders them separately, so neither may carry the other's job.
4044 Summary string
45 Usage string
4146 ReadsStdin bool
4247 ReadOnly bool // safe for read-scoped API tokens
4348 SSHOnly bool // refused over the HTTP API (credential minting)
@@ -161,13 +166,44 @@ func init() {
161166 register(Command{
162167 Path: []string{"help"},
163168 Summary: "list available commands",
169 Usage: "help [<prefix>...]",
164170 ReadOnly: true,
165 Run: func(c *Ctx, args []string) int {
166 for _, cmd := range registry {
167 fmt.Fprintf(c.Stdout, "%-24s %s\n", joinPath(cmd.Path), cmd.Summary)
171 Run: runHelp,
172 })
173}
174
175// helpEntry is one row of the registry as help reports it.
176type helpEntry struct {
177 Path string `json:"path"`
178 Summary string `json:"summary"`
179 Usage string `json:"usage"`
180}
181
182// runHelp lists the registry, sorted, so a noun's commands sit together.
183// A prefix narrows the listing and adds each command's argument syntax —
184// the only place flags are written down. The unfiltered listing stays one
185// line per command.
186func runHelp(c *Ctx, args []string) int {
187 prefix := joinPath(args)
188 var matched []helpEntry
189 for _, cmd := range registry {
190 p := joinPath(cmd.Path)
191 if prefix != "" && p != prefix && !strings.HasPrefix(p, prefix+" ") {
192 continue
193 }
194 matched = append(matched, helpEntry{Path: p, Summary: cmd.Summary, Usage: cmd.Usage})
195 }
196 if len(matched) == 0 {
197 return c.fail(protocol.ExitNotFound, "no command matches %q; try: help", prefix)
198 }
199 slices.SortFunc(matched, func(a, b helpEntry) int { return strings.Compare(a.Path, b.Path) })
200 return c.emit(matched, func(w io.Writer) {
201 for _, e := range matched {
202 fmt.Fprintf(w, "%-24s %s\n", e.Path, e.Summary)
203 if prefix != "" {
204 fmt.Fprintf(w, " %s\n", e.Usage)
168205 }
169 return protocol.ExitOK
170 },
206 }
171207 })
172208}
173209
internal/control/control_test.go +77
@@ -1,6 +1,10 @@
11package control
22
33import (
4 "bytes"
5 "encoding/json"
6 "io"
7 "slices"
48 "strings"
59 "testing"
610
@@ -86,3 +90,76 @@ func TestMRDedupKeyCannotCollideWithASHA(t *testing.T) {
8690 t.Error("different merge requests share a dedup key")
8791 }
8892}
93
94// TestEveryCommandDocumentsItsUsage is what makes `help <prefix>` and
95// `gitbay <cmd> --help` worth typing: both render Usage, so a command that
96// omits it documents nothing. Usage opens with the command path so the
97// printed line can be typed as-is, and the summary must not carry the
98// argument syntax it used to.
99func TestEveryCommandDocumentsItsUsage(t *testing.T) {
100 for _, cmd := range Commands() {
101 path := strings.Join(cmd.Path, " ")
102 if cmd.Usage == "" {
103 t.Errorf("command %q has no Usage", path)
104 continue
105 }
106 if cmd.Usage != path && !strings.HasPrefix(cmd.Usage, path+" ") {
107 t.Errorf("command %q has Usage %q, which does not open with the command path", path, cmd.Usage)
108 }
109 if strings.Contains(cmd.Summary, ": "+path) {
110 t.Errorf("command %q still carries its usage in the summary: %q", path, cmd.Summary)
111 }
112 }
113}
114
115// TestHelpPrefixNarrowsAndShowsFlags covers the reason the command exists:
116// before this, reading one command's flags meant reading all of them.
117func TestHelpPrefixNarrowsAndShowsFlags(t *testing.T) {
118 var buf bytes.Buffer
119 c := &Ctx{Stdout: &buf, Stderr: io.Discard}
120 if code := runHelp(c, []string{"issue"}); code != protocol.ExitOK {
121 t.Fatalf("help issue exited %d", code)
122 }
123 out := buf.String()
124 for _, line := range strings.Split(strings.TrimSpace(out), "\n") {
125 if strings.HasPrefix(line, " ") {
126 continue // the indented usage line
127 }
128 if !strings.HasPrefix(line, "issue ") {
129 t.Errorf("help issue listed an unrelated command: %q", line)
130 }
131 }
132 if !strings.Contains(out, "--state open|closed|all") {
133 t.Error("help issue did not print issue list's flags")
134 }
135}
136
137func TestHelpUnknownPrefixIsNotFound(t *testing.T) {
138 var buf bytes.Buffer
139 c := &Ctx{Stdout: &buf, Stderr: io.Discard}
140 if code := runHelp(c, []string{"nope"}); code != protocol.ExitNotFound {
141 t.Errorf("help nope exited %d, want %d", code, protocol.ExitNotFound)
142 }
143}
144
145// TestHelpListsEveryCommandSorted pins the unfiltered listing: one row per
146// registered command, ordered so a noun's commands sit together.
147func TestHelpListsEveryCommandSorted(t *testing.T) {
148 var buf bytes.Buffer
149 c := &Ctx{Stdout: &buf, Stderr: io.Discard, JSON: true}
150 if code := runHelp(c, nil); code != protocol.ExitOK {
151 t.Fatalf("help exited %d", code)
152 }
153 var env struct {
154 Data []helpEntry `json:"data"`
155 }
156 if err := json.Unmarshal(buf.Bytes(), &env); err != nil {
157 t.Fatalf("help --json: %v", err)
158 }
159 if len(env.Data) != len(Commands()) {
160 t.Errorf("help listed %d commands, registry has %d", len(env.Data), len(Commands()))
161 }
162 if !slices.IsSortedFunc(env.Data, func(a, b helpEntry) int { return strings.Compare(a.Path, b.Path) }) {
163 t.Error("help output is not sorted by path")
164 }
165}
internal/control/dashboard.go +3 −1
@@ -16,9 +16,11 @@ import (
1616func init() {
1717 register(Command{Path: []string{"dashboard"},
1818 Summary: "one read for the account dashboard: review queue, assigned and open work, pins, activity, builds",
19 Usage: "dashboard",
1920 ReadOnly: true, Run: runDashboard})
2021 register(Command{Path: []string{"feed"},
21 Summary: "activity on repositories you can reach: feed [--limit <n>] [--cursor <c>]",
22 Summary: "activity on repositories you can reach",
23 Usage: "feed [--limit <n>] [--cursor <c>]",
2224 ReadOnly: true, Run: runFeed})
2325}
2426
internal/control/deploykey.go +6 −3
@@ -14,12 +14,15 @@ import (
1414
1515func init() {
1616 register(Command{Path: []string{"repo", "deploy-key", "add"},
17 Summary: "bind a read-only (or --rw) key to one repository: repo deploy-key add <owner/name> [--rw] < key.pub",
17 Summary: "bind a read-only (or --rw) key to one repository",
18 Usage: "repo deploy-key add <owner/name> [--rw] < key.pub",
1819 ReadsStdin: true, Run: runDeployKeyAdd})
1920 register(Command{Path: []string{"repo", "deploy-key", "list"},
20 Summary: "list deploy keys: repo deploy-key list <owner/name>", ReadOnly: true, Run: runDeployKeyList})
21 Summary: "list deploy keys",
22 Usage: "repo deploy-key list <owner/name>", ReadOnly: true, Run: runDeployKeyList})
2123 register(Command{Path: []string{"repo", "deploy-key", "remove"},
22 Summary: "remove a deploy key: repo deploy-key remove <owner/name> <fingerprint>", Run: runDeployKeyRemove})
24 Summary: "remove a deploy key",
25 Usage: "repo deploy-key remove <owner/name> <fingerprint>", Run: runDeployKeyRemove})
2326}
2427
2528func runDeployKeyAdd(c *Ctx, args []string) int {
internal/control/deps.go +6 −3
@@ -15,11 +15,14 @@ func init() {
1515 // public registry what the repository depends on, which is the owner's
1616 // disclosure to make, not the instance's.
1717 register(Command{Path: []string{"repo", "deps", "enable"},
18 Summary: "check dependencies for updates: repo deps enable <owner/name>", Run: runDepsEnable})
18 Summary: "check dependencies for updates",
19 Usage: "repo deps enable <owner/name>", Run: runDepsEnable})
1920 register(Command{Path: []string{"repo", "deps", "disable"},
20 Summary: "stop checking dependencies: repo deps disable <owner/name>", Run: runDepsDisable})
21 Summary: "stop checking dependencies",
22 Usage: "repo deps disable <owner/name>", Run: runDepsDisable})
2123 register(Command{Path: []string{"repo", "deps", "status"},
22 Summary: "show dependency check state: repo deps status <owner/name>", ReadOnly: true, Run: runDepsStatus})
24 Summary: "show dependency check state",
25 Usage: "repo deps status <owner/name>", ReadOnly: true, Run: runDepsStatus})
2326}
2427
2528func runDepsEnable(c *Ctx, args []string) int {
internal/control/diffcomment.go +8 −4
@@ -16,14 +16,18 @@ import (
1616
1717func init() {
1818 register(Command{Path: []string{"mr", "diff-comment"},
19 Summary: "comment on a diff line: mr diff-comment <owner/name> <n> --path <file> --line <l> [--old] [--reply <id>] [--message <m> | --file -]",
19 Summary: "comment on a diff line",
20 Usage: "mr diff-comment <owner/name> <n> --path <file> --line <l> [--old] [--reply <id>] [--message <m> | --file -]",
2021 ReadsStdin: true, Run: runDiffComment})
2122 register(Command{Path: []string{"mr", "threads"},
22 Summary: "review threads on an MR: mr threads <owner/name> <n>", ReadOnly: true, Run: runMRThreads})
23 Summary: "review threads on an MR",
24 Usage: "mr threads <owner/name> <n>", ReadOnly: true, Run: runMRThreads})
2325 register(Command{Path: []string{"mr", "resolve"},
24 Summary: "resolve a review thread: mr resolve <owner/name> <n> <thread-id>", Run: runMRResolve})
26 Summary: "resolve a review thread",
27 Usage: "mr resolve <owner/name> <n> <thread-id>", Run: runMRResolve})
2528 register(Command{Path: []string{"mr", "unresolve"},
26 Summary: "reopen a review thread: mr unresolve <owner/name> <n> <thread-id>", Run: runMRUnresolve})
29 Summary: "reopen a review thread",
30 Usage: "mr unresolve <owner/name> <n> <thread-id>", Run: runMRUnresolve})
2731}
2832
2933func runDiffComment(c *Ctx, args []string) int {
internal/control/explore.go +4 −2
@@ -12,7 +12,8 @@ import (
1212func init() {
1313 register(Command{
1414 Path: []string{"explore"},
15 Summary: "list public repositories: explore [--limit <n>] [--cursor <c>]",
15 Summary: "list public repositories",
16 Usage: "explore [--limit <n>] [--cursor <c>]",
1617 ReadOnly: true,
1718 Run: runExplore,
1819 })
@@ -20,7 +21,8 @@ func init() {
2021 Path: []string{"repo", "download"},
2122 // Not "repo archive": that name is taken by the read-only flag,
2223 // and renaming it would break every script that sets it.
23 Summary: "write a tar.gz of a ref to stdout: repo download <owner/name> [--ref <r>] > repo.tar.gz",
24 Summary: "write a tar.gz of a ref to stdout",
25 Usage: "repo download <owner/name> [--ref <r>] > repo.tar.gz",
2426 ReadOnly: true,
2527 Run: runRepoDownload,
2628 })
internal/control/ghimport.go +2 −1
@@ -20,7 +20,8 @@ import (
2020
2121func init() {
2222 register(Command{Path: []string{"repo", "import-issues"},
23 Summary: "import GitHub issue and PR history: repo import-issues <owner/name> --from <ghowner/ghrepo> [--token-stdin] [--api-base <url>]",
23 Summary: "import GitHub issue and PR history",
24 Usage: "repo import-issues <owner/name> --from <ghowner/ghrepo> [--token-stdin] [--api-base <url>]",
2425 ReadsStdin: true, Run: runImportIssues})
2526}
2627
internal/control/identity.go +5 −1
@@ -15,24 +15,28 @@ func init() {
1515 register(Command{
1616 Path: []string{"whoami"},
1717 Summary: "show the authenticated account",
18 Usage: "whoami",
1819 ReadOnly: true,
1920 Run: runWhoami,
2021 })
2122 register(Command{
2223 Path: []string{"keys", "list"},
2324 Summary: "list registered SSH keys",
25 Usage: "keys list",
2426 ReadOnly: true,
2527 Run: runKeysList,
2628 })
2729 register(Command{
2830 Path: []string{"keys", "add"},
29 Summary: "register an SSH public key (authorized_keys format on stdin) [--scope full|git]",
31 Summary: "register an SSH public key (authorized_keys format)",
32 Usage: "keys add [--scope full|git] < key.pub",
3033 ReadsStdin: true,
3134 Run: runKeysAdd,
3235 })
3336 register(Command{
3437 Path: []string{"keys", "remove"},
3538 Summary: "remove an SSH key by fingerprint",
39 Usage: "keys remove <fingerprint>",
3640 Run: runKeysRemove,
3741 })
3842}
internal/control/import.go +2 −1
@@ -17,7 +17,8 @@ import (
1717
1818func init() {
1919 register(Command{Path: []string{"repo", "import"},
20 Summary: "server-side mirror of a foreign repository: repo import <owner/name> --from <url> [--private] [--token-stdin]",
20 Summary: "server-side mirror of a foreign repository",
21 Usage: "repo import <owner/name> --from <url> [--private] [--token-stdin]",
2122 ReadsStdin: true, Run: runRepoImport})
2223}
2324
internal/control/issue.go +18 −9
@@ -16,26 +16,35 @@ const maxBodyBytes = 64 << 10
1616
1717func init() {
1818 register(Command{Path: []string{"issue", "create"},
19 Summary: "open an issue: issue create <owner/name> --title <t> [--body <b> | --file -] [--format md|org]",
19 Summary: "open an issue",
20 Usage: "issue create <owner/name> --title <t> [--body <b> | --file -] [--format md|org]",
2021 ReadsStdin: true, Run: runIssueCreate})
2122 register(Command{Path: []string{"issue", "list"},
22 Summary: "list issues: issue list <owner/name> [--state open|closed|all] [--limit <n>] [--cursor <c>]", ReadOnly: true, Run: runIssueList})
23 Summary: "list issues",
24 Usage: "issue list <owner/name> [--state open|closed|all] [--limit <n>] [--cursor <c>]", ReadOnly: true, Run: runIssueList})
2325 register(Command{Path: []string{"issue", "show"},
24 Summary: "show an issue with comments: issue show <owner/name> <n>", ReadOnly: true, Run: runIssueShow})
26 Summary: "show an issue with comments",
27 Usage: "issue show <owner/name> <n>", ReadOnly: true, Run: runIssueShow})
2528 register(Command{Path: []string{"issue", "edit"},
26 Summary: "edit title or body: issue edit <owner/name> <n> [--title <t>] [--body <b> | --file -] [--format md|org]",
29 Summary: "edit title or body",
30 Usage: "issue edit <owner/name> <n> [--title <t>] [--body <b> | --file -] [--format md|org]",
2731 ReadsStdin: true, Run: runIssueEdit})
2832 register(Command{Path: []string{"issue", "comment"},
29 Summary: "comment: issue comment <owner/name> <n> [--message <m> | --file -] [--format md|org]",
33 Summary: "comment",
34 Usage: "issue comment <owner/name> <n> [--message <m> | --file -] [--format md|org]",
3035 ReadsStdin: true, Run: runIssueComment})
3136 register(Command{Path: []string{"issue", "close"},
32 Summary: "close an issue: issue close <owner/name> <n>", Run: runIssueClose})
37 Summary: "close an issue",
38 Usage: "issue close <owner/name> <n>", Run: runIssueClose})
3339 register(Command{Path: []string{"issue", "reopen"},
34 Summary: "reopen an issue: issue reopen <owner/name> <n>", Run: runIssueReopen})
40 Summary: "reopen an issue",
41 Usage: "issue reopen <owner/name> <n>", Run: runIssueReopen})
3542 register(Command{Path: []string{"issue", "label"},
36 Summary: "labels: issue label <owner/name> <n> [--add <l>]... [--remove <l>]...", Run: runIssueLabel})
43 Summary: "labels",
44 Usage: "issue label <owner/name> <n> [--add <l>]... [--remove <l>]...", Run: runIssueLabel})
3745 register(Command{Path: []string{"issue", "assign"},
38 Summary: "assignees: issue assign <owner/name> <n> [--add <user>]... [--remove <user>]...", Run: runIssueAssign})
46 Summary: "assignees",
47 Usage: "issue assign <owner/name> <n> [--add <user>]... [--remove <user>]...", Run: runIssueAssign})
3948}
4049
4150// issueArgs parses "<owner/name> <n>" plus flags handled by the caller.
internal/control/migrate.go +4 −2
@@ -13,10 +13,12 @@ import (
1313
1414func init() {
1515 register(Command{Path: []string{"account", "export"},
16 Summary: "write your account bundle (profile, repos, issues, MRs) as JSON to stdout",
16 Summary: "write your account bundle (profile, repos, issues, MRs) as JSON",
17 Usage: "account export > bundle.json",
1718 ReadOnly: true, Run: runAccountExport})
1819 register(Command{Path: []string{"account", "import-bundle"},
19 Summary: "replay an account bundle from stdin (see gitbay migrate)",
20 Summary: "replay an account bundle (see gitbay migrate)",
21 Usage: "account import-bundle [--source <host>] < bundle.json",
2022 ReadsStdin: true, Run: runAccountImportBundle})
2123}
2224
internal/control/milestone.go +14 −7
@@ -16,19 +16,26 @@ import (
1616
1717func init() {
1818 register(Command{Path: []string{"milestone", "create"},
19 Summary: "create a milestone: milestone create <owner/name> <title> [--description <d>] [--due YYYY-MM-DD]", Run: runMilestoneCreate})
19 Summary: "create a milestone",
20 Usage: "milestone create <owner/name> <title> [--description <d>] [--due YYYY-MM-DD]", Run: runMilestoneCreate})
2021 register(Command{Path: []string{"milestone", "list"},
21 Summary: "list milestones with progress: milestone list <owner/name> [--state open|closed|all]", ReadOnly: true, Run: runMilestoneList})
22 Summary: "list milestones with progress",
23 Usage: "milestone list <owner/name> [--state open|closed|all]", ReadOnly: true, Run: runMilestoneList})
2224 register(Command{Path: []string{"milestone", "close"},
23 Summary: "close a milestone: milestone close <owner/name> <title>", Run: runMilestoneClose})
25 Summary: "close a milestone",
26 Usage: "milestone close <owner/name> <title>", Run: runMilestoneClose})
2427 register(Command{Path: []string{"milestone", "reopen"},
25 Summary: "reopen a milestone: milestone reopen <owner/name> <title>", Run: runMilestoneReopen})
28 Summary: "reopen a milestone",
29 Usage: "milestone reopen <owner/name> <title>", Run: runMilestoneReopen})
2630 register(Command{Path: []string{"issue", "milestone"},
27 Summary: "set or clear an issue's milestone: issue milestone <owner/name> <n> <title|none>", Run: runIssueMilestone})
31 Summary: "set or clear an issue's milestone",
32 Usage: "issue milestone <owner/name> <n> <title|none>", Run: runIssueMilestone})
2833 register(Command{Path: []string{"mr", "milestone"},
29 Summary: "set or clear an MR's milestone: mr milestone <owner/name> <n> <title|none>", Run: runMRMilestone})
34 Summary: "set or clear an MR's milestone",
35 Usage: "mr milestone <owner/name> <n> <title|none>", Run: runMRMilestone})
3036 register(Command{Path: []string{"issue", "templates"},
31 Summary: "list issue templates (.gitbay/issue-template*.md): issue templates <owner/name>", ReadOnly: true, Run: runIssueTemplates})
37 Summary: "list issue templates (.gitbay/issue-template*.md)",
38 Usage: "issue templates <owner/name>", ReadOnly: true, Run: runIssueTemplates})
3239}
3340
3441var duePat = regexp.MustCompile(`^\d{4}-\d{2}-\d{2}$`)
internal/control/mirrorcmd.go +8 −4
@@ -16,14 +16,18 @@ import (
1616
1717func init() {
1818 register(Command{Path: []string{"repo", "mirror", "add"},
19 Summary: "mirror to or from a remote: repo mirror add <owner/name> <https-url> --direction push|pull [--username <u>] [--token-stdin]",
19 Summary: "mirror to or from a remote",
20 Usage: "repo mirror add <owner/name> <https-url> --direction push|pull [--username <u>] [--token-stdin]",
2021 ReadsStdin: true, SSHOnly: true, Run: runMirrorAdd})
2122 register(Command{Path: []string{"repo", "mirror", "list"},
22 Summary: "list mirrors with sync status: repo mirror list <owner/name>", ReadOnly: true, Run: runMirrorList})
23 Summary: "list mirrors with sync status",
24 Usage: "repo mirror list <owner/name>", ReadOnly: true, Run: runMirrorList})
2325 register(Command{Path: []string{"repo", "mirror", "remove"},
24 Summary: "remove a mirror: repo mirror remove <owner/name> <id>", Run: runMirrorRemove})
26 Summary: "remove a mirror",
27 Usage: "repo mirror remove <owner/name> <id>", Run: runMirrorRemove})
2528 register(Command{Path: []string{"repo", "mirror", "sync"},
26 Summary: "schedule an immediate sync: repo mirror sync <owner/name>", Run: runMirrorSync})
29 Summary: "schedule an immediate sync",
30 Usage: "repo mirror sync <owner/name>", Run: runMirrorSync})
2731}
2832
2933func runMirrorAdd(c *Ctx, args []string) int {
internal/control/mr.go +30 −15
@@ -17,38 +17,53 @@ import (
1717
1818func init() {
1919 register(Command{Path: []string{"repo", "fork"},
20 Summary: "fork a repository under your account: repo fork <owner/name> [--name <n>]", Run: runRepoFork})
20 Summary: "fork a repository under your account",
21 Usage: "repo fork <owner/name> [--name <n>]", Run: runRepoFork})
2122 register(Command{Path: []string{"repo", "settings", "require-approvals"},
22 Summary: "require N fresh approvals to merge: repo settings require-approvals <owner/name> <n> (0 = off)", Run: runRequireApprovals})
23 Summary: "require N fresh approvals to merge",
24 Usage: "repo settings require-approvals <owner/name> <n> (0 = off)", Run: runRequireApprovals})
2325 register(Command{Path: []string{"repo", "settings", "require-resolved"},
24 Summary: "require all review threads resolved to merge: repo settings require-resolved <owner/name> on|off", Run: runRequireResolved})
26 Summary: "require all review threads resolved to merge",
27 Usage: "repo settings require-resolved <owner/name> on|off", Run: runRequireResolved})
2528 register(Command{Path: []string{"repo", "settings", "require-checks"},
26 Summary: "gate merges on green statuses: repo settings require-checks <owner/name> on|off", Run: runRequireChecks})
29 Summary: "gate merges on green statuses",
30 Usage: "repo settings require-checks <owner/name> on|off", Run: runRequireChecks})
2731 register(Command{Path: []string{"repo", "settings", "require-signed"},
28 Summary: "require verified commit signatures: repo settings require-signed <owner/name> on|off", Run: runRequireSigned})
32 Summary: "require verified commit signatures",
33 Usage: "repo settings require-signed <owner/name> on|off", Run: runRequireSigned})
2934 register(Command{Path: []string{"mr", "create"},
30 Summary: "open a merge request: mr create <target owner/name> --source [owner/name:]<branch> --target <branch> --title <t> [--body <b> | --file -] [--format md|org]",
35 Summary: "open a merge request",
36 Usage: "mr create <target owner/name> --source [owner/name:]<branch> --target <branch> --title <t> [--body <b> | --file -] [--format md|org]",
3137 ReadsStdin: true, Run: runMRCreate})
3238 register(Command{Path: []string{"mr", "list"},
33 Summary: "list merge requests: mr list <owner/name> [--state open|merged|closed|source_gone|all] [--limit <n>] [--cursor <c>]", ReadOnly: true, Run: runMRList})
39 Summary: "list merge requests",
40 Usage: "mr list <owner/name> [--state open|merged|closed|source_gone|all] [--limit <n>] [--cursor <c>]", ReadOnly: true, Run: runMRList})
3441 register(Command{Path: []string{"mr", "show"},
35 Summary: "show a merge request: mr show <owner/name> <n>", ReadOnly: true, Run: runMRShow})
42 Summary: "show a merge request",
43 Usage: "mr show <owner/name> <n>", ReadOnly: true, Run: runMRShow})
3644 register(Command{Path: []string{"mr", "diff"},
37 Summary: "show the diff: mr diff <owner/name> <n>", ReadOnly: true, Run: runMRDiff})
45 Summary: "show the diff",
46 Usage: "mr diff <owner/name> <n>", ReadOnly: true, Run: runMRDiff})
3847 register(Command{Path: []string{"mr", "edit"},
39 Summary: "edit title or body: mr edit <owner/name> <n> [--title <t>] [--body <b> | --file -] [--format md|org]",
48 Summary: "edit title or body",
49 Usage: "mr edit <owner/name> <n> [--title <t>] [--body <b> | --file -] [--format md|org]",
4050 ReadsStdin: true, Run: runMREdit})
4151 register(Command{Path: []string{"mr", "retarget"},
42 Summary: "retarget onto another branch: mr retarget <owner/name> <n> <branch>", Run: runMRRetarget})
52 Summary: "retarget onto another branch",
53 Usage: "mr retarget <owner/name> <n> <branch>", Run: runMRRetarget})
4354 register(Command{Path: []string{"mr", "comment"},
44 Summary: "comment: mr comment <owner/name> <n> [--message <m> | --file -] [--format md|org]",
55 Summary: "comment",
56 Usage: "mr comment <owner/name> <n> [--message <m> | --file -] [--format md|org]",
4557 ReadsStdin: true, Run: runMRComment})
4658 register(Command{Path: []string{"mr", "review"},
47 Summary: "review: mr review <owner/name> <n> --approve|--request-changes|--comment", Run: runMRReview})
59 Summary: "review",
60 Usage: "mr review <owner/name> <n> --approve|--request-changes|--comment", Run: runMRReview})
4861 register(Command{Path: []string{"mr", "merge"},
49 Summary: "merge: mr merge <owner/name> <n> [--strategy ff|merge|squash|rebase]", Run: runMRMerge})
62 Summary: "merge",
63 Usage: "mr merge <owner/name> <n> [--strategy ff|merge|squash|rebase]", Run: runMRMerge})
5064 register(Command{Path: []string{"mr", "close"},
51 Summary: "close without merging: mr close <owner/name> <n>", Run: runMRClose})
65 Summary: "close without merging",
66 Usage: "mr close <owner/name> <n>", Run: runMRClose})
5267}
5368
5469func runRepoFork(c *Ctx, args []string) int {
internal/control/org.go +16 −8
@@ -14,21 +14,29 @@ import (
1414
1515func init() {
1616 register(Command{Path: []string{"org", "create"},
17 Summary: "create an organization (you become its first admin): org create <name>", Run: runOrgCreate})
17 Summary: "create an organization (you become its first admin)",
18 Usage: "org create <name>", Run: runOrgCreate})
1819 register(Command{Path: []string{"org", "list"},
19 Summary: "list organizations you belong to", ReadOnly: true, Run: runOrgList})
20 Summary: "list organizations you belong to",
21 Usage: "org list", ReadOnly: true, Run: runOrgList})
2022 register(Command{Path: []string{"org", "show"},
21 Summary: "show an organization and its members: org show <name>", ReadOnly: true, Run: runOrgShow})
23 Summary: "show an organization and its members",
24 Usage: "org show <name>", ReadOnly: true, Run: runOrgShow})
2225 register(Command{Path: []string{"org", "rename"},
23 Summary: "rename an organization: org rename <old> <new> (clone URLs change)", Run: runOrgRename})
26 Summary: "rename an organization",
27 Usage: "org rename <old> <new> (clone URLs change)", Run: runOrgRename})
2428 register(Command{Path: []string{"org", "delete"},
25 Summary: "delete an empty organization: org delete <name> --yes", Run: runOrgDelete})
29 Summary: "delete an empty organization",
30 Usage: "org delete <name> --yes", Run: runOrgDelete})
2631 register(Command{Path: []string{"org", "members", "add"},
27 Summary: "add or update a member: org members add <org> <user> [--role member|admin]", Run: runOrgMembersAdd})
32 Summary: "add or update a member",
33 Usage: "org members add <org> <user> [--role member|admin]", Run: runOrgMembersAdd})
2834 register(Command{Path: []string{"org", "members", "remove"},
29 Summary: "remove a member: org members remove <org> <user>", Run: runOrgMembersRemove})
35 Summary: "remove a member",
36 Usage: "org members remove <org> <user>", Run: runOrgMembersRemove})
3037 register(Command{Path: []string{"org", "members", "list"},
31 Summary: "list members: org members list <org>", ReadOnly: true, Run: runOrgMembersList})
38 Summary: "list members",
39 Usage: "org members list <org>", ReadOnly: true, Run: runOrgMembersList})
3240}
3341
3442// orgAdmin loads an org and requires the caller to be one of its admins.
internal/control/pagescmd.go +8 −4
@@ -20,13 +20,17 @@ import (
2020
2121func init() {
2222 register(Command{Path: []string{"repo", "domain", "add"},
23 Summary: "claim a custom pages domain (verify with a DNS TXT record): repo domain add <owner/name> <domain>", Run: runDomainAdd})
23 Summary: "claim a custom pages domain (verify with a DNS TXT record)",
24 Usage: "repo domain add <owner/name> <domain>", Run: runDomainAdd})
2425 register(Command{Path: []string{"repo", "domain", "verify"},
25 Summary: "check the DNS challenge and activate a claim: repo domain verify <owner/name> <domain>", Run: runDomainVerify})
26 Summary: "check the DNS challenge and activate a claim",
27 Usage: "repo domain verify <owner/name> <domain>", Run: runDomainVerify})
2628 register(Command{Path: []string{"repo", "domain", "remove"},
27 Summary: "remove a custom pages domain: repo domain remove <owner/name> <domain>", Run: runDomainRemove})
29 Summary: "remove a custom pages domain",
30 Usage: "repo domain remove <owner/name> <domain>", Run: runDomainRemove})
2831 register(Command{Path: []string{"repo", "domain", "list"},
29 Summary: "list custom pages domains: repo domain list <owner/name>", ReadOnly: true, Run: runDomainList})
32 Summary: "list custom pages domains",
33 Usage: "repo domain list <owner/name>", ReadOnly: true, Run: runDomainList})
3034}
3135
3236// challengeLabel prefixes the domain for the ownership TXT record.
internal/control/profile.go +6 −3
@@ -16,11 +16,14 @@ import (
1616
1717func init() {
1818 register(Command{Path: []string{"profile", "show"},
19 Summary: "show a user's or org's profile: profile show [name]", ReadOnly: true, Run: runProfileShow})
19 Summary: "show a user's or org's profile",
20 Usage: "profile show [name]", ReadOnly: true, Run: runProfileShow})
2021 register(Command{Path: []string{"profile", "set"},
21 Summary: "set your profile: profile set [--description <d>] [--website <url>] [--about <text>|--file -] [--about-format md|org] [--link <label|url>]... ('' clears)", ReadsStdin: true, Run: runProfileSet})
22 Summary: "set your profile",
23 Usage: "profile set [--description <d>] [--website <url>] [--about <text>|--file -] [--about-format md|org] [--link <label|url>]... ('' clears)", ReadsStdin: true, Run: runProfileSet})
2224 register(Command{Path: []string{"org", "profile"},
23 Summary: "show or set an org's profile: org profile <org> [--description <d>] [--website <url>] [--about <text>|--file -] [--about-format md|org] [--link <label|url>]...", ReadsStdin: true, Run: runOrgProfile})
25 Summary: "show or set an org's profile",
26 Usage: "org profile <org> [--description <d>] [--website <url>] [--about <text>|--file -] [--about-format md|org] [--link <label|url>]...", ReadsStdin: true, Run: runOrgProfile})
2427}
2528
2629// maxProfileLinks caps the free-form link list. A profile is a header,
internal/control/read.go +8 −4
@@ -18,25 +18,29 @@ import (
1818func init() {
1919 register(Command{
2020 Path: []string{"repo", "tree"},
21 Summary: "list a directory: repo tree <owner/name> [<path>] [--ref <ref>]",
21 Summary: "list a directory",
22 Usage: "repo tree <owner/name> [<path>] [--ref <ref>]",
2223 ReadOnly: true,
2324 Run: runRepoTree,
2425 })
2526 register(Command{
2627 Path: []string{"repo", "cat"},
27 Summary: "read a file: repo cat <owner/name> <path> [--ref <ref>]",
28 Summary: "read a file",
29 Usage: "repo cat <owner/name> <path> [--ref <ref>]",
2830 ReadOnly: true,
2931 Run: runRepoCat,
3032 })
3133 register(Command{
3234 Path: []string{"repo", "blame"},
33 Summary: "attribute lines to commits: repo blame <owner/name> <path> [--ref <ref>] [--from <n>] [--to <n>]",
35 Summary: "attribute lines to commits",
36 Usage: "repo blame <owner/name> <path> [--ref <ref>] [--from <n>] [--to <n>]",
3437 ReadOnly: true,
3538 Run: runRepoBlame,
3639 })
3740 register(Command{
3841 Path: []string{"repo", "refs"},
39 Summary: "list branches and tags: repo refs <owner/name>",
42 Summary: "list branches and tags",
43 Usage: "repo refs <owner/name>",
4044 ReadOnly: true,
4145 Run: runRepoRefs,
4246 })
internal/control/register.go +5 −2
@@ -19,15 +19,18 @@ import (
1919func init() {
2020 register(Command{Path: []string{"register"},
2121 Summary: "create an account (only meaningful for unregistered keys)",
22 Usage: "register --username <name> [--email <address> | --invite <code>]",
2223 Run: func(c *Ctx, args []string) int {
2324 return c.fail(protocol.ExitUsage,
2425 "this SSH key already belongs to %s. To register a new account, connect with the key it should use:\n ssh -F /dev/null -i <newkey> git@<host> register ...",
2526 c.User.Username)
2627 }})
2728 register(Command{Path: []string{"email", "add"},
28 Summary: "add an address and mail a verification code: email add <address>", Run: runEmailAdd})
29 Summary: "add an address and mail a verification code",
30 Usage: "email add <address>", Run: runEmailAdd})
2931 register(Command{Path: []string{"email", "verify"},
30 Summary: "confirm a verification code: email verify <code>", Run: runEmailVerify})
32 Summary: "confirm a verification code",
33 Usage: "email verify <code>", Run: runEmailVerify})
3134}
3235
3336func siteHost(cfg config.Config) string {
internal/control/release.go +16 −8
@@ -19,25 +19,33 @@ import (
1919
2020func init() {
2121 register(Command{Path: []string{"release", "create"},
22 Summary: "create a release on a tag: release create <owner/name> <tag> [--title <t>] [--notes <n> | --file -] [--format md|org]",
22 Summary: "create a release on a tag",
23 Usage: "release create <owner/name> <tag> [--title <t>] [--notes <n> | --file -] [--format md|org]",
2324 ReadsStdin: true, Run: runReleaseCreate})
2425 register(Command{Path: []string{"release", "edit"},
25 Summary: "update a release's title and notes: release edit <owner/name> <tag> [--title <t>] [--notes <n> | --file -] [--format md|org]",
26 Summary: "update a release's title and notes",
27 Usage: "release edit <owner/name> <tag> [--title <t>] [--notes <n> | --file -] [--format md|org]",
2628 ReadsStdin: true, Run: runReleaseEdit})
2729 register(Command{Path: []string{"release", "list"},
28 Summary: "list releases: release list <owner/name>", ReadOnly: true, Run: runReleaseList})
30 Summary: "list releases",
31 Usage: "release list <owner/name>", ReadOnly: true, Run: runReleaseList})
2932 register(Command{Path: []string{"release", "show"},
30 Summary: "show a release with assets: release show <owner/name> <tag>", ReadOnly: true, Run: runReleaseShow})
33 Summary: "show a release with assets",
34 Usage: "release show <owner/name> <tag>", ReadOnly: true, Run: runReleaseShow})
3135 register(Command{Path: []string{"release", "delete"},
32 Summary: "delete a release and its assets: release delete <owner/name> <tag> --yes", Run: runReleaseDelete})
36 Summary: "delete a release and its assets",
37 Usage: "release delete <owner/name> <tag> --yes", Run: runReleaseDelete})
3338 register(Command{Path: []string{"release", "asset", "add"},
34 Summary: "upload an asset from stdin: release asset add <owner/name> <tag> <filename> < file",
39 Summary: "upload an asset from stdin",
40 Usage: "release asset add <owner/name> <tag> <filename> < file",
3541 ReadsStdin: true, Run: runAssetAdd})
3642 register(Command{Path: []string{"release", "asset", "get"},
37 Summary: "write an asset to stdout: release asset get <owner/name> <tag> <filename> > file",
43 Summary: "write an asset to stdout",
44 Usage: "release asset get <owner/name> <tag> <filename> > file",
3845 ReadOnly: true, Run: runAssetGet})
3946 register(Command{Path: []string{"release", "asset", "remove"},
40 Summary: "remove an asset: release asset remove <owner/name> <tag> <filename>", Run: runAssetRemove})
47 Summary: "remove an asset",
48 Usage: "release asset remove <owner/name> <tag> <filename>", Run: runAssetRemove})
4149}
4250
4351var assetNamePat = regexp.MustCompile(`^[A-Za-z0-9][A-Za-z0-9._+-]{0,199}$`)
internal/control/repo.go +48 −24
@@ -25,53 +25,77 @@ func HooksDir(root string) string { return filepath.Join(root, "hooks") }
2525
2626func init() {
2727 register(Command{Path: []string{"repo", "create"},
28 Summary: "create a repository: repo create <owner/name> [--private]", Run: runRepoCreate})
28 Summary: "create a repository",
29 Usage: "repo create <owner/name> [--private]", Run: runRepoCreate})
2930 register(Command{Path: []string{"repo", "list"},
30 Summary: "list repositories you own or can access: repo list [--limit <n>] [--cursor <c>]", ReadOnly: true, Run: runRepoList})
31 Summary: "list repositories you own or can access",
32 Usage: "repo list [--limit <n>] [--cursor <c>]", ReadOnly: true, Run: runRepoList})
3133 register(Command{Path: []string{"repo", "show"},
32 Summary: "show repository details: repo show <owner/name>", ReadOnly: true, Run: runRepoShow})
34 Summary: "show repository details",
35 Usage: "repo show <owner/name>", ReadOnly: true, Run: runRepoShow})
3336 register(Command{Path: []string{"repo", "transfer"},
34 Summary: "move a repository to another owner: repo transfer <owner/name> <new-owner> (clone URLs change)", Run: runRepoTransfer})
37 Summary: "move a repository to another owner",
38 Usage: "repo transfer <owner/name> <new-owner> (clone URLs change)", Run: runRepoTransfer})
3539 register(Command{Path: []string{"repo", "delete"},
36 Summary: "delete a repository: repo delete <owner/name> --yes", Run: runRepoDelete})
40 Summary: "delete a repository",
41 Usage: "repo delete <owner/name> --yes", Run: runRepoDelete})
3742 register(Command{Path: []string{"repo", "access", "grant"},
38 Summary: "grant access: repo access grant <owner/name> <user> read|write|admin", Run: runAccessGrant})
43 Summary: "grant access",
44 Usage: "repo access grant <owner/name> <user> read|write|admin", Run: runAccessGrant})
3945 register(Command{Path: []string{"repo", "access", "revoke"},
40 Summary: "revoke access: repo access revoke <owner/name> <user>", Run: runAccessRevoke})
46 Summary: "revoke access",
47 Usage: "repo access revoke <owner/name> <user>", Run: runAccessRevoke})
4148 register(Command{Path: []string{"repo", "access", "list"},
42 Summary: "list access grants: repo access list <owner/name>", ReadOnly: true, Run: runAccessList})
49 Summary: "list access grants",
50 Usage: "repo access list <owner/name>", ReadOnly: true, Run: runAccessList})
4351 register(Command{Path: []string{"repo", "settings", "show"},
44 Summary: "show settings: repo settings show <owner/name>", ReadOnly: true, Run: runSettingsShow})
52 Summary: "show settings",
53 Usage: "repo settings show <owner/name>", ReadOnly: true, Run: runSettingsShow})
4554 register(Command{Path: []string{"repo", "settings", "protect"},
46 Summary: "protect a branch: repo settings protect <owner/name> <branch>", Run: runProtect})
55 Summary: "protect a branch",
56 Usage: "repo settings protect <owner/name> <branch>", Run: runProtect})
4757 register(Command{Path: []string{"repo", "settings", "unprotect"},
48 Summary: "unprotect a branch: repo settings unprotect <owner/name> <branch>", Run: runUnprotect})
58 Summary: "unprotect a branch",
59 Usage: "repo settings unprotect <owner/name> <branch>", Run: runUnprotect})
4960 register(Command{Path: []string{"repo", "settings", "description"},
50 Summary: "set the repository description: repo settings description <owner/name> <text> ('' clears)", Run: runSetDescription})
61 Summary: "set the repository description",
62 Usage: "repo settings description <owner/name> <text> ('' clears)", Run: runSetDescription})
5163 register(Command{Path: []string{"repo", "settings", "visibility"},
52 Summary: "set repository visibility: repo settings visibility <owner/name> public|private", Run: runSetVisibility})
64 Summary: "set repository visibility",
65 Usage: "repo settings visibility <owner/name> public|private", Run: runSetVisibility})
5366 register(Command{Path: []string{"repo", "settings", "website"},
54 Summary: "set the repository website: repo settings website <owner/name> <url> ('' clears)", Run: runSetWebsite})
67 Summary: "set the repository website",
68 Usage: "repo settings website <owner/name> <url> ('' clears)", Run: runSetWebsite})
5569 register(Command{Path: []string{"repo", "settings", "git-daemon"},
56 Summary: "expose over git://: repo settings git-daemon <owner/name> on|off", Run: runGitDaemon})
70 Summary: "expose over git://",
71 Usage: "repo settings git-daemon <owner/name> on|off", Run: runGitDaemon})
5772 register(Command{Path: []string{"repo", "archive"},
58 Summary: "archive a repository (read-only: pushes and issue/MR writes refused): repo archive <owner/name>", Run: runArchive})
73 Summary: "archive a repository (read-only: pushes and issue/MR writes refused)",
74 Usage: "repo archive <owner/name>", Run: runArchive})
5975 register(Command{Path: []string{"repo", "unarchive"},
60 Summary: "unarchive a repository: repo unarchive <owner/name>", Run: runUnarchive})
76 Summary: "unarchive a repository",
77 Usage: "repo unarchive <owner/name>", Run: runUnarchive})
6178 register(Command{Path: []string{"repo", "topics"},
62 Summary: "list topics: repo topics <owner/name>", ReadOnly: true, Run: runTopicsList})
79 Summary: "list topics",
80 Usage: "repo topics <owner/name>", ReadOnly: true, Run: runTopicsList})
6381 register(Command{Path: []string{"repo", "topics", "add"},
64 Summary: "add topics: repo topics add <owner/name> <topic>...", Run: runTopicsAdd})
82 Summary: "add topics",
83 Usage: "repo topics add <owner/name> <topic>...", Run: runTopicsAdd})
6584 register(Command{Path: []string{"repo", "topics", "remove"},
66 Summary: "remove topics: repo topics remove <owner/name> <topic>...", Run: runTopicsRemove})
85 Summary: "remove topics",
86 Usage: "repo topics remove <owner/name> <topic>...", Run: runTopicsRemove})
6787 register(Command{Path: []string{"repo", "search"},
68 Summary: "find repositories by name, description, or topic: repo search <query>", ReadOnly: true, Run: runRepoSearch})
88 Summary: "find repositories by name, description, or topic",
89 Usage: "repo search <query>", ReadOnly: true, Run: runRepoSearch})
6990 register(Command{Path: []string{"repo", "grep"},
70 Summary: "search file contents: repo grep <owner/name> <query> [--ref <ref>]", ReadOnly: true, Run: runRepoGrep})
91 Summary: "search file contents",
92 Usage: "repo grep <owner/name> <query> [--ref <ref>]", ReadOnly: true, Run: runRepoGrep})
7193 register(Command{Path: []string{"repo", "pin"},
72 Summary: "pin a repository to your dashboard: repo pin <owner/name>", Run: runRepoPin})
94 Summary: "pin a repository to your dashboard",
95 Usage: "repo pin <owner/name>", Run: runRepoPin})
7396 register(Command{Path: []string{"repo", "unpin"},
74 Summary: "unpin a repository: repo unpin <owner/name>", Run: runRepoUnpin})
97 Summary: "unpin a repository",
98 Usage: "repo unpin <owner/name>", Run: runRepoUnpin})
7599}
76100
77101const (
internal/control/sig.go +10 −5
@@ -18,16 +18,21 @@ import (
1818
1919func init() {
2020 register(Command{Path: []string{"pgp", "add"},
21 Summary: "register an OpenPGP public key (armored, on stdin)", ReadsStdin: true, Run: runPGPAdd})
21 Summary: "register an OpenPGP public key (armored)",
22 Usage: "pgp add < key.asc", ReadsStdin: true, Run: runPGPAdd})
2223 register(Command{Path: []string{"pgp", "list"},
23 Summary: "list registered OpenPGP keys", ReadOnly: true, Run: runPGPList})
24 Summary: "list registered OpenPGP keys",
25 Usage: "pgp list", ReadOnly: true, Run: runPGPList})
2426 register(Command{Path: []string{"pgp", "remove"},
25 Summary: "remove an OpenPGP key by fingerprint", Run: runPGPRemove})
27 Summary: "remove an OpenPGP key by fingerprint",
28 Usage: "pgp remove <fingerprint>", Run: runPGPRemove})
2629 register(Command{Path: []string{"repo", "commit"},
27 Summary: "show one commit with its patch: repo commit <owner/name> <sha>",
30 Summary: "show one commit with its patch",
31 Usage: "repo commit <owner/name> <sha>",
2832 ReadOnly: true, Run: runRepoCommit})
2933 register(Command{Path: []string{"repo", "log"},
30 Summary: "commit log with signature states: repo log <owner/name> [--ref <r>] [--limit n] [--path <file>]", ReadOnly: true, Run: runRepoLog})
34 Summary: "commit log with signature states",
35 Usage: "repo log <owner/name> [--ref <r>] [--limit n] [--path <file>]", ReadOnly: true, Run: runRepoLog})
3136}
3237
3338func runPGPAdd(c *Ctx, args []string) int {
internal/control/status.go +4 −2
@@ -13,10 +13,12 @@ import (
1313
1414func init() {
1515 register(Command{Path: []string{"status", "set"},
16 Summary: "report a commit status (CI): status set <owner/name> <sha> --context <c> --state pending|success|failure|error [--description <d>] [--url <u>]",
16 Summary: "report a commit status (CI)",
17 Usage: "status set <owner/name> <sha> --context <c> --state pending|success|failure|error [--description <d>] [--url <u>]",
1718 Run: runStatusSet})
1819 register(Command{Path: []string{"status", "list"},
19 Summary: "statuses on a commit: status list <owner/name> <sha>", ReadOnly: true, Run: runStatusList})
20 Summary: "statuses on a commit",
21 Usage: "status list <owner/name> <sha>", ReadOnly: true, Run: runStatusList})
2022}
2123
2224var validStatusState = map[string]bool{"pending": true, "success": true, "failure": true, "error": true}
internal/control/teams.go +18 −9
@@ -14,23 +14,32 @@ import (
1414
1515func init() {
1616 register(Command{Path: []string{"org", "team", "create"},
17 Summary: "create a team: org team create <org> <team>", Run: runTeamCreate})
17 Summary: "create a team",
18 Usage: "org team create <org> <team>", Run: runTeamCreate})
1819 register(Command{Path: []string{"org", "team", "delete"},
19 Summary: "delete a team (its grants with it): org team delete <org> <team>", Run: runTeamDelete})
20 Summary: "delete a team (its grants with it)",
21 Usage: "org team delete <org> <team>", Run: runTeamDelete})
2022 register(Command{Path: []string{"org", "team", "list"},
21 Summary: "list an org's teams: org team list <org>", ReadOnly: true, Run: runTeamList})
23 Summary: "list an org's teams",
24 Usage: "org team list <org>", ReadOnly: true, Run: runTeamList})
2225 register(Command{Path: []string{"org", "team", "show"},
23 Summary: "show a team's members and grants: org team show <org> <team>", ReadOnly: true, Run: runTeamShow})
26 Summary: "show a team's members and grants",
27 Usage: "org team show <org> <team>", ReadOnly: true, Run: runTeamShow})
2428 register(Command{Path: []string{"org", "team", "add"},
25 Summary: "add org members to a team: org team add <org> <team> <user>...", Run: runTeamAdd})
29 Summary: "add org members to a team",
30 Usage: "org team add <org> <team> <user>...", Run: runTeamAdd})
2631 register(Command{Path: []string{"org", "team", "remove"},
27 Summary: "remove members from a team: org team remove <org> <team> <user>...", Run: runTeamRemove})
32 Summary: "remove members from a team",
33 Usage: "org team remove <org> <team> <user>...", Run: runTeamRemove})
2834 register(Command{Path: []string{"org", "team", "grant"},
29 Summary: "grant a team a role on an org repo: org team grant <org> <team> <owner/name> read|write|admin", Run: runTeamGrant})
35 Summary: "grant a team a role on an org repo",
36 Usage: "org team grant <org> <team> <owner/name> read|write|admin", Run: runTeamGrant})
3037 register(Command{Path: []string{"org", "team", "revoke"},
31 Summary: "revoke a team's grant: org team revoke <org> <team> <owner/name>", Run: runTeamRevoke})
38 Summary: "revoke a team's grant",
39 Usage: "org team revoke <org> <team> <owner/name>", Run: runTeamRevoke})
3240 register(Command{Path: []string{"org", "settings", "members-role"},
33 Summary: "role plain membership implies on every org repo: org settings members-role <org> write|read|none (default write)", Run: runOrgMembersRole})
41 Summary: "role plain membership implies on every org repo",
42 Usage: "org settings members-role <org> write|read|none (default write)", Run: runOrgMembersRole})
3443}
3544
3645// orgAdminRef resolves an org and requires the caller to admin it.
internal/control/token.go +6 −3
@@ -14,12 +14,15 @@ import (
1414
1515func init() {
1616 register(Command{Path: []string{"token", "create"},
17 Summary: "mint an API token (shown once): token create --name <n> [--scope full|read] [--ttl 30d|720h]",
17 Summary: "mint an API token (shown once)",
18 Usage: "token create --name <n> [--scope full|read] [--ttl 30d|720h]",
1819 SSHOnly: true, Run: runTokenCreate})
1920 register(Command{Path: []string{"token", "list"},
20 Summary: "list API tokens", ReadOnly: true, SSHOnly: true, Run: runTokenList})
21 Summary: "list API tokens",
22 Usage: "token list", ReadOnly: true, SSHOnly: true, Run: runTokenList})
2123 register(Command{Path: []string{"token", "revoke"},
22 Summary: "revoke an API token by name: token revoke <name>",
24 Summary: "revoke an API token by name",
25 Usage: "token revoke <name>",
2326 SSHOnly: true, Run: runTokenRevoke})
2427}
2528
internal/control/web.go +2 −1
@@ -13,7 +13,8 @@ func newStoredToken() (token, hash string, err error) { return store.NewToken()
1313
1414func init() {
1515 register(Command{Path: []string{"web", "login"},
16 Summary: "mint a one-time browser login URL", Run: runWebLogin})
16 Summary: "mint a one-time browser login URL",
17 Usage: "web login", Run: runWebLogin})
1718}
1819
1920func runWebLogin(c *Ctx, args []string) int {
internal/control/webhook.go +10 −5
@@ -14,15 +14,20 @@ import (
1414
1515func init() {
1616 register(Command{Path: []string{"webhook", "add"},
17 Summary: "add a webhook: webhook add <owner/name> <url> [--secret <s>] [--events push,issue.created|*]", Run: runWebhookAdd})
17 Summary: "add a webhook",
18 Usage: "webhook add <owner/name> <url> [--secret <s>] [--events push,issue.created|*]", Run: runWebhookAdd})
1819 register(Command{Path: []string{"webhook", "list"},
19 Summary: "list webhooks: webhook list <owner/name>", ReadOnly: true, Run: runWebhookList})
20 Summary: "list webhooks",
21 Usage: "webhook list <owner/name>", ReadOnly: true, Run: runWebhookList})
2022 register(Command{Path: []string{"webhook", "remove"},
21 Summary: "remove a webhook: webhook remove <owner/name> <id>", Run: runWebhookRemove})
23 Summary: "remove a webhook",
24 Usage: "webhook remove <owner/name> <id>", Run: runWebhookRemove})
2225 register(Command{Path: []string{"webhook", "deliveries"},
23 Summary: "recent deliveries: webhook deliveries <owner/name> [--limit n]", ReadOnly: true, Run: runWebhookDeliveries})
26 Summary: "recent deliveries",
27 Usage: "webhook deliveries <owner/name> [--limit n]", ReadOnly: true, Run: runWebhookDeliveries})
2428 register(Command{Path: []string{"webhook", "redeliver"},
25 Summary: "queue a delivery again: webhook redeliver <owner/name> <delivery-id>", Run: runWebhookRedeliver})
29 Summary: "queue a delivery again",
30 Usage: "webhook redeliver <owner/name> <delivery-id>", Run: runWebhookRedeliver})
2631}
2732
2833func runWebhookAdd(c *Ctx, args []string) int {
internal/control/wiki.go +4 −2
@@ -17,13 +17,15 @@ import (
1717func init() {
1818 register(Command{
1919 Path: []string{"wiki", "list"},
20 Summary: "list a repository's wiki pages: wiki list <owner/name>",
20 Summary: "list a repository's wiki pages",
21 Usage: "wiki list <owner/name>",
2122 ReadOnly: true,
2223 Run: runWikiList,
2324 })
2425 register(Command{
2526 Path: []string{"wiki", "show"},
26 Summary: "print a wiki page: wiki show <owner/name> [<page>]",
27 Summary: "print a wiki page",
28 Usage: "wiki show <owner/name> [<page>]",
2729 ReadOnly: true,
2830 Run: runWikiShow,
2931 })
internal/httpd/api.go +1 −1
@@ -79,7 +79,7 @@ func (s *Server) apiCmd(w http.ResponseWriter, r *http.Request) {
7979 status := statusForExit(code)
8080
8181 // Commands normally emit exactly one JSON envelope; inject exit_code.
82 // A few (mr diff, help) write raw text instead — wrap those.
82 // A few (mr diff, repo download) write raw bytes instead — wrap those.
8383 var body map[string]any
8484 if err := json.Unmarshal(stdout.Bytes(), &body); err != nil || body == nil {
8585 body = map[string]any{