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

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

36 files changed, +468 −160

Layout: unified · split

cmd/gitbay/coverage_test.go +1 −1
@@ -13,7 +13,7 @@ import (
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{