Commit dca7370cd1

dca7370cd168dc7cf1f262252ab9548f66cc7dcc

parent: 6c97fd8119

Verified · cmc ci/build: success

cmc <hello@cleberg.net> · 2026-08-31 05:00 UTC

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

Layout: unified · split

cmd/gitbay/coverage_test.go +1 −1
@@ -13,7 +13,7 @@ import (
13// registry must be reachable by typing it, or the CLI is not the 13// registry must be reachable by typing it, or the CLI is not the
14// complete interface the design claims. 14// complete interface the design claims.
15var notInCLI = map[string]string{ 15var notInCLI = map[string]string{
16 "help": "cobra provides its own", 16 "help": "cobra owns `gitbay help`; the registry's is reached by `<cmd> --help`",
17 "runner next": "the CI runner's wire protocol, not for humans", 17 "runner next": "the CI runner's wire protocol, not for humans",
18 "runner done": "the CI runner's wire protocol, not for humans", 18 "runner done": "the CI runner's wire protocol, not for humans",
19 "runner log": "the CI runner's wire protocol, not for humans", 19 "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 {
119 }, 119 },
120 DisableFlagParsing: true, 120 DisableFlagParsing: true,
121 RunE: func(cmd *cobra.Command, args []string) error { 121 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.
123 for _, a := range args { 125 for _, a := range args {
124 if a == "--help" || a == "-h" { 126 if a == "--help" || a == "-h" {
125 return cmd.Help() 127 os.Exit(runServerHelp(o))
126 } 128 }
127 } 129 }
128 os.Exit(runPass(o, args)) 130 os.Exit(runPass(o, args))
@@ -131,6 +133,16 @@ func pass(use, short string, o passOpts) *cobra.Command {
131 } 133 }
132} 134}
133 135
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
134func runPass(o passOpts, args []string) int { 146func runPass(o passOpts, args []string) int {
135 t, err := resolveTarget() 147 t, err := resolveTarget()
136 if err != nil { 148 if err != nil {
e2e/api_test.go +35 −3
@@ -7,6 +7,8 @@ import (
7 "io" 7 "io"
8 "net/http" 8 "net/http"
9 "net/url" 9 "net/url"
10 "os"
11 "path/filepath"
10 "strings" 12 "strings"
11 "testing" 13 "testing"
12 "time" 14 "time"
@@ -96,11 +98,41 @@ func TestJSONAPI(t *testing.T) {
96 t.Fatalf("unknown command: %d", status) 98 t.Fatalf("unknown command: %d", status)
97 } 99 }
98 100
99 // Raw-output commands (no envelope) are wrapped. 101 // Raw-output commands (no envelope) are wrapped. Reading a file is the
100 status, body = inst.apiCall(t, token, []string{"help"}, "") 102 // cheapest raw output, so give the repo one commit to read from.
101 if status != 200 || !strings.Contains(body["output"].(string), "repo create") { 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 {
102 t.Fatalf("help via API: %d %v", status, body) 126 t.Fatalf("help via API: %d %v", status, body)
103 } 127 }
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 }
104 136
105 // Git transport is refused by name. 137 // Git transport is refused by name.
106 if status, _ = inst.apiCall(t, token, []string{"git-upload-pack", "alice/proj"}, ""); status != 400 { 138 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 (
10 10
11func init() { 11func init() {
12 register(Command{Path: []string{"audit"}, 12 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})
14} 15}
15 16
16func runAudit(c *Ctx, args []string) int { 17func runAudit(c *Ctx, args []string) int {
internal/control/build.go +22 −11
@@ -18,37 +18,48 @@ import (
18 18
19func init() { 19func init() {
20 register(Command{Path: []string{"build", "list"}, 20 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})
22 register(Command{Path: []string{"build", "show"}, 23 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})
24 register(Command{Path: []string{"build", "log"}, 26 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})
26 29
27 register(Command{Path: []string{"build", "jobs"}, 30 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})
29 33
30 register(Command{Path: []string{"build", "trigger"}, 34 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})
32 // Secrets: set over stdin, listed by name only, injected into the 37 // Secrets: set over stdin, listed by name only, injected into the
33 // repo's builds as environment variables. Same discipline as mirror 38 // repo's builds as environment variables. Same discipline as mirror
34 // tokens — the value never appears in argv, logs, or output. 39 // tokens — the value never appears in argv, logs, or output.
35 register(Command{Path: []string{"repo", "secret", "set"}, 40 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)",
37 ReadsStdin: true, SSHOnly: true, Run: runSecretSet}) 43 ReadsStdin: true, SSHOnly: true, Run: runSecretSet})
38 register(Command{Path: []string{"repo", "secret", "remove"}, 44 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})
40 register(Command{Path: []string{"repo", "secret", "list"}, 47 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})
42 50
43 // Runner commands: the claim/report loop for gitbay-runner. Admin-only — 51 // Runner commands: the claim/report loop for gitbay-runner. Admin-only —
44 // a runner executes arbitrary repo code, so handing out jobs is the 52 // a runner executes arbitrary repo code, so handing out jobs is the
45 // instance operator's call. 53 // instance operator's call.
46 register(Command{Path: []string{"runner", "next"}, 54 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})
48 register(Command{Path: []string{"runner", "log"}, 57 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})
50 register(Command{Path: []string{"runner", "done"}, 60 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})
52} 63}
53 64
54type buildOut struct { 65type buildOut struct {
internal/control/commitfile.go +3 −2
@@ -12,8 +12,9 @@ import (
12 12
13func init() { 13func init() {
14 register(Command{ 14 register(Command{
15 Path: []string{"repo", "commit-file"}, 15 Path: []string{"repo", "commit-file"},
16 Summary: "write a file and commit it: repo commit-file <owner/name> <path> " + 16 Summary: "write a file and commit it",
17 Usage: "repo commit-file <owner/name> <path> " +
17 "--ref <branch> [--message <m>] [--file -]", 18 "--ref <branch> [--message <m>] [--file -]",
18 ReadsStdin: true, 19 ReadsStdin: true,
19 Run: runCommitFile, 20 Run: runCommitFile,
internal/control/control.go +42 −6
@@ -9,6 +9,7 @@ import (
9 "io" 9 "io"
10 "reflect" 10 "reflect"
11 "slices" 11 "slices"
12 "strings"
12 13
13 "gitbay.org/gitbay/internal/config" 14 "gitbay.org/gitbay/internal/config"
14 "gitbay.org/gitbay/internal/protocol" 15 "gitbay.org/gitbay/internal/protocol"
@@ -36,8 +37,12 @@ type Ctx struct {
36} 37}
37 38
38type Command struct { 39type 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.
40 Summary string 44 Summary string
45 Usage string
41 ReadsStdin bool 46 ReadsStdin bool
42 ReadOnly bool // safe for read-scoped API tokens 47 ReadOnly bool // safe for read-scoped API tokens
43 SSHOnly bool // refused over the HTTP API (credential minting) 48 SSHOnly bool // refused over the HTTP API (credential minting)
@@ -161,13 +166,44 @@ func init() {
161 register(Command{ 166 register(Command{
162 Path: []string{"help"}, 167 Path: []string{"help"},
163 Summary: "list available commands", 168 Summary: "list available commands",
169 Usage: "help [<prefix>...]",
164 ReadOnly: true, 170 ReadOnly: true,
165 Run: func(c *Ctx, args []string) int { 171 Run: runHelp,
166 for _, cmd := range registry { 172 })
167 fmt.Fprintf(c.Stdout, "%-24s %s\n", joinPath(cmd.Path), cmd.Summary) 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)
168 } 205 }
169 return protocol.ExitOK 206 }
170 },
171 }) 207 })
172} 208}
173 209
internal/control/control_test.go +77
@@ -1,6 +1,10 @@
1package control 1package control
2 2
3import ( 3import (
4 "bytes"
5 "encoding/json"
6 "io"
7 "slices"
4 "strings" 8 "strings"
5 "testing" 9 "testing"
6 10
@@ -86,3 +90,76 @@ func TestMRDedupKeyCannotCollideWithASHA(t *testing.T) {
86 t.Error("different merge requests share a dedup key") 90 t.Error("different merge requests share a dedup key")
87 } 91 }
88} 92}
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 (
16func init() { 16func init() {
17 register(Command{Path: []string{"dashboard"}, 17 register(Command{Path: []string{"dashboard"},
18 Summary: "one read for the account dashboard: review queue, assigned and open work, pins, activity, builds", 18 Summary: "one read for the account dashboard: review queue, assigned and open work, pins, activity, builds",
19 Usage: "dashboard",
19 ReadOnly: true, Run: runDashboard}) 20 ReadOnly: true, Run: runDashboard})
20 register(Command{Path: []string{"feed"}, 21 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>]",
22 ReadOnly: true, Run: runFeed}) 24 ReadOnly: true, Run: runFeed})
23} 25}
24 26
internal/control/deploykey.go +6 −3
@@ -14,12 +14,15 @@ import (
14 14
15func init() { 15func init() {
16 register(Command{Path: []string{"repo", "deploy-key", "add"}, 16 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",
18 ReadsStdin: true, Run: runDeployKeyAdd}) 19 ReadsStdin: true, Run: runDeployKeyAdd})
19 register(Command{Path: []string{"repo", "deploy-key", "list"}, 20 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})
21 register(Command{Path: []string{"repo", "deploy-key", "remove"}, 23 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})
23} 26}
24 27
25func runDeployKeyAdd(c *Ctx, args []string) int { 28func runDeployKeyAdd(c *Ctx, args []string) int {
internal/control/deps.go +6 −3
@@ -15,11 +15,14 @@ func init() {
15 // public registry what the repository depends on, which is the owner's 15 // public registry what the repository depends on, which is the owner's
16 // disclosure to make, not the instance's. 16 // disclosure to make, not the instance's.
17 register(Command{Path: []string{"repo", "deps", "enable"}, 17 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})
19 register(Command{Path: []string{"repo", "deps", "disable"}, 20 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})
21 register(Command{Path: []string{"repo", "deps", "status"}, 23 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})
23} 26}
24 27
25func runDepsEnable(c *Ctx, args []string) int { 28func runDepsEnable(c *Ctx, args []string) int {
internal/control/diffcomment.go +8 −4
@@ -16,14 +16,18 @@ import (
16 16
17func init() { 17func init() {
18 register(Command{Path: []string{"mr", "diff-comment"}, 18 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 -]",
20 ReadsStdin: true, Run: runDiffComment}) 21 ReadsStdin: true, Run: runDiffComment})
21 register(Command{Path: []string{"mr", "threads"}, 22 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})
23 register(Command{Path: []string{"mr", "resolve"}, 25 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})
25 register(Command{Path: []string{"mr", "unresolve"}, 28 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})
27} 31}
28 32
29func runDiffComment(c *Ctx, args []string) int { 33func runDiffComment(c *Ctx, args []string) int {
internal/control/explore.go +4 −2
@@ -12,7 +12,8 @@ import (
12func init() { 12func init() {
13 register(Command{ 13 register(Command{
14 Path: []string{"explore"}, 14 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>]",
16 ReadOnly: true, 17 ReadOnly: true,
17 Run: runExplore, 18 Run: runExplore,
18 }) 19 })
@@ -20,7 +21,8 @@ func init() {
20 Path: []string{"repo", "download"}, 21 Path: []string{"repo", "download"},
21 // Not "repo archive": that name is taken by the read-only flag, 22 // Not "repo archive": that name is taken by the read-only flag,
22 // and renaming it would break every script that sets it. 23 // 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",
24 ReadOnly: true, 26 ReadOnly: true,
25 Run: runRepoDownload, 27 Run: runRepoDownload,
26 }) 28 })
internal/control/ghimport.go +2 −1
@@ -20,7 +20,8 @@ import (
20 20
21func init() { 21func init() {
22 register(Command{Path: []string{"repo", "import-issues"}, 22 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>]",
24 ReadsStdin: true, Run: runImportIssues}) 25 ReadsStdin: true, Run: runImportIssues})
25} 26}
26 27
internal/control/identity.go +5 −1
@@ -15,24 +15,28 @@ func init() {
15 register(Command{ 15 register(Command{
16 Path: []string{"whoami"}, 16 Path: []string{"whoami"},
17 Summary: "show the authenticated account", 17 Summary: "show the authenticated account",
18 Usage: "whoami",
18 ReadOnly: true, 19 ReadOnly: true,
19 Run: runWhoami, 20 Run: runWhoami,
20 }) 21 })
21 register(Command{ 22 register(Command{
22 Path: []string{"keys", "list"}, 23 Path: []string{"keys", "list"},
23 Summary: "list registered SSH keys", 24 Summary: "list registered SSH keys",
25 Usage: "keys list",
24 ReadOnly: true, 26 ReadOnly: true,
25 Run: runKeysList, 27 Run: runKeysList,
26 }) 28 })
27 register(Command{ 29 register(Command{
28 Path: []string{"keys", "add"}, 30 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",
30 ReadsStdin: true, 33 ReadsStdin: true,
31 Run: runKeysAdd, 34 Run: runKeysAdd,
32 }) 35 })
33 register(Command{ 36 register(Command{
34 Path: []string{"keys", "remove"}, 37 Path: []string{"keys", "remove"},
35 Summary: "remove an SSH key by fingerprint", 38 Summary: "remove an SSH key by fingerprint",
39 Usage: "keys remove <fingerprint>",
36 Run: runKeysRemove, 40 Run: runKeysRemove,
37 }) 41 })
38} 42}
internal/control/import.go +2 −1
@@ -17,7 +17,8 @@ import (
17 17
18func init() { 18func init() {
19 register(Command{Path: []string{"repo", "import"}, 19 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]",
21 ReadsStdin: true, Run: runRepoImport}) 22 ReadsStdin: true, Run: runRepoImport})
22} 23}
23 24
internal/control/issue.go +18 −9
@@ -16,26 +16,35 @@ const maxBodyBytes = 64 << 10
16 16
17func init() { 17func init() {
18 register(Command{Path: []string{"issue", "create"}, 18 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]",
20 ReadsStdin: true, Run: runIssueCreate}) 21 ReadsStdin: true, Run: runIssueCreate})
21 register(Command{Path: []string{"issue", "list"}, 22 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})
23 register(Command{Path: []string{"issue", "show"}, 25 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})
25 register(Command{Path: []string{"issue", "edit"}, 28 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]",
27 ReadsStdin: true, Run: runIssueEdit}) 31 ReadsStdin: true, Run: runIssueEdit})
28 register(Command{Path: []string{"issue", "comment"}, 32 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]",
30 ReadsStdin: true, Run: runIssueComment}) 35 ReadsStdin: true, Run: runIssueComment})
31 register(Command{Path: []string{"issue", "close"}, 36 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})
33 register(Command{Path: []string{"issue", "reopen"}, 39 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})
35 register(Command{Path: []string{"issue", "label"}, 42 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})
37 register(Command{Path: []string{"issue", "assign"}, 45 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})
39} 48}
40 49
41// issueArgs parses "<owner/name> <n>" plus flags handled by the caller. 50// issueArgs parses "<owner/name> <n>" plus flags handled by the caller.
internal/control/migrate.go +4 −2
@@ -13,10 +13,12 @@ import (
13 13
14func init() { 14func init() {
15 register(Command{Path: []string{"account", "export"}, 15 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",
17 ReadOnly: true, Run: runAccountExport}) 18 ReadOnly: true, Run: runAccountExport})
18 register(Command{Path: []string{"account", "import-bundle"}, 19 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",
20 ReadsStdin: true, Run: runAccountImportBundle}) 22 ReadsStdin: true, Run: runAccountImportBundle})
21} 23}
22 24
internal/control/milestone.go +14 −7
@@ -16,19 +16,26 @@ import (
16 16
17func init() { 17func init() {
18 register(Command{Path: []string{"milestone", "create"}, 18 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})
20 register(Command{Path: []string{"milestone", "list"}, 21 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})
22 register(Command{Path: []string{"milestone", "close"}, 24 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})
24 register(Command{Path: []string{"milestone", "reopen"}, 27 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})
26 register(Command{Path: []string{"issue", "milestone"}, 30 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})
28 register(Command{Path: []string{"mr", "milestone"}, 33 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})
30 register(Command{Path: []string{"issue", "templates"}, 36 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})
32} 39}
33 40
34var duePat = regexp.MustCompile(`^\d{4}-\d{2}-\d{2}$`) 41var duePat = regexp.MustCompile(`^\d{4}-\d{2}-\d{2}$`)
internal/control/mirrorcmd.go +8 −4
@@ -16,14 +16,18 @@ import (
16 16
17func init() { 17func init() {
18 register(Command{Path: []string{"repo", "mirror", "add"}, 18 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]",
20 ReadsStdin: true, SSHOnly: true, Run: runMirrorAdd}) 21 ReadsStdin: true, SSHOnly: true, Run: runMirrorAdd})
21 register(Command{Path: []string{"repo", "mirror", "list"}, 22 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})
23 register(Command{Path: []string{"repo", "mirror", "remove"}, 25 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})
25 register(Command{Path: []string{"repo", "mirror", "sync"}, 28 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})
27} 31}
28 32
29func runMirrorAdd(c *Ctx, args []string) int { 33func runMirrorAdd(c *Ctx, args []string) int {
internal/control/mr.go +30 −15
@@ -17,38 +17,53 @@ import (
17 17
18func init() { 18func init() {
19 register(Command{Path: []string{"repo", "fork"}, 19 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})
21 register(Command{Path: []string{"repo", "settings", "require-approvals"}, 22 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})
23 register(Command{Path: []string{"repo", "settings", "require-resolved"}, 25 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})
25 register(Command{Path: []string{"repo", "settings", "require-checks"}, 28 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})
27 register(Command{Path: []string{"repo", "settings", "require-signed"}, 31 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})
29 register(Command{Path: []string{"mr", "create"}, 34 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]",
31 ReadsStdin: true, Run: runMRCreate}) 37 ReadsStdin: true, Run: runMRCreate})
32 register(Command{Path: []string{"mr", "list"}, 38 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})
34 register(Command{Path: []string{"mr", "show"}, 41 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})
36 register(Command{Path: []string{"mr", "diff"}, 44 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})
38 register(Command{Path: []string{"mr", "edit"}, 47 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]",
40 ReadsStdin: true, Run: runMREdit}) 50 ReadsStdin: true, Run: runMREdit})
41 register(Command{Path: []string{"mr", "retarget"}, 51 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})
43 register(Command{Path: []string{"mr", "comment"}, 54 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]",
45 ReadsStdin: true, Run: runMRComment}) 57 ReadsStdin: true, Run: runMRComment})
46 register(Command{Path: []string{"mr", "review"}, 58 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})
48 register(Command{Path: []string{"mr", "merge"}, 61 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})
50 register(Command{Path: []string{"mr", "close"}, 64 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})
52} 67}
53 68
54func runRepoFork(c *Ctx, args []string) int { 69func runRepoFork(c *Ctx, args []string) int {
internal/control/org.go +16 −8
@@ -14,21 +14,29 @@ import (
14 14
15func init() { 15func init() {
16 register(Command{Path: []string{"org", "create"}, 16 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})
18 register(Command{Path: []string{"org", "list"}, 19 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})
20 register(Command{Path: []string{"org", "show"}, 22 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})
22 register(Command{Path: []string{"org", "rename"}, 25 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})
24 register(Command{Path: []string{"org", "delete"}, 28 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})
26 register(Command{Path: []string{"org", "members", "add"}, 31 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})
28 register(Command{Path: []string{"org", "members", "remove"}, 34 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})
30 register(Command{Path: []string{"org", "members", "list"}, 37 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})
32} 40}
33 41
34// orgAdmin loads an org and requires the caller to be one of its admins. 42// 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 (
20 20
21func init() { 21func init() {
22 register(Command{Path: []string{"repo", "domain", "add"}, 22 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})
24 register(Command{Path: []string{"repo", "domain", "verify"}, 25 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})
26 register(Command{Path: []string{"repo", "domain", "remove"}, 28 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})
28 register(Command{Path: []string{"repo", "domain", "list"}, 31 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})
30} 34}
31 35
32// challengeLabel prefixes the domain for the ownership TXT record. 36// challengeLabel prefixes the domain for the ownership TXT record.
internal/control/profile.go +6 −3
@@ -16,11 +16,14 @@ import (
16 16
17func init() { 17func init() {
18 register(Command{Path: []string{"profile", "show"}, 18 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})
20 register(Command{Path: []string{"profile", "set"}, 21 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})
22 register(Command{Path: []string{"org", "profile"}, 24 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})
24} 27}
25 28
26// maxProfileLinks caps the free-form link list. A profile is a header, 29// maxProfileLinks caps the free-form link list. A profile is a header,
internal/control/read.go +8 −4
@@ -18,25 +18,29 @@ import (
18func init() { 18func init() {
19 register(Command{ 19 register(Command{
20 Path: []string{"repo", "tree"}, 20 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>]",
22 ReadOnly: true, 23 ReadOnly: true,
23 Run: runRepoTree, 24 Run: runRepoTree,
24 }) 25 })
25 register(Command{ 26 register(Command{
26 Path: []string{"repo", "cat"}, 27 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>]",
28 ReadOnly: true, 30 ReadOnly: true,
29 Run: runRepoCat, 31 Run: runRepoCat,
30 }) 32 })
31 register(Command{ 33 register(Command{
32 Path: []string{"repo", "blame"}, 34 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>]",
34 ReadOnly: true, 37 ReadOnly: true,
35 Run: runRepoBlame, 38 Run: runRepoBlame,
36 }) 39 })
37 register(Command{ 40 register(Command{
38 Path: []string{"repo", "refs"}, 41 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>",
40 ReadOnly: true, 44 ReadOnly: true,
41 Run: runRepoRefs, 45 Run: runRepoRefs,
42 }) 46 })
internal/control/register.go +5 −2
@@ -19,15 +19,18 @@ import (
19func init() { 19func init() {
20 register(Command{Path: []string{"register"}, 20 register(Command{Path: []string{"register"},
21 Summary: "create an account (only meaningful for unregistered keys)", 21 Summary: "create an account (only meaningful for unregistered keys)",
22 Usage: "register --username <name> [--email <address> | --invite <code>]",
22 Run: func(c *Ctx, args []string) int { 23 Run: func(c *Ctx, args []string) int {
23 return c.fail(protocol.ExitUsage, 24 return c.fail(protocol.ExitUsage,
24 "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 ...", 25 "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 ...",
25 c.User.Username) 26 c.User.Username)
26 }}) 27 }})
27 register(Command{Path: []string{"email", "add"}, 28 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})
29 register(Command{Path: []string{"email", "verify"}, 31 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})
31} 34}
32 35
33func siteHost(cfg config.Config) string { 36func siteHost(cfg config.Config) string {
internal/control/release.go +16 −8
@@ -19,25 +19,33 @@ import (
19 19
20func init() { 20func init() {
21 register(Command{Path: []string{"release", "create"}, 21 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]",
23 ReadsStdin: true, Run: runReleaseCreate}) 24 ReadsStdin: true, Run: runReleaseCreate})
24 register(Command{Path: []string{"release", "edit"}, 25 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]",
26 ReadsStdin: true, Run: runReleaseEdit}) 28 ReadsStdin: true, Run: runReleaseEdit})
27 register(Command{Path: []string{"release", "list"}, 29 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})
29 register(Command{Path: []string{"release", "show"}, 32 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})
31 register(Command{Path: []string{"release", "delete"}, 35 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})
33 register(Command{Path: []string{"release", "asset", "add"}, 38 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",
35 ReadsStdin: true, Run: runAssetAdd}) 41 ReadsStdin: true, Run: runAssetAdd})
36 register(Command{Path: []string{"release", "asset", "get"}, 42 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",
38 ReadOnly: true, Run: runAssetGet}) 45 ReadOnly: true, Run: runAssetGet})
39 register(Command{Path: []string{"release", "asset", "remove"}, 46 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})
41} 49}
42 50
43var assetNamePat = regexp.MustCompile(`^[A-Za-z0-9][A-Za-z0-9._+-]{0,199}$`) 51var 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") }
25 25
26func init() { 26func init() {
27 register(Command{Path: []string{"repo", "create"}, 27 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})
29 register(Command{Path: []string{"repo", "list"}, 30 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})
31 register(Command{Path: []string{"repo", "show"}, 33 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})
33 register(Command{Path: []string{"repo", "transfer"}, 36 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})
35 register(Command{Path: []string{"repo", "delete"}, 39 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})
37 register(Command{Path: []string{"repo", "access", "grant"}, 42 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})
39 register(Command{Path: []string{"repo", "access", "revoke"}, 45 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})
41 register(Command{Path: []string{"repo", "access", "list"}, 48 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})
43 register(Command{Path: []string{"repo", "settings", "show"}, 51 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})
45 register(Command{Path: []string{"repo", "settings", "protect"}, 54 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})
47 register(Command{Path: []string{"repo", "settings", "unprotect"}, 57 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})
49 register(Command{Path: []string{"repo", "settings", "description"}, 60 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})
51 register(Command{Path: []string{"repo", "settings", "visibility"}, 63 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})
53 register(Command{Path: []string{"repo", "settings", "website"}, 66 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})
55 register(Command{Path: []string{"repo", "settings", "git-daemon"}, 69 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})
57 register(Command{Path: []string{"repo", "archive"}, 72 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})
59 register(Command{Path: []string{"repo", "unarchive"}, 75 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})
61 register(Command{Path: []string{"repo", "topics"}, 78 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})
63 register(Command{Path: []string{"repo", "topics", "add"}, 81 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})
65 register(Command{Path: []string{"repo", "topics", "remove"}, 84 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})
67 register(Command{Path: []string{"repo", "search"}, 87 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})
69 register(Command{Path: []string{"repo", "grep"}, 90 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})
71 register(Command{Path: []string{"repo", "pin"}, 93 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})
73 register(Command{Path: []string{"repo", "unpin"}, 96 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})
75} 99}
76 100
77const ( 101const (
internal/control/sig.go +10 −5
@@ -18,16 +18,21 @@ import (
18 18
19func init() { 19func init() {
20 register(Command{Path: []string{"pgp", "add"}, 20 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})
22 register(Command{Path: []string{"pgp", "list"}, 23 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})
24 register(Command{Path: []string{"pgp", "remove"}, 26 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})
26 register(Command{Path: []string{"repo", "commit"}, 29 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>",
28 ReadOnly: true, Run: runRepoCommit}) 32 ReadOnly: true, Run: runRepoCommit})
29 register(Command{Path: []string{"repo", "log"}, 33 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})
31} 36}
32 37
33func runPGPAdd(c *Ctx, args []string) int { 38func runPGPAdd(c *Ctx, args []string) int {
internal/control/status.go +4 −2
@@ -13,10 +13,12 @@ import (
13 13
14func init() { 14func init() {
15 register(Command{Path: []string{"status", "set"}, 15 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>]",
17 Run: runStatusSet}) 18 Run: runStatusSet})
18 register(Command{Path: []string{"status", "list"}, 19 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})
20} 22}
21 23
22var validStatusState = map[string]bool{"pending": true, "success": true, "failure": true, "error": true} 24var validStatusState = map[string]bool{"pending": true, "success": true, "failure": true, "error": true}
internal/control/teams.go +18 −9
@@ -14,23 +14,32 @@ import (
14 14
15func init() { 15func init() {
16 register(Command{Path: []string{"org", "team", "create"}, 16 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})
18 register(Command{Path: []string{"org", "team", "delete"}, 19 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})
20 register(Command{Path: []string{"org", "team", "list"}, 22 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})
22 register(Command{Path: []string{"org", "team", "show"}, 25 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})
24 register(Command{Path: []string{"org", "team", "add"}, 28 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})
26 register(Command{Path: []string{"org", "team", "remove"}, 31 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})
28 register(Command{Path: []string{"org", "team", "grant"}, 34 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})
30 register(Command{Path: []string{"org", "team", "revoke"}, 37 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})
32 register(Command{Path: []string{"org", "settings", "members-role"}, 40 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})
34} 43}
35 44
36// orgAdminRef resolves an org and requires the caller to admin it. 45// orgAdminRef resolves an org and requires the caller to admin it.
internal/control/token.go +6 −3
@@ -14,12 +14,15 @@ import (
14 14
15func init() { 15func init() {
16 register(Command{Path: []string{"token", "create"}, 16 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]",
18 SSHOnly: true, Run: runTokenCreate}) 19 SSHOnly: true, Run: runTokenCreate})
19 register(Command{Path: []string{"token", "list"}, 20 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})
21 register(Command{Path: []string{"token", "revoke"}, 23 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>",
23 SSHOnly: true, Run: runTokenRevoke}) 26 SSHOnly: true, Run: runTokenRevoke})
24} 27}
25 28
internal/control/web.go +2 −1
@@ -13,7 +13,8 @@ func newStoredToken() (token, hash string, err error) { return store.NewToken()
13 13
14func init() { 14func init() {
15 register(Command{Path: []string{"web", "login"}, 15 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})
17} 18}
18 19
19func runWebLogin(c *Ctx, args []string) int { 20func runWebLogin(c *Ctx, args []string) int {
internal/control/webhook.go +10 −5
@@ -14,15 +14,20 @@ import (
14 14
15func init() { 15func init() {
16 register(Command{Path: []string{"webhook", "add"}, 16 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})
18 register(Command{Path: []string{"webhook", "list"}, 19 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})
20 register(Command{Path: []string{"webhook", "remove"}, 22 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})
22 register(Command{Path: []string{"webhook", "deliveries"}, 25 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})
24 register(Command{Path: []string{"webhook", "redeliver"}, 28 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})
26} 31}
27 32
28func runWebhookAdd(c *Ctx, args []string) int { 33func runWebhookAdd(c *Ctx, args []string) int {
internal/control/wiki.go +4 −2
@@ -17,13 +17,15 @@ import (
17func init() { 17func init() {
18 register(Command{ 18 register(Command{
19 Path: []string{"wiki", "list"}, 19 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>",
21 ReadOnly: true, 22 ReadOnly: true,
22 Run: runWikiList, 23 Run: runWikiList,
23 }) 24 })
24 register(Command{ 25 register(Command{
25 Path: []string{"wiki", "show"}, 26 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>]",
27 ReadOnly: true, 29 ReadOnly: true,
28 Run: runWikiShow, 30 Run: runWikiShow,
29 }) 31 })
internal/httpd/api.go +1 −1
@@ -79,7 +79,7 @@ func (s *Server) apiCmd(w http.ResponseWriter, r *http.Request) {
79 status := statusForExit(code) 79 status := statusForExit(code)
80 80
81 // Commands normally emit exactly one JSON envelope; inject exit_code. 81 // 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.
83 var body map[string]any 83 var body map[string]any
84 if err := json.Unmarshal(stdout.Bytes(), &body); err != nil || body == nil { 84 if err := json.Unmarshal(stdout.Bytes(), &body); err != nil || body == nil {
85 body = map[string]any{ 85 body = map[string]any{