Commit 94884a2232

94884a22329740d40876a987689c17ab77aa9965

parent: 2147a76cd5

Verified · cmc ci/build: success ci/test: success ci/vuln: success

cmc <hello@cleberg.net> · 2026-09-04 00:55 UTC

control: one flag parser for every command

Thirty-eight commands walked argv by hand and each decided on its own
whether an unknown --flag was an error, a positional or nothing at
all; mr review --approve --bogus reported repository --bogus as not
found. parseFlags reads argv against a spec of value flags, repeatable
flags, switches and a positional limit, and answers every mistake the
same way with the command's usage attached: unknown flag, missing
value, too many arguments. A value is taken verbatim, - included, and
-- ends flag parsing. Only the page-flag pre-pass keeps its own loop.

Closes #96

Layout: unified · split

internal/control/admin.go +13 −30
@@ -91,19 +91,12 @@ func runAdminUserList(c *Ctx, args []string) int {
9191 if code >= 0 {
9292 return code
9393 }
94 state := ""
95 for i := 0; i < len(args); i++ {
96 switch args[i] {
97 case "--state":
98 if i+1 >= len(args) {
99 return c.fail(protocol.ExitUsage, "--state requires active|pending|disabled|admin")
100 }
101 state = args[i+1]
102 i++
103 default:
104 return c.fail(protocol.ExitUsage, "usage: admin user list [--state active|pending|disabled|admin] [--limit <n>] [--cursor <c>]")
105 }
94 f, err := parseFlags(args, flagSpec{Values: []string{"--state"}, MaxPos: 0,
95 Usage: "admin user list [--state active|pending|disabled|admin] [--limit <n>] [--cursor <c>]"})
96 if err != nil {
97 return c.fail(protocol.ExitUsage, "%v", err)
10698 }
99 state := f.Value("--state")
107100 switch state {
108101 case "", "active", "pending", "disabled", "admin":
109102 default:
@@ -344,24 +337,14 @@ func runAdminRepoList(c *Ctx, args []string) int {
344337 if code >= 0 {
345338 return code
346339 }
347 var owner, visibility string
348 for i := 0; i < len(args); i++ {
349 switch args[i] {
350 case "--owner":
351 if i+1 >= len(args) {
352 return c.fail(protocol.ExitUsage, "--owner requires a value")
353 }
354 owner = args[i+1]
355 i++
356 case "--visibility":
357 if i+1 >= len(args) || (args[i+1] != "public" && args[i+1] != "private") {
358 return c.fail(protocol.ExitUsage, "--visibility requires public|private")
359 }
360 visibility = args[i+1]
361 i++
362 default:
363 return c.fail(protocol.ExitUsage, "usage: admin repo list [--owner <name>] [--visibility public|private] [--limit <n>] [--cursor <c>]")
364 }
340 f, err := parseFlags(args, flagSpec{Values: []string{"--owner", "--visibility"}, MaxPos: 0,
341 Usage: "admin repo list [--owner <name>] [--visibility public|private] [--limit <n>] [--cursor <c>]"})
342 if err != nil {
343 return c.fail(protocol.ExitUsage, "%v", err)
344 }
345 owner, visibility := f.Value("--owner"), f.Value("--visibility")
346 if visibility != "" && visibility != "public" && visibility != "private" {
347 return c.fail(protocol.ExitUsage, "--visibility requires public|private")
365348 }
366349 repos, err := c.Store.ListReposAdmin(owner, visibility, p.queryLimit(), p.key)
367350 if err != nil {
internal/control/adminhost.go +11 −26
@@ -56,32 +56,17 @@ func runAdminUserCreate(c *Ctx, args []string) int {
5656 return code
5757 }
5858 const usage = "usage: admin user create <username> [--admin] [--email <address> [--verified]] [--key -] < key.pub"
59 var username, email string
60 var isAdmin, verified, withKey bool
61 for i := 0; i < len(args); i++ {
62 switch args[i] {
63 case "--admin":
64 isAdmin = true
65 case "--verified":
66 verified = true
67 case "--email":
68 if i+1 >= len(args) {
69 return c.fail(protocol.ExitUsage, "--email requires a value")
70 }
71 email = args[i+1]
72 i++
73 case "--key":
74 if i+1 >= len(args) || args[i+1] != "-" {
75 return c.fail(protocol.ExitUsage, "--key only supports - (the public key on stdin)")
76 }
77 withKey = true
78 i++
79 default:
80 if username != "" || len(args[i]) == 0 || args[i][0] == '-' {
81 return c.fail(protocol.ExitUsage, usage)
82 }
83 username = args[i]
84 }
59 f, err := parseFlags(args, flagSpec{Values: []string{"--email", "--key"}, Bools: []string{"--admin", "--verified"}, MaxPos: 1, Usage: usage})
60 if err != nil {
61 return c.fail(protocol.ExitUsage, "%v", err)
62 }
63 username, email := f.pos(0), f.Value("--email")
64 isAdmin, verified, withKey := f.Has("--admin"), f.Has("--verified"), f.Has("--key")
65 if withKey && f.Value("--key") != "-" {
66 return c.fail(protocol.ExitUsage, "--key only supports - (the public key on stdin)")
67 }
68 if username == "" || username[0] == '-' {
69 return c.fail(protocol.ExitUsage, usage)
8570 }
8671 if username == "" || (verified && email == "") {
8772 return c.fail(protocol.ExitUsage, usage)
internal/control/audit.go +16 −24
@@ -25,32 +25,24 @@ func runAudit(c *Ctx, args []string) int {
2525 return c.fail(protocol.ExitDenied, "the audit log is for instance admins")
2626 }
2727 f := store.AuditFilter{Limit: 100}
28 for i := 0; i < len(args); i++ {
29 if i+1 >= len(args) {
30 return c.fail(protocol.ExitUsage, auditUsage)
28 fl, err := parseFlags(args, flagSpec{Values: []string{"--limit", "--actor", "--action", "--since"}, MaxPos: 0, Usage: auditUsage})
29 if err != nil {
30 return c.fail(protocol.ExitUsage, "%v", err)
31 }
32 if fl.Has("--limit") {
33 n, err := strconv.Atoi(fl.Value("--limit"))
34 if err != nil || n < 1 || n > 10000 {
35 return c.fail(protocol.ExitUsage, "--limit must be 1 to 10000")
3136 }
32 v := args[i+1]
33 switch args[i] {
34 case "--limit":
35 n, err := strconv.Atoi(v)
36 if err != nil || n < 1 || n > 10000 {
37 return c.fail(protocol.ExitUsage, "--limit must be 1 to 10000")
38 }
39 f.Limit = n
40 case "--actor":
41 f.Actor = v
42 case "--action":
43 f.ActionPrefix = v
44 case "--since":
45 t, ok := parseSince(v, time.Now())
46 if !ok {
47 return c.fail(protocol.ExitUsage, "--since takes a duration (30m, 24h, 7d) or a date (2026-09-01, RFC 3339)")
48 }
49 f.Since = t.UTC().Format("2006-01-02T15:04:05.000Z")
50 default:
51 return c.fail(protocol.ExitUsage, auditUsage)
37 f.Limit = n
38 }
39 f.Actor, f.ActionPrefix = fl.Value("--actor"), fl.Value("--action")
40 if fl.Has("--since") {
41 t, ok := parseSince(fl.Value("--since"), time.Now())
42 if !ok {
43 return c.fail(protocol.ExitUsage, "--since takes a duration (30m, 24h, 7d) or a date (2026-09-01, RFC 3339)")
5244 }
53 i++
45 f.Since = t.UTC().Format("2006-01-02T15:04:05.000Z")
5446 }
5547 entries, err := c.Store.AuditEntries(f)
5648 if err != nil {
internal/control/commitfile.go +5 −23
@@ -35,30 +35,12 @@ const maxCommitFileBytes = 1 << 20
3535// command rather than writing a commit its own policy would reject.
3636func runCommitFile(c *Ctx, args []string) int {
3737 const usage = "repo commit-file <owner/name> <path> --ref <branch> [--message <m>] [--file -]"
38 var rest []string
39 var ref, message, file string
40 for i := 0; i < len(args); i++ {
41 switch args[i] {
42 case "--ref", "--message", "--file":
43 if i+1 >= len(args) {
44 return c.fail(protocol.ExitUsage, "%s requires a value", args[i])
45 }
46 switch args[i] {
47 case "--ref":
48 ref = args[i+1]
49 case "--message":
50 message = args[i+1]
51 case "--file":
52 file = args[i+1]
53 }
54 i++
55 default:
56 if strings.HasPrefix(args[i], "--") {
57 return c.fail(protocol.ExitUsage, "unknown flag %q\nusage: %s", args[i], usage)
58 }
59 rest = append(rest, args[i])
60 }
38 f, err := parseFlags(args, flagSpec{Values: []string{"--ref", "--message", "--file"}, MaxPos: -1, Usage: usage})
39 if err != nil {
40 return c.fail(protocol.ExitUsage, "%v", err)
6141 }
42 rest := f.Pos
43 ref, message, file := f.Value("--ref"), f.Value("--message"), f.Value("--file")
6244 if len(rest) != 2 || ref == "" {
6345 return c.fail(protocol.ExitUsage, "usage: %s", usage)
6446 }
internal/control/diffcomment.go +19 −35
@@ -31,43 +31,27 @@ func init() {
3131}
3232
3333func runDiffComment(c *Ctx, args []string) int {
34 var rest []string
35 var path, message, file string
34 f, err := parseFlags(args, flagSpec{Values: []string{"--path", "--line", "--reply", "--message", "--file"}, Bools: []string{"--old"}, MaxPos: -1,
35 Usage: "mr diff-comment <owner/name> <n> --path <file> --line <l> [--old] [--reply <id>] [--message <m> | --file -]"})
36 if err != nil {
37 return c.fail(protocol.ExitUsage, "%v", err)
38 }
39 rest := f.Pos
40 path, message, file, old := f.Value("--path"), f.Value("--message"), f.Value("--file"), f.Has("--old")
3641 var line, replyTo int64
37 old := false
38 for i := 0; i < len(args); i++ {
39 switch args[i] {
40 case "--path", "--line", "--reply", "--message", "--file":
41 if i+1 >= len(args) {
42 return c.fail(protocol.ExitUsage, "%s requires a value", args[i])
43 }
44 v := args[i+1]
45 switch args[i] {
46 case "--path":
47 path = v
48 case "--line":
49 n, err := strconv.ParseInt(v, 10, 64)
50 if err != nil || n < 1 {
51 return c.fail(protocol.ExitUsage, "--line must be a positive number")
52 }
53 line = n
54 case "--reply":
55 n, err := strconv.ParseInt(v, 10, 64)
56 if err != nil || n < 1 {
57 return c.fail(protocol.ExitUsage, "--reply must be a thread id")
58 }
59 replyTo = n
60 case "--message":
61 message = v
62 case "--file":
63 file = v
64 }
65 i++
66 case "--old":
67 old = true
68 default:
69 rest = append(rest, args[i])
42 if f.Has("--line") {
43 n, err := strconv.ParseInt(f.Value("--line"), 10, 64)
44 if err != nil || n < 1 {
45 return c.fail(protocol.ExitUsage, "--line must be a positive number")
46 }
47 line = n
48 }
49 if f.Has("--reply") {
50 n, err := strconv.ParseInt(f.Value("--reply"), 10, 64)
51 if err != nil || n < 1 {
52 return c.fail(protocol.ExitUsage, "--reply must be a thread id")
7053 }
54 replyTo = n
7155 }
7256 repo, mr, code := mrRef(c, rest, policy.CanRead)
7357 if code >= 0 {
internal/control/explore.go +4 −13
@@ -80,20 +80,11 @@ func runExplore(c *Ctx, args []string) int {
8080// same bytes with a Content-Disposition on them.
8181func runRepoDownload(c *Ctx, args []string) int {
8282 const usage = "repo download <owner/name> [--ref <r>] > repo.tar.gz"
83 var rest []string
84 var ref string
85 for i := 0; i < len(args); i++ {
86 switch args[i] {
87 case "--ref":
88 if i+1 >= len(args) {
89 return c.fail(protocol.ExitUsage, "--ref requires a value")
90 }
91 ref = args[i+1]
92 i++
93 default:
94 rest = append(rest, args[i])
95 }
83 f, err := parseFlags(args, flagSpec{Values: []string{"--ref"}, MaxPos: -1, Usage: usage})
84 if err != nil {
85 return c.fail(protocol.ExitUsage, "%v", err)
9686 }
87 rest, ref := f.Pos, f.Value("--ref")
9788 if len(rest) != 1 {
9889 return c.fail(protocol.ExitUsage, "usage: %s", usage)
9990 }
internal/control/flags.go added +101
@@ -0,0 +1,101 @@
1package control
2
3import (
4 "fmt"
5 "strings"
6)
7
8// flagSpec is what a command accepts: flags that take one value, flags
9// that take a value and may repeat, switches, and how many positional
10// arguments are allowed (-1 for any number). Every command used to walk
11// argv by hand and decided on its own whether an unknown --flag was an
12// error, a positional or nothing at all (#96); parseFlags decides once.
13type flagSpec struct {
14 Values []string
15 Multi []string
16 Bools []string
17 MaxPos int
18 Usage string
19}
20
21// flags is a parsed argv: positionals in order, and each flag by name.
22type flags struct {
23 Pos []string
24 vals map[string]string
25 multi map[string][]string
26 seen map[string]bool
27}
28
29// Value is the last value given for a value flag, or "".
30func (f flags) Value(name string) string { return f.vals[name] }
31
32// Has reports whether a flag of any kind was given.
33func (f flags) Has(name string) bool { return f.seen[name] }
34
35// List is every value given for a repeatable flag, in order.
36func (f flags) List(name string) []string { return f.multi[name] }
37
38// parseFlags reads args against spec. A flag must be one the spec names;
39// a value flag consumes the next argument verbatim, "-" included; "--"
40// ends flag parsing. The error, when there is one, is the usage message.
41func parseFlags(args []string, spec flagSpec) (flags, error) {
42 f := flags{vals: map[string]string{}, multi: map[string][]string{}, seen: map[string]bool{}}
43 kind := map[string]byte{}
44 for _, n := range spec.Values {
45 kind[n] = 'v'
46 }
47 for _, n := range spec.Multi {
48 kind[n] = 'm'
49 }
50 for _, n := range spec.Bools {
51 kind[n] = 'b'
52 }
53 usage := func(format string, a ...any) error {
54 msg := fmt.Sprintf(format, a...)
55 if spec.Usage != "" {
56 msg += "\nusage: " + strings.TrimPrefix(spec.Usage, "usage: ")
57 }
58 return fmt.Errorf("%s", msg)
59 }
60 onlyPos := false
61 for i := 0; i < len(args); i++ {
62 a := args[i]
63 if !onlyPos && a == "--" {
64 onlyPos = true
65 continue
66 }
67 if !onlyPos && strings.HasPrefix(a, "--") {
68 switch kind[a] {
69 case 'b':
70 f.seen[a] = true
71 case 'v', 'm':
72 if i+1 >= len(args) {
73 return f, usage("%s requires a value", a)
74 }
75 f.seen[a] = true
76 if kind[a] == 'v' {
77 f.vals[a] = args[i+1]
78 } else {
79 f.multi[a] = append(f.multi[a], args[i+1])
80 }
81 i++
82 default:
83 return f, usage("unknown flag %q", a)
84 }
85 continue
86 }
87 if spec.MaxPos >= 0 && len(f.Pos) >= spec.MaxPos {
88 return f, usage("unexpected argument %q", a)
89 }
90 f.Pos = append(f.Pos, a)
91 }
92 return f, nil
93}
94
95// pos is the nth positional argument, or "" when absent.
96func (f flags) pos(n int) string {
97 if n < len(f.Pos) {
98 return f.Pos[n]
99 }
100 return ""
101}
internal/control/flags_test.go added +33
@@ -0,0 +1,33 @@
1package control
2
3import (
4 "strings"
5 "testing"
6)
7
8func TestParseFlags(t *testing.T) {
9 spec := flagSpec{Values: []string{"--title", "--file"}, Multi: []string{"--add"}, Bools: []string{"--private"}, MaxPos: 1, Usage: "cmd <owner/name> [--title <t>]"}
10 f, err := parseFlags([]string{"a/b", "--title", "-x", "--add", "one", "--private", "--add", "two", "--file", "-"}, spec)
11 if err != nil {
12 t.Fatal(err)
13 }
14 if f.pos(0) != "a/b" || f.Value("--title") != "-x" || f.Value("--file") != "-" || !f.Has("--private") ||
15 strings.Join(f.List("--add"), ",") != "one,two" || f.Has("--nope") || f.pos(1) != "" {
16 t.Fatalf("parsed wrong: %+v", f)
17 }
18 for _, bad := range [][]string{
19 {"a/b", "--bogus"}, // unknown flag
20 {"a/b", "--title"}, // missing value
21 {"a/b", "c/d"}, // too many positionals
22 {"--private", "a/b", "x"}, // too many, flags first
23 } {
24 if _, err := parseFlags(bad, spec); err == nil || !strings.Contains(err.Error(), "usage: cmd") {
25 t.Errorf("%v: err=%v", bad, err)
26 }
27 }
28 // "--" ends flags; MaxPos -1 takes any number.
29 f, err = parseFlags([]string{"--", "--title", "x"}, flagSpec{Values: []string{"--title"}, MaxPos: -1})
30 if err != nil || strings.Join(f.Pos, " ") != "--title x" || f.Has("--title") {
31 t.Fatalf("-- handling: %v %+v", err, f)
32 }
33}
internal/control/ghimport.go +5 −22
@@ -100,29 +100,12 @@ func attribution(src string, n int64, kind, login, date string) string {
100100}
101101
102102func runImportIssues(c *Ctx, args []string) int {
103 var path, from, apiBase string
104 tokenStdin := false
105 for i := 0; i < len(args); i++ {
106 switch args[i] {
107 case "--from", "--api-base":
108 if i+1 >= len(args) {
109 return c.fail(protocol.ExitUsage, "%s requires a value", args[i])
110 }
111 if args[i] == "--from" {
112 from = args[i+1]
113 } else {
114 apiBase = args[i+1]
115 }
116 i++
117 case "--token-stdin":
118 tokenStdin = true
119 default:
120 if path != "" {
121 return c.fail(protocol.ExitUsage, "unexpected argument %q", args[i])
122 }
123 path = args[i]
124 }
103 f, err := parseFlags(args, flagSpec{Values: []string{"--from", "--api-base"}, Bools: []string{"--token-stdin"}, MaxPos: 1,
104 Usage: "repo import-issues <owner/name> --from <owner/repo> [--api-base <url>] [--token-stdin]"})
105 if err != nil {
106 return c.fail(protocol.ExitUsage, "%v", err)
125107 }
108 path, from, apiBase, tokenStdin := f.pos(0), f.Value("--from"), f.Value("--api-base"), f.Has("--token-stdin")
126109 if path == "" || from == "" {
127110 return c.fail(protocol.ExitUsage, "usage: repo import-issues <owner/name> --from <ghowner/ghrepo> [--token-stdin]")
128111 }
internal/control/identity.go +6 −11
@@ -81,18 +81,13 @@ func runKeysList(c *Ctx, args []string) int {
8181}
8282
8383func runKeysAdd(c *Ctx, args []string) int {
84 f, err := parseFlags(args, flagSpec{Values: []string{"--scope"}, MaxPos: 0, Usage: "keys add [--scope full|git|runner] < key.pub"})
85 if err != nil {
86 return c.fail(protocol.ExitUsage, "%v", err)
87 }
8488 scope := "full"
85 for i := 0; i < len(args); i++ {
86 switch args[i] {
87 case "--scope":
88 if i+1 >= len(args) {
89 return c.fail(protocol.ExitUsage, "--scope requires a value")
90 }
91 scope = args[i+1]
92 i++
93 default:
94 return c.fail(protocol.ExitUsage, "usage: keys add [--scope full|git|runner] < key.pub")
95 }
89 if f.Has("--scope") {
90 scope = f.Value("--scope")
9691 }
9792 if scope != "full" && scope != "git" && scope != "runner" {
9893 // deploy:* scopes are granted via repo settings, not self-service.
internal/control/import.go +5 −21
@@ -33,28 +33,12 @@ esac
3333`
3434
3535func runRepoImport(c *Ctx, args []string) int {
36 var path, from string
37 private := false
38 tokenStdin := false
39 for i := 0; i < len(args); i++ {
40 switch args[i] {
41 case "--from":
42 if i+1 >= len(args) {
43 return c.fail(protocol.ExitUsage, "--from requires a URL")
44 }
45 from = args[i+1]
46 i++
47 case "--private":
48 private = true
49 case "--token-stdin":
50 tokenStdin = true
51 default:
52 if path != "" {
53 return c.fail(protocol.ExitUsage, "unexpected argument %q", args[i])
54 }
55 path = args[i]
56 }
36 f, err := parseFlags(args, flagSpec{Values: []string{"--from"}, Bools: []string{"--private", "--token-stdin"}, MaxPos: 1,
37 Usage: "repo import <owner/name> --from <url> [--private] [--token-stdin]"})
38 if err != nil {
39 return c.fail(protocol.ExitUsage, "%v", err)
5740 }
41 path, from, private, tokenStdin := f.pos(0), f.Value("--from"), f.Has("--private"), f.Has("--token-stdin")
5842 if path == "" || from == "" {
5943 return c.fail(protocol.ExitUsage, "usage: repo import <owner/name> --from <url> [--private] [--token-stdin]")
6044 }
internal/control/issue.go +30 −123
@@ -123,40 +123,12 @@ func issueToOut(i store.Issue, withBody bool) issueOut {
123123}
124124
125125func runIssueCreate(c *Ctx, args []string) int {
126 var path, title, body, file, format string
127 for i := 0; i < len(args); i++ {
128 switch args[i] {
129 case "--format":
130 if i+1 >= len(args) {
131 return c.fail(protocol.ExitUsage, "--format requires a value")
132 }
133 format = args[i+1]
134 i++
135 case "--title":
136 if i+1 >= len(args) {
137 return c.fail(protocol.ExitUsage, "--title requires a value")
138 }
139 title = args[i+1]
140 i++
141 case "--body":
142 if i+1 >= len(args) {
143 return c.fail(protocol.ExitUsage, "--body requires a value")
144 }
145 body = args[i+1]
146 i++
147 case "--file":
148 if i+1 >= len(args) {
149 return c.fail(protocol.ExitUsage, "--file requires a value")
150 }
151 file = args[i+1]
152 i++
153 default:
154 if path != "" {
155 return c.fail(protocol.ExitUsage, "unexpected argument %q", args[i])
156 }
157 path = args[i]
158 }
126 f, err := parseFlags(args, flagSpec{Values: []string{"--format", "--title", "--body", "--file"}, MaxPos: 1,
127 Usage: "issue create <owner/name> --title <t> [--body <b> | --file -] [--format md|org]"})
128 if err != nil {
129 return c.fail(protocol.ExitUsage, "%v", err)
159130 }
131 path, title, body, file, format := f.pos(0), f.Value("--title"), f.Value("--body"), f.Value("--file"), f.Value("--format")
160132 if path == "" || title == "" {
161133 return c.fail(protocol.ExitUsage, "usage: issue create <owner/name> --title <t> [--body <b> | --file -] [--format md|org]")
162134 }
@@ -200,33 +172,15 @@ func runIssueList(c *Ctx, args []string) int {
200172 }
201173 const usage = "usage: issue list <owner/name> [--state open|closed|all] [--label <l>] [--assignee <user>] [--author <user>] [--milestone <title>|none] [--limit <n>] [--cursor <c>]"
202174 f := store.IssueFilter{State: "open"}
203 var path string
204 for i := 0; i < len(args); i++ {
205 var target *string
206 switch args[i] {
207 case "--state":
208 target = &f.State
209 case "--label":
210 target = &f.Label
211 case "--assignee":
212 target = &f.Assignee
213 case "--author":
214 target = &f.Author
215 case "--milestone":
216 target = &f.Milestone
217 default:
218 if path != "" || strings.HasPrefix(args[i], "--") {
219 return c.fail(protocol.ExitUsage, "unexpected argument %q\n%s", args[i], usage)
220 }
221 path = args[i]
222 continue
223 }
224 if i+1 >= len(args) {
225 return c.fail(protocol.ExitUsage, "%s requires a value", args[i])
226 }
227 *target = args[i+1]
228 i++
175 fl, err := parseFlags(args, flagSpec{Values: []string{"--state", "--label", "--assignee", "--author", "--milestone"}, MaxPos: 1, Usage: usage})
176 if err != nil {
177 return c.fail(protocol.ExitUsage, "%v", err)
178 }
179 path := fl.pos(0)
180 if fl.Has("--state") {
181 f.State = fl.Value("--state")
229182 }
183 f.Label, f.Assignee, f.Author, f.Milestone = fl.Value("--label"), fl.Value("--assignee"), fl.Value("--author"), fl.Value("--milestone")
230184 if path == "" || (f.State != "open" && f.State != "closed" && f.State != "all") {
231185 return c.fail(protocol.ExitUsage, usage)
232186 }
@@ -298,32 +252,13 @@ func runIssueShow(c *Ctx, args []string) int {
298252}
299253
300254func runIssueComment(c *Ctx, args []string) int {
301 var rest []string
302 var message, file, format string
303 for i := 0; i < len(args); i++ {
304 switch args[i] {
305 case "--format":
306 if i+1 >= len(args) {
307 return c.fail(protocol.ExitUsage, "--format requires a value")
308 }
309 format = args[i+1]
310 i++
311 case "--message":
312 if i+1 >= len(args) {
313 return c.fail(protocol.ExitUsage, "--message requires a value")
314 }
315 message = args[i+1]
316 i++
317 case "--file":
318 if i+1 >= len(args) {
319 return c.fail(protocol.ExitUsage, "--file requires a value")
320 }
321 file = args[i+1]
322 i++
323 default:
324 rest = append(rest, args[i])
325 }
255 f, err := parseFlags(args, flagSpec{Values: []string{"--format", "--message", "--file"}, MaxPos: -1,
256 Usage: "issue comment <owner/name> <n> [--message <m> | --file -] [--format md|org]"})
257 if err != nil {
258 return c.fail(protocol.ExitUsage, "%v", err)
326259 }
260 rest := f.Pos
261 message, file, format := f.Value("--message"), f.Value("--file"), f.Value("--format")
327262 fmtName, err := markupFormat(format)
328263 if err != nil {
329264 return c.failErr(err)
@@ -398,29 +333,14 @@ func setIssueState(c *Ctx, args []string, state string) int {
398333// editText parses --title/--body/--file -/--format and authorizes: author or
399334// write. A nil format means the stored markup format stays as it is.
400335func editText(c *Ctx, args []string, kind string) (rest []string, title, body, format *string, code int) {
401 var titleV, bodyV, file, formatV string
402 haveTitle, haveBody := false, false
403 for i := 0; i < len(args); i++ {
404 switch args[i] {
405 case "--title", "--body", "--file", "--format":
406 if i+1 >= len(args) {
407 return nil, nil, nil, nil, c.fail(protocol.ExitUsage, "%s requires a value", args[i])
408 }
409 switch args[i] {
410 case "--title":
411 titleV, haveTitle = args[i+1], true
412 case "--body":
413 bodyV, haveBody = args[i+1], true
414 case "--file":
415 file = args[i+1]
416 case "--format":
417 formatV = args[i+1]
418 }
419 i++
420 default:
421 rest = append(rest, args[i])
422 }
336 f, err := parseFlags(args, flagSpec{Values: []string{"--title", "--body", "--file", "--format"}, MaxPos: -1,
337 Usage: kind + " edit <owner/name> <n> [--title <t>] [--body <b> | --file -] [--format md|org]"})
338 if err != nil {
339 return nil, nil, nil, nil, c.fail(protocol.ExitUsage, "%v", err)
423340 }
341 rest = f.Pos
342 titleV, bodyV, file, formatV := f.Value("--title"), f.Value("--body"), f.Value("--file"), f.Value("--format")
343 haveTitle, haveBody := f.Has("--title"), f.Has("--body")
424344 if file != "" {
425345 b, err := bodyFrom(c, "", file)
426346 if err != nil {
@@ -482,24 +402,11 @@ func runIssueReopen(c *Ctx, args []string) int { return setIssueState(c, args, "
482402
483403// addRemoveFlags parses repeated --add/--remove flags.
484404func addRemoveFlags(args []string) (rest, adds, removes []string, err error) {
485 for i := 0; i < len(args); i++ {
486 switch args[i] {
487 case "--add":
488 if i+1 >= len(args) {
489 return nil, nil, nil, errors.New("--add requires a value")
490 }
491 adds = append(adds, args[i+1])
492 i++
493 case "--remove":
494 if i+1 >= len(args) {
495 return nil, nil, nil, errors.New("--remove requires a value")
496 }
497 removes = append(removes, args[i+1])
498 i++
499 default:
500 rest = append(rest, args[i])
501 }
405 f, err := parseFlags(args, flagSpec{Multi: []string{"--add", "--remove"}, MaxPos: -1})
406 if err != nil {
407 return nil, nil, nil, err
502408 }
409 rest, adds, removes = f.Pos, f.List("--add"), f.List("--remove")
503410 return rest, adds, removes, nil
504411}
505412
internal/control/label.go +5 −12
@@ -50,19 +50,12 @@ func runLabelList(c *Ctx, args []string) int {
5050
5151func runLabelSet(c *Ctx, args []string) int {
5252 const usage = "usage: label set <owner/name> <label> [--color #rrggbb|'']"
53 var rest []string
54 color, colorSet := "", false
55 for i := 0; i < len(args); i++ {
56 if args[i] == "--color" {
57 if i+1 >= len(args) {
58 return c.fail(protocol.ExitUsage, "--color requires rrggbb, or '' to clear")
59 }
60 color, colorSet = strings.ToLower(args[i+1]), true
61 i++
62 continue
63 }
64 rest = append(rest, args[i])
53 f, err := parseFlags(args, flagSpec{Values: []string{"--color"}, MaxPos: -1, Usage: usage})
54 if err != nil {
55 return c.fail(protocol.ExitUsage, "%v", err)
6556 }
57 rest := f.Pos
58 color, colorSet := strings.ToLower(f.Value("--color")), f.Has("--color")
6659 if len(rest) != 2 {
6760 return c.fail(protocol.ExitUsage, usage)
6861 }
internal/control/migrate.go +4 −8
@@ -139,15 +139,11 @@ func migAttribution(src, kind, author, date string, n int64) string {
139139// the git push that follows cannot be refused by them. Resumable: markers
140140// skip everything already imported.
141141func runAccountImportBundle(c *Ctx, args []string) int {
142 var src string
143 for i := 0; i < len(args); i++ {
144 if args[i] == "--source" && i+1 < len(args) {
145 src = args[i+1]
146 i++
147 } else {
148 return c.fail(protocol.ExitUsage, "usage: account import-bundle [--source <host>] < bundle.json")
149 }
142 f, err := parseFlags(args, flagSpec{Values: []string{"--source"}, MaxPos: 0, Usage: "account import-bundle [--source <host>] < bundle.json"})
143 if err != nil {
144 return c.fail(protocol.ExitUsage, "%v", err)
150145 }
146 src := f.Value("--source")
151147 if src == "" {
152148 src = "the previous instance"
153149 }
internal/control/milestone.go +12 −36
@@ -41,29 +41,12 @@ func init() {
4141var duePat = regexp.MustCompile(`^\d{4}-\d{2}-\d{2}$`)
4242
4343func runMilestoneCreate(c *Ctx, args []string) int {
44 var path, title, description, due string
45 for i := 0; i < len(args); i++ {
46 switch args[i] {
47 case "--description", "--due":
48 if i+1 >= len(args) {
49 return c.fail(protocol.ExitUsage, "%s requires a value", args[i])
50 }
51 if args[i] == "--description" {
52 description = args[i+1]
53 } else {
54 due = args[i+1]
55 }
56 i++
57 default:
58 if path == "" {
59 path = args[i]
60 } else if title == "" {
61 title = args[i]
62 } else {
63 return c.fail(protocol.ExitUsage, "usage: milestone create <owner/name> <title> [--description <d>] [--due YYYY-MM-DD]")
64 }
65 }
44 f, err := parseFlags(args, flagSpec{Values: []string{"--description", "--due"}, MaxPos: 2,
45 Usage: "milestone create <owner/name> <title> [--description <d>] [--due YYYY-MM-DD]"})
46 if err != nil {
47 return c.fail(protocol.ExitUsage, "%v", err)
6648 }
49 path, title, description, due := f.pos(0), f.pos(1), f.Value("--description"), f.Value("--due")
6750 if path == "" || title == "" {
6851 return c.fail(protocol.ExitUsage, "usage: milestone create <owner/name> <title> [--description <d>] [--due YYYY-MM-DD]")
6952 }
@@ -86,20 +69,13 @@ func runMilestoneCreate(c *Ctx, args []string) int {
8669}
8770
8871func runMilestoneList(c *Ctx, args []string) int {
89 state := "open"
90 var path string
91 for i := 0; i < len(args); i++ {
92 if args[i] == "--state" {
93 if i+1 >= len(args) {
94 return c.fail(protocol.ExitUsage, "--state requires open|closed|all")
95 }
96 state = args[i+1]
97 i++
98 } else if path == "" {
99 path = args[i]
100 } else {
101 return c.fail(protocol.ExitUsage, "usage: milestone list <owner/name> [--state open|closed|all]")
102 }
72 f, err := parseFlags(args, flagSpec{Values: []string{"--state"}, MaxPos: 1, Usage: "milestone list <owner/name> [--state open|closed|all]"})
73 if err != nil {
74 return c.fail(protocol.ExitUsage, "%v", err)
75 }
76 state, path := "open", f.pos(0)
77 if f.Has("--state") {
78 state = f.Value("--state")
10379 }
10480 if path == "" || (state != "open" && state != "closed" && state != "all") {
10581 return c.fail(protocol.ExitUsage, "usage: milestone list <owner/name> [--state open|closed|all]")
internal/control/mirrorcmd.go +6 −25
@@ -31,32 +31,13 @@ func init() {
3131}
3232
3333func runMirrorAdd(c *Ctx, args []string) int {
34 var path, urlArg, direction, username string
35 tokenStdin := false
36 for i := 0; i < len(args); i++ {
37 switch args[i] {
38 case "--direction", "--username":
39 if i+1 >= len(args) {
40 return c.fail(protocol.ExitUsage, "%s requires a value", args[i])
41 }
42 if args[i] == "--direction" {
43 direction = args[i+1]
44 } else {
45 username = args[i+1]
46 }
47 i++
48 case "--token-stdin":
49 tokenStdin = true
50 default:
51 if path == "" {
52 path = args[i]
53 } else if urlArg == "" {
54 urlArg = args[i]
55 } else {
56 return c.fail(protocol.ExitUsage, "unexpected argument %q", args[i])
57 }
58 }
34 f, err := parseFlags(args, flagSpec{Values: []string{"--direction", "--username"}, Bools: []string{"--token-stdin"}, MaxPos: 2,
35 Usage: "repo mirror add <owner/name> <url> --direction push|pull [--username <u>] [--token-stdin]"})
36 if err != nil {
37 return c.fail(protocol.ExitUsage, "%v", err)
5938 }
39 path, urlArg := f.pos(0), f.pos(1)
40 direction, username, tokenStdin := f.Value("--direction"), f.Value("--username"), f.Has("--token-stdin")
6041 if path == "" || urlArg == "" || (direction != "push" && direction != "pull") {
6142 return c.fail(protocol.ExitUsage, "usage: repo mirror add <owner/name> <https-url> --direction push|pull [--username <u>] [--token-stdin]")
6243 }
internal/control/mr.go +28 −98
@@ -68,22 +68,11 @@ func init() {
6868}
6969
7070func runRepoFork(c *Ctx, args []string) int {
71 var path, name string
72 for i := 0; i < len(args); i++ {
73 switch args[i] {
74 case "--name":
75 if i+1 >= len(args) {
76 return c.fail(protocol.ExitUsage, "--name requires a value")
77 }
78 name = args[i+1]
79 i++
80 default:
81 if path != "" {
82 return c.fail(protocol.ExitUsage, "usage: repo fork <owner/name> [--name <n>]")
83 }
84 path = args[i]
85 }
71 f, err := parseFlags(args, flagSpec{Values: []string{"--name"}, MaxPos: 1, Usage: "repo fork <owner/name> [--name <n>]"})
72 if err != nil {
73 return c.fail(protocol.ExitUsage, "%v", err)
8674 }
75 path, name := f.pos(0), f.Value("--name")
8776 if path == "" {
8877 return c.fail(protocol.ExitUsage, "usage: repo fork <owner/name> [--name <n>]")
8978 }
@@ -231,36 +220,13 @@ func mrRef(c *Ctx, args []string, perm func(store.User, store.Repo, string) bool
231220func mrHeadRef(n int64) string { return fmt.Sprintf("refs/merge-requests/%d/head", n) }
232221
233222func runMRCreate(c *Ctx, args []string) int {
234 var path, source, target, title, body, file, format string
235 for i := 0; i < len(args); i++ {
236 switch args[i] {
237 case "--source", "--target", "--title", "--body", "--file", "--format":
238 if i+1 >= len(args) {
239 return c.fail(protocol.ExitUsage, "%s requires a value", args[i])
240 }
241 v := args[i+1]
242 switch args[i] {
243 case "--source":
244 source = v
245 case "--target":
246 target = v
247 case "--title":
248 title = v
249 case "--body":
250 body = v
251 case "--file":
252 file = v
253 case "--format":
254 format = v
255 }
256 i++
257 default:
258 if path != "" {
259 return c.fail(protocol.ExitUsage, "unexpected argument %q", args[i])
260 }
261 path = args[i]
262 }
223 f, err := parseFlags(args, flagSpec{Values: []string{"--source", "--target", "--title", "--body", "--file", "--format"}, MaxPos: 1,
224 Usage: "mr create <target owner/name> --source [owner/name:]<branch> --target <branch> --title <t>"})
225 if err != nil {
226 return c.fail(protocol.ExitUsage, "%v", err)
263227 }
228 path, source, target := f.pos(0), f.Value("--source"), f.Value("--target")
229 title, body, file, format := f.Value("--title"), f.Value("--body"), f.Value("--file"), f.Value("--format")
264230 if path == "" || source == "" || title == "" {
265231 return c.fail(protocol.ExitUsage, "usage: mr create <target owner/name> --source [owner/name:]<branch> --target <branch> --title <t>")
266232 }
@@ -415,29 +381,15 @@ func runMRList(c *Ctx, args []string) int {
415381 }
416382 const usage = "usage: mr list <owner/name> [--state open|merged|closed|source_gone|all] [--author <user>] [--milestone <title>|none] [--limit <n>] [--cursor <c>]"
417383 f := store.MRFilter{State: "open"}
418 var path string
419 for i := 0; i < len(args); i++ {
420 var target *string
421 switch args[i] {
422 case "--state":
423 target = &f.State
424 case "--author":
425 target = &f.Author
426 case "--milestone":
427 target = &f.Milestone
428 default:
429 if path != "" || strings.HasPrefix(args[i], "--") {
430 return c.fail(protocol.ExitUsage, "unexpected argument %q\n%s", args[i], usage)
431 }
432 path = args[i]
433 continue
434 }
435 if i+1 >= len(args) {
436 return c.fail(protocol.ExitUsage, "%s requires a value", args[i])
437 }
438 *target = args[i+1]
439 i++
384 fl, err := parseFlags(args, flagSpec{Values: []string{"--state", "--author", "--milestone"}, MaxPos: 1, Usage: usage})
385 if err != nil {
386 return c.fail(protocol.ExitUsage, "%v", err)
440387 }
388 path := fl.pos(0)
389 if fl.Has("--state") {
390 f.State = fl.Value("--state")
391 }
392 f.Author, f.Milestone = fl.Value("--author"), fl.Value("--milestone")
441393 valid := map[string]bool{"open": true, "merged": true, "closed": true, "source_gone": true, "all": true}
442394 if path == "" || !valid[f.State] {
443395 return c.fail(protocol.ExitUsage, usage)
@@ -728,27 +680,13 @@ func runMRRetarget(c *Ctx, args []string) int {
728680}
729681
730682func runMRComment(c *Ctx, args []string) int {
731 var rest []string
732 var message, file, format string
733 for i := 0; i < len(args); i++ {
734 switch args[i] {
735 case "--message", "--file", "--format":
736 if i+1 >= len(args) {
737 return c.fail(protocol.ExitUsage, "%s requires a value", args[i])
738 }
739 switch args[i] {
740 case "--message":
741 message = args[i+1]
742 case "--file":
743 file = args[i+1]
744 case "--format":
745 format = args[i+1]
746 }
747 i++
748 default:
749 rest = append(rest, args[i])
750 }
683 f, err := parseFlags(args, flagSpec{Values: []string{"--message", "--file", "--format"}, MaxPos: -1,
684 Usage: "mr comment <owner/name> <n> [--message <m> | --file -] [--format md|org]"})
685 if err != nil {
686 return c.fail(protocol.ExitUsage, "%v", err)
751687 }
688 rest := f.Pos
689 message, file, format := f.Value("--message"), f.Value("--file"), f.Value("--format")
752690 fmtName, err := markupFormat(format)
753691 if err != nil {
754692 return c.failErr(err)
@@ -824,19 +762,11 @@ func runMRReview(c *Ctx, args []string) int {
824762}
825763
826764func runMRMerge(c *Ctx, args []string) int {
827 strategy := ""
828 var rest []string
829 for i := 0; i < len(args); i++ {
830 if args[i] == "--strategy" {
831 if i+1 >= len(args) {
832 return c.fail(protocol.ExitUsage, "--strategy requires ff|merge|squash|rebase")
833 }
834 strategy = args[i+1]
835 i++
836 continue
837 }
838 rest = append(rest, args[i])
765 f, err := parseFlags(args, flagSpec{Values: []string{"--strategy"}, MaxPos: -1, Usage: "mr merge <owner/name> <n> [--strategy ff|merge|squash|rebase]"})
766 if err != nil {
767 return c.fail(protocol.ExitUsage, "%v", err)
839768 }
769 strategy, rest := f.Value("--strategy"), f.Pos
840770 valid := map[string]bool{"": true, "ff": true, "merge": true, "squash": true, "rebase": true}
841771 if !valid[strategy] {
842772 return c.fail(protocol.ExitUsage, "--strategy must be ff, merge, squash, or rebase")
internal/control/org.go +7 −12
@@ -192,18 +192,13 @@ func runOrgDelete(c *Ctx, args []string) int {
192192}
193193
194194func runOrgMembersAdd(c *Ctx, args []string) int {
195 role := "member"
196 var rest []string
197 for i := 0; i < len(args); i++ {
198 if args[i] == "--role" {
199 if i+1 >= len(args) {
200 return c.fail(protocol.ExitUsage, "--role requires member|admin")
201 }
202 role = args[i+1]
203 i++
204 continue
205 }
206 rest = append(rest, args[i])
195 f, err := parseFlags(args, flagSpec{Values: []string{"--role"}, MaxPos: -1, Usage: "org members add <org> <user> [--role member|admin]"})
196 if err != nil {
197 return c.fail(protocol.ExitUsage, "%v", err)
198 }
199 role, rest := "member", f.Pos
200 if f.Has("--role") {
201 role = f.Value("--role")
207202 }
208203 if len(rest) != 2 || (role != "member" && role != "admin") {
209204 return c.fail(protocol.ExitUsage, "usage: org members add <org> <user> [--role member|admin]")
internal/control/profile.go +36 −34
@@ -52,41 +52,43 @@ func parseProfileFlags(c *Ctx, args []string) (rest []string, e profileEdit, err
5252 about, file := "", ""
5353 sawAbout := false
5454 var links []store.ProfileLink
55 for i := 0; i < len(args); i++ {
56 switch args[i] {
57 case "--description", "--website", "--about", "--about-format", "--file", "--link":
58 if i+1 >= len(args) {
59 return nil, e, fmt.Errorf("%s requires a value", args[i])
60 }
61 v := args[i+1]
62 switch args[i] {
63 case "--description":
64 e.Description = &v
65 case "--website":
66 e.Website = &v
67 case "--about":
68 about, sawAbout = v, true
69 case "--about-format":
70 e.AboutFormat = &v
71 case "--file":
72 file, sawAbout = v, true
73 case "--link":
74 if v == "" {
75 links = nil
76 e.Links = &links
77 break
78 }
79 l, lerr := parseProfileLink(v)
80 if lerr != nil {
81 return nil, e, lerr
82 }
83 links = append(links, l)
84 e.Links = &links
85 }
86 i++
87 default:
88 rest = append(rest, args[i])
55 f, err := parseFlags(args, flagSpec{Values: []string{"--description", "--website", "--about", "--about-format", "--file"}, Multi: []string{"--link"}, MaxPos: -1})
56 if err != nil {
57 return nil, e, err
58 }
59 rest = f.Pos
60 for _, name := range []string{"--description", "--website", "--about-format"} {
61 if !f.Has(name) {
62 continue
63 }
64 v := f.Value(name)
65 switch name {
66 case "--description":
67 e.Description = &v
68 case "--website":
69 e.Website = &v
70 case "--about-format":
71 e.AboutFormat = &v
72 }
73 }
74 if f.Has("--about") {
75 about, sawAbout = f.Value("--about"), true
76 }
77 if f.Has("--file") {
78 file, sawAbout = f.Value("--file"), true
79 }
80 for _, v := range f.List("--link") {
81 if v == "" {
82 links = nil
83 e.Links = &links
84 continue
85 }
86 l, lerr := parseProfileLink(v)
87 if lerr != nil {
88 return nil, e, lerr
8989 }
90 links = append(links, l)
91 e.Links = &links
9092 }
9193 if sawAbout {
9294 body, berr := bodyFrom(c, about, file)
internal/control/read.go +17 −45
@@ -94,36 +94,21 @@ const BlameSpan = 1000
9494
9595func runRepoBlame(c *Ctx, args []string) int {
9696 const usage = "repo blame <owner/name> <path> [--ref <ref>] [--from <n>] [--to <n>]"
97 var rest []string
98 var ref string
97 f, err := parseFlags(args, flagSpec{Values: []string{"--ref", "--from", "--to"}, MaxPos: -1, Usage: usage})
98 if err != nil {
99 return c.fail(protocol.ExitUsage, "%v", err)
100 }
101 rest, ref := f.Pos, f.Value("--ref")
99102 from, to := 0, 0
100 for i := 0; i < len(args); i++ {
101 switch args[i] {
102 case "--ref", "--from", "--to":
103 if i+1 >= len(args) {
104 return c.fail(protocol.ExitUsage, "%s requires a value", args[i])
105 }
106 v := args[i+1]
107 if args[i] == "--ref" {
108 ref = v
109 } else {
110 n, err := strconv.Atoi(v)
111 if err != nil || n < 1 {
112 return c.fail(protocol.ExitUsage, "%s must be a positive line number", args[i])
113 }
114 if args[i] == "--from" {
115 from = n
116 } else {
117 to = n
118 }
119 }
120 i++
121 default:
122 if strings.HasPrefix(args[i], "--") {
123 return c.fail(protocol.ExitUsage, "unknown flag %q\nusage: %s", args[i], usage)
124 }
125 rest = append(rest, args[i])
103 for name, dst := range map[string]*int{"--from": &from, "--to": &to} {
104 if !f.Has(name) {
105 continue
106 }
107 n, err := strconv.Atoi(f.Value(name))
108 if err != nil || n < 1 {
109 return c.fail(protocol.ExitUsage, "%s must be a positive line number", name)
126110 }
111 *dst = n
127112 }
128113 if len(rest) != 2 {
129114 return c.fail(protocol.ExitUsage, "usage: %s", usage)
@@ -219,24 +204,11 @@ func runRepoBlame(c *Ctx, args []string) int {
219204// off argv. Positionals are returned in order so each command can name them
220205// in its own usage message.
221206func readArgs(c *Ctx, args []string, usage string, maxPos int) (pos []string, ref string, code int) {
222 for i := 0; i < len(args); i++ {
223 switch args[i] {
224 case "--ref":
225 if i+1 >= len(args) {
226 return nil, "", c.fail(protocol.ExitUsage, "--ref requires a value")
227 }
228 ref = args[i+1]
229 i++
230 default:
231 if strings.HasPrefix(args[i], "--") {
232 return nil, "", c.fail(protocol.ExitUsage, "unknown flag %q\nusage: %s", args[i], usage)
233 }
234 if len(pos) >= maxPos {
235 return nil, "", c.fail(protocol.ExitUsage, "usage: %s", usage)
236 }
237 pos = append(pos, args[i])
238 }
207 f, err := parseFlags(args, flagSpec{Values: []string{"--ref"}, MaxPos: maxPos, Usage: usage})
208 if err != nil {
209 return nil, "", c.fail(protocol.ExitUsage, "%v", err)
239210 }
211 pos, ref = f.Pos, f.Value("--ref")
240212 return pos, ref, -1
241213}
242214
internal/control/register.go +6 −22
@@ -120,29 +120,13 @@ func runEmailVerify(c *Ctx, args []string) int {
120120// that registration is enabled and that argv[0] == "register".
121121func RunRegister(cfg config.Config, st *store.Store, pub ssh.PublicKey, argv []string,
122122 stdout, stderr io.Writer) int {
123 var username, email, invite string
124 args := argv[1:]
125 for i := 0; i < len(args); i++ {
126 switch args[i] {
127 case "--username", "--email", "--invite":
128 if i+1 >= len(args) {
129 fmt.Fprintf(stderr, "%s requires a value\n", args[i])
130 return protocol.ExitUsage
131 }
132 switch args[i] {
133 case "--username":
134 username = args[i+1]
135 case "--email":
136 email = args[i+1]
137 case "--invite":
138 invite = args[i+1]
139 }
140 i++
141 default:
142 fmt.Fprintf(stderr, "unexpected argument %q\n", args[i])
143 return protocol.ExitUsage
144 }
123 f, err := parseFlags(argv[1:], flagSpec{Values: []string{"--username", "--email", "--invite"}, MaxPos: 0,
124 Usage: "register --username <n> --email <a> | --invite <code>"})
125 if err != nil {
126 fmt.Fprintln(stderr, err)
127 return protocol.ExitUsage
145128 }
129 username, email, invite := f.Value("--username"), f.Value("--email"), f.Value("--invite")
146130 fail := func(code int, format string, a ...any) int {
147131 fmt.Fprintf(stderr, format+"\n", a...)
148132 return code
internal/control/release.go +11 −55
@@ -77,34 +77,12 @@ func releaseRef(c *Ctx, args []string, perm func(store.User, store.Repo, string)
7777
7878func runReleaseCreate(c *Ctx, args []string) int {
7979 const usage = "usage: release create <owner/name> <tag> [--title <t>] [--notes <n> | --file -] [--format md|org]"
80 var path, tag, title, notes, file, format string
81 for i := 0; i < len(args); i++ {
82 switch args[i] {
83 case "--title", "--notes", "--file", "--format":
84 if i+1 >= len(args) {
85 return c.fail(protocol.ExitUsage, "%s requires a value", args[i])
86 }
87 switch args[i] {
88 case "--title":
89 title = args[i+1]
90 case "--notes":
91 notes = args[i+1]
92 case "--file":
93 file = args[i+1]
94 case "--format":
95 format = args[i+1]
96 }
97 i++
98 default:
99 if path == "" {
100 path = args[i]
101 } else if tag == "" {
102 tag = args[i]
103 } else {
104 return c.fail(protocol.ExitUsage, usage)
105 }
106 }
80 f, err := parseFlags(args, flagSpec{Values: []string{"--title", "--notes", "--file", "--format"}, MaxPos: 2, Usage: usage})
81 if err != nil {
82 return c.fail(protocol.ExitUsage, "%v", err)
10783 }
84 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")
10886 if path == "" || tag == "" {
10987 return c.fail(protocol.ExitUsage, usage)
11088 }
@@ -172,35 +150,13 @@ func releaseToOut(r store.Release, withNotes bool) releaseOut {
172150
173151func runReleaseEdit(c *Ctx, args []string) int {
174152 const usage = "usage: release edit <owner/name> <tag> [--title <t>] [--notes <n> | --file -] [--format md|org]"
175 var path, tag, title, notes, file, format string
176 var setTitle, setNotes bool
177 for i := 0; i < len(args); i++ {
178 switch args[i] {
179 case "--title", "--notes", "--file", "--format":
180 if i+1 >= len(args) {
181 return c.fail(protocol.ExitUsage, "%s requires a value", args[i])
182 }
183 switch args[i] {
184 case "--title":
185 title, setTitle = args[i+1], true
186 case "--notes":
187 notes, setNotes = args[i+1], true
188 case "--file":
189 file, setNotes = args[i+1], true
190 case "--format":
191 format = args[i+1]
192 }
193 i++
194 default:
195 if path == "" {
196 path = args[i]
197 } else if tag == "" {
198 tag = args[i]
199 } else {
200 return c.fail(protocol.ExitUsage, usage)
201 }
202 }
153 f, err := parseFlags(args, flagSpec{Values: []string{"--title", "--notes", "--file", "--format"}, MaxPos: 2, Usage: usage})
154 if err != nil {
155 return c.fail(protocol.ExitUsage, "%v", err)
203156 }
157 path, tag := f.pos(0), f.pos(1)
158 title, notes, file, format := f.Value("--title"), f.Value("--notes"), f.Value("--file"), f.Value("--format")
159 setTitle, setNotes := f.Has("--title"), f.Has("--notes") || f.Has("--file")
204160 fmtName, err := markupFormat(format)
205161 if err != nil {
206162 return c.failErr(err)
internal/control/repo.go +11 −36
@@ -146,24 +146,13 @@ func resolveRepo(c *Ctx, path string, check func(store.User, store.Repo, string)
146146}
147147
148148func runRepoCreate(c *Ctx, args []string) int {
149 visibility := "public"
150 var path, description string
151 for i := 0; i < len(args); i++ {
152 switch args[i] {
153 case "--private":
154 visibility = "private"
155 case "--description":
156 if i+1 >= len(args) {
157 return c.fail(protocol.ExitUsage, "--description requires a value")
158 }
159 description = args[i+1]
160 i++
161 default:
162 if path != "" {
163 return c.fail(protocol.ExitUsage, "usage: repo create <owner/name> [--private] [--description <text>]")
164 }
165 path = args[i]
166 }
149 f, err := parseFlags(args, flagSpec{Values: []string{"--description"}, Bools: []string{"--private"}, MaxPos: 1, Usage: "repo create <owner/name> [--private] [--description <text>]"})
150 if err != nil {
151 return c.fail(protocol.ExitUsage, "%v", err)
152 }
153 visibility, path, description := "public", f.pos(0), f.Value("--description")
154 if f.Has("--private") {
155 visibility = "private"
167156 }
168157 owner, name, ok := strings.Cut(path, "/")
169158 if !ok {
@@ -810,25 +799,11 @@ func matchesRepo(q string, r store.Repo, desc string, topics []string) bool {
810799}
811800
812801func runRepoGrep(c *Ctx, args []string) int {
813 var path, query, ref string
814 for i := 0; i < len(args); i++ {
815 switch args[i] {
816 case "--ref":
817 if i+1 >= len(args) {
818 return c.fail(protocol.ExitUsage, "--ref requires a value")
819 }
820 ref = args[i+1]
821 i++
822 default:
823 if path == "" {
824 path = args[i]
825 } else if query == "" {
826 query = args[i]
827 } else {
828 return c.fail(protocol.ExitUsage, "usage: repo grep <owner/name> <query> [--ref <ref>]")
829 }
830 }
802 f, err := parseFlags(args, flagSpec{Values: []string{"--ref"}, MaxPos: 2, Usage: "repo grep <owner/name> <query> [--ref <ref>]"})
803 if err != nil {
804 return c.fail(protocol.ExitUsage, "%v", err)
831805 }
806 path, query, ref := f.pos(0), f.pos(1), f.Value("--ref")
832807 if path == "" || query == "" {
833808 return c.fail(protocol.ExitUsage, "usage: repo grep <owner/name> <query> [--ref <ref>]")
834809 }
internal/control/sig.go +10 −31
@@ -127,38 +127,17 @@ func VerifyCommitCached(st *store.Store, repo store.Repo, parsed *sig.Commit, sh
127127}
128128
129129func runRepoLog(c *Ctx, args []string) int {
130 limit := 30
131 var path, filePath, ref string
132 for i := 0; i < len(args); i++ {
133 switch args[i] {
134 case "--ref":
135 if i+1 >= len(args) {
136 return c.fail(protocol.ExitUsage, "--ref requires a value")
137 }
138 ref = args[i+1]
139 i++
140 case "--limit":
141 if i+1 >= len(args) {
142 return c.fail(protocol.ExitUsage, "--limit requires a value")
143 }
144 n, err := strconv.Atoi(args[i+1])
145 if err != nil || n < 1 || n > 1000 {
146 return c.fail(protocol.ExitUsage, "--limit must be 1..1000")
147 }
148 limit = n
149 i++
150 case "--path":
151 if i+1 >= len(args) {
152 return c.fail(protocol.ExitUsage, "--path requires a value")
153 }
154 filePath = args[i+1]
155 i++
156 default:
157 if path != "" {
158 return c.fail(protocol.ExitUsage, "usage: repo log <owner/name> [--ref <r>] [--limit n] [--path <file>]")
159 }
160 path = args[i]
130 f, perr := parseFlags(args, flagSpec{Values: []string{"--ref", "--limit", "--path"}, MaxPos: 1, Usage: "repo log <owner/name> [--ref <r>] [--limit n] [--path <file>]"})
131 if perr != nil {
132 return c.fail(protocol.ExitUsage, "%v", perr)
133 }
134 limit, path, filePath, ref := 30, f.pos(0), f.Value("--path"), f.Value("--ref")
135 if f.Has("--limit") {
136 n, err := strconv.Atoi(f.Value("--limit"))
137 if err != nil || n < 1 || n > 1000 {
138 return c.fail(protocol.ExitUsage, "--limit must be 1..1000")
161139 }
140 limit = n
162141 }
163142 if path == "" {
164143 return c.fail(protocol.ExitUsage, "usage: repo log <owner/name> [--ref <r>] [--limit n] [--path <file>]")
internal/control/token.go +7 −19
@@ -39,25 +39,13 @@ func parseTTL(s string) (time.Duration, error) {
3939}
4040
4141func runTokenCreate(c *Ctx, args []string) int {
42 name, scope, ttl := "", "full", ""
43 for i := 0; i < len(args); i++ {
44 switch args[i] {
45 case "--name", "--scope", "--ttl":
46 if i+1 >= len(args) {
47 return c.fail(protocol.ExitUsage, "%s requires a value", args[i])
48 }
49 switch args[i] {
50 case "--name":
51 name = args[i+1]
52 case "--scope":
53 scope = args[i+1]
54 case "--ttl":
55 ttl = args[i+1]
56 }
57 i++
58 default:
59 return c.fail(protocol.ExitUsage, "usage: token create --name <n> [--scope full|read] [--ttl 30d]")
60 }
42 f, err := parseFlags(args, flagSpec{Values: []string{"--name", "--scope", "--ttl"}, MaxPos: 0, Usage: "token create --name <n> [--scope full|read] [--ttl 30d]"})
43 if err != nil {
44 return c.fail(protocol.ExitUsage, "%v", err)
45 }
46 name, scope, ttl := f.Value("--name"), "full", f.Value("--ttl")
47 if f.Has("--scope") {
48 scope = f.Value("--scope")
6149 }
6250 if name == "" || (scope != "full" && scope != "read") {
6351 return c.fail(protocol.ExitUsage, "usage: token create --name <n> [--scope full|read] [--ttl 30d]")
internal/control/webhook.go +17 −41
@@ -31,29 +31,13 @@ func init() {
3131}
3232
3333func runWebhookAdd(c *Ctx, args []string) int {
34 var path, url, secret string
35 events := "*"
36 for i := 0; i < len(args); i++ {
37 switch args[i] {
38 case "--secret", "--events":
39 if i+1 >= len(args) {
40 return c.fail(protocol.ExitUsage, "%s requires a value", args[i])
41 }
42 if args[i] == "--secret" {
43 secret = args[i+1]
44 } else {
45 events = args[i+1]
46 }
47 i++
48 default:
49 if path == "" {
50 path = args[i]
51 } else if url == "" {
52 url = args[i]
53 } else {
54 return c.fail(protocol.ExitUsage, "unexpected argument %q", args[i])
55 }
56 }
34 f, err := parseFlags(args, flagSpec{Values: []string{"--secret", "--events"}, MaxPos: 2, Usage: "webhook add <owner/name> <url> [--secret <s>] [--events push,issue.created|*]"})
35 if err != nil {
36 return c.fail(protocol.ExitUsage, "%v", err)
37 }
38 path, url, secret, events := f.pos(0), f.pos(1), f.Value("--secret"), "*"
39 if f.Has("--events") {
40 events = f.Value("--events")
5741 }
5842 if path == "" || url == "" {
5943 return c.fail(protocol.ExitUsage, "usage: webhook add <owner/name> <url> [--secret <s>] [--events <k1,k2>|*]")
@@ -128,25 +112,17 @@ func runWebhookRemove(c *Ctx, args []string) int {
128112}
129113
130114func runWebhookDeliveries(c *Ctx, args []string) int {
131 limit := 20
132 var path string
133 for i := 0; i < len(args); i++ {
134 if args[i] == "--limit" {
135 if i+1 >= len(args) {
136 return c.fail(protocol.ExitUsage, "--limit requires a value")
137 }
138 n, err := strconv.Atoi(args[i+1])
139 if err != nil || n < 1 || n > 200 {
140 return c.fail(protocol.ExitUsage, "--limit must be 1..200")
141 }
142 limit = n
143 i++
144 continue
145 }
146 if path != "" {
147 return c.fail(protocol.ExitUsage, "usage: webhook deliveries <owner/name> [--limit n]")
115 f, err := parseFlags(args, flagSpec{Values: []string{"--limit"}, MaxPos: 1, Usage: "webhook deliveries <owner/name> [--limit n]"})
116 if err != nil {
117 return c.fail(protocol.ExitUsage, "%v", err)
118 }
119 limit, path := 20, f.pos(0)
120 if f.Has("--limit") {
121 n, err := strconv.Atoi(f.Value("--limit"))
122 if err != nil || n < 1 || n > 200 {
123 return c.fail(protocol.ExitUsage, "--limit must be 1..200")
148124 }
149 path = args[i]
125 limit = n
150126 }
151127 if path == "" {
152128 return c.fail(protocol.ExitUsage, "usage: webhook deliveries <owner/name> [--limit n]")