CLI output sweep (#183) !388

merged merged by cmc on 2026-09-12 06:14 UTC · krz/gitbay:cli-sweep into main

45 files changed, +346 −276

Layout: unified · split

.gitbay/wiki/Admin.org +1 −1
@@ -434,7 +434,7 @@ podman. A runner without it builds trusted commits only.
434that has polled as a runner: the account, the key's fingerprint, when it 434that has polled as a runner: the account, the key's fingerprint, when it
435last polled, the repositories it may claim — its attachments for a runner 435last polled, the repositories it may claim — its attachments for a runner
436key, the =-repos= it asked for or =any= for an admin key — and the build 436key, the =-repos= it asked for or =any= for an admin key — and the build
437it holds; =admin runners forget <fingerprint>= drops the row for a key 437it holds; =admin runners remove <fingerprint>= (=forget= until the next release) drops the row for a key
438that polled by mistake, the key itself untouched. =admin runners= also 438that polled by mistake, the key itself untouched. =admin runners= also
439heads the list with the queue: builds 439heads the list with the queue: builds
440pending now, and over the last day how many were claimed, how long they 440pending now, and over the last day how many were claimed, how long they
.gitbay/wiki/Users.org +28
@@ -656,6 +656,34 @@ someone else.
656You are never mailed about your own actions, and only verified primary 656You are never mailed about your own actions, and only verified primary
657addresses receive anything. Delivery retries on relay failure. 657addresses receive anything. Delivery retries on relay failure.
658 658
659* Output rules
660
661=--json= is the contract; the plain output is for a person at a
662terminal, and follows these rules so every noun reads the same way.
663
664- A list command prints one row per item, tab-separated, no header.
665 Columns run identifier, state, then description; a trailing column
666 may carry a word (=due 2027-01-01=, =via team=). The =gitbay= CLI pads
667 the tabs into aligned columns when stdout is a terminal and leaves
668 them as tabs when piped, so =cut -f= sees the same bytes stock ssh
669 prints. Under =--json= nothing is touched.
670- An empty list prints nothing on stdout and =nothing to list= on
671 stderr.
672- A mutation prints one line: verb, object, identifier
673 (=created krz/gitbay#7=). A second line appears only for something
674 to copy: a URL, a token shown once.
675- A bad invocation prints =usage:= and the command's registered usage,
676 the same text =help <noun>= shows. Exit 2.
677- A refusal says who may and what to do instead: =only admins of acme
678 can manage teams; ask one to add you=. Exit 4. A thing that does not
679 exist, or that you may not know exists, is exit 3.
680- stdout is the result; stderr is everything else: progress, a note
681 that output was truncated, errors.
682- Verbs: =create= and =delete= for things with their own identity
683 (repository, issue, release, team), =add= and =remove= for attaching
684 something to them (a key, a member, a label on an issue), =set= for
685 a value, =revoke= for a credential, =show= and =list= for reads.
686
659* Scripting 687* Scripting
660 688
661Every read command takes =--json= and emits one envelope: 689Every read command takes =--json= and emits one envelope:
CHANGELOG.org +16
@@ -4,6 +4,22 @@ Versioning follows semver from v0.1.0. Database migrations run
4automatically on daemon start; upgrade notes appear per release when 4automatically on daemon start; upgrade notes appear per release when
5anything beyond "replace the binary and restart" is needed. 5anything beyond "replace the binary and restart" is needed.
6 6
7* v1.22.0 — unreleased
8
9The CLI output sweep (#183). The rules are on the wiki's Users page,
10under "Output rules".
11
12- A bad invocation prints the command's registered usage, the one
13 source of it; the hand-written copies are gone.
14- The =gitbay= CLI pads list rows into aligned columns at a terminal
15 and leaves the tabs alone when piped.
16- An empty list prints nothing on stdout and =nothing to list= on
17 stderr.
18- Every refusal (exit 4) says what to do instead.
19- =status list= and =repo topics= print rows.
20- =admin runners remove=; =forget= stays as an alias until the next
21 release.
22
7* v1.21.0 — 2026-09-12 23* v1.21.0 — 2026-09-12
8 24
9The web UI/UX sweep (#182). 25The web UI/UX sweep (#182).
cmd/gitbay/main.go +2 −1
@@ -129,7 +129,8 @@ func newRoot() *cobra.Command {
129 pass("invite", "issue a registration invite and mail its code: --email <address>", passOpts{server: []string{"admin", "invite"}}), 129 pass("invite", "issue a registration invite and mail its code: --email <address>", passOpts{server: []string{"admin", "invite"}}),
130 pass("stats", "instance statistics: counts and per-repository disk usage", passOpts{server: []string{"admin", "stats"}}), 130 pass("stats", "instance statistics: counts and per-repository disk usage", passOpts{server: []string{"admin", "stats"}}),
131 withSub(pass("runners", "the build queue and runner keys: last poll, scope, the build each holds", passOpts{server: []string{"admin", "runners"}}), 131 withSub(pass("runners", "the build queue and runner keys: last poll, scope, the build each holds", passOpts{server: []string{"admin", "runners"}}),
132 pass("forget", "drop a key's heartbeat row: <fingerprint>", passOpts{server: []string{"admin", "runners", "forget"}})), 132 pass("remove", "drop a key's heartbeat row: <fingerprint>", passOpts{server: []string{"admin", "runners", "remove"}}),
133 pass("forget", "alias of remove: <fingerprint>", passOpts{server: []string{"admin", "runners", "forget"}})),
133 group("repo", "any repository, for moderation (audited)", 134 group("repo", "any repository, for moderation (audited)",
134 pass("list", "every repository with size and last push: [--owner o] [--visibility v] [--limit n] [--cursor c]", passOpts{server: []string{"admin", "repo", "list"}}), 135 pass("list", "every repository with size and last push: [--owner o] [--visibility v] [--limit n] [--cursor c]", passOpts{server: []string{"admin", "repo", "list"}}),
135 pass("archive", "archive a repository: <owner/name>", passOpts{server: []string{"admin", "repo", "archive"}}), 136 pass("archive", "archive a repository: <owner/name>", passOpts{server: []string{"admin", "repo", "archive"}}),
cmd/gitbay/ssh.go +41
@@ -1,8 +1,11 @@
1package main 1package main
2 2
3import ( 3import (
4 "text/tabwriter"
5
4 "encoding/json" 6 "encoding/json"
5 "fmt" 7 "fmt"
8 "golang.org/x/term"
6 "io" 9 "io"
7 "os" 10 "os"
8 "os/exec" 11 "os/exec"
@@ -120,7 +123,15 @@ func runSSH(t target, serverArgv []string, stdin io.Reader) int {
120 cmd.Stdin = stdin 123 cmd.Stdin = stdin
121 cmd.Stdout = os.Stdout 124 cmd.Stdout = os.Stdout
122 cmd.Stderr = os.Stderr 125 cmd.Stderr = os.Stderr
126 var tw *tabwriter.Writer
127 if alignColumns(serverArgv) {
128 tw = tabwriter.NewWriter(os.Stdout, 0, 8, 2, ' ', 0)
129 cmd.Stdout = tw
130 }
123 err := cmd.Run() 131 err := cmd.Run()
132 if tw != nil {
133 tw.Flush()
134 }
124 if err == nil { 135 if err == nil {
125 return 0 136 return 0
126 } 137 }
@@ -193,3 +204,33 @@ func withRepo(t target, args []string) ([]string, error) {
193 } 204 }
194 return append([]string{t.repo}, args...), nil 205 return append([]string{t.repo}, args...), nil
195} 206}
207
208// listVerbs are the server commands whose plain output is one row per
209// item with tab-separated columns.
210var listVerbs = map[string]bool{
211 "list": true, "runners": true, "deliveries": true, "refs": true,
212 "revisions": true, "threads": true, "bookmarks": true, "jobs": true,
213}
214
215// alignColumns reports whether a command's rows should be padded into
216// columns: a list command, printed for a person at a terminal. Piped
217// output keeps the server's tabs so cut and awk see the same bytes stock
218// ssh prints, and --json is never touched.
219func alignColumns(serverArgv []string) bool {
220 verb := ""
221 for _, a := range serverArgv {
222 if strings.HasPrefix(a, "-") {
223 break
224 }
225 if a == "--json" {
226 return false
227 }
228 verb = a
229 }
230 for _, a := range serverArgv {
231 if a == "--json" {
232 return false
233 }
234 }
235 return listVerbs[verb] && term.IsTerminal(int(os.Stdout.Fd()))
236}
internal/control/admin.go +16 −10
@@ -33,8 +33,14 @@ func init() {
33 Summary: "the build queue and runner accounts: last poll, scope, the build each holds (instance admins)", 33 Summary: "the build queue and runner accounts: last poll, scope, the build each holds (instance admins)",
34 Usage: "admin runners", 34 Usage: "admin runners",
35 ReadOnly: true, SSHOnly: true, Run: runAdminRunners}) 35 ReadOnly: true, SSHOnly: true, Run: runAdminRunners})
36 register(Command{Path: []string{"admin", "runners", "forget"}, 36 register(Command{Path: []string{"admin", "runners", "remove"},
37 Summary: "drop a key's runner heartbeat row, e.g. one that polled once by mistake (instance admins)", 37 Summary: "drop a key's runner heartbeat row, e.g. one that polled once by mistake (instance admins)",
38 Usage: "admin runners remove <fingerprint>",
39 SSHOnly: true, Run: runAdminRunnersForget})
40 // forget is the name this shipped under in v1.18; remove is the verb
41 // every other noun uses. Both stay for one release.
42 register(Command{Path: []string{"admin", "runners", "forget"},
43 Summary: "alias of admin runners remove",
38 Usage: "admin runners forget <fingerprint>", 44 Usage: "admin runners forget <fingerprint>",
39 SSHOnly: true, Run: runAdminRunnersForget}) 45 SSHOnly: true, Run: runAdminRunnersForget})
40 register(Command{Path: []string{"admin", "repo", "list"}, 46 register(Command{Path: []string{"admin", "repo", "list"},
@@ -62,7 +68,7 @@ func init() {
62// requireInstanceAdmin gates the admin noun. -1 means proceed. 68// requireInstanceAdmin gates the admin noun. -1 means proceed.
63func requireInstanceAdmin(c *Ctx) int { 69func requireInstanceAdmin(c *Ctx) int {
64 if !c.User.IsAdmin { 70 if !c.User.IsAdmin {
65 return c.fail(protocol.ExitDenied, "admin commands are for instance admins") 71 return c.fail(protocol.ExitDenied, "admin commands are for instance admins; ask one")
66 } 72 }
67 return -1 73 return -1
68} 74}
@@ -131,7 +137,7 @@ func runAdminUserShow(c *Ctx, args []string) int {
131 return code 137 return code
132 } 138 }
133 if len(args) != 1 { 139 if len(args) != 1 {
134 return c.fail(protocol.ExitUsage, "usage: admin user show <username>") 140 return c.usage()
135 } 141 }
136 name := args[0] 142 name := args[0]
137 u, err := c.Store.UserByUsername(name) 143 u, err := c.Store.UserByUsername(name)
@@ -291,7 +297,7 @@ func setAdmin(c *Ctx, args []string, admin bool) int {
291 verb = "promote" 297 verb = "promote"
292 } 298 }
293 if len(args) != 1 { 299 if len(args) != 1 {
294 return c.fail(protocol.ExitUsage, "usage: admin user %s <username>", verb) 300 return c.usage()
295 } 301 }
296 u, err := c.Store.UserByUsername(args[0]) 302 u, err := c.Store.UserByUsername(args[0])
297 if errors.Is(err, store.ErrNotFound) { 303 if errors.Is(err, store.ErrNotFound) {
@@ -389,7 +395,7 @@ func adminArchive(c *Ctx, args []string, archived bool) int {
389 verb = "unarchive" 395 verb = "unarchive"
390 } 396 }
391 if len(args) != 1 { 397 if len(args) != 1 {
392 return c.fail(protocol.ExitUsage, "usage: admin repo %s <owner/name>", verb) 398 return c.usage()
393 } 399 }
394 repo, code := adminRepo(c, args[0]) 400 repo, code := adminRepo(c, args[0])
395 if code >= 0 { 401 if code >= 0 {
@@ -404,7 +410,7 @@ func adminArchive(c *Ctx, args []string, archived bool) int {
404 410
405func runAdminRepoVisibility(c *Ctx, args []string) int { 411func runAdminRepoVisibility(c *Ctx, args []string) int {
406 if len(args) != 2 || (args[1] != "public" && args[1] != "private") { 412 if len(args) != 2 || (args[1] != "public" && args[1] != "private") {
407 return c.fail(protocol.ExitUsage, "usage: admin repo visibility <owner/name> public|private") 413 return c.usage()
408 } 414 }
409 repo, code := adminRepo(c, args[0]) 415 repo, code := adminRepo(c, args[0])
410 if code >= 0 { 416 if code >= 0 {
@@ -426,11 +432,11 @@ func runAdminRepoDelete(c *Ctx, args []string) int {
426 } else if path == "" { 432 } else if path == "" {
427 path = a 433 path = a
428 } else { 434 } else {
429 return c.fail(protocol.ExitUsage, "usage: admin repo delete <owner/name> --yes") 435 return c.usage()
430 } 436 }
431 } 437 }
432 if path == "" { 438 if path == "" {
433 return c.fail(protocol.ExitUsage, "usage: admin repo delete <owner/name> --yes") 439 return c.usage()
434 } 440 }
435 repo, code := adminRepo(c, path) 441 repo, code := adminRepo(c, path)
436 if code >= 0 { 442 if code >= 0 {
@@ -451,7 +457,7 @@ func runAdminRunnersForget(c *Ctx, args []string) int {
451 return code 457 return code
452 } 458 }
453 if len(args) != 1 { 459 if len(args) != 1 {
454 return c.fail(protocol.ExitUsage, "usage: admin runners forget <fingerprint>") 460 return c.usage()
455 } 461 }
456 if err := c.Store.ForgetRunner(args[0]); err != nil { 462 if err := c.Store.ForgetRunner(args[0]); err != nil {
457 if errors.Is(err, store.ErrNotFound) { 463 if errors.Is(err, store.ErrNotFound) {
@@ -470,7 +476,7 @@ func runAdminRunners(c *Ctx, args []string) int {
470 return code 476 return code
471 } 477 }
472 if len(args) != 0 { 478 if len(args) != 0 {
473 return c.fail(protocol.ExitUsage, "usage: admin runners") 479 return c.usage()
474 } 480 }
475 runners, err := c.Store.ListRunners() 481 runners, err := c.Store.ListRunners()
476 if err != nil { 482 if err != nil {
internal/control/adminhost.go +7 −8
@@ -55,8 +55,7 @@ func runAdminUserCreate(c *Ctx, args []string) int {
55 if code := requireInstanceAdmin(c); code >= 0 { 55 if code := requireInstanceAdmin(c); code >= 0 {
56 return code 56 return code
57 } 57 }
58 const usage = "usage: admin user create <username> [--admin] [--email <address> [--verified]] [--key -] < key.pub" 58 f, err := parseFlags(args, flagSpec{Values: []string{"--email", "--key"}, Bools: []string{"--admin", "--verified"}, MaxPos: 1, Usage: c.Cmd.Usage})
59 f, err := parseFlags(args, flagSpec{Values: []string{"--email", "--key"}, Bools: []string{"--admin", "--verified"}, MaxPos: 1, Usage: usage})
60 if err != nil { 59 if err != nil {
61 return c.fail(protocol.ExitUsage, "%v", err) 60 return c.fail(protocol.ExitUsage, "%v", err)
62 } 61 }
@@ -66,10 +65,10 @@ func runAdminUserCreate(c *Ctx, args []string) int {
66 return c.fail(protocol.ExitUsage, "--key only supports - (the public key on stdin)") 65 return c.fail(protocol.ExitUsage, "--key only supports - (the public key on stdin)")
67 } 66 }
68 if username == "" || username[0] == '-' { 67 if username == "" || username[0] == '-' {
69 return c.fail(protocol.ExitUsage, usage) 68 return c.usage()
70 } 69 }
71 if username == "" || (verified && email == "") { 70 if username == "" || (verified && email == "") {
72 return c.fail(protocol.ExitUsage, usage) 71 return c.usage()
73 } 72 }
74 if err := policy.ValidateOwnerName(username); err != nil { 73 if err := policy.ValidateOwnerName(username); err != nil {
75 return c.failInput(err) 74 return c.failInput(err)
@@ -128,7 +127,7 @@ func adminUserArg(c *Ctx, args []string, usage string) (store.User, int) {
128 return store.User{}, code 127 return store.User{}, code
129 } 128 }
130 if len(args) != 1 { 129 if len(args) != 1 {
131 return store.User{}, c.fail(protocol.ExitUsage, "usage: %s", usage) 130 return store.User{}, c.usage()
132 } 131 }
133 u, err := c.Store.UserByUsername(args[0]) 132 u, err := c.Store.UserByUsername(args[0])
134 if errors.Is(err, store.ErrNotFound) { 133 if errors.Is(err, store.ErrNotFound) {
@@ -201,7 +200,7 @@ func runAdminEmailVerify(c *Ctx, args []string) int {
201 return code 200 return code
202 } 201 }
203 if len(args) != 2 { 202 if len(args) != 2 {
204 return c.fail(protocol.ExitUsage, "usage: admin email verify <username> <address>") 203 return c.usage()
205 } 204 }
206 u, err := c.Store.UserByUsername(args[0]) 205 u, err := c.Store.UserByUsername(args[0])
207 if errors.Is(err, store.ErrNotFound) { 206 if errors.Is(err, store.ErrNotFound) {
@@ -228,7 +227,7 @@ func runAdminInvite(c *Ctx, args []string) int {
228 email = args[1] 227 email = args[1]
229 } 228 }
230 if email == "" { 229 if email == "" {
231 return c.fail(protocol.ExitUsage, "usage: admin invite --email <address>") 230 return c.usage()
232 } 231 }
233 if used, err := c.Store.EmailInUse(email); err != nil { 232 if used, err := c.Store.EmailInUse(email); err != nil {
234 return c.fail(protocol.ExitFailure, "%v", err) 233 return c.fail(protocol.ExitFailure, "%v", err)
@@ -271,7 +270,7 @@ func runAdminStats(c *Ctx, args []string) int {
271 return code 270 return code
272 } 271 }
273 if len(args) != 0 { 272 if len(args) != 0 {
274 return c.fail(protocol.ExitUsage, "usage: admin stats") 273 return c.usage()
275 } 274 }
276 counts, err := c.Store.InstanceCounts() 275 counts, err := c.Store.InstanceCounts()
277 if err != nil { 276 if err != nil {
internal/control/audit.go +2 −4
@@ -18,14 +18,12 @@ func init() {
18 ReadOnly: true, SSHOnly: true, Run: runAudit}) 18 ReadOnly: true, SSHOnly: true, Run: runAudit})
19} 19}
20 20
21const auditUsage = "usage: audit [--actor <user>|-] [--action <prefix>] [--since <duration|date>] [--limit <n>]"
22
23func runAudit(c *Ctx, args []string) int { 21func runAudit(c *Ctx, args []string) int {
24 if !c.User.IsAdmin { 22 if !c.User.IsAdmin {
25 return c.fail(protocol.ExitDenied, "the audit log is for instance admins") 23 return c.fail(protocol.ExitDenied, "the audit log is for instance admins; ask one")
26 } 24 }
27 f := store.AuditFilter{Limit: 100} 25 f := store.AuditFilter{Limit: 100}
28 fl, err := parseFlags(args, flagSpec{Values: []string{"--limit", "--actor", "--action", "--since"}, MaxPos: 0, Usage: auditUsage}) 26 fl, err := parseFlags(args, flagSpec{Values: []string{"--limit", "--actor", "--action", "--since"}, MaxPos: 0, Usage: c.Cmd.Usage})
29 if err != nil { 27 if err != nil {
30 return c.fail(protocol.ExitUsage, "%v", err) 28 return c.fail(protocol.ExitUsage, "%v", err)
31 } 29 }
internal/control/build.go +12 −12
@@ -104,7 +104,7 @@ func buildRef(c *Ctx, args []string) (store.Repo, store.Build, int) {
104 104
105func runBuildList(c *Ctx, args []string) int { 105func runBuildList(c *Ctx, args []string) int {
106 if len(args) != 1 { 106 if len(args) != 1 {
107 return c.fail(protocol.ExitUsage, "usage: build list <owner/name>") 107 return c.usage()
108 } 108 }
109 repo, code := resolveRepo(c, args[0], policy.CanRead) 109 repo, code := resolveRepo(c, args[0], policy.CanRead)
110 if code >= 0 { 110 if code >= 0 {
@@ -182,7 +182,7 @@ func repoJobs(c *Ctx, repo store.Repo) ([]ci.Job, string, int) {
182// that can read the repository's git could offer the choice. 182// that can read the repository's git could offer the choice.
183func runBuildJobs(c *Ctx, args []string) int { 183func runBuildJobs(c *Ctx, args []string) int {
184 if len(args) != 1 { 184 if len(args) != 1 {
185 return c.fail(protocol.ExitUsage, "usage: build jobs <owner/name>") 185 return c.usage()
186 } 186 }
187 repo, code := resolveRepo(c, args[0], policy.CanRead) 187 repo, code := resolveRepo(c, args[0], policy.CanRead)
188 if code >= 0 { 188 if code >= 0 {
@@ -212,7 +212,7 @@ func runBuildJobs(c *Ctx, args []string) int {
212 212
213func runBuildTrigger(c *Ctx, args []string) int { 213func runBuildTrigger(c *Ctx, args []string) int {
214 if len(args) != 2 { 214 if len(args) != 2 {
215 return c.fail(protocol.ExitUsage, "usage: build trigger <owner/name> <job>") 215 return c.usage()
216 } 216 }
217 repo, code := resolveRepo(c, args[0], policy.CanWrite) 217 repo, code := resolveRepo(c, args[0], policy.CanWrite)
218 if code >= 0 { 218 if code >= 0 {
@@ -246,7 +246,7 @@ var secretName = regexp.MustCompile(`^[A-Z_][A-Z0-9_]{0,63}$`)
246 246
247func runSecretSet(c *Ctx, args []string) int { 247func runSecretSet(c *Ctx, args []string) int {
248 if len(args) != 2 { 248 if len(args) != 2 {
249 return c.fail(protocol.ExitUsage, "usage: repo secret set <owner/name> <NAME> (value on stdin)") 249 return c.usage()
250 } 250 }
251 if !secretName.MatchString(args[1]) { 251 if !secretName.MatchString(args[1]) {
252 return c.fail(protocol.ExitUsage, "secret names are env-var shaped: uppercase letters, digits, _") 252 return c.fail(protocol.ExitUsage, "secret names are env-var shaped: uppercase letters, digits, _")
@@ -273,7 +273,7 @@ func runSecretSet(c *Ctx, args []string) int {
273 273
274func runSecretRemove(c *Ctx, args []string) int { 274func runSecretRemove(c *Ctx, args []string) int {
275 if len(args) != 2 { 275 if len(args) != 2 {
276 return c.fail(protocol.ExitUsage, "usage: repo secret remove <owner/name> <NAME>") 276 return c.usage()
277 } 277 }
278 repo, code := resolveRepo(c, args[0], policy.CanAdmin) 278 repo, code := resolveRepo(c, args[0], policy.CanAdmin)
279 if code >= 0 { 279 if code >= 0 {
@@ -292,7 +292,7 @@ func runSecretRemove(c *Ctx, args []string) int {
292 292
293func runSecretList(c *Ctx, args []string) int { 293func runSecretList(c *Ctx, args []string) int {
294 if len(args) != 1 { 294 if len(args) != 1 {
295 return c.fail(protocol.ExitUsage, "usage: repo secret list <owner/name>") 295 return c.usage()
296 } 296 }
297 repo, code := resolveRepo(c, args[0], policy.CanAdmin) 297 repo, code := resolveRepo(c, args[0], policy.CanAdmin)
298 if code >= 0 { 298 if code >= 0 {
@@ -376,7 +376,7 @@ func runRunnerNext(c *Ctx, args []string) int {
376 return c.fail(protocol.ExitFailure, "%v", err) 376 return c.fail(protocol.ExitFailure, "%v", err)
377 } 377 }
378 if !ok { 378 if !ok {
379 return c.fail(protocol.ExitDenied, "this key is not attached to %s", repo.Path()) 379 return c.fail(protocol.ExitDenied, "this key is not attached to %s; a repository admin attaches it with repo runner add", repo.Path())
380 } 380 }
381 repoIDs = append(repoIDs, repo.ID) 381 repoIDs = append(repoIDs, repo.ID)
382 } 382 }
@@ -464,7 +464,7 @@ func runRunnerLog(c *Ctx, args []string) int {
464 return code 464 return code
465 } 465 }
466 if len(args) != 1 { 466 if len(args) != 1 {
467 return c.fail(protocol.ExitUsage, "usage: runner log <build-id> (chunk on stdin)") 467 return c.usage()
468 } 468 }
469 id, err := strconv.ParseInt(args[0], 10, 64) 469 id, err := strconv.ParseInt(args[0], 10, 64)
470 if err != nil { 470 if err != nil {
@@ -475,7 +475,7 @@ func runRunnerLog(c *Ctx, args []string) int {
475 } else if ok, err := runnerMayBuild(c, key, b.RepoID); err != nil { 475 } else if ok, err := runnerMayBuild(c, key, b.RepoID); err != nil {
476 return c.fail(protocol.ExitFailure, "%v", err) 476 return c.fail(protocol.ExitFailure, "%v", err)
477 } else if !ok { 477 } else if !ok {
478 return c.fail(protocol.ExitDenied, "this key is not attached to the build's repository") 478 return c.fail(protocol.ExitDenied, "this key is not attached to the build's repository; a repository admin attaches it with repo runner add")
479 } 479 }
480 // Stream stdin into the log in chunks so long builds appear live. An 480 // Stream stdin into the log in chunks so long builds appear live. An
481 // append that fails drops its chunk and the loop keeps draining: ending 481 // append that fails drops its chunk and the loop keeps draining: ending
@@ -542,7 +542,7 @@ func runRunnerDone(c *Ctx, args []string) int {
542 return code 542 return code
543 } 543 }
544 if len(args) != 2 || (args[1] != "success" && args[1] != "failure") { 544 if len(args) != 2 || (args[1] != "success" && args[1] != "failure") {
545 return c.fail(protocol.ExitUsage, "usage: runner done <build-id> success|failure") 545 return c.usage()
546 } 546 }
547 id, err := strconv.ParseInt(args[0], 10, 64) 547 id, err := strconv.ParseInt(args[0], 10, 64)
548 if err != nil { 548 if err != nil {
@@ -555,7 +555,7 @@ func runRunnerDone(c *Ctx, args []string) int {
555 if ok, err := runnerMayBuild(c, key, b.RepoID); err != nil { 555 if ok, err := runnerMayBuild(c, key, b.RepoID); err != nil {
556 return c.fail(protocol.ExitFailure, "%v", err) 556 return c.fail(protocol.ExitFailure, "%v", err)
557 } else if !ok { 557 } else if !ok {
558 return c.fail(protocol.ExitDenied, "this key is not attached to the build's repository") 558 return c.fail(protocol.ExitDenied, "this key is not attached to the build's repository; a repository admin attaches it with repo runner add")
559 } 559 }
560 // Cancelled underneath the runner: its report is late, not wrong. 560 // Cancelled underneath the runner: its report is late, not wrong.
561 // The row, the status and the log were settled by the cancel. 561 // The row, the status and the log were settled by the cancel.
@@ -829,7 +829,7 @@ func runBuildCancel(c *Ctx, args []string) int {
829 return c.fail(protocol.ExitFailure, "%v", err) 829 return c.fail(protocol.ExitFailure, "%v", err)
830 } 830 }
831 if !policy.CanWrite(c.User, repo, grant) { 831 if !policy.CanWrite(c.User, repo, grant) {
832 return c.fail(protocol.ExitDenied, "cancelling a build needs write access to %s", repo.Path()) 832 return c.fail(protocol.ExitDenied, "cancelling a build needs write access to %s; ask its owner", repo.Path())
833 } 833 }
834 if b.Status != "pending" && b.Status != "running" { 834 if b.Status != "pending" && b.Status != "running" {
835 return c.fail(protocol.ExitUsage, "build %d is %s; only a queued or running build can be cancelled", b.Number, b.Status) 835 return c.fail(protocol.ExitUsage, "build %d is %s; only a queued or running build can be cancelled", b.Number, b.Status)
internal/control/commitfile.go +3 −4
@@ -35,15 +35,14 @@ const maxCommitFileBytes = 1 << 20
35// A repository that requires verified signatures therefore refuses the 35// A repository that requires verified signatures therefore refuses the
36// command rather than writing a commit its own policy would reject. 36// command rather than writing a commit its own policy would reject.
37func runCommitFile(c *Ctx, args []string) int { 37func runCommitFile(c *Ctx, args []string) int {
38 const usage = "repo commit-file <owner/name> <path> --ref <branch> [--message <m>] [--file -]" 38 f, err := parseFlags(args, flagSpec{Values: []string{"--ref", "--message", "--file"}, MaxPos: -1, Usage: c.Cmd.Usage})
39 f, err := parseFlags(args, flagSpec{Values: []string{"--ref", "--message", "--file"}, MaxPos: -1, Usage: usage})
40 if err != nil { 39 if err != nil {
41 return c.fail(protocol.ExitUsage, "%v", err) 40 return c.fail(protocol.ExitUsage, "%v", err)
42 } 41 }
43 rest := f.Pos 42 rest := f.Pos
44 ref, message, file := f.Value("--ref"), f.Value("--message"), f.Value("--file") 43 ref, message, file := f.Value("--ref"), f.Value("--message"), f.Value("--file")
45 if len(rest) != 2 || ref == "" { 44 if len(rest) != 2 || ref == "" {
46 return c.fail(protocol.ExitUsage, "usage: %s", usage) 45 return c.usage()
47 } 46 }
48 repo, code := resolveRepo(c, rest[0], policy.CanWrite) 47 repo, code := resolveRepo(c, rest[0], policy.CanWrite)
49 if code >= 0 { 48 if code >= 0 {
@@ -57,7 +56,7 @@ func runCommitFile(c *Ctx, args []string) int {
57 return c.fail(protocol.ExitUsage, "path must stay inside the repository") 56 return c.fail(protocol.ExitUsage, "path must stay inside the repository")
58 } 57 }
59 if repo.Settings.RequireMR && slices.Contains(repo.Settings.ProtectedBranches, ref) { 58 if repo.Settings.RequireMR && slices.Contains(repo.Settings.ProtectedBranches, ref) {
60 return c.fail(protocol.ExitDenied, "branch %s accepts changes through merge requests only", ref) 59 return c.fail(protocol.ExitDenied, "branch %s accepts changes through merge requests only; push another branch and open one", ref)
61 } 60 }
62 // The server authors this commit, so it cannot sign it. 61 // The server authors this commit, so it cannot sign it.
63 if repo.Settings.RequireSignedCommits { 62 if repo.Settings.RequireSignedCommits {
internal/control/control.go +22 −5
@@ -36,6 +36,15 @@ type Ctx struct {
36 // Source identifies the credential behind this session for the audit 36 // Source identifies the credential behind this session for the audit
37 // log: an SSH key fingerprint, or "api" for token requests. 37 // log: an SSH key fingerprint, or "api" for token requests.
38 Source string 38 Source string
39 // Cmd is the command being run, set by Dispatch, so a usage error can
40 // print the registered usage rather than a copy of it.
41 Cmd Command
42}
43
44// usage reports a bad invocation with the command's registered usage,
45// the one source of it.
46func (c *Ctx) usage() int {
47 return c.fail(protocol.ExitUsage, "usage: %s", c.Cmd.Usage)
39} 48}
40 49
41type Command struct { 50type Command struct {
@@ -82,6 +91,7 @@ func Dispatch(c *Ctx, argv []string) int {
82 return c.fail(protocol.ExitUsage, "no command given; try: ssh <host> help") 91 return c.fail(protocol.ExitUsage, "no command given; try: ssh <host> help")
83 } 92 }
84 cmd, rest, ok := Lookup(argv) 93 cmd, rest, ok := Lookup(argv)
94 c.Cmd = cmd
85 if !ok { 95 if !ok {
86 return c.fail(protocol.ExitUsage, "unknown command %q", argv[0]) 96 return c.fail(protocol.ExitUsage, "unknown command %q", argv[0])
87 } 97 }
@@ -99,23 +109,23 @@ func Dispatch(c *Ctx, argv []string) int {
99 // A runner-scoped key reaches the runner protocol and nothing else, so 109 // A runner-scoped key reaches the runner protocol and nothing else, so
100 // the key a CI host holds cannot administer the instance. 110 // the key a CI host holds cannot administer the instance.
101 if c.Scope != "full" && !(c.Scope == "runner" && cmd.Path[0] == "runner") { 111 if c.Scope != "full" && !(c.Scope == "runner" && cmd.Path[0] == "runner") {
102 return c.fail(protocol.ExitDenied, "this key's scope (%s) does not allow control commands", c.Scope) 112 return c.fail(protocol.ExitDenied, "this key's scope (%s) does not allow control commands; use a key added with --scope full", c.Scope)
103 } 113 }
104 if c.ViaAPI && cmd.SSHOnly { 114 if c.ViaAPI && cmd.SSHOnly {
105 return c.fail(protocol.ExitDenied, "%s is only available over SSH", joinPath(cmd.Path)) 115 return c.fail(protocol.ExitDenied, "%s is only available over SSH", joinPath(cmd.Path))
106 } 116 }
107 if c.ReadOnly && !cmd.ReadOnly { 117 if c.ReadOnly && !cmd.ReadOnly {
108 return c.fail(protocol.ExitDenied, "this token is read-only; %s modifies state", joinPath(cmd.Path)) 118 return c.fail(protocol.ExitDenied, "this token is read-only; %s modifies state — mint one with --scope full", joinPath(cmd.Path))
109 } 119 }
110 // The SSH listener refuses a disabled account before it gets here; the 120 // The SSH listener refuses a disabled account before it gets here; the
111 // API and the web reach Dispatch directly, so the check lives here too. 121 // API and the web reach Dispatch directly, so the check lives here too.
112 if c.User.Disabled { 122 if c.User.Disabled {
113 return c.fail(protocol.ExitDenied, "this account is disabled") 123 return c.fail(protocol.ExitDenied, "this account is disabled; ask an instance admin to enable it")
114 } 124 }
115 // The admin noun is gated here as well as in each handler, so a new 125 // The admin noun is gated here as well as in each handler, so a new
116 // admin command that forgets requireInstanceAdmin is still refused. 126 // admin command that forgets requireInstanceAdmin is still refused.
117 if cmd.Path[0] == "admin" && !c.User.IsAdmin { 127 if cmd.Path[0] == "admin" && !c.User.IsAdmin {
118 return c.fail(protocol.ExitDenied, "admin commands are for instance admins") 128 return c.fail(protocol.ExitDenied, "admin commands are for instance admins; ask one")
119 } 129 }
120 if c.User.Pending && !pendingAllowed(cmd.Path) { 130 if c.User.Pending && !pendingAllowed(cmd.Path) {
121 return c.fail(protocol.ExitDenied, 131 return c.fail(protocol.ExitDenied,
@@ -208,9 +218,16 @@ func (emptyReader) Read([]byte) (int, error) { return 0, io.EOF }
208// otherwise via the plain formatter. 218// otherwise via the plain formatter.
209func (c *Ctx) emit(data any, plain func(w io.Writer)) int { 219func (c *Ctx) emit(data any, plain func(w io.Writer)) int {
210 // A nil slice would serialize as null; consumers should see []. 220 // A nil slice would serialize as null; consumers should see [].
211 if v := reflect.ValueOf(data); v.Kind() == reflect.Slice && v.IsNil() { 221 v := reflect.ValueOf(data)
222 if v.Kind() == reflect.Slice && v.IsNil() {
212 data = reflect.MakeSlice(v.Type(), 0, 0).Interface() 223 data = reflect.MakeSlice(v.Type(), 0, 0).Interface()
213 } 224 }
225 // An empty list prints nothing a script would read; the person at
226 // the terminal hears about it on stderr.
227 if !c.JSON && v.Kind() == reflect.Slice && v.Len() == 0 {
228 fmt.Fprintln(c.Stderr, "nothing to list")
229 return protocol.ExitOK
230 }
214 if c.JSON { 231 if c.JSON {
215 enc := json.NewEncoder(c.Stdout) 232 enc := json.NewEncoder(c.Stdout)
216 enc.SetEscapeHTML(false) 233 enc.SetEscapeHTML(false)
internal/control/dashboard.go +2 −2
@@ -84,7 +84,7 @@ type DashboardOut struct {
84 84
85func runDashboard(c *Ctx, args []string) int { 85func runDashboard(c *Ctx, args []string) int {
86 if len(args) != 0 { 86 if len(args) != 0 {
87 return c.fail(protocol.ExitUsage, "usage: dashboard") 87 return c.usage()
88 } 88 }
89 d := DashboardOut{ 89 d := DashboardOut{
90 Reviews: []DashboardItem{}, Assigned: []DashboardItem{}, MRs: []DashboardItem{}, 90 Reviews: []DashboardItem{}, Assigned: []DashboardItem{}, MRs: []DashboardItem{},
@@ -260,7 +260,7 @@ func runFeed(c *Ctx, args []string) int {
260 return code 260 return code
261 } 261 }
262 if len(rest) != 0 { 262 if len(rest) != 0 {
263 return c.fail(protocol.ExitUsage, "usage: feed [--limit <n>] [--cursor <c>]") 263 return c.usage()
264 } 264 }
265 if p.limit == 0 { 265 if p.limit == 0 {
266 p.limit = feedDefaultLimit 266 p.limit = feedDefaultLimit
internal/control/deploykey.go +4 −4
@@ -34,13 +34,13 @@ func runDeployKeyAdd(c *Ctx, args []string) int {
34 mode = "rw" 34 mode = "rw"
35 default: 35 default:
36 if path != "" { 36 if path != "" {
37 return c.fail(protocol.ExitUsage, "usage: repo deploy-key add <owner/name> [--rw] < key.pub") 37 return c.usage()
38 } 38 }
39 path = a 39 path = a
40 } 40 }
41 } 41 }
42 if path == "" { 42 if path == "" {
43 return c.fail(protocol.ExitUsage, "usage: repo deploy-key add <owner/name> [--rw] < key.pub") 43 return c.usage()
44 } 44 }
45 repo, code := resolveRepo(c, path, policy.CanAdmin) 45 repo, code := resolveRepo(c, path, policy.CanAdmin)
46 if code >= 0 { 46 if code >= 0 {
@@ -73,7 +73,7 @@ func runDeployKeyAdd(c *Ctx, args []string) int {
73 73
74func runDeployKeyList(c *Ctx, args []string) int { 74func runDeployKeyList(c *Ctx, args []string) int {
75 if len(args) != 1 { 75 if len(args) != 1 {
76 return c.fail(protocol.ExitUsage, "usage: repo deploy-key list <owner/name>") 76 return c.usage()
77 } 77 }
78 repo, code := resolveRepo(c, args[0], policy.CanAdmin) 78 repo, code := resolveRepo(c, args[0], policy.CanAdmin)
79 if code >= 0 { 79 if code >= 0 {
@@ -106,7 +106,7 @@ func runDeployKeyList(c *Ctx, args []string) int {
106 106
107func runDeployKeyRemove(c *Ctx, args []string) int { 107func runDeployKeyRemove(c *Ctx, args []string) int {
108 if len(args) != 2 { 108 if len(args) != 2 {
109 return c.fail(protocol.ExitUsage, "usage: repo deploy-key remove <owner/name> <fingerprint>") 109 return c.usage()
110 } 110 }
111 repo, code := resolveRepo(c, args[0], policy.CanAdmin) 111 repo, code := resolveRepo(c, args[0], policy.CanAdmin)
112 if code >= 0 { 112 if code >= 0 {
internal/control/deps.go +3 −3
@@ -46,7 +46,7 @@ type DepBehind struct {
46 46
47func runDepsEnable(c *Ctx, args []string) int { 47func runDepsEnable(c *Ctx, args []string) int {
48 if len(args) != 1 { 48 if len(args) != 1 {
49 return c.fail(protocol.ExitUsage, "usage: repo deps enable <owner/name>") 49 return c.usage()
50 } 50 }
51 repo, code := resolveRepo(c, args[0], policy.CanAdmin) 51 repo, code := resolveRepo(c, args[0], policy.CanAdmin)
52 if code >= 0 { 52 if code >= 0 {
@@ -62,7 +62,7 @@ func runDepsEnable(c *Ctx, args []string) int {
62 62
63func runDepsDisable(c *Ctx, args []string) int { 63func runDepsDisable(c *Ctx, args []string) int {
64 if len(args) != 1 { 64 if len(args) != 1 {
65 return c.fail(protocol.ExitUsage, "usage: repo deps disable <owner/name>") 65 return c.usage()
66 } 66 }
67 repo, code := resolveRepo(c, args[0], policy.CanAdmin) 67 repo, code := resolveRepo(c, args[0], policy.CanAdmin)
68 if code >= 0 { 68 if code >= 0 {
@@ -78,7 +78,7 @@ func runDepsDisable(c *Ctx, args []string) int {
78 78
79func runDepsStatus(c *Ctx, args []string) int { 79func runDepsStatus(c *Ctx, args []string) int {
80 if len(args) != 1 { 80 if len(args) != 1 {
81 return c.fail(protocol.ExitUsage, "usage: repo deps status <owner/name>") 81 return c.usage()
82 } 82 }
83 repo, code := resolveRepo(c, args[0], policy.CanRead) 83 repo, code := resolveRepo(c, args[0], policy.CanRead)
84 if code >= 0 { 84 if code >= 0 {
internal/control/diffcomment.go +2 −2
@@ -143,7 +143,7 @@ func runMRThreads(c *Ctx, args []string) int {
143 return code 143 return code
144 } 144 }
145 if len(args) != 2 { 145 if len(args) != 2 {
146 return c.fail(protocol.ExitUsage, "usage: mr threads <owner/name> <n>") 146 return c.usage()
147 } 147 }
148 comments, err := c.Store.ListDiffComments(mr.ID, c.User.ID) 148 comments, err := c.Store.ListDiffComments(mr.ID, c.User.ID)
149 if err != nil { 149 if err != nil {
@@ -202,7 +202,7 @@ func runMRThreads(c *Ctx, args []string) int {
202 202
203func setThreadResolved(c *Ctx, args []string, resolved bool) int { 203func setThreadResolved(c *Ctx, args []string, resolved bool) int {
204 if len(args) != 3 { 204 if len(args) != 3 {
205 return c.fail(protocol.ExitUsage, "usage: mr resolve|unresolve <owner/name> <n> <thread-id>") 205 return c.usage()
206 } 206 }
207 repo, mr, code := mrRef(c, args[:2], policy.CanRead) 207 repo, mr, code := mrRef(c, args[:2], policy.CanRead)
208 if code >= 0 { 208 if code >= 0 {
internal/control/explore.go +3 −4
@@ -37,7 +37,7 @@ func runExplore(c *Ctx, args []string) int {
37 return code 37 return code
38 } 38 }
39 if len(rest) != 0 { 39 if len(rest) != 0 {
40 return c.fail(protocol.ExitUsage, "usage: explore [--limit <n>] [--cursor <c>]") 40 return c.usage()
41 } 41 }
42 repos, err := c.Store.ListPublicRepos() 42 repos, err := c.Store.ListPublicRepos()
43 if err != nil { 43 if err != nil {
@@ -79,14 +79,13 @@ func runExplore(c *Ctx, args []string) int {
79// release asset get writes an asset. The web's /archive route is the 79// release asset get writes an asset. The web's /archive route is the
80// same bytes with a Content-Disposition on them. 80// same bytes with a Content-Disposition on them.
81func runRepoDownload(c *Ctx, args []string) int { 81func runRepoDownload(c *Ctx, args []string) int {
82 const usage = "repo download <owner/name> [--ref <r>] > repo.tar.gz" 82 f, err := parseFlags(args, flagSpec{Values: []string{"--ref"}, MaxPos: -1, Usage: c.Cmd.Usage})
83 f, err := parseFlags(args, flagSpec{Values: []string{"--ref"}, MaxPos: -1, Usage: usage})
84 if err != nil { 83 if err != nil {
85 return c.fail(protocol.ExitUsage, "%v", err) 84 return c.fail(protocol.ExitUsage, "%v", err)
86 } 85 }
87 rest, ref := f.Pos, f.Value("--ref") 86 rest, ref := f.Pos, f.Value("--ref")
88 if len(rest) != 1 { 87 if len(rest) != 1 {
89 return c.fail(protocol.ExitUsage, "usage: %s", usage) 88 return c.usage()
90 } 89 }
91 repo, code := resolveRepo(c, rest[0], policy.CanRead) 90 repo, code := resolveRepo(c, rest[0], policy.CanRead)
92 if code >= 0 { 91 if code >= 0 {
internal/control/ghimport.go +1 −1
@@ -149,7 +149,7 @@ func runImportIssues(c *Ctx, args []string) int {
149 } 149 }
150 path, from, apiBase, tokenStdin := f.pos(0), f.Value("--from"), f.Value("--api-base"), f.Has("--token-stdin") 150 path, from, apiBase, tokenStdin := f.pos(0), f.Value("--from"), f.Value("--api-base"), f.Has("--token-stdin")
151 if path == "" || from == "" { 151 if path == "" || from == "" {
152 return c.fail(protocol.ExitUsage, "usage: repo import-issues <owner/name> --from <owner/repo> [--token-stdin] [--api-base <url>]") 152 return c.usage()
153 } 153 }
154 if apiBase == "" { 154 if apiBase == "" {
155 apiBase = "https://api.github.com" 155 apiBase = "https://api.github.com"
internal/control/identity.go +4 −4
@@ -51,7 +51,7 @@ func init() {
51 51
52func runWhoami(c *Ctx, args []string) int { 52func runWhoami(c *Ctx, args []string) int {
53 if len(args) != 0 { 53 if len(args) != 0 {
54 return c.fail(protocol.ExitUsage, "usage: whoami [--json]") 54 return c.usage()
55 } 55 }
56 type out struct { 56 type out struct {
57 Username string `json:"username"` 57 Username string `json:"username"`
@@ -66,7 +66,7 @@ func runWhoami(c *Ctx, args []string) int {
66 66
67func runKeysList(c *Ctx, args []string) int { 67func runKeysList(c *Ctx, args []string) int {
68 if len(args) != 0 { 68 if len(args) != 0 {
69 return c.fail(protocol.ExitUsage, "usage: keys list [--json]") 69 return c.usage()
70 } 70 }
71 keys, err := c.Store.ListSSHKeys(c.User.ID) 71 keys, err := c.Store.ListSSHKeys(c.User.ID)
72 if err != nil { 72 if err != nil {
@@ -161,7 +161,7 @@ func runKeysAdd(c *Ctx, args []string) int {
161 161
162func runKeysLabel(c *Ctx, args []string) int { 162func runKeysLabel(c *Ctx, args []string) int {
163 if len(args) < 1 || len(args) > 2 { 163 if len(args) < 1 || len(args) > 2 {
164 return c.fail(protocol.ExitUsage, "usage: keys label <fingerprint> [<text>]") 164 return c.usage()
165 } 165 }
166 label := "" 166 label := ""
167 if len(args) == 2 { 167 if len(args) == 2 {
@@ -189,7 +189,7 @@ func runKeysLabel(c *Ctx, args []string) int {
189 189
190func runKeysRemove(c *Ctx, args []string) int { 190func runKeysRemove(c *Ctx, args []string) int {
191 if len(args) != 1 { 191 if len(args) != 1 {
192 return c.fail(protocol.ExitUsage, "usage: keys remove <fingerprint>") 192 return c.usage()
193 } 193 }
194 if err := c.Store.RemoveSSHKey(c.User.ID, args[0]); err != nil { 194 if err := c.Store.RemoveSSHKey(c.User.ID, args[0]); err != nil {
195 if errors.Is(err, store.ErrNotFound) { 195 if errors.Is(err, store.ErrNotFound) {
internal/control/import.go +2 −2
@@ -40,11 +40,11 @@ func runRepoImport(c *Ctx, args []string) int {
40 } 40 }
41 path, from, private, tokenStdin := f.pos(0), f.Value("--from"), f.Has("--private"), f.Has("--token-stdin") 41 path, from, private, tokenStdin := f.pos(0), f.Value("--from"), f.Has("--private"), f.Has("--token-stdin")
42 if path == "" || from == "" { 42 if path == "" || from == "" {
43 return c.fail(protocol.ExitUsage, "usage: repo import <owner/name> --from <url> [--private] [--token-stdin]") 43 return c.usage()
44 } 44 }
45 owner, name, ok := strings.Cut(path, "/") 45 owner, name, ok := strings.Cut(path, "/")
46 if !ok { 46 if !ok {
47 return c.fail(protocol.ExitUsage, "usage: repo import <owner/name> --from <url>") 47 return c.usage()
48 } 48 }
49 if err := policy.ValidateName(name); err != nil { 49 if err := policy.ValidateName(name); err != nil {
50 return c.failInput(err) 50 return c.failInput(err)
internal/control/issue.go +8 −9
@@ -123,7 +123,7 @@ func runIssueCreate(c *Ctx, args []string) int {
123 } 123 }
124 path, title, body, file, format := f.pos(0), f.Value("--title"), f.Value("--body"), f.Value("--file"), f.Value("--format") 124 path, title, body, file, format := f.pos(0), f.Value("--title"), f.Value("--body"), f.Value("--file"), f.Value("--format")
125 if path == "" || title == "" { 125 if path == "" || title == "" {
126 return c.fail(protocol.ExitUsage, "usage: issue create <owner/name> --title <t> [--body <b> | --file -] [--format md|org]") 126 return c.usage()
127 } 127 }
128 fmtName, err := markupFormat(format) 128 fmtName, err := markupFormat(format)
129 if err != nil { 129 if err != nil {
@@ -168,9 +168,8 @@ func runIssueList(c *Ctx, args []string) int {
168 if code >= 0 { 168 if code >= 0 {
169 return code 169 return code
170 } 170 }
171 const usage = "usage: issue list <owner/name> [--state open|closed|all] [--label <l>] [--assignee <user>] [--author <user>] [--milestone <title>|none] [--search <text>] [--limit <n>] [--cursor <c>]"
172 f := store.IssueFilter{State: "open"} 171 f := store.IssueFilter{State: "open"}
173 fl, err := parseFlags(args, flagSpec{Values: []string{"--state", "--label", "--assignee", "--author", "--milestone", "--search"}, MaxPos: 1, Usage: usage}) 172 fl, err := parseFlags(args, flagSpec{Values: []string{"--state", "--label", "--assignee", "--author", "--milestone", "--search"}, MaxPos: 1, Usage: c.Cmd.Usage})
174 if err != nil { 173 if err != nil {
175 return c.fail(protocol.ExitUsage, "%v", err) 174 return c.fail(protocol.ExitUsage, "%v", err)
176 } 175 }
@@ -186,7 +185,7 @@ func runIssueList(c *Ctx, args []string) int {
186 } 185 }
187 } 186 }
188 if path == "" || (f.State != "open" && f.State != "closed" && f.State != "all") { 187 if path == "" || (f.State != "open" && f.State != "closed" && f.State != "all") {
189 return c.fail(protocol.ExitUsage, usage) 188 return c.usage()
190 } 189 }
191 repo, code := resolveRepo(c, path, policy.CanRead) 190 repo, code := resolveRepo(c, path, policy.CanRead)
192 if code >= 0 { 191 if code >= 0 {
@@ -217,7 +216,7 @@ func runIssueShow(c *Ctx, args []string) int {
217 return code 216 return code
218 } 217 }
219 if len(args) != 2 { 218 if len(args) != 2 {
220 return c.fail(protocol.ExitUsage, "usage: issue show <owner/name> <n>") 219 return c.usage()
221 } 220 }
222 comments, err := c.Store.ListIssueComments(issue.ID) 221 comments, err := c.Store.ListIssueComments(issue.ID)
223 if err != nil { 222 if err != nil {
@@ -265,7 +264,7 @@ func setIssueState(c *Ctx, args []string, state string) int {
265 return code 264 return code
266 } 265 }
267 if len(args) != 2 { 266 if len(args) != 2 {
268 return c.fail(protocol.ExitUsage, "usage: issue %s <owner/name> <n>", state) 267 return c.usage()
269 } 268 }
270 if code := authorOrWrite(c, repo, issue.Author, map[string]string{"open": "reopen", "closed": "close"}[state]+" this issue"); code >= 0 { 269 if code := authorOrWrite(c, repo, issue.Author, map[string]string{"open": "reopen", "closed": "close"}[state]+" this issue"); code >= 0 {
271 return code 270 return code
@@ -312,7 +311,7 @@ func editText(c *Ctx, args []string, kind string) (rest []string, title, body, f
312 return nil, nil, nil, nil, c.failInput(err) 311 return nil, nil, nil, nil, c.failInput(err)
313 } 312 }
314 if !haveTitle && !haveBody && fmtName == "" { 313 if !haveTitle && !haveBody && fmtName == "" {
315 return nil, nil, nil, nil, c.fail(protocol.ExitUsage, "usage: %s edit <owner/name> <n> [--title <t>] [--body <b> | --file -] [--format md|org]", kind) 314 return nil, nil, nil, nil, c.usage()
316 } 315 }
317 if haveTitle { 316 if haveTitle {
318 if strings.TrimSpace(titleV) == "" { 317 if strings.TrimSpace(titleV) == "" {
@@ -378,7 +377,7 @@ func runIssueLabel(c *Ctx, args []string) int {
378 return c.failInput(err) 377 return c.failInput(err)
379 } 378 }
380 if len(adds)+len(removes) == 0 { 379 if len(adds)+len(removes) == 0 {
381 return c.fail(protocol.ExitUsage, "usage: issue label <owner/name> <n> [--add <l>]... [--remove <l>]...") 380 return c.usage()
382 } 381 }
383 repo, issue, code := issueRef(c, rest, policy.CanWrite) 382 repo, issue, code := issueRef(c, rest, policy.CanWrite)
384 if code >= 0 { 383 if code >= 0 {
@@ -417,7 +416,7 @@ func runIssueAssign(c *Ctx, args []string) int {
417 return c.failInput(err) 416 return c.failInput(err)
418 } 417 }
419 if len(adds)+len(removes) == 0 { 418 if len(adds)+len(removes) == 0 {
420 return c.fail(protocol.ExitUsage, "usage: issue assign <owner/name> <n> [--add <user>]... [--remove <user>]...") 419 return c.usage()
421 } 420 }
422 repo, issue, code := issueRef(c, rest, policy.CanWrite) 421 repo, issue, code := issueRef(c, rest, policy.CanWrite)
423 if code >= 0 { 422 if code >= 0 {
internal/control/label.go +4 −5
@@ -31,7 +31,7 @@ var labelColorPat = regexp.MustCompile(`^#?[0-9a-fA-F]{6}$`)
31 31
32func runLabelList(c *Ctx, args []string) int { 32func runLabelList(c *Ctx, args []string) int {
33 if len(args) != 1 { 33 if len(args) != 1 {
34 return c.fail(protocol.ExitUsage, "usage: label list <owner/name>") 34 return c.usage()
35 } 35 }
36 repo, code := resolveRepo(c, args[0], policy.CanRead) 36 repo, code := resolveRepo(c, args[0], policy.CanRead)
37 if code >= 0 { 37 if code >= 0 {
@@ -53,15 +53,14 @@ func runLabelList(c *Ctx, args []string) int {
53} 53}
54 54
55func runLabelSet(c *Ctx, args []string) int { 55func runLabelSet(c *Ctx, args []string) int {
56 const usage = "usage: label set <owner/name> <label> [--color #rrggbb|'']" 56 f, err := parseFlags(args, flagSpec{Values: []string{"--color"}, MaxPos: -1, Usage: c.Cmd.Usage})
57 f, err := parseFlags(args, flagSpec{Values: []string{"--color"}, MaxPos: -1, Usage: usage})
58 if err != nil { 57 if err != nil {
59 return c.fail(protocol.ExitUsage, "%v", err) 58 return c.fail(protocol.ExitUsage, "%v", err)
60 } 59 }
61 rest := f.Pos 60 rest := f.Pos
62 color, colorSet := strings.ToLower(f.Value("--color")), f.Has("--color") 61 color, colorSet := strings.ToLower(f.Value("--color")), f.Has("--color")
63 if len(rest) != 2 { 62 if len(rest) != 2 {
64 return c.fail(protocol.ExitUsage, usage) 63 return c.usage()
65 } 64 }
66 if colorSet && color != "" { 65 if colorSet && color != "" {
67 if !labelColorPat.MatchString(color) { 66 if !labelColorPat.MatchString(color) {
@@ -103,7 +102,7 @@ func runLabelSet(c *Ctx, args []string) int {
103 102
104func runLabelRemove(c *Ctx, args []string) int { 103func runLabelRemove(c *Ctx, args []string) int {
105 if len(args) != 2 { 104 if len(args) != 2 {
106 return c.fail(protocol.ExitUsage, "usage: label remove <owner/name> <label>") 105 return c.usage()
107 } 106 }
108 repo, code := resolveRepo(c, args[0], policy.CanWrite) 107 repo, code := resolveRepo(c, args[0], policy.CanWrite)
109 if code >= 0 { 108 if code >= 0 {
internal/control/migrate.go +1 −1
@@ -81,7 +81,7 @@ type bundle struct {
81 81
82func runAccountExport(c *Ctx, args []string) int { 82func runAccountExport(c *Ctx, args []string) int {
83 if len(args) != 0 { 83 if len(args) != 0 {
84 return c.fail(protocol.ExitUsage, "usage: account export > bundle.json") 84 return c.usage()
85 } 85 }
86 b := bundle{Bundle: bundleVersion, Username: c.User.Username} 86 b := bundle{Bundle: bundleVersion, Username: c.User.Username}
87 b.Profile, _ = c.Store.OwnerProfile("user", c.User.ID) 87 b.Profile, _ = c.Store.OwnerProfile("user", c.User.ID)
internal/control/milestone.go +6 −6
@@ -48,7 +48,7 @@ func runMilestoneCreate(c *Ctx, args []string) int {
48 } 48 }
49 path, title, description, due := f.pos(0), f.pos(1), f.Value("--description"), f.Value("--due") 49 path, title, description, due := f.pos(0), f.pos(1), f.Value("--description"), f.Value("--due")
50 if path == "" || title == "" { 50 if path == "" || title == "" {
51 return c.fail(protocol.ExitUsage, "usage: milestone create <owner/name> <title> [--description <d>] [--due YYYY-MM-DD]") 51 return c.usage()
52 } 52 }
53 if due != "" && !duePat.MatchString(due) { 53 if due != "" && !duePat.MatchString(due) {
54 return c.fail(protocol.ExitUsage, "--due must be YYYY-MM-DD") 54 return c.fail(protocol.ExitUsage, "--due must be YYYY-MM-DD")
@@ -81,7 +81,7 @@ func runMilestoneList(c *Ctx, args []string) int {
81 state = f.Value("--state") 81 state = f.Value("--state")
82 } 82 }
83 if path == "" || (state != "open" && state != "closed" && state != "all") { 83 if path == "" || (state != "open" && state != "closed" && state != "all") {
84 return c.fail(protocol.ExitUsage, "usage: milestone list <owner/name> [--state open|closed|all]") 84 return c.usage()
85 } 85 }
86 repo, code := resolveRepo(c, path, policy.CanRead) 86 repo, code := resolveRepo(c, path, policy.CanRead)
87 if code >= 0 { 87 if code >= 0 {
@@ -137,7 +137,7 @@ func setMilestoneState(c *Ctx, args []string, state string) int {
137 verb = "reopen" 137 verb = "reopen"
138 } 138 }
139 if len(args) != 2 { 139 if len(args) != 2 {
140 return c.fail(protocol.ExitUsage, "usage: milestone %s <owner/name> <title>", verb) 140 return c.usage()
141 } 141 }
142 repo, code := resolveRepo(c, args[0], policy.CanWrite) 142 repo, code := resolveRepo(c, args[0], policy.CanWrite)
143 if code >= 0 { 143 if code >= 0 {
@@ -177,7 +177,7 @@ func runIssueMilestone(c *Ctx, args []string) int {
177 return code 177 return code
178 } 178 }
179 if len(args) != 3 { 179 if len(args) != 3 {
180 return c.fail(protocol.ExitUsage, "usage: issue milestone <owner/name> <n> <title|none>") 180 return c.usage()
181 } 181 }
182 return setItemMilestone(c, repo, "issue", issue.Number, args[2], func(id int64) error { 182 return setItemMilestone(c, repo, "issue", issue.Number, args[2], func(id int64) error {
183 return c.Store.SetIssueMilestone(issue.ID, id) 183 return c.Store.SetIssueMilestone(issue.ID, id)
@@ -193,7 +193,7 @@ func runMRMilestone(c *Ctx, args []string) int {
193 return code 193 return code
194 } 194 }
195 if len(args) != 3 { 195 if len(args) != 3 {
196 return c.fail(protocol.ExitUsage, "usage: mr milestone <owner/name> <n> <title|none>") 196 return c.usage()
197 } 197 }
198 return setItemMilestone(c, repo, "mr", mr.Number, args[2], func(id int64) error { 198 return setItemMilestone(c, repo, "mr", mr.Number, args[2], func(id int64) error {
199 return c.Store.SetMRMilestone(mr.ID, id) 199 return c.Store.SetMRMilestone(mr.ID, id)
@@ -232,7 +232,7 @@ func setItemMilestone(c *Ctx, repo store.Repo, noun string, number int64, title
232// runIssueTemplates lists .gitbay/issue-template*.md at the default branch. 232// runIssueTemplates lists .gitbay/issue-template*.md at the default branch.
233func runIssueTemplates(c *Ctx, args []string) int { 233func runIssueTemplates(c *Ctx, args []string) int {
234 if len(args) != 1 { 234 if len(args) != 1 {
235 return c.fail(protocol.ExitUsage, "usage: issue templates <owner/name>") 235 return c.usage()
236 } 236 }
237 repo, code := resolveRepo(c, args[0], policy.CanRead) 237 repo, code := resolveRepo(c, args[0], policy.CanRead)
238 if code >= 0 { 238 if code >= 0 {
internal/control/mirrorcmd.go +4 −4
@@ -39,7 +39,7 @@ func runMirrorAdd(c *Ctx, args []string) int {
39 path, urlArg := f.pos(0), f.pos(1) 39 path, urlArg := f.pos(0), f.pos(1)
40 direction, username, tokenStdin := f.Value("--direction"), f.Value("--username"), f.Has("--token-stdin") 40 direction, username, tokenStdin := f.Value("--direction"), f.Value("--username"), f.Has("--token-stdin")
41 if path == "" || urlArg == "" || (direction != "push" && direction != "pull") { 41 if path == "" || urlArg == "" || (direction != "push" && direction != "pull") {
42 return c.fail(protocol.ExitUsage, "usage: repo mirror add <owner/name> <https-url> --direction push|pull [--username <u>] [--token-stdin]") 42 return c.usage()
43 } 43 }
44 // The worker's git process dials this URL from the server: same SSRF 44 // The worker's git process dials this URL from the server: same SSRF
45 // surface as a webhook target, same rules. 45 // surface as a webhook target, same rules.
@@ -76,7 +76,7 @@ func runMirrorAdd(c *Ctx, args []string) int {
76 76
77func runMirrorList(c *Ctx, args []string) int { 77func runMirrorList(c *Ctx, args []string) int {
78 if len(args) != 1 { 78 if len(args) != 1 {
79 return c.fail(protocol.ExitUsage, "usage: repo mirror list <owner/name>") 79 return c.usage()
80 } 80 }
81 repo, code := resolveRepo(c, args[0], policy.CanAdmin) 81 repo, code := resolveRepo(c, args[0], policy.CanAdmin)
82 if code >= 0 { 82 if code >= 0 {
@@ -123,7 +123,7 @@ func orDash(s string) string {
123 123
124func runMirrorRemove(c *Ctx, args []string) int { 124func runMirrorRemove(c *Ctx, args []string) int {
125 if len(args) != 2 { 125 if len(args) != 2 {
126 return c.fail(protocol.ExitUsage, "usage: repo mirror remove <owner/name> <id>") 126 return c.usage()
127 } 127 }
128 repo, code := resolveRepo(c, args[0], policy.CanAdmin) 128 repo, code := resolveRepo(c, args[0], policy.CanAdmin)
129 if code >= 0 { 129 if code >= 0 {
@@ -146,7 +146,7 @@ func runMirrorRemove(c *Ctx, args []string) int {
146 146
147func runMirrorSync(c *Ctx, args []string) int { 147func runMirrorSync(c *Ctx, args []string) int {
148 if len(args) != 1 { 148 if len(args) != 1 {
149 return c.fail(protocol.ExitUsage, "usage: repo mirror sync <owner/name>") 149 return c.usage()
150 } 150 }
151 repo, code := resolveRepo(c, args[0], policy.CanAdmin) 151 repo, code := resolveRepo(c, args[0], policy.CanAdmin)
152 if code >= 0 { 152 if code >= 0 {
internal/control/mr.go +19 −26
@@ -104,7 +104,7 @@ func runRepoFork(c *Ctx, args []string) int {
104 } 104 }
105 path, name := f.pos(0), f.Value("--name") 105 path, name := f.pos(0), f.Value("--name")
106 if path == "" { 106 if path == "" {
107 return c.fail(protocol.ExitUsage, "usage: repo fork <owner/name> [--name <n>]") 107 return c.usage()
108 } 108 }
109 src, code := resolveRepo(c, path, policy.CanRead) 109 src, code := resolveRepo(c, path, policy.CanRead)
110 if code >= 0 { 110 if code >= 0 {
@@ -150,7 +150,7 @@ func runRepoFork(c *Ctx, args []string) int {
150 150
151func runRequireApprovals(c *Ctx, args []string) int { 151func runRequireApprovals(c *Ctx, args []string) int {
152 if len(args) != 2 { 152 if len(args) != 2 {
153 return c.fail(protocol.ExitUsage, "usage: repo settings require-approvals <owner/name> <n>") 153 return c.usage()
154 } 154 }
155 n, err := strconv.Atoi(args[1]) 155 n, err := strconv.Atoi(args[1])
156 if err != nil || n < 0 || n > 20 { 156 if err != nil || n < 0 || n > 20 {
@@ -171,7 +171,7 @@ func runRequireApprovals(c *Ctx, args []string) int {
171 171
172func runRequireResolved(c *Ctx, args []string) int { 172func runRequireResolved(c *Ctx, args []string) int {
173 if len(args) != 2 || (args[1] != "on" && args[1] != "off") { 173 if len(args) != 2 || (args[1] != "on" && args[1] != "off") {
174 return c.fail(protocol.ExitUsage, "usage: repo settings require-resolved <owner/name> on|off") 174 return c.usage()
175 } 175 }
176 repo, code := resolveRepo(c, args[0], policy.CanAdmin) 176 repo, code := resolveRepo(c, args[0], policy.CanAdmin)
177 if code >= 0 { 177 if code >= 0 {
@@ -188,7 +188,7 @@ func runRequireResolved(c *Ctx, args []string) int {
188 188
189func runRequireCodeowners(c *Ctx, args []string) int { 189func runRequireCodeowners(c *Ctx, args []string) int {
190 if len(args) != 2 || (args[1] != "on" && args[1] != "off") { 190 if len(args) != 2 || (args[1] != "on" && args[1] != "off") {
191 return c.fail(protocol.ExitUsage, "usage: repo settings require-codeowners <owner/name> on|off") 191 return c.usage()
192 } 192 }
193 repo, code := resolveRepo(c, args[0], policy.CanAdmin) 193 repo, code := resolveRepo(c, args[0], policy.CanAdmin)
194 if code >= 0 { 194 if code >= 0 {
@@ -205,7 +205,7 @@ func runRequireCodeowners(c *Ctx, args []string) int {
205 205
206func runRequireChecks(c *Ctx, args []string) int { 206func runRequireChecks(c *Ctx, args []string) int {
207 if len(args) != 2 || (args[1] != "on" && args[1] != "off") { 207 if len(args) != 2 || (args[1] != "on" && args[1] != "off") {
208 return c.fail(protocol.ExitUsage, "usage: repo settings require-checks <owner/name> on|off") 208 return c.usage()
209 } 209 }
210 repo, code := resolveRepo(c, args[0], policy.CanAdmin) 210 repo, code := resolveRepo(c, args[0], policy.CanAdmin)
211 if code >= 0 { 211 if code >= 0 {
@@ -222,7 +222,7 @@ func runRequireChecks(c *Ctx, args []string) int {
222 222
223func runRequireMR(c *Ctx, args []string) int { 223func runRequireMR(c *Ctx, args []string) int {
224 if len(args) != 2 || (args[1] != "on" && args[1] != "off") { 224 if len(args) != 2 || (args[1] != "on" && args[1] != "off") {
225 return c.fail(protocol.ExitUsage, "usage: repo settings require-mr <owner/name> on|off") 225 return c.usage()
226 } 226 }
227 repo, code := resolveRepo(c, args[0], policy.CanAdmin) 227 repo, code := resolveRepo(c, args[0], policy.CanAdmin)
228 if code >= 0 { 228 if code >= 0 {
@@ -239,7 +239,7 @@ func runRequireMR(c *Ctx, args []string) int {
239 239
240func runRequireSigned(c *Ctx, args []string) int { 240func runRequireSigned(c *Ctx, args []string) int {
241 if len(args) != 2 || (args[1] != "on" && args[1] != "off") { 241 if len(args) != 2 || (args[1] != "on" && args[1] != "off") {
242 return c.fail(protocol.ExitUsage, "usage: repo settings require-signed <owner/name> on|off") 242 return c.usage()
243 } 243 }
244 repo, code := resolveRepo(c, args[0], policy.CanAdmin) 244 repo, code := resolveRepo(c, args[0], policy.CanAdmin)
245 if code >= 0 { 245 if code >= 0 {
@@ -282,7 +282,7 @@ func runMRCreate(c *Ctx, args []string) int {
282 path, source, target := f.pos(0), f.Value("--source"), f.Value("--target") 282 path, source, target := f.pos(0), f.Value("--source"), f.Value("--target")
283 title, body, file, format := f.Value("--title"), f.Value("--body"), f.Value("--file"), f.Value("--format") 283 title, body, file, format := f.Value("--title"), f.Value("--body"), f.Value("--file"), f.Value("--format")
284 if path == "" || source == "" || title == "" { 284 if path == "" || source == "" || title == "" {
285 return c.fail(protocol.ExitUsage, "usage: mr create <target owner/name> --source [owner/name:]<branch> --target <branch> --title <t> [--draft]") 285 return c.usage()
286 } 286 }
287 fmtName, err := markupFormat(format) 287 fmtName, err := markupFormat(format)
288 if err != nil { 288 if err != nil {
@@ -441,9 +441,8 @@ func runMRList(c *Ctx, args []string) int {
441 if code >= 0 { 441 if code >= 0 {
442 return code 442 return code
443 } 443 }
444 const usage = "usage: mr list <owner/name> [--state open|merged|closed|source_gone|all] [--author <user>] [--milestone <title>|none] [--search <text>] [--limit <n>] [--cursor <c>]"
445 f := store.MRFilter{State: "open"} 444 f := store.MRFilter{State: "open"}
446 fl, err := parseFlags(args, flagSpec{Values: []string{"--state", "--author", "--milestone", "--search"}, MaxPos: 1, Usage: usage}) 445 fl, err := parseFlags(args, flagSpec{Values: []string{"--state", "--author", "--milestone", "--search"}, MaxPos: 1, Usage: c.Cmd.Usage})
447 if err != nil { 446 if err != nil {
448 return c.fail(protocol.ExitUsage, "%v", err) 447 return c.fail(protocol.ExitUsage, "%v", err)
449 } 448 }
@@ -460,7 +459,7 @@ func runMRList(c *Ctx, args []string) int {
460 } 459 }
461 valid := map[string]bool{"open": true, "merged": true, "closed": true, "source_gone": true, "all": true} 460 valid := map[string]bool{"open": true, "merged": true, "closed": true, "source_gone": true, "all": true}
462 if path == "" || !valid[f.State] { 461 if path == "" || !valid[f.State] {
463 return c.fail(protocol.ExitUsage, usage) 462 return c.usage()
464 } 463 }
465 repo, code := resolveRepo(c, path, policy.CanRead) 464 repo, code := resolveRepo(c, path, policy.CanRead)
466 if code >= 0 { 465 if code >= 0 {
@@ -510,7 +509,7 @@ func runMRShow(c *Ctx, args []string) int {
510 return code 509 return code
511 } 510 }
512 if len(args) != 2 { 511 if len(args) != 2 {
513 return c.fail(protocol.ExitUsage, "usage: mr show <owner/name> <n>") 512 return c.usage()
514 } 513 }
515 comments, err := c.Store.ListMRComments(mr.ID) 514 comments, err := c.Store.ListMRComments(mr.ID)
516 if err != nil { 515 if err != nil {
@@ -701,7 +700,7 @@ func runMREdit(c *Ctx, args []string) int {
701// same repository. 700// same repository.
702func runMRRetarget(c *Ctx, args []string) int { 701func runMRRetarget(c *Ctx, args []string) int {
703 if len(args) != 3 { 702 if len(args) != 3 {
704 return c.fail(protocol.ExitUsage, "usage: mr retarget <owner/name> <n> <branch>") 703 return c.usage()
705 } 704 }
706 repo, mr, code := mrRef(c, args[:2], policy.CanRead) 705 repo, mr, code := mrRef(c, args[:2], policy.CanRead)
707 if code >= 0 { 706 if code >= 0 {
@@ -779,12 +778,11 @@ func runMRReview(c *Ctx, args []string) int {
779 rest = append(rest, a) 778 rest = append(rest, a)
780 } 779 }
781 } 780 }
782 const usage = "mr review <owner/name> <n> --approve|--request-changes|--comment|--discard"
783 if discard && verdict != "" { 781 if discard && verdict != "" {
784 return c.fail(protocol.ExitUsage, "--discard throws the batch away; it takes no verdict") 782 return c.fail(protocol.ExitUsage, "--discard throws the batch away; it takes no verdict")
785 } 783 }
786 if verdict == "" && !discard { 784 if verdict == "" && !discard {
787 return c.fail(protocol.ExitUsage, "usage: %s", usage) 785 return c.usage()
788 } 786 }
789 repo, mr, code := mrRef(c, rest, policy.CanRead) 787 repo, mr, code := mrRef(c, rest, policy.CanRead)
790 if code >= 0 { 788 if code >= 0 {
@@ -848,7 +846,7 @@ func runMRReviewRequest(c *Ctx, args []string) int {
848 return c.failInput(err) 846 return c.failInput(err)
849 } 847 }
850 if len(adds)+len(removes) == 0 { 848 if len(adds)+len(removes) == 0 {
851 return c.fail(protocol.ExitUsage, "usage: mr review request <owner/name> <n> [--add <user>]... [--remove <user>]...") 849 return c.usage()
852 } 850 }
853 repo, mr, code := mrRef(c, rest, policy.CanWrite) 851 repo, mr, code := mrRef(c, rest, policy.CanWrite)
854 if code >= 0 { 852 if code >= 0 {
@@ -1404,10 +1402,6 @@ func runMRDraft(c *Ctx, args []string) int { return setMRDraft(c, args, true) }
1404func runMRReady(c *Ctx, args []string) int { return setMRDraft(c, args, false) } 1402func runMRReady(c *Ctx, args []string) int { return setMRDraft(c, args, false) }
1405 1403
1406func setMRDraft(c *Ctx, args []string, draft bool) int { 1404func setMRDraft(c *Ctx, args []string, draft bool) int {
1407 verb := "ready"
1408 if draft {
1409 verb = "draft"
1410 }
1411 repo, mr, code := mrRef(c, args, policy.CanRead) 1405 repo, mr, code := mrRef(c, args, policy.CanRead)
1412 if code >= 0 { 1406 if code >= 0 {
1413 return code 1407 return code
@@ -1416,7 +1410,7 @@ func setMRDraft(c *Ctx, args []string, draft bool) int {
1416 return code 1410 return code
1417 } 1411 }
1418 if len(args) != 2 { 1412 if len(args) != 2 {
1419 return c.fail(protocol.ExitUsage, "usage: mr %s <owner/name> <n>", verb) 1413 return c.usage()
1420 } 1414 }
1421 if code := authorOrWrite(c, repo, mr.Author, "change this merge request"); code >= 0 { 1415 if code := authorOrWrite(c, repo, mr.Author, "change this merge request"); code >= 0 {
1422 return code 1416 return code
@@ -1471,7 +1465,7 @@ func runMRClose(c *Ctx, args []string) int {
1471 return code 1465 return code
1472 } 1466 }
1473 if len(args) != 2 { 1467 if len(args) != 2 {
1474 return c.fail(protocol.ExitUsage, "usage: mr close <owner/name> <n>") 1468 return c.usage()
1475 } 1469 }
1476 if code := authorOrWrite(c, repo, mr.Author, "close this merge request"); code >= 0 { 1470 if code := authorOrWrite(c, repo, mr.Author, "close this merge request"); code >= 0 {
1477 return code 1471 return code
@@ -1532,7 +1526,7 @@ func runMRRevisions(c *Ctx, args []string) int {
1532 return code 1526 return code
1533 } 1527 }
1534 if len(args) != 2 { 1528 if len(args) != 2 {
1535 return c.fail(protocol.ExitUsage, "usage: mr revisions <owner/name> <n>") 1529 return c.usage()
1536 } 1530 }
1537 revs, err := mrRevisions(c, mr) 1531 revs, err := mrRevisions(c, mr)
1538 if err != nil { 1532 if err != nil {
@@ -1554,8 +1548,7 @@ func runMRRevisions(c *Ctx, args []string) int {
1554} 1548}
1555 1549
1556func runMRRangeDiff(c *Ctx, args []string) int { 1550func runMRRangeDiff(c *Ctx, args []string) int {
1557 const usage = "mr range-diff <owner/name> <n> [--from <sha>] [--to <sha>]" 1551 f, err := parseFlags(args, flagSpec{Values: []string{"--from", "--to"}, MaxPos: 2, Usage: c.Cmd.Usage})
1558 f, err := parseFlags(args, flagSpec{Values: []string{"--from", "--to"}, MaxPos: 2, Usage: usage})
1559 if err != nil { 1552 if err != nil {
1560 return c.fail(protocol.ExitUsage, "%v", err) 1553 return c.fail(protocol.ExitUsage, "%v", err)
1561 } 1554 }
@@ -1564,7 +1557,7 @@ func runMRRangeDiff(c *Ctx, args []string) int {
1564 return code 1557 return code
1565 } 1558 }
1566 if len(f.Pos) != 2 { 1559 if len(f.Pos) != 2 {
1567 return c.fail(protocol.ExitUsage, "usage: %s", usage) 1560 return c.usage()
1568 } 1561 }
1569 revs, err := mrRevisions(c, mr) 1562 revs, err := mrRevisions(c, mr)
1570 if err != nil { 1563 if err != nil {
internal/control/notifications.go +7 −9
@@ -168,7 +168,7 @@ func emitNotificationSettings(c *Ctx) int {
168 168
169func runNotificationsSettingsShow(c *Ctx, args []string) int { 169func runNotificationsSettingsShow(c *Ctx, args []string) int {
170 if len(args) != 0 { 170 if len(args) != 0 {
171 return c.fail(protocol.ExitUsage, "usage: notifications settings show") 171 return c.usage()
172 } 172 }
173 return emitNotificationSettings(c) 173 return emitNotificationSettings(c)
174} 174}
@@ -177,7 +177,7 @@ func runNotificationsSettingsShow(c *Ctx, args []string) int {
177// inbox is filed either way, the mail half consults it (#194). 177// inbox is filed either way, the mail half consults it (#194).
178func runNotificationsSettingsMail(c *Ctx, args []string) int { 178func runNotificationsSettingsMail(c *Ctx, args []string) int {
179 if len(args) != 1 || (args[0] != "on" && args[0] != "off") { 179 if len(args) != 1 || (args[0] != "on" && args[0] != "off") {
180 return c.fail(protocol.ExitUsage, "usage: notifications settings mail on|off") 180 return c.usage()
181 } 181 }
182 if err := c.Store.SetMailEnabled(c.User.ID, args[0] == "on"); err != nil { 182 if err := c.Store.SetMailEnabled(c.User.ID, args[0] == "on"); err != nil {
183 return c.fail(protocol.ExitFailure, "%v", err) 183 return c.fail(protocol.ExitFailure, "%v", err)
@@ -190,7 +190,7 @@ func runNotificationsSettingsMail(c *Ctx, args []string) int {
190// delivered, so a grant or a revoke needs no watch row of its own (#194). 190// delivered, so a grant or a revoke needs no watch row of its own (#194).
191func runNotificationsSettingsWatch(c *Ctx, args []string) int { 191func runNotificationsSettingsWatch(c *Ctx, args []string) int {
192 if len(args) != 1 || (args[0] != "on" && args[0] != "off") { 192 if len(args) != 1 || (args[0] != "on" && args[0] != "off") {
193 return c.fail(protocol.ExitUsage, "usage: notifications settings watch on|off") 193 return c.usage()
194 } 194 }
195 if err := c.Store.SetWatchEnabled(c.User.ID, args[0] == "on"); err != nil { 195 if err := c.Store.SetWatchEnabled(c.User.ID, args[0] == "on"); err != nil {
196 return c.fail(protocol.ExitFailure, "%v", err) 196 return c.fail(protocol.ExitFailure, "%v", err)
@@ -202,12 +202,11 @@ func runNotificationsSettingsWatch(c *Ctx, args []string) int {
202const noticesDefaultLimit = 50 202const noticesDefaultLimit = 50
203 203
204func runNotificationsList(c *Ctx, args []string) int { 204func runNotificationsList(c *Ctx, args []string) int {
205 const usage = "notifications list [--all] [--limit <n>] [--cursor <c>]"
206 rest, p, code := parsePageFlags(c, args, "notifications", true) 205 rest, p, code := parsePageFlags(c, args, "notifications", true)
207 if code >= 0 { 206 if code >= 0 {
208 return code 207 return code
209 } 208 }
210 fl, err := parseFlags(rest, flagSpec{Bools: []string{"--all"}, Usage: usage}) 209 fl, err := parseFlags(rest, flagSpec{Bools: []string{"--all"}, Usage: c.Cmd.Usage})
211 if err != nil { 210 if err != nil {
212 return c.fail(protocol.ExitUsage, "%v", err) 211 return c.fail(protocol.ExitUsage, "%v", err)
213 } 212 }
@@ -249,15 +248,14 @@ func runNotificationsList(c *Ctx, args []string) int {
249} 248}
250 249
251func runNotificationsRead(c *Ctx, args []string) int { 250func runNotificationsRead(c *Ctx, args []string) int {
252 const usage = "notifications read <id>... | --all" 251 fl, err := parseFlags(args, flagSpec{Bools: []string{"--all"}, MaxPos: -1, Usage: c.Cmd.Usage})
253 fl, err := parseFlags(args, flagSpec{Bools: []string{"--all"}, MaxPos: -1, Usage: usage})
254 if err != nil { 252 if err != nil {
255 return c.fail(protocol.ExitUsage, "%v", err) 253 return c.fail(protocol.ExitUsage, "%v", err)
256 } 254 }
257 // --all and a list of ids are two ways of saying which rows: taking 255 // --all and a list of ids are two ways of saying which rows: taking
258 // both would leave which one won unstated. 256 // both would leave which one won unstated.
259 if fl.Has("--all") == (len(fl.Pos) > 0) { 257 if fl.Has("--all") == (len(fl.Pos) > 0) {
260 return c.fail(protocol.ExitUsage, "usage: %s", usage) 258 return c.usage()
261 } 259 }
262 var ids []int64 260 var ids []int64
263 for _, a := range fl.Pos { 261 for _, a := range fl.Pos {
@@ -285,7 +283,7 @@ func runRepoUnwatch(c *Ctx, args []string) int { return setWatch(c, args, "unwat
285// same way. 283// same way.
286func setWatch(c *Ctx, args []string, verb, state string) int { 284func setWatch(c *Ctx, args []string, verb, state string) int {
287 if len(args) != 1 { 285 if len(args) != 1 {
288 return c.fail(protocol.ExitUsage, "usage: repo %s <owner/name>", verb) 286 return c.usage()
289 } 287 }
290 repo, code := resolveRepo(c, args[0], policy.CanRead) 288 repo, code := resolveRepo(c, args[0], policy.CanRead)
291 if code >= 0 { 289 if code >= 0 {
internal/control/org.go +9 −9
@@ -53,14 +53,14 @@ func orgAdmin(c *Ctx, name string) (store.Org, int) {
53 return org, c.fail(protocol.ExitFailure, "%v", err) 53 return org, c.fail(protocol.ExitFailure, "%v", err)
54 } 54 }
55 if role != "admin" { 55 if role != "admin" {
56 return org, c.fail(protocol.ExitDenied, "only admins of %s can do that", name) 56 return org, c.fail(protocol.ExitDenied, "only admins of %s can do that; ask one", name)
57 } 57 }
58 return org, -1 58 return org, -1
59} 59}
60 60
61func runOrgCreate(c *Ctx, args []string) int { 61func runOrgCreate(c *Ctx, args []string) int {
62 if len(args) != 1 { 62 if len(args) != 1 {
63 return c.fail(protocol.ExitUsage, "usage: org create <name>") 63 return c.usage()
64 } 64 }
65 if err := policy.ValidateOwnerName(args[0]); err != nil { 65 if err := policy.ValidateOwnerName(args[0]); err != nil {
66 return c.failInput(err) 66 return c.failInput(err)
@@ -95,7 +95,7 @@ func runOrgList(c *Ctx, args []string) int {
95 95
96func runOrgShow(c *Ctx, args []string) int { 96func runOrgShow(c *Ctx, args []string) int {
97 if len(args) != 1 { 97 if len(args) != 1 {
98 return c.fail(protocol.ExitUsage, "usage: org show <name>") 98 return c.usage()
99 } 99 }
100 org, err := c.Store.OrgByName(args[0]) 100 org, err := c.Store.OrgByName(args[0])
101 if errors.Is(err, store.ErrNotFound) { 101 if errors.Is(err, store.ErrNotFound) {
@@ -130,7 +130,7 @@ func runOrgShow(c *Ctx, args []string) int {
130 130
131func runOrgRename(c *Ctx, args []string) int { 131func runOrgRename(c *Ctx, args []string) int {
132 if len(args) != 2 { 132 if len(args) != 2 {
133 return c.fail(protocol.ExitUsage, "usage: org rename <old> <new>") 133 return c.usage()
134 } 134 }
135 org, code := orgAdmin(c, args[0]) 135 org, code := orgAdmin(c, args[0])
136 if code >= 0 { 136 if code >= 0 {
@@ -170,11 +170,11 @@ func runOrgDelete(c *Ctx, args []string) int {
170 } else if name == "" { 170 } else if name == "" {
171 name = a 171 name = a
172 } else { 172 } else {
173 return c.fail(protocol.ExitUsage, "usage: org delete <name> --yes") 173 return c.usage()
174 } 174 }
175 } 175 }
176 if name == "" { 176 if name == "" {
177 return c.fail(protocol.ExitUsage, "usage: org delete <name> --yes") 177 return c.usage()
178 } 178 }
179 org, code := orgAdmin(c, name) 179 org, code := orgAdmin(c, name)
180 if code >= 0 { 180 if code >= 0 {
@@ -201,7 +201,7 @@ func runOrgMembersAdd(c *Ctx, args []string) int {
201 role = f.Value("--role") 201 role = f.Value("--role")
202 } 202 }
203 if len(rest) != 2 || (role != "member" && role != "admin") { 203 if len(rest) != 2 || (role != "member" && role != "admin") {
204 return c.fail(protocol.ExitUsage, "usage: org members add <org> <user> [--role member|admin]") 204 return c.usage()
205 } 205 }
206 org, code := orgAdmin(c, rest[0]) 206 org, code := orgAdmin(c, rest[0])
207 if code >= 0 { 207 if code >= 0 {
@@ -224,7 +224,7 @@ func runOrgMembersAdd(c *Ctx, args []string) int {
224 224
225func runOrgMembersRemove(c *Ctx, args []string) int { 225func runOrgMembersRemove(c *Ctx, args []string) int {
226 if len(args) != 2 { 226 if len(args) != 2 {
227 return c.fail(protocol.ExitUsage, "usage: org members remove <org> <user>") 227 return c.usage()
228 } 228 }
229 org, code := orgAdmin(c, args[0]) 229 org, code := orgAdmin(c, args[0])
230 if code >= 0 { 230 if code >= 0 {
@@ -250,7 +250,7 @@ func runOrgMembersRemove(c *Ctx, args []string) int {
250 250
251func runOrgMembersList(c *Ctx, args []string) int { 251func runOrgMembersList(c *Ctx, args []string) int {
252 if len(args) != 1 { 252 if len(args) != 1 {
253 return c.fail(protocol.ExitUsage, "usage: org members list <org>") 253 return c.usage()
254 } 254 }
255 return runOrgShow(c, args) 255 return runOrgShow(c, args)
256} 256}
internal/control/orglabel.go +9 −11
@@ -56,21 +56,20 @@ func orgReader(c *Ctx, name string) (store.Org, []int64, int) {
56 return org, nil, c.fail(protocol.ExitFailure, "%v", err) 56 return org, nil, c.fail(protocol.ExitFailure, "%v", err)
57 } 57 }
58 if role == "" && len(readable) == 0 { 58 if role == "" && len(readable) == 0 {
59 return org, nil, c.fail(protocol.ExitDenied, "labels and milestones of %s are visible to its members", name) 59 return org, nil, c.fail(protocol.ExitDenied, "labels and milestones of %s are visible to its members; ask an admin to add you", name)
60 } 60 }
61 return org, readable, -1 61 return org, readable, -1
62} 62}
63 63
64func runOrgLabelSet(c *Ctx, args []string) int { 64func runOrgLabelSet(c *Ctx, args []string) int {
65 const usage = "usage: org label set <org> <label> [--color rrggbb|'']" 65 f, err := parseFlags(args, flagSpec{Values: []string{"--color"}, MaxPos: 2, Usage: c.Cmd.Usage})
66 f, err := parseFlags(args, flagSpec{Values: []string{"--color"}, MaxPos: 2, Usage: usage})
67 if err != nil { 66 if err != nil {
68 return c.fail(protocol.ExitUsage, "%v", err) 67 return c.fail(protocol.ExitUsage, "%v", err)
69 } 68 }
70 orgName, name := f.pos(0), f.pos(1) 69 orgName, name := f.pos(0), f.pos(1)
71 color, colorSet := strings.ToLower(f.Value("--color")), f.Has("--color") 70 color, colorSet := strings.ToLower(f.Value("--color")), f.Has("--color")
72 if orgName == "" || name == "" { 71 if orgName == "" || name == "" {
73 return c.fail(protocol.ExitUsage, usage) 72 return c.usage()
74 } 73 }
75 if name == "" || len(name) > 50 { 74 if name == "" || len(name) > 50 {
76 return c.fail(protocol.ExitUsage, "a label is 1 to 50 characters") 75 return c.fail(protocol.ExitUsage, "a label is 1 to 50 characters")
@@ -118,7 +117,7 @@ func runOrgLabelSet(c *Ctx, args []string) int {
118 117
119func runOrgLabelList(c *Ctx, args []string) int { 118func runOrgLabelList(c *Ctx, args []string) int {
120 if len(args) != 1 { 119 if len(args) != 1 {
121 return c.fail(protocol.ExitUsage, "usage: org label list <org>") 120 return c.usage()
122 } 121 }
123 org, readable, code := orgReader(c, args[0]) 122 org, readable, code := orgReader(c, args[0])
124 if code >= 0 { 123 if code >= 0 {
@@ -137,7 +136,7 @@ func runOrgLabelList(c *Ctx, args []string) int {
137 136
138func runOrgLabelRemove(c *Ctx, args []string) int { 137func runOrgLabelRemove(c *Ctx, args []string) int {
139 if len(args) != 2 { 138 if len(args) != 2 {
140 return c.fail(protocol.ExitUsage, "usage: org label remove <org> <label>") 139 return c.usage()
141 } 140 }
142 org, code := orgAdmin(c, args[0]) 141 org, code := orgAdmin(c, args[0])
143 if code >= 0 { 142 if code >= 0 {
@@ -155,14 +154,13 @@ func runOrgLabelRemove(c *Ctx, args []string) int {
155} 154}
156 155
157func runOrgMilestoneCreate(c *Ctx, args []string) int { 156func runOrgMilestoneCreate(c *Ctx, args []string) int {
158 const usage = "usage: org milestone create <org> <title> [--description <d>] [--due YYYY-MM-DD]" 157 f, err := parseFlags(args, flagSpec{Values: []string{"--description", "--due"}, MaxPos: 2, Usage: c.Cmd.Usage})
159 f, err := parseFlags(args, flagSpec{Values: []string{"--description", "--due"}, MaxPos: 2, Usage: usage})
160 if err != nil { 158 if err != nil {
161 return c.fail(protocol.ExitUsage, "%v", err) 159 return c.fail(protocol.ExitUsage, "%v", err)
162 } 160 }
163 orgName, title, description, due := f.pos(0), f.pos(1), f.Value("--description"), f.Value("--due") 161 orgName, title, description, due := f.pos(0), f.pos(1), f.Value("--description"), f.Value("--due")
164 if orgName == "" || title == "" { 162 if orgName == "" || title == "" {
165 return c.fail(protocol.ExitUsage, usage) 163 return c.usage()
166 } 164 }
167 if due != "" && !duePat.MatchString(due) { 165 if due != "" && !duePat.MatchString(due) {
168 return c.fail(protocol.ExitUsage, "--due must be YYYY-MM-DD") 166 return c.fail(protocol.ExitUsage, "--due must be YYYY-MM-DD")
@@ -197,7 +195,7 @@ func runOrgMilestoneList(c *Ctx, args []string) int {
197 state = f.Value("--state") 195 state = f.Value("--state")
198 } 196 }
199 if orgName == "" || (state != "open" && state != "closed" && state != "all") { 197 if orgName == "" || (state != "open" && state != "closed" && state != "all") {
200 return c.fail(protocol.ExitUsage, "usage: org milestone list <org> [--state open|closed|all]") 198 return c.usage()
201 } 199 }
202 org, readable, code := orgReader(c, orgName) 200 org, readable, code := orgReader(c, orgName)
203 if code >= 0 { 201 if code >= 0 {
@@ -219,7 +217,7 @@ func setOrgMilestoneState(c *Ctx, args []string, state string) int {
219 verb = "reopen" 217 verb = "reopen"
220 } 218 }
221 if len(args) != 2 { 219 if len(args) != 2 {
222 return c.fail(protocol.ExitUsage, "usage: org milestone %s <org> <title>", verb) 220 return c.usage()
223 } 221 }
224 org, code := orgAdmin(c, args[0]) 222 org, code := orgAdmin(c, args[0])
225 if code >= 0 { 223 if code >= 0 {
internal/control/pagescmd.go +4 −4
@@ -70,7 +70,7 @@ func challengeRecord(domain, token string) (name, value string) {
70 70
71func runDomainAdd(c *Ctx, args []string) int { 71func runDomainAdd(c *Ctx, args []string) int {
72 if len(args) != 2 { 72 if len(args) != 2 {
73 return c.fail(protocol.ExitUsage, "usage: repo domain add <owner/name> <domain>") 73 return c.usage()
74 } 74 }
75 domain := strings.ToLower(args[1]) 75 domain := strings.ToLower(args[1])
76 if err := validatePageDomain(c, domain); err != nil { 76 if err := validatePageDomain(c, domain); err != nil {
@@ -121,7 +121,7 @@ func lookupTXT(name string) ([]string, error) {
121 121
122func runDomainVerify(c *Ctx, args []string) int { 122func runDomainVerify(c *Ctx, args []string) int {
123 if len(args) != 2 { 123 if len(args) != 2 {
124 return c.fail(protocol.ExitUsage, "usage: repo domain verify <owner/name> <domain>") 124 return c.usage()
125 } 125 }
126 repo, code := resolveRepo(c, args[0], policy.CanAdmin) 126 repo, code := resolveRepo(c, args[0], policy.CanAdmin)
127 if code >= 0 { 127 if code >= 0 {
@@ -167,7 +167,7 @@ func runDomainVerify(c *Ctx, args []string) int {
167 167
168func runDomainRemove(c *Ctx, args []string) int { 168func runDomainRemove(c *Ctx, args []string) int {
169 if len(args) != 2 { 169 if len(args) != 2 {
170 return c.fail(protocol.ExitUsage, "usage: repo domain remove <owner/name> <domain>") 170 return c.usage()
171 } 171 }
172 repo, code := resolveRepo(c, args[0], policy.CanAdmin) 172 repo, code := resolveRepo(c, args[0], policy.CanAdmin)
173 if code >= 0 { 173 if code >= 0 {
@@ -187,7 +187,7 @@ func runDomainRemove(c *Ctx, args []string) int {
187 187
188func runDomainList(c *Ctx, args []string) int { 188func runDomainList(c *Ctx, args []string) int {
189 if len(args) != 1 { 189 if len(args) != 1 {
190 return c.fail(protocol.ExitUsage, "usage: repo domain list <owner/name>") 190 return c.usage()
191 } 191 }
192 repo, code := resolveRepo(c, args[0], policy.CanRead) 192 repo, code := resolveRepo(c, args[0], policy.CanRead)
193 if code >= 0 { 193 if code >= 0 {
internal/control/profile.go +3 −5
@@ -257,7 +257,7 @@ func runProfileShow(c *Ctx, args []string) int {
257 if len(args) == 1 { 257 if len(args) == 1 {
258 name = args[0] 258 name = args[0]
259 } else if len(args) > 1 { 259 } else if len(args) > 1 {
260 return c.fail(protocol.ExitUsage, "usage: profile show [name]") 260 return c.usage()
261 } 261 }
262 kind, id := "", int64(0) 262 kind, id := "", int64(0)
263 if u, err := c.Store.UserByUsername(name); err == nil { 263 if u, err := c.Store.UserByUsername(name); err == nil {
@@ -360,8 +360,7 @@ func runProfileSet(c *Ctx, args []string) int {
360 return c.failInput(err) 360 return c.failInput(err)
361 } 361 }
362 if len(rest) != 0 { 362 if len(rest) != 0 {
363 return c.fail(protocol.ExitUsage, 363 return c.usage()
364 "usage: profile set [--description <d>] [--website <url>] [--about <text>|--file -] [--about-format md|org] [--link <label|url>]...")
365 } 364 }
366 if e.empty() { 365 if e.empty() {
367 return c.fail(protocol.ExitUsage, "nothing to set: pass --description, --website, --about and/or --link") 366 return c.fail(protocol.ExitUsage, "nothing to set: pass --description, --website, --about and/or --link")
@@ -388,8 +387,7 @@ func runOrgProfile(c *Ctx, args []string) int {
388 return c.failInput(err) 387 return c.failInput(err)
389 } 388 }
390 if len(rest) != 1 { 389 if len(rest) != 1 {
391 return c.fail(protocol.ExitUsage, 390 return c.usage()
392 "usage: org profile <org> [--description <d>] [--website <url>] [--about <text>|--file -] [--about-format md|org] [--link <label|url>]...")
393 } 391 }
394 name := rest[0] 392 name := rest[0]
395 if e.empty() { 393 if e.empty() {
internal/control/quota.go +2 −2
@@ -93,7 +93,7 @@ func runAdminUserLimits(c *Ctx, args []string) int {
93 return code 93 return code
94 } 94 }
95 if len(args) < 1 { 95 if len(args) < 1 {
96 return c.fail(protocol.ExitUsage, "usage: admin user limits <username> [--repos <n>|default] [--bytes <n>|default]") 96 return c.usage()
97 } 97 }
98 u, err := c.Store.UserByUsername(args[0]) 98 u, err := c.Store.UserByUsername(args[0])
99 if err != nil { 99 if err != nil {
@@ -116,7 +116,7 @@ func runAdminUserLimits(c *Ctx, args []string) int {
116 case "--bytes": 116 case "--bytes":
117 target = &l.Bytes 117 target = &l.Bytes
118 default: 118 default:
119 return c.fail(protocol.ExitUsage, "usage: admin user limits <username> [--repos <n>|default] [--bytes <n>|default]") 119 return c.usage()
120 } 120 }
121 if v == "default" { 121 if v == "default" {
122 *target = nil 122 *target = nil
internal/control/read.go +8 −11
@@ -48,7 +48,7 @@ func init() {
48 48
49func runRepoRefs(c *Ctx, args []string) int { 49func runRepoRefs(c *Ctx, args []string) int {
50 if len(args) != 1 { 50 if len(args) != 1 {
51 return c.fail(protocol.ExitUsage, "usage: repo refs <owner/name>") 51 return c.usage()
52 } 52 }
53 repo, code := resolveRepo(c, args[0], policy.CanRead) 53 repo, code := resolveRepo(c, args[0], policy.CanRead)
54 if code >= 0 { 54 if code >= 0 {
@@ -93,8 +93,7 @@ func runRepoRefs(c *Ctx, args []string) int {
93const BlameSpan = 1000 93const BlameSpan = 1000
94 94
95func runRepoBlame(c *Ctx, args []string) int { 95func runRepoBlame(c *Ctx, args []string) int {
96 const usage = "repo blame <owner/name> <path> [--ref <ref>] [--from <n>] [--to <n>]" 96 f, err := parseFlags(args, flagSpec{Values: []string{"--ref", "--from", "--to"}, MaxPos: -1, Usage: c.Cmd.Usage})
97 f, err := parseFlags(args, flagSpec{Values: []string{"--ref", "--from", "--to"}, MaxPos: -1, Usage: usage})
98 if err != nil { 97 if err != nil {
99 return c.fail(protocol.ExitUsage, "%v", err) 98 return c.fail(protocol.ExitUsage, "%v", err)
100 } 99 }
@@ -111,7 +110,7 @@ func runRepoBlame(c *Ctx, args []string) int {
111 *dst = n 110 *dst = n
112 } 111 }
113 if len(rest) != 2 { 112 if len(rest) != 2 {
114 return c.fail(protocol.ExitUsage, "usage: %s", usage) 113 return c.usage()
115 } 114 }
116 repo, code := resolveRepo(c, rest[0], policy.CanRead) 115 repo, code := resolveRepo(c, rest[0], policy.CanRead)
117 if code >= 0 { 116 if code >= 0 {
@@ -204,7 +203,7 @@ func runRepoBlame(c *Ctx, args []string) int {
204// off argv. Positionals are returned in order so each command can name them 203// off argv. Positionals are returned in order so each command can name them
205// in its own usage message. 204// in its own usage message.
206func readArgs(c *Ctx, args []string, usage string, maxPos int) (pos []string, ref string, code int) { 205func readArgs(c *Ctx, args []string, usage string, maxPos int) (pos []string, ref string, code int) {
207 f, err := parseFlags(args, flagSpec{Values: []string{"--ref"}, MaxPos: maxPos, Usage: usage}) 206 f, err := parseFlags(args, flagSpec{Values: []string{"--ref"}, MaxPos: maxPos, Usage: c.Cmd.Usage})
208 if err != nil { 207 if err != nil {
209 return nil, "", c.fail(protocol.ExitUsage, "%v", err) 208 return nil, "", c.fail(protocol.ExitUsage, "%v", err)
210 } 209 }
@@ -239,13 +238,12 @@ type entryOut struct {
239} 238}
240 239
241func runRepoTree(c *Ctx, args []string) int { 240func runRepoTree(c *Ctx, args []string) int {
242 const usage = "repo tree <owner/name> [<path>] [--ref <ref>]" 241 pos, ref, code := readArgs(c, args, c.Cmd.Usage, 2)
243 pos, ref, code := readArgs(c, args, usage, 2)
244 if code >= 0 { 242 if code >= 0 {
245 return code 243 return code
246 } 244 }
247 if len(pos) == 0 { 245 if len(pos) == 0 {
248 return c.fail(protocol.ExitUsage, "usage: %s", usage) 246 return c.usage()
249 } 247 }
250 repo, code := resolveRepo(c, pos[0], policy.CanRead) 248 repo, code := resolveRepo(c, pos[0], policy.CanRead)
251 if code >= 0 { 249 if code >= 0 {
@@ -303,13 +301,12 @@ func sizeCol(e entryOut) string {
303} 301}
304 302
305func runRepoCat(c *Ctx, args []string) int { 303func runRepoCat(c *Ctx, args []string) int {
306 const usage = "repo cat <owner/name> <path> [--ref <ref>]" 304 pos, ref, code := readArgs(c, args, c.Cmd.Usage, 2)
307 pos, ref, code := readArgs(c, args, usage, 2)
308 if code >= 0 { 305 if code >= 0 {
309 return code 306 return code
310 } 307 }
311 if len(pos) != 2 { 308 if len(pos) != 2 {
312 return c.fail(protocol.ExitUsage, "usage: %s", usage) 309 return c.usage()
313 } 310 }
314 repo, code := resolveRepo(c, pos[0], policy.CanRead) 311 repo, code := resolveRepo(c, pos[0], policy.CanRead)
315 if code >= 0 { 312 if code >= 0 {
internal/control/register.go +5 −5
@@ -45,7 +45,7 @@ func init() {
45 45
46func runEmailList(c *Ctx, args []string) int { 46func runEmailList(c *Ctx, args []string) int {
47 if len(args) != 0 { 47 if len(args) != 0 {
48 return c.fail(protocol.ExitUsage, "usage: email list [--json]") 48 return c.usage()
49 } 49 }
50 emails, err := c.Store.ListEmails(c.User.ID) 50 emails, err := c.Store.ListEmails(c.User.ID)
51 if err != nil { 51 if err != nil {
@@ -89,7 +89,7 @@ func emailErr(c *Ctx, verb string, err error) int {
89 89
90func runEmailRemove(c *Ctx, args []string) int { 90func runEmailRemove(c *Ctx, args []string) int {
91 if len(args) != 1 { 91 if len(args) != 1 {
92 return c.fail(protocol.ExitUsage, "usage: email remove <address>") 92 return c.usage()
93 } 93 }
94 if err := c.Store.RemoveEmail(c.User.ID, args[0]); err != nil { 94 if err := c.Store.RemoveEmail(c.User.ID, args[0]); err != nil {
95 return emailErr(c, "removing address", err) 95 return emailErr(c, "removing address", err)
@@ -101,7 +101,7 @@ func runEmailRemove(c *Ctx, args []string) int {
101 101
102func runEmailPrimary(c *Ctx, args []string) int { 102func runEmailPrimary(c *Ctx, args []string) int {
103 if len(args) != 1 { 103 if len(args) != 1 {
104 return c.fail(protocol.ExitUsage, "usage: email primary <address>") 104 return c.usage()
105 } 105 }
106 if err := c.Store.SetPrimaryEmail(c.User.ID, args[0]); err != nil { 106 if err := c.Store.SetPrimaryEmail(c.User.ID, args[0]); err != nil {
107 return emailErr(c, "setting primary", err) 107 return emailErr(c, "setting primary", err)
@@ -136,7 +136,7 @@ const maxEmailAddsPerHour = 5
136 136
137func runEmailAdd(c *Ctx, args []string) int { 137func runEmailAdd(c *Ctx, args []string) int {
138 if len(args) != 1 || !strings.Contains(args[0], "@") { 138 if len(args) != 1 || !strings.Contains(args[0], "@") {
139 return c.fail(protocol.ExitUsage, "usage: email add <address>") 139 return c.usage()
140 } 140 }
141 if c.Cfg.Mail.SMTPHost == "" { 141 if c.Cfg.Mail.SMTPHost == "" {
142 return c.fail(protocol.ExitFailure, "this instance has no SMTP configured; ask an admin to verify the address (gitbayd admin email verify)") 142 return c.fail(protocol.ExitFailure, "this instance has no SMTP configured; ask an admin to verify the address (gitbayd admin email verify)")
@@ -161,7 +161,7 @@ func runEmailAdd(c *Ctx, args []string) int {
161 161
162func runEmailVerify(c *Ctx, args []string) int { 162func runEmailVerify(c *Ctx, args []string) int {
163 if len(args) != 1 { 163 if len(args) != 1 {
164 return c.fail(protocol.ExitUsage, "usage: email verify <code>") 164 return c.usage()
165 } 165 }
166 hash := store.HashToken(args[0]) 166 hash := store.HashToken(args[0])
167 address, err := c.Store.ConsumeEmailToken(c.User.ID, hash) 167 address, err := c.Store.ConsumeEmailToken(c.User.ID, hash)
internal/control/release.go +8 −10
@@ -76,15 +76,14 @@ func releaseRef(c *Ctx, args []string, perm func(store.User, store.Repo, string)
76} 76}
77 77
78func runReleaseCreate(c *Ctx, args []string) int { 78func runReleaseCreate(c *Ctx, args []string) int {
79 const usage = "usage: release create <owner/name> <tag> [--title <t>] [--notes <n> | --file -] [--format md|org]" 79 f, err := parseFlags(args, flagSpec{Values: []string{"--title", "--notes", "--file", "--format"}, MaxPos: 2, Usage: c.Cmd.Usage})
80 f, err := parseFlags(args, flagSpec{Values: []string{"--title", "--notes", "--file", "--format"}, MaxPos: 2, Usage: usage})
81 if err != nil { 80 if err != nil {
82 return c.fail(protocol.ExitUsage, "%v", err) 81 return c.fail(protocol.ExitUsage, "%v", err)
83 } 82 }
84 path, tag := f.pos(0), f.pos(1) 83 path, tag := f.pos(0), f.pos(1)
85 title, notes, file, format := f.Value("--title"), f.Value("--notes"), f.Value("--file"), f.Value("--format") 84 title, notes, file, format := f.Value("--title"), f.Value("--notes"), f.Value("--file"), f.Value("--format")
86 if path == "" || tag == "" { 85 if path == "" || tag == "" {
87 return c.fail(protocol.ExitUsage, usage) 86 return c.usage()
88 } 87 }
89 fmtName, err := markupFormat(format) 88 fmtName, err := markupFormat(format)
90 if err != nil { 89 if err != nil {
@@ -149,8 +148,7 @@ func releaseToOut(r store.Release, withNotes bool) releaseOut {
149} 148}
150 149
151func runReleaseEdit(c *Ctx, args []string) int { 150func runReleaseEdit(c *Ctx, args []string) int {
152 const usage = "usage: release edit <owner/name> <tag> [--title <t>] [--notes <n> | --file -] [--format md|org]" 151 f, err := parseFlags(args, flagSpec{Values: []string{"--title", "--notes", "--file", "--format"}, MaxPos: 2, Usage: c.Cmd.Usage})
153 f, err := parseFlags(args, flagSpec{Values: []string{"--title", "--notes", "--file", "--format"}, MaxPos: 2, Usage: usage})
154 if err != nil { 152 if err != nil {
155 return c.fail(protocol.ExitUsage, "%v", err) 153 return c.fail(protocol.ExitUsage, "%v", err)
156 } 154 }
@@ -162,7 +160,7 @@ func runReleaseEdit(c *Ctx, args []string) int {
162 return c.failInput(err) 160 return c.failInput(err)
163 } 161 }
164 if path == "" || tag == "" || (!setTitle && !setNotes && fmtName == "") { 162 if path == "" || tag == "" || (!setTitle && !setNotes && fmtName == "") {
165 return c.fail(protocol.ExitUsage, usage) 163 return c.usage()
166 } 164 }
167 repo, code := resolveRepo(c, path, policy.CanWrite) 165 repo, code := resolveRepo(c, path, policy.CanWrite)
168 if code >= 0 { 166 if code >= 0 {
@@ -200,7 +198,7 @@ func runReleaseEdit(c *Ctx, args []string) int {
200 198
201func runReleaseList(c *Ctx, args []string) int { 199func runReleaseList(c *Ctx, args []string) int {
202 if len(args) != 1 { 200 if len(args) != 1 {
203 return c.fail(protocol.ExitUsage, "usage: release list <owner/name>") 201 return c.usage()
204 } 202 }
205 repo, code := resolveRepo(c, args[0], policy.CanRead) 203 repo, code := resolveRepo(c, args[0], policy.CanRead)
206 if code >= 0 { 204 if code >= 0 {
@@ -267,7 +265,7 @@ func runReleaseDelete(c *Ctx, args []string) int {
267 265
268func runAssetAdd(c *Ctx, args []string) int { 266func runAssetAdd(c *Ctx, args []string) int {
269 if len(args) != 3 { 267 if len(args) != 3 {
270 return c.fail(protocol.ExitUsage, "usage: release asset add <owner/name> <tag> <filename> < file") 268 return c.usage()
271 } 269 }
272 repo, rel, code := releaseRef(c, args[:2], policy.CanWrite) 270 repo, rel, code := releaseRef(c, args[:2], policy.CanWrite)
273 if code >= 0 { 271 if code >= 0 {
@@ -319,7 +317,7 @@ func runAssetAdd(c *Ctx, args []string) int {
319 317
320func runAssetGet(c *Ctx, args []string) int { 318func runAssetGet(c *Ctx, args []string) int {
321 if len(args) != 3 { 319 if len(args) != 3 {
322 return c.fail(protocol.ExitUsage, "usage: release asset get <owner/name> <tag> <filename> > file") 320 return c.usage()
323 } 321 }
324 repo, rel, code := releaseRef(c, args[:2], policy.CanRead) 322 repo, rel, code := releaseRef(c, args[:2], policy.CanRead)
325 if code >= 0 { 323 if code >= 0 {
@@ -342,7 +340,7 @@ func runAssetGet(c *Ctx, args []string) int {
342 340
343func runAssetRemove(c *Ctx, args []string) int { 341func runAssetRemove(c *Ctx, args []string) int {
344 if len(args) != 3 { 342 if len(args) != 3 {
345 return c.fail(protocol.ExitUsage, "usage: release asset remove <owner/name> <tag> <filename>") 343 return c.usage()
346 } 344 }
347 repo, rel, code := releaseRef(c, args[:2], policy.CanWrite) 345 repo, rel, code := releaseRef(c, args[:2], policy.CanWrite)
348 if code >= 0 { 346 if code >= 0 {
internal/control/repo.go +32 −38
@@ -141,7 +141,7 @@ func validQuery(q string) error {
141// stay available so an archived repo can be managed and unarchived. 141// stay available so an archived repo can be managed and unarchived.
142func refuseArchived(c *Ctx, repo store.Repo) int { 142func refuseArchived(c *Ctx, repo store.Repo) int {
143 if repo.Settings.Archived { 143 if repo.Settings.Archived {
144 return c.fail(protocol.ExitDenied, "%s is archived and read-only", repo.Path()) 144 return c.fail(protocol.ExitDenied, "%s is archived and read-only; unarchive it first", repo.Path())
145 } 145 }
146 return -1 146 return -1
147} 147}
@@ -165,7 +165,7 @@ func resolveRepo(c *Ctx, path string, check func(store.User, store.Repo, string)
165 // Invisible repos 404, per the enumeration rule. 165 // Invisible repos 404, per the enumeration rule.
166 return repo, c.fail(protocol.ExitNotFound, "repository %s not found", path) 166 return repo, c.fail(protocol.ExitNotFound, "repository %s not found", path)
167 } 167 }
168 return repo, c.fail(protocol.ExitDenied, "permission denied on %s", path) 168 return repo, c.fail(protocol.ExitDenied, "permission denied on %s; ask its owner for access", path)
169 } 169 }
170 return repo, -1 170 return repo, -1
171} 171}
@@ -181,7 +181,7 @@ func runRepoCreate(c *Ctx, args []string) int {
181 } 181 }
182 owner, name, ok := strings.Cut(path, "/") 182 owner, name, ok := strings.Cut(path, "/")
183 if !ok { 183 if !ok {
184 return c.fail(protocol.ExitUsage, "usage: repo create <owner/name> [--private]") 184 return c.usage()
185 } 185 }
186 if err := policyValidateRepoName(name); err != nil { 186 if err := policyValidateRepoName(name); err != nil {
187 return c.failInput(err) 187 return c.failInput(err)
@@ -247,7 +247,7 @@ func runRepoList(c *Ctx, args []string) int {
247 return code 247 return code
248 } 248 }
249 if len(args) != 0 { 249 if len(args) != 0 {
250 return c.fail(protocol.ExitUsage, "usage: repo list [--limit <n>] [--cursor <c>]") 250 return c.usage()
251 } 251 }
252 repos, err := c.Store.ListReposForUser(c.User.ID, p.queryLimit(), p.key) 252 repos, err := c.Store.ListReposForUser(c.User.ID, p.queryLimit(), p.key)
253 if err != nil { 253 if err != nil {
@@ -278,7 +278,7 @@ func runRepoList(c *Ctx, args []string) int {
278 278
279func runRepoShow(c *Ctx, args []string) int { 279func runRepoShow(c *Ctx, args []string) int {
280 if len(args) != 1 { 280 if len(args) != 1 {
281 return c.fail(protocol.ExitUsage, "usage: repo show <owner/name>") 281 return c.usage()
282 } 282 }
283 repo, code := resolveRepo(c, args[0], policy.CanRead) 283 repo, code := resolveRepo(c, args[0], policy.CanRead)
284 if code >= 0 { 284 if code >= 0 {
@@ -393,7 +393,7 @@ func runRepoShow(c *Ctx, args []string) int {
393 393
394func runRepoTransfer(c *Ctx, args []string) int { 394func runRepoTransfer(c *Ctx, args []string) int {
395 if len(args) != 2 { 395 if len(args) != 2 {
396 return c.fail(protocol.ExitUsage, "usage: repo transfer <owner/name> <new-owner>") 396 return c.usage()
397 } 397 }
398 repo, code := resolveRepo(c, args[0], policy.CanAdmin) 398 repo, code := resolveRepo(c, args[0], policy.CanAdmin)
399 if code >= 0 { 399 if code >= 0 {
@@ -452,7 +452,7 @@ func runRepoTransfer(c *Ctx, args []string) int {
452 452
453func runRepoRename(c *Ctx, args []string) int { 453func runRepoRename(c *Ctx, args []string) int {
454 if len(args) != 2 { 454 if len(args) != 2 {
455 return c.fail(protocol.ExitUsage, "usage: repo rename <owner/name> <new-name>") 455 return c.usage()
456 } 456 }
457 repo, code := resolveRepo(c, args[0], policy.CanAdmin) 457 repo, code := resolveRepo(c, args[0], policy.CanAdmin)
458 if code >= 0 { 458 if code >= 0 {
@@ -496,11 +496,11 @@ func runRepoDelete(c *Ctx, args []string) int {
496 } else if path == "" { 496 } else if path == "" {
497 path = a 497 path = a
498 } else { 498 } else {
499 return c.fail(protocol.ExitUsage, "usage: repo delete <owner/name> --yes") 499 return c.usage()
500 } 500 }
501 } 501 }
502 if path == "" { 502 if path == "" {
503 return c.fail(protocol.ExitUsage, "usage: repo delete <owner/name> --yes") 503 return c.usage()
504 } 504 }
505 repo, code := resolveRepo(c, path, policy.CanAdmin) 505 repo, code := resolveRepo(c, path, policy.CanAdmin)
506 if code >= 0 { 506 if code >= 0 {
@@ -540,7 +540,7 @@ func deleteRepo(c *Ctx, repo store.Repo) int {
540 540
541func runAccessGrant(c *Ctx, args []string) int { 541func runAccessGrant(c *Ctx, args []string) int {
542 if len(args) != 3 || !slices.Contains([]string{"read", "write", "admin"}, args[2]) { 542 if len(args) != 3 || !slices.Contains([]string{"read", "write", "admin"}, args[2]) {
543 return c.fail(protocol.ExitUsage, "usage: repo access grant <owner/name> <user> read|write|admin") 543 return c.usage()
544 } 544 }
545 repo, code := resolveRepo(c, args[0], policy.CanAdmin) 545 repo, code := resolveRepo(c, args[0], policy.CanAdmin)
546 if code >= 0 { 546 if code >= 0 {
@@ -559,7 +559,7 @@ func runAccessGrant(c *Ctx, args []string) int {
559 559
560func runAccessRevoke(c *Ctx, args []string) int { 560func runAccessRevoke(c *Ctx, args []string) int {
561 if len(args) != 2 { 561 if len(args) != 2 {
562 return c.fail(protocol.ExitUsage, "usage: repo access revoke <owner/name> <user>") 562 return c.usage()
563 } 563 }
564 repo, code := resolveRepo(c, args[0], policy.CanAdmin) 564 repo, code := resolveRepo(c, args[0], policy.CanAdmin)
565 if code >= 0 { 565 if code >= 0 {
@@ -581,7 +581,7 @@ func runAccessRevoke(c *Ctx, args []string) int {
581 581
582func runAccessList(c *Ctx, args []string) int { 582func runAccessList(c *Ctx, args []string) int {
583 if len(args) != 1 { 583 if len(args) != 1 {
584 return c.fail(protocol.ExitUsage, "usage: repo access list <owner/name>") 584 return c.usage()
585 } 585 }
586 repo, code := resolveRepo(c, args[0], policy.CanAdmin) 586 repo, code := resolveRepo(c, args[0], policy.CanAdmin)
587 if code >= 0 { 587 if code >= 0 {
@@ -609,7 +609,7 @@ func runAccessList(c *Ctx, args []string) int {
609 609
610func runSettingsShow(c *Ctx, args []string) int { 610func runSettingsShow(c *Ctx, args []string) int {
611 if len(args) != 1 { 611 if len(args) != 1 {
612 return c.fail(protocol.ExitUsage, "usage: repo settings show <owner/name>") 612 return c.usage()
613 } 613 }
614 repo, code := resolveRepo(c, args[0], policy.CanAdmin) 614 repo, code := resolveRepo(c, args[0], policy.CanAdmin)
615 if code >= 0 { 615 if code >= 0 {
@@ -623,7 +623,7 @@ func runSettingsShow(c *Ctx, args []string) int {
623 623
624func runSetDescription(c *Ctx, args []string) int { 624func runSetDescription(c *Ctx, args []string) int {
625 if len(args) != 2 { 625 if len(args) != 2 {
626 return c.fail(protocol.ExitUsage, "usage: repo settings description <owner/name> <text>") 626 return c.usage()
627 } 627 }
628 repo, code := resolveRepo(c, args[0], policy.CanAdmin) 628 repo, code := resolveRepo(c, args[0], policy.CanAdmin)
629 if code >= 0 { 629 if code >= 0 {
@@ -640,7 +640,7 @@ func runSetDescription(c *Ctx, args []string) int {
640 640
641func runSetDefaultBranch(c *Ctx, args []string) int { 641func runSetDefaultBranch(c *Ctx, args []string) int {
642 if len(args) != 2 { 642 if len(args) != 2 {
643 return c.fail(protocol.ExitUsage, "usage: repo settings default-branch <owner/name> <branch>") 643 return c.usage()
644 } 644 }
645 repo, code := resolveRepo(c, args[0], policy.CanAdmin) 645 repo, code := resolveRepo(c, args[0], policy.CanAdmin)
646 if code >= 0 { 646 if code >= 0 {
@@ -664,7 +664,7 @@ func runSetDefaultBranch(c *Ctx, args []string) int {
664 664
665func runSetWebsite(c *Ctx, args []string) int { 665func runSetWebsite(c *Ctx, args []string) int {
666 if len(args) != 2 { 666 if len(args) != 2 {
667 return c.fail(protocol.ExitUsage, "usage: repo settings website <owner/name> <url>") 667 return c.usage()
668 } 668 }
669 site := strings.TrimSpace(args[1]) 669 site := strings.TrimSpace(args[1])
670 if err := validateWebsite(site); err != nil { 670 if err := validateWebsite(site); err != nil {
@@ -691,7 +691,7 @@ func runSetWebsite(c *Ctx, args []string) int {
691 691
692func runSetVisibility(c *Ctx, args []string) int { 692func runSetVisibility(c *Ctx, args []string) int {
693 if len(args) != 2 || (args[1] != "public" && args[1] != "private") { 693 if len(args) != 2 || (args[1] != "public" && args[1] != "private") {
694 return c.fail(protocol.ExitUsage, "usage: repo settings visibility <owner/name> public|private") 694 return c.usage()
695 } 695 }
696 repo, code := resolveRepo(c, args[0], policy.CanAdmin) 696 repo, code := resolveRepo(c, args[0], policy.CanAdmin)
697 if code >= 0 { 697 if code >= 0 {
@@ -724,7 +724,7 @@ func setRepoVisibility(c *Ctx, repo store.Repo, visibility string) int {
724 724
725func runGitDaemon(c *Ctx, args []string) int { 725func runGitDaemon(c *Ctx, args []string) int {
726 if len(args) != 2 || (args[1] != "on" && args[1] != "off") { 726 if len(args) != 2 || (args[1] != "on" && args[1] != "off") {
727 return c.fail(protocol.ExitUsage, "usage: repo settings git-daemon <owner/name> on|off") 727 return c.usage()
728 } 728 }
729 repo, code := resolveRepo(c, args[0], policy.CanAdmin) 729 repo, code := resolveRepo(c, args[0], policy.CanAdmin)
730 if code >= 0 { 730 if code >= 0 {
@@ -748,12 +748,8 @@ func runArchive(c *Ctx, args []string) int { return setArchived(c, args, true)
748func runUnarchive(c *Ctx, args []string) int { return setArchived(c, args, false) } 748func runUnarchive(c *Ctx, args []string) int { return setArchived(c, args, false) }
749 749
750func setArchived(c *Ctx, args []string, archived bool) int { 750func setArchived(c *Ctx, args []string, archived bool) int {
751 verb := "archive"
752 if !archived {
753 verb = "unarchive"
754 }
755 if len(args) != 1 { 751 if len(args) != 1 {
756 return c.fail(protocol.ExitUsage, "usage: repo %s <owner/name>", verb) 752 return c.usage()
757 } 753 }
758 repo, code := resolveRepo(c, args[0], policy.CanAdmin) 754 repo, code := resolveRepo(c, args[0], policy.CanAdmin)
759 if code >= 0 { 755 if code >= 0 {
@@ -782,7 +778,7 @@ func archiveRepo(c *Ctx, repo store.Repo, archived bool) int {
782 778
783func runTopicsList(c *Ctx, args []string) int { 779func runTopicsList(c *Ctx, args []string) int {
784 if len(args) != 1 { 780 if len(args) != 1 {
785 return c.fail(protocol.ExitUsage, "usage: repo topics <owner/name>") 781 return c.usage()
786 } 782 }
787 repo, code := resolveRepo(c, args[0], policy.CanRead) 783 repo, code := resolveRepo(c, args[0], policy.CanRead)
788 if code >= 0 { 784 if code >= 0 {
@@ -803,12 +799,8 @@ func runTopicsAdd(c *Ctx, args []string) int { return editTopics(c, args, tru
803func runTopicsRemove(c *Ctx, args []string) int { return editTopics(c, args, false) } 799func runTopicsRemove(c *Ctx, args []string) int { return editTopics(c, args, false) }
804 800
805func editTopics(c *Ctx, args []string, add bool) int { 801func editTopics(c *Ctx, args []string, add bool) int {
806 verb := "add"
807 if !add {
808 verb = "remove"
809 }
810 if len(args) < 2 { 802 if len(args) < 2 {
811 return c.fail(protocol.ExitUsage, "usage: repo topics %s <owner/name> <topic>...", verb) 803 return c.usage()
812 } 804 }
813 repo, code := resolveRepo(c, args[0], policy.CanAdmin) 805 repo, code := resolveRepo(c, args[0], policy.CanAdmin)
814 if code >= 0 { 806 if code >= 0 {
@@ -854,7 +846,9 @@ func editTopics(c *Ctx, args []string, add bool) int {
854 return c.fail(protocol.ExitFailure, "%v", err) 846 return c.fail(protocol.ExitFailure, "%v", err)
855 } 847 }
856 return c.emit(now, func(w io.Writer) { 848 return c.emit(now, func(w io.Writer) {
857 fmt.Fprintf(w, "topics on %s: %s\n", repo.Path(), strings.Join(now, ", ")) 849 for _, t := range now {
850 fmt.Fprintln(w, t)
851 }
858 }) 852 })
859} 853}
860 854
@@ -862,7 +856,7 @@ func editTopics(c *Ctx, args []string, add bool) int {
862// and topics of every repository the caller can see. 856// and topics of every repository the caller can see.
863func runRepoSearch(c *Ctx, args []string) int { 857func runRepoSearch(c *Ctx, args []string) int {
864 if len(args) != 1 { 858 if len(args) != 1 {
865 return c.fail(protocol.ExitUsage, "usage: repo search <query>") 859 return c.usage()
866 } 860 }
867 if err := validQuery(args[0]); err != nil { 861 if err := validQuery(args[0]); err != nil {
868 return c.failInput(err) 862 return c.failInput(err)
@@ -929,7 +923,7 @@ func runRepoGrep(c *Ctx, args []string) int {
929 } 923 }
930 path, query, ref := f.pos(0), f.pos(1), f.Value("--ref") 924 path, query, ref := f.pos(0), f.pos(1), f.Value("--ref")
931 if path == "" || query == "" { 925 if path == "" || query == "" {
932 return c.fail(protocol.ExitUsage, "usage: repo grep <owner/name> <query> [--ref <ref>]") 926 return c.usage()
933 } 927 }
934 if err := validQuery(query); err != nil { 928 if err := validQuery(query); err != nil {
935 return c.failInput(err) 929 return c.failInput(err)
@@ -974,7 +968,7 @@ func setPinned(c *Ctx, args []string, pin bool) int {
974 verb = "unpin" 968 verb = "unpin"
975 } 969 }
976 if len(args) != 1 { 970 if len(args) != 1 {
977 return c.fail(protocol.ExitUsage, "usage: repo %s <owner/name>", verb) 971 return c.usage()
978 } 972 }
979 repo, code := resolveRepo(c, args[0], policy.CanRead) 973 repo, code := resolveRepo(c, args[0], policy.CanRead)
980 if code >= 0 { 974 if code >= 0 {
@@ -1008,7 +1002,7 @@ func setBookmarked(c *Ctx, args []string, on bool) int {
1008 verb = "unbookmark" 1002 verb = "unbookmark"
1009 } 1003 }
1010 if len(args) != 1 { 1004 if len(args) != 1 {
1011 return c.fail(protocol.ExitUsage, "usage: repo %s <owner/name>", verb) 1005 return c.usage()
1012 } 1006 }
1013 repo, code := resolveRepo(c, args[0], policy.CanRead) 1007 repo, code := resolveRepo(c, args[0], policy.CanRead)
1014 if code >= 0 { 1008 if code >= 0 {
@@ -1040,7 +1034,7 @@ type BookmarkOut struct {
1040 1034
1041func runRepoBookmarks(c *Ctx, args []string) int { 1035func runRepoBookmarks(c *Ctx, args []string) int {
1042 if len(args) != 0 { 1036 if len(args) != 0 {
1043 return c.fail(protocol.ExitUsage, "usage: repo bookmarks") 1037 return c.usage()
1044 } 1038 }
1045 repos, err := c.Store.ListBookmarks(c.User.ID) 1039 repos, err := c.Store.ListBookmarks(c.User.ID)
1046 if err != nil { 1040 if err != nil {
@@ -1077,7 +1071,7 @@ func runUnprotectTag(c *Ctx, args []string) int { return setProtectTag(c, args,
1077 1071
1078func setProtectTag(c *Ctx, args []string, protect bool) int { 1072func setProtectTag(c *Ctx, args []string, protect bool) int {
1079 if len(args) != 2 { 1073 if len(args) != 2 {
1080 return c.fail(protocol.ExitUsage, "usage: repo settings protect-tag|unprotect-tag <owner/name> <glob>") 1074 return c.usage()
1081 } 1075 }
1082 glob := args[1] 1076 glob := args[1]
1083 if _, err := path.Match(glob, "x"); err != nil || glob == "" { 1077 if _, err := path.Match(glob, "x"); err != nil || glob == "" {
@@ -1114,7 +1108,7 @@ func runUnprotect(c *Ctx, args []string) int { return setProtect(c, args, false)
1114 1108
1115func setProtect(c *Ctx, args []string, protect bool) int { 1109func setProtect(c *Ctx, args []string, protect bool) int {
1116 if len(args) != 2 { 1110 if len(args) != 2 {
1117 return c.fail(protocol.ExitUsage, "usage: repo settings protect|unprotect <owner/name> <branch>") 1111 return c.usage()
1118 } 1112 }
1119 repo, code := resolveRepo(c, args[0], policy.CanAdmin) 1113 repo, code := resolveRepo(c, args[0], policy.CanAdmin)
1120 if code >= 0 { 1114 if code >= 0 {
@@ -1149,7 +1143,7 @@ func setProtect(c *Ctx, args []string, protect bool) int {
1149func runRepoDiff(c *Ctx, args []string) int { 1143func runRepoDiff(c *Ctx, args []string) int {
1150 f, err := parseFlags(args, flagSpec{MaxPos: 3, Usage: "repo diff <owner/name> <base> <head>"}) 1144 f, err := parseFlags(args, flagSpec{MaxPos: 3, Usage: "repo diff <owner/name> <base> <head>"})
1151 if err != nil || len(f.Pos) != 3 { 1145 if err != nil || len(f.Pos) != 3 {
1152 return c.fail(protocol.ExitUsage, "usage: repo diff <owner/name> <base> <head>") 1146 return c.usage()
1153 } 1147 }
1154 repo, code := resolveRepo(c, f.pos(0), policy.CanRead) 1148 repo, code := resolveRepo(c, f.pos(0), policy.CanRead)
1155 if code >= 0 { 1149 if code >= 0 {
internal/control/runnerrepo.go +3 −3
@@ -33,7 +33,7 @@ func init() {
33func runRepoRunnerAdd(c *Ctx, args []string) int { 33func runRepoRunnerAdd(c *Ctx, args []string) int {
34 f, err := parseFlags(args, flagSpec{MaxPos: 1, Usage: "repo runner add <owner/name> < key.pub"}) 34 f, err := parseFlags(args, flagSpec{MaxPos: 1, Usage: "repo runner add <owner/name> < key.pub"})
35 if err != nil || len(f.Pos) != 1 { 35 if err != nil || len(f.Pos) != 1 {
36 return c.fail(protocol.ExitUsage, "usage: repo runner add <owner/name> < key.pub") 36 return c.usage()
37 } 37 }
38 repo, code := resolveRepo(c, f.Pos[0], policy.CanAdmin) 38 repo, code := resolveRepo(c, f.Pos[0], policy.CanAdmin)
39 if code >= 0 { 39 if code >= 0 {
@@ -95,7 +95,7 @@ func runRepoRunnerAdd(c *Ctx, args []string) int {
95 95
96func runRepoRunnerList(c *Ctx, args []string) int { 96func runRepoRunnerList(c *Ctx, args []string) int {
97 if len(args) != 1 { 97 if len(args) != 1 {
98 return c.fail(protocol.ExitUsage, "usage: repo runner list <owner/name>") 98 return c.usage()
99 } 99 }
100 repo, code := resolveRepo(c, args[0], policy.CanAdmin) 100 repo, code := resolveRepo(c, args[0], policy.CanAdmin)
101 if code >= 0 { 101 if code >= 0 {
@@ -125,7 +125,7 @@ func runRepoRunnerList(c *Ctx, args []string) int {
125 125
126func runRepoRunnerRemove(c *Ctx, args []string) int { 126func runRepoRunnerRemove(c *Ctx, args []string) int {
127 if len(args) != 2 { 127 if len(args) != 2 {
128 return c.fail(protocol.ExitUsage, "usage: repo runner remove <owner/name> <fingerprint>") 128 return c.usage()
129 } 129 }
130 repo, code := resolveRepo(c, args[0], policy.CanAdmin) 130 repo, code := resolveRepo(c, args[0], policy.CanAdmin)
131 if code >= 0 { 131 if code >= 0 {
internal/control/search.go +2 −3
@@ -77,13 +77,12 @@ func Search(st *store.Store, root string, userID int64, q string, kinds []string
77} 77}
78 78
79func runSearch(c *Ctx, args []string) int { 79func runSearch(c *Ctx, args []string) int {
80 const usage = "search <query> [--kind repo|issue|mr]" 80 f, err := parseFlags(args, flagSpec{Multi: []string{"--kind"}, MaxPos: 1, Usage: c.Cmd.Usage})
81 f, err := parseFlags(args, flagSpec{Multi: []string{"--kind"}, MaxPos: 1, Usage: usage})
82 if err != nil { 81 if err != nil {
83 return c.fail(protocol.ExitUsage, "%v", err) 82 return c.fail(protocol.ExitUsage, "%v", err)
84 } 83 }
85 if len(f.Pos) != 1 { 84 if len(f.Pos) != 1 {
86 return c.fail(protocol.ExitUsage, "usage: %s", usage) 85 return c.usage()
87 } 86 }
88 if err := validQuery(f.Pos[0]); err != nil { 87 if err := validQuery(f.Pos[0]); err != nil {
89 return c.failInput(err) 88 return c.failInput(err)
internal/control/sig.go +4 −5
@@ -37,7 +37,7 @@ func init() {
37 37
38func runPGPAdd(c *Ctx, args []string) int { 38func runPGPAdd(c *Ctx, args []string) int {
39 if len(args) != 0 { 39 if len(args) != 0 {
40 return c.fail(protocol.ExitUsage, "usage: pgp add < key.asc") 40 return c.usage()
41 } 41 }
42 raw, err := io.ReadAll(io.LimitReader(c.Stdin, 1<<20)) 42 raw, err := io.ReadAll(io.LimitReader(c.Stdin, 1<<20))
43 if err != nil { 43 if err != nil {
@@ -88,7 +88,7 @@ func runPGPList(c *Ctx, args []string) int {
88 88
89func runPGPRemove(c *Ctx, args []string) int { 89func runPGPRemove(c *Ctx, args []string) int {
90 if len(args) != 1 { 90 if len(args) != 1 {
91 return c.fail(protocol.ExitUsage, "usage: pgp remove <fingerprint>") 91 return c.usage()
92 } 92 }
93 if err := c.Store.RemovePGPKey(c.User.ID, args[0]); err != nil { 93 if err := c.Store.RemovePGPKey(c.User.ID, args[0]); err != nil {
94 if errors.Is(err, store.ErrNotFound) { 94 if errors.Is(err, store.ErrNotFound) {
@@ -140,7 +140,7 @@ func runRepoLog(c *Ctx, args []string) int {
140 limit = n 140 limit = n
141 } 141 }
142 if path == "" { 142 if path == "" {
143 return c.fail(protocol.ExitUsage, "usage: repo log <owner/name> [--ref <r>] [--limit n] [--path <file>]") 143 return c.usage()
144 } 144 }
145 repo, code := resolveRepo(c, path, policy.CanRead) 145 repo, code := resolveRepo(c, path, policy.CanRead)
146 if code >= 0 { 146 if code >= 0 {
@@ -221,9 +221,8 @@ func runRepoLog(c *Ctx, args []string) int {
221// statuses, and its patch. The web's commit page read these straight from 221// statuses, and its patch. The web's commit page read these straight from
222// git, which is why no other surface could open a commit. 222// git, which is why no other surface could open a commit.
223func runRepoCommit(c *Ctx, args []string) int { 223func runRepoCommit(c *Ctx, args []string) int {
224 const usage = "repo commit <owner/name> <sha>"
225 if len(args) != 2 { 224 if len(args) != 2 {
226 return c.fail(protocol.ExitUsage, "usage: %s", usage) 225 return c.usage()
227 } 226 }
228 repo, code := resolveRepo(c, args[0], policy.CanRead) 227 repo, code := resolveRepo(c, args[0], policy.CanRead)
229 if code >= 0 { 228 if code >= 0 {
internal/control/snippet.go +11 −13
@@ -93,7 +93,7 @@ func snippetRef(c *Ctx, id string, write bool) (store.Snippet, int) {
93 return sn, c.fail(protocol.ExitNotFound, "no snippet %q", id) 93 return sn, c.fail(protocol.ExitNotFound, "no snippet %q", id)
94 } 94 }
95 if write && !policy.CanWriteSnippet(c.User, sn) { 95 if write && !policy.CanWriteSnippet(c.User, sn) {
96 return sn, c.fail(protocol.ExitDenied, "snippet %s belongs to %s", id, sn.OwnerName) 96 return sn, c.fail(protocol.ExitDenied, "snippet %s belongs to %s; only they can change it", id, sn.OwnerName)
97 } 97 }
98 return sn, -1 98 return sn, -1
99} 99}
@@ -132,14 +132,13 @@ func newSnippetID() string {
132} 132}
133 133
134func runSnippetCreate(c *Ctx, args []string) int { 134func runSnippetCreate(c *Ctx, args []string) int {
135 const usage = "usage: snippet create <filename> [--description <d>] [--visibility public|unlisted|private] < file" 135 f, err := parseFlags(args, flagSpec{Values: []string{"--description", "--visibility"}, MaxPos: 1, Usage: c.Cmd.Usage})
136 f, err := parseFlags(args, flagSpec{Values: []string{"--description", "--visibility"}, MaxPos: 1, Usage: usage})
137 if err != nil { 136 if err != nil {
138 return c.fail(protocol.ExitUsage, "%v", err) 137 return c.fail(protocol.ExitUsage, "%v", err)
139 } 138 }
140 name := f.pos(0) 139 name := f.pos(0)
141 if name == "" { 140 if name == "" {
142 return c.fail(protocol.ExitUsage, usage) 141 return c.usage()
143 } 142 }
144 if code := checkSnippetFileName(c, name); code >= 0 { 143 if code := checkSnippetFileName(c, name); code >= 0 {
145 return code 144 return code
@@ -186,7 +185,7 @@ func runSnippetCreate(c *Ctx, args []string) int {
186 185
187func runSnippetShow(c *Ctx, args []string) int { 186func runSnippetShow(c *Ctx, args []string) int {
188 if len(args) != 1 { 187 if len(args) != 1 {
189 return c.fail(protocol.ExitUsage, "usage: snippet show <id>") 188 return c.usage()
190 } 189 }
191 sn, code := snippetRef(c, args[0], false) 190 sn, code := snippetRef(c, args[0], false)
192 if code >= 0 { 191 if code >= 0 {
@@ -215,7 +214,7 @@ func runSnippetList(c *Ctx, args []string) int {
215 return code 214 return code
216 } 215 }
217 if len(rest) > 1 { 216 if len(rest) > 1 {
218 return c.fail(protocol.ExitUsage, "usage: snippet list [<owner>] [--limit n] [--cursor c]") 217 return c.usage()
219 } 218 }
220 owner := c.User 219 owner := c.User
221 if len(rest) == 1 { 220 if len(rest) == 1 {
@@ -253,13 +252,12 @@ func runSnippetList(c *Ctx, args []string) int {
253} 252}
254 253
255func runSnippetEdit(c *Ctx, args []string) int { 254func runSnippetEdit(c *Ctx, args []string) int {
256 const usage = "usage: snippet edit <id> [--description <d>] [--visibility public|unlisted|private]" 255 f, err := parseFlags(args, flagSpec{Values: []string{"--description", "--visibility"}, MaxPos: 1, Usage: c.Cmd.Usage})
257 f, err := parseFlags(args, flagSpec{Values: []string{"--description", "--visibility"}, MaxPos: 1, Usage: usage})
258 if err != nil { 256 if err != nil {
259 return c.fail(protocol.ExitUsage, "%v", err) 257 return c.fail(protocol.ExitUsage, "%v", err)
260 } 258 }
261 if f.pos(0) == "" || (!f.Has("--description") && !f.Has("--visibility")) { 259 if f.pos(0) == "" || (!f.Has("--description") && !f.Has("--visibility")) {
262 return c.fail(protocol.ExitUsage, usage) 260 return c.usage()
263 } 261 }
264 sn, code := snippetRef(c, f.pos(0), true) 262 sn, code := snippetRef(c, f.pos(0), true)
265 if code >= 0 { 263 if code >= 0 {
@@ -289,7 +287,7 @@ func runSnippetEdit(c *Ctx, args []string) int {
289 287
290func runSnippetDelete(c *Ctx, args []string) int { 288func runSnippetDelete(c *Ctx, args []string) int {
291 if len(args) != 1 { 289 if len(args) != 1 {
292 return c.fail(protocol.ExitUsage, "usage: snippet delete <id>") 290 return c.usage()
293 } 291 }
294 sn, code := snippetRef(c, args[0], true) 292 sn, code := snippetRef(c, args[0], true)
295 if code >= 0 { 293 if code >= 0 {
@@ -305,7 +303,7 @@ func runSnippetDelete(c *Ctx, args []string) int {
305 303
306func runSnippetFileSet(c *Ctx, args []string) int { 304func runSnippetFileSet(c *Ctx, args []string) int {
307 if len(args) != 2 { 305 if len(args) != 2 {
308 return c.fail(protocol.ExitUsage, "usage: snippet file set <id> <filename> < file") 306 return c.usage()
309 } 307 }
310 sn, code := snippetRef(c, args[0], true) 308 sn, code := snippetRef(c, args[0], true)
311 if code >= 0 { 309 if code >= 0 {
@@ -336,7 +334,7 @@ func runSnippetFileSet(c *Ctx, args []string) int {
336 334
337func runSnippetFileGet(c *Ctx, args []string) int { 335func runSnippetFileGet(c *Ctx, args []string) int {
338 if len(args) != 2 { 336 if len(args) != 2 {
339 return c.fail(protocol.ExitUsage, "usage: snippet file get <id> <filename> > file") 337 return c.usage()
340 } 338 }
341 sn, code := snippetRef(c, args[0], false) 339 sn, code := snippetRef(c, args[0], false)
342 if code >= 0 { 340 if code >= 0 {
@@ -360,7 +358,7 @@ func runSnippetFileGet(c *Ctx, args []string) int {
360 358
361func runSnippetFileRemove(c *Ctx, args []string) int { 359func runSnippetFileRemove(c *Ctx, args []string) int {
362 if len(args) != 2 { 360 if len(args) != 2 {
363 return c.fail(protocol.ExitUsage, "usage: snippet file remove <id> <filename>") 361 return c.usage()
364 } 362 }
365 sn, code := snippetRef(c, args[0], true) 363 sn, code := snippetRef(c, args[0], true)
366 if code >= 0 { 364 if code >= 0 {
internal/control/status.go +4 −8
@@ -55,7 +55,7 @@ func runStatusSet(c *Ctx, args []string) int {
55 } 55 }
56 } 56 }
57 if path == "" || sha == "" || context == "" || !validStatusState[state] { 57 if path == "" || sha == "" || context == "" || !validStatusState[state] {
58 return c.fail(protocol.ExitUsage, "usage: status set <owner/name> <sha> --context <c> --state pending|success|failure|error") 58 return c.usage()
59 } 59 }
60 if url != "" && !strings.HasPrefix(url, "https://") && !strings.HasPrefix(url, "http://") { 60 if url != "" && !strings.HasPrefix(url, "https://") && !strings.HasPrefix(url, "http://") {
61 return c.fail(protocol.ExitUsage, "--url must be http(s)") 61 return c.fail(protocol.ExitUsage, "--url must be http(s)")
@@ -86,7 +86,7 @@ func runStatusSet(c *Ctx, args []string) int {
86 86
87func runStatusList(c *Ctx, args []string) int { 87func runStatusList(c *Ctx, args []string) int {
88 if len(args) != 2 { 88 if len(args) != 2 {
89 return c.fail(protocol.ExitUsage, "usage: status list <owner/name> <sha>") 89 return c.usage()
90 } 90 }
91 repo, code := resolveRepo(c, args[0], policy.CanRead) 91 repo, code := resolveRepo(c, args[0], policy.CanRead)
92 if code >= 0 { 92 if code >= 0 {
@@ -118,13 +118,9 @@ func runStatusList(c *Ctx, args []string) int {
118 Statuses []out `json:"statuses"` 118 Statuses []out `json:"statuses"`
119 }{full, combinedOf(statuses), ds} 119 }{full, combinedOf(statuses), ds}
120 return c.emit(d, func(w io.Writer) { 120 return c.emit(d, func(w io.Writer) {
121 fmt.Fprintf(w, "%.10s: %s\n", d.SHA, orNone(d.Combined)) 121 fmt.Fprintf(w, "combined\t%s\t%.10s\n", orNone(d.Combined), d.SHA)
122 for _, x := range ds { 122 for _, x := range ds {
123 extra := "" 123 fmt.Fprintf(w, "%s\t%s\t%s\n", x.Context, x.State, x.Description)
124 if x.Description != "" {
125 extra = "\t" + x.Description
126 }
127 fmt.Fprintf(w, " %s\t%s%s\n", x.Context, x.State, extra)
128 } 124 }
129 }) 125 })
130} 126}
internal/control/teams.go +10 −10
@@ -53,7 +53,7 @@ func orgAdminRef(c *Ctx, name string) (store.Org, int) {
53 return org, c.fail(protocol.ExitFailure, "%v", err) 53 return org, c.fail(protocol.ExitFailure, "%v", err)
54 } 54 }
55 if role != "admin" { 55 if role != "admin" {
56 return org, c.fail(protocol.ExitDenied, "only admins of %s can manage teams", name) 56 return org, c.fail(protocol.ExitDenied, "only admins of %s can manage teams; ask one to add you", name)
57 } 57 }
58 return org, -1 58 return org, -1
59} 59}
@@ -71,7 +71,7 @@ func orgMemberRef(c *Ctx, name string) (store.Org, int) {
71 return org, c.fail(protocol.ExitFailure, "%v", err) 71 return org, c.fail(protocol.ExitFailure, "%v", err)
72 } 72 }
73 if role == "" { 73 if role == "" {
74 return org, c.fail(protocol.ExitDenied, "teams of %s are visible to its members", name) 74 return org, c.fail(protocol.ExitDenied, "teams of %s are visible to its members; ask an admin to add you", name)
75 } 75 }
76 return org, -1 76 return org, -1
77} 77}
@@ -89,7 +89,7 @@ func teamRef(c *Ctx, org store.Org, name string) (store.Team, int) {
89 89
90func runTeamCreate(c *Ctx, args []string) int { 90func runTeamCreate(c *Ctx, args []string) int {
91 if len(args) != 2 { 91 if len(args) != 2 {
92 return c.fail(protocol.ExitUsage, "usage: org team create <org> <team>") 92 return c.usage()
93 } 93 }
94 org, code := orgAdminRef(c, args[0]) 94 org, code := orgAdminRef(c, args[0])
95 if code >= 0 { 95 if code >= 0 {
@@ -108,7 +108,7 @@ func runTeamCreate(c *Ctx, args []string) int {
108 108
109func runTeamDelete(c *Ctx, args []string) int { 109func runTeamDelete(c *Ctx, args []string) int {
110 if len(args) != 2 { 110 if len(args) != 2 {
111 return c.fail(protocol.ExitUsage, "usage: org team delete <org> <team>") 111 return c.usage()
112 } 112 }
113 org, code := orgAdminRef(c, args[0]) 113 org, code := orgAdminRef(c, args[0])
114 if code >= 0 { 114 if code >= 0 {
@@ -128,7 +128,7 @@ func runTeamDelete(c *Ctx, args []string) int {
128 128
129func runTeamList(c *Ctx, args []string) int { 129func runTeamList(c *Ctx, args []string) int {
130 if len(args) != 1 { 130 if len(args) != 1 {
131 return c.fail(protocol.ExitUsage, "usage: org team list <org>") 131 return c.usage()
132 } 132 }
133 org, code := orgMemberRef(c, args[0]) 133 org, code := orgMemberRef(c, args[0])
134 if code >= 0 { 134 if code >= 0 {
@@ -151,7 +151,7 @@ func runTeamList(c *Ctx, args []string) int {
151 151
152func runTeamShow(c *Ctx, args []string) int { 152func runTeamShow(c *Ctx, args []string) int {
153 if len(args) != 2 { 153 if len(args) != 2 {
154 return c.fail(protocol.ExitUsage, "usage: org team show <org> <team>") 154 return c.usage()
155 } 155 }
156 org, code := orgMemberRef(c, args[0]) 156 org, code := orgMemberRef(c, args[0])
157 if code >= 0 { 157 if code >= 0 {
@@ -191,7 +191,7 @@ func editTeamMembers(c *Ctx, args []string, add bool) int {
191 verb = "remove" 191 verb = "remove"
192 } 192 }
193 if len(args) < 3 { 193 if len(args) < 3 {
194 return c.fail(protocol.ExitUsage, "usage: org team %s <org> <team> <user>...", verb) 194 return c.usage()
195 } 195 }
196 org, code := orgAdminRef(c, args[0]) 196 org, code := orgAdminRef(c, args[0])
197 if code >= 0 { 197 if code >= 0 {
@@ -233,7 +233,7 @@ func editTeamMembers(c *Ctx, args []string, add bool) int {
233 233
234func runTeamGrant(c *Ctx, args []string) int { 234func runTeamGrant(c *Ctx, args []string) int {
235 if len(args) != 4 || !slices.Contains([]string{"read", "write", "admin"}, args[3]) { 235 if len(args) != 4 || !slices.Contains([]string{"read", "write", "admin"}, args[3]) {
236 return c.fail(protocol.ExitUsage, "usage: org team grant <org> <team> <owner/name> read|write|admin") 236 return c.usage()
237 } 237 }
238 org, code := orgAdminRef(c, args[0]) 238 org, code := orgAdminRef(c, args[0])
239 if code >= 0 { 239 if code >= 0 {
@@ -257,7 +257,7 @@ func runTeamGrant(c *Ctx, args []string) int {
257 257
258func runTeamRevoke(c *Ctx, args []string) int { 258func runTeamRevoke(c *Ctx, args []string) int {
259 if len(args) != 3 { 259 if len(args) != 3 {
260 return c.fail(protocol.ExitUsage, "usage: org team revoke <org> <team> <owner/name>") 260 return c.usage()
261 } 261 }
262 org, code := orgAdminRef(c, args[0]) 262 org, code := orgAdminRef(c, args[0])
263 if code >= 0 { 263 if code >= 0 {
@@ -284,7 +284,7 @@ func runTeamRevoke(c *Ctx, args []string) int {
284 284
285func runOrgMembersRole(c *Ctx, args []string) int { 285func runOrgMembersRole(c *Ctx, args []string) int {
286 if len(args) != 2 || !slices.Contains([]string{"write", "read", "none"}, args[1]) { 286 if len(args) != 2 || !slices.Contains([]string{"write", "read", "none"}, args[1]) {
287 return c.fail(protocol.ExitUsage, "usage: org settings members-role <org> write|read|none") 287 return c.usage()
288 } 288 }
289 org, code := orgAdminRef(c, args[0]) 289 org, code := orgAdminRef(c, args[0])
290 if code >= 0 { 290 if code >= 0 {
internal/control/token.go +2 −2
@@ -48,7 +48,7 @@ func runTokenCreate(c *Ctx, args []string) int {
48 scope = f.Value("--scope") 48 scope = f.Value("--scope")
49 } 49 }
50 if name == "" || (scope != "full" && scope != "read") { 50 if name == "" || (scope != "full" && scope != "read") {
51 return c.fail(protocol.ExitUsage, "usage: token create --name <n> [--scope full|read] [--ttl 30d]") 51 return c.usage()
52 } 52 }
53 var expires *time.Time 53 var expires *time.Time
54 if ttl != "" { 54 if ttl != "" {
@@ -108,7 +108,7 @@ func runTokenList(c *Ctx, args []string) int {
108 108
109func runTokenRevoke(c *Ctx, args []string) int { 109func runTokenRevoke(c *Ctx, args []string) int {
110 if len(args) != 1 { 110 if len(args) != 1 {
111 return c.fail(protocol.ExitUsage, "usage: token revoke <name>") 111 return c.usage()
112 } 112 }
113 if err := c.Store.RevokeAPIToken(c.User.ID, args[0]); err != nil { 113 if err := c.Store.RevokeAPIToken(c.User.ID, args[0]); err != nil {
114 if errors.Is(err, store.ErrNotFound) { 114 if errors.Is(err, store.ErrNotFound) {
internal/control/web.go +3 −3
@@ -26,7 +26,7 @@ func init() {
26 26
27func runWebSessionsList(c *Ctx, args []string) int { 27func runWebSessionsList(c *Ctx, args []string) int {
28 if len(args) != 0 { 28 if len(args) != 0 {
29 return c.fail(protocol.ExitUsage, "usage: web sessions list") 29 return c.usage()
30 } 30 }
31 sessions, err := c.Store.ListWebSessions(c.User.ID) 31 sessions, err := c.Store.ListWebSessions(c.User.ID)
32 if err != nil { 32 if err != nil {
@@ -41,7 +41,7 @@ func runWebSessionsList(c *Ctx, args []string) int {
41 41
42func runWebSessionsRevoke(c *Ctx, args []string) int { 42func runWebSessionsRevoke(c *Ctx, args []string) int {
43 if len(args) != 1 { 43 if len(args) != 1 {
44 return c.fail(protocol.ExitUsage, "usage: web sessions revoke <id>|--all") 44 return c.usage()
45 } 45 }
46 if args[0] == "--all" { 46 if args[0] == "--all" {
47 n, err := c.Store.RevokeAllWebSessions(c.User.ID) 47 n, err := c.Store.RevokeAllWebSessions(c.User.ID)
@@ -65,7 +65,7 @@ func runWebSessionsRevoke(c *Ctx, args []string) int {
65 65
66func runWebLogin(c *Ctx, args []string) int { 66func runWebLogin(c *Ctx, args []string) int {
67 if len(args) != 0 { 67 if len(args) != 0 {
68 return c.fail(protocol.ExitUsage, "usage: web login [--json]") 68 return c.usage()
69 } 69 }
70 if c.Cfg.Web.Mode != "accounts" { 70 if c.Cfg.Web.Mode != "accounts" {
71 return c.fail(protocol.ExitDenied, 71 return c.fail(protocol.ExitDenied,
internal/control/webhook.go +5 −5
@@ -40,7 +40,7 @@ func runWebhookAdd(c *Ctx, args []string) int {
40 events = f.Value("--events") 40 events = f.Value("--events")
41 } 41 }
42 if path == "" || url == "" { 42 if path == "" || url == "" {
43 return c.fail(protocol.ExitUsage, "usage: webhook add <owner/name> <url> [--secret <s>] [--events <k1,k2>|*]") 43 return c.usage()
44 } 44 }
45 repo, code := resolveRepo(c, path, policy.CanAdmin) 45 repo, code := resolveRepo(c, path, policy.CanAdmin)
46 if code >= 0 { 46 if code >= 0 {
@@ -68,7 +68,7 @@ func runWebhookAdd(c *Ctx, args []string) int {
68 68
69func runWebhookList(c *Ctx, args []string) int { 69func runWebhookList(c *Ctx, args []string) int {
70 if len(args) != 1 { 70 if len(args) != 1 {
71 return c.fail(protocol.ExitUsage, "usage: webhook list <owner/name>") 71 return c.usage()
72 } 72 }
73 repo, code := resolveRepo(c, args[0], policy.CanAdmin) 73 repo, code := resolveRepo(c, args[0], policy.CanAdmin)
74 if code >= 0 { 74 if code >= 0 {
@@ -98,7 +98,7 @@ func runWebhookList(c *Ctx, args []string) int {
98 98
99func runWebhookRemove(c *Ctx, args []string) int { 99func runWebhookRemove(c *Ctx, args []string) int {
100 if len(args) != 2 { 100 if len(args) != 2 {
101 return c.fail(protocol.ExitUsage, "usage: webhook remove <owner/name> <id>") 101 return c.usage()
102 } 102 }
103 repo, code := resolveRepo(c, args[0], policy.CanAdmin) 103 repo, code := resolveRepo(c, args[0], policy.CanAdmin)
104 if code >= 0 { 104 if code >= 0 {
@@ -133,7 +133,7 @@ func runWebhookDeliveries(c *Ctx, args []string) int {
133 limit = n 133 limit = n
134 } 134 }
135 if path == "" { 135 if path == "" {
136 return c.fail(protocol.ExitUsage, "usage: webhook deliveries <owner/name> [--limit n]") 136 return c.usage()
137 } 137 }
138 repo, code := resolveRepo(c, path, policy.CanAdmin) 138 repo, code := resolveRepo(c, path, policy.CanAdmin)
139 if code >= 0 { 139 if code >= 0 {
@@ -169,7 +169,7 @@ func runWebhookDeliveries(c *Ctx, args []string) int {
169 169
170func runWebhookRedeliver(c *Ctx, args []string) int { 170func runWebhookRedeliver(c *Ctx, args []string) int {
171 if len(args) != 2 { 171 if len(args) != 2 {
172 return c.fail(protocol.ExitUsage, "usage: webhook redeliver <owner/name> <delivery-id>") 172 return c.usage()
173 } 173 }
174 repo, code := resolveRepo(c, args[0], policy.CanAdmin) 174 repo, code := resolveRepo(c, args[0], policy.CanAdmin)
175 if code >= 0 { 175 if code >= 0 {
internal/control/wiki.go +2 −2
@@ -79,7 +79,7 @@ func wikiPages(dir, branch string) []string {
79 79
80func runWikiList(c *Ctx, args []string) int { 80func runWikiList(c *Ctx, args []string) int {
81 if len(args) != 1 { 81 if len(args) != 1 {
82 return c.fail(protocol.ExitUsage, "usage: wiki list <owner/name>") 82 return c.usage()
83 } 83 }
84 repo, dir, branch, code := wikiDir(c, args[0]) 84 repo, dir, branch, code := wikiDir(c, args[0])
85 if code >= 0 { 85 if code >= 0 {
@@ -120,7 +120,7 @@ func wikiHome(pages []string) string {
120 120
121func runWikiShow(c *Ctx, args []string) int { 121func runWikiShow(c *Ctx, args []string) int {
122 if len(args) < 1 || len(args) > 2 { 122 if len(args) < 1 || len(args) > 2 {
123 return c.fail(protocol.ExitUsage, "usage: wiki show <owner/name> [<page>]") 123 return c.usage()
124 } 124 }
125 repo, dir, branch, code := wikiDir(c, args[0]) 125 repo, dir, branch, code := wikiDir(c, args[0])
126 if code >= 0 { 126 if code >= 0 {