A CLI-first git forge.

cli forge git self-hosted

https://gitbay.org

Commit dca7370cd1

dca7370cd168dc7cf1f262252ab9548f66cc7dcc

parent: 6c97fd8119

Verified · cmc ci/build: success

cmc <hello@cleberg.net> · 2026-08-31T05:00:22Z

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

Command.Summary carried both prose and argument syntax joined by ": ",
which left the syntax unreadable in a listing of every command, and
unreachable from the CLI. Split it: Summary is prose, Usage is the
argument syntax, opening with the command path.

Sixteen commands had no usage text at all. keys remove, pgp remove and
register take arguments that were documented nowhere; they have Usage
now, as does everything else.

help takes a prefix, so `help issue` lists the issue commands with their
flags. The unfiltered listing is sorted and stays one line per command.
It emits through emit(), so --json and the API return the registry as
data rather than raw text.

gitbay <cmd> --help asks the server for that command's usage instead of
reprinting the one-line summary cobra holds.

The API test used help as its raw-output case; help is enveloped now, so
it uses repo download, which is not.

Closes #57
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{