CLI output: help from the registry !470

merged merged by cmc on 2026-09-24 15:08 UTC · krz/gitbay:cli-output-help into main

49 files changed, +2111 −721

Layout: unified · split

cmd/gitbay/main.go +305 −262
@@ -12,6 +12,7 @@ import (
12 12
13 "github.com/spf13/cobra" 13 "github.com/spf13/cobra"
14 "github.com/spf13/cobra/doc" 14 "github.com/spf13/cobra/doc"
15 "golang.org/x/term"
15 16
16 "gitbay.org/gitbay/internal/protocol" 17 "gitbay.org/gitbay/internal/protocol"
17) 18)
@@ -38,68 +39,55 @@ func newRoot() *cobra.Command {
38 root.AddCommand( 39 root.AddCommand(
39 authCmd(), 40 authCmd(),
40 group("label", "issue labels", 41 group("label", "issue labels",
41 pass("list", "labels with colour and use", passOpts{server: []string{"label", "list"}, needsRepo: true}), 42 pass("list", passOpts{server: []string{"label", "list"}, needsRepo: true}),
42 pass("set", "create a label or set its colour: <label> [--color rrggbb|'']", passOpts{server: []string{"label", "set"}, needsRepo: true}), 43 pass("set", passOpts{server: []string{"label", "set"}, needsRepo: true}),
43 pass("remove", "remove a label everywhere: <label>", passOpts{server: []string{"label", "remove"}, needsRepo: true}), 44 pass("remove", passOpts{server: []string{"label", "remove"}, needsRepo: true}),
44 ), 45 ),
45 group("status", "commit statuses (CI)", 46 group("status", "commit statuses (CI)",
46 pass("set", "report a status: <sha> --context <c> --state <s> [--description d] [--url u]", passOpts{server: []string{"status", "set"}, needsRepo: true}), 47 pass("set", passOpts{server: []string{"status", "set"}, needsRepo: true}),
47 pass("list", "statuses on a commit: <sha>", passOpts{server: []string{"status", "list"}, needsRepo: true}), 48 pass("list", passOpts{server: []string{"status", "list"}, needsRepo: true}),
48 ), 49 ),
49 group("build", "CI builds", 50 group("build", "CI builds",
50 pass("list", "recent builds: <owner/name>", passOpts{server: []string{"build", "list"}, needsRepo: true}), 51 pass("list", passOpts{server: []string{"build", "list"}, needsRepo: true}),
51 pass("show", "one build: <owner/name> <n>", passOpts{server: []string{"build", "show"}, needsRepo: true}), 52 pass("show", passOpts{server: []string{"build", "show"}, needsRepo: true}),
52 pass("log", "a build's log: <owner/name> <n> [--follow]", passOpts{server: []string{"build", "log"}, needsRepo: true}), 53 pass("log", passOpts{server: []string{"build", "log"}, needsRepo: true}),
53 pass("jobs", "list the jobs a trigger can name", passOpts{server: []string{"build", "jobs"}, needsRepo: true}), 54 pass("jobs", passOpts{server: []string{"build", "jobs"}, needsRepo: true}),
54 pass("trigger", "queue a job now: <job>", passOpts{server: []string{"build", "trigger"}, needsRepo: true}), 55 pass("trigger", passOpts{server: []string{"build", "trigger"}, needsRepo: true}),
55 pass("cancel", "withdraw a queued build: <n>", passOpts{server: []string{"build", "cancel"}, needsRepo: true}), 56 pass("cancel", passOpts{server: []string{"build", "cancel"}, needsRepo: true}),
56 ), 57 ),
57 pass("dashboard", "one read for the account dashboard: pinned repos, open MRs, assigned issues, recent builds", 58 withShort(pass("dashboard", passOpts{server: []string{"dashboard"}}), "pinned repos, open MRs, assigned issues, recent builds"),
58 passOpts{server: []string{"dashboard"}}), 59 pass("feed", passOpts{server: []string{"feed"}}),
59 pass("feed", "activity on repositories you can reach [--limit n] [--cursor c]", 60 withShort(pass("explore", passOpts{server: []string{"explore"}}), "public repositories on this instance"),
60 passOpts{server: []string{"feed"}}), 61 withShort(pass("search", passOpts{server: []string{"search"}}), "find repositories, issues and merge requests"),
61 pass("explore", "public repositories on this instance [--limit n] [--cursor c]",
62 passOpts{server: []string{"explore"}}),
63 pass("search", "find repositories, issues and merge requests: <query> [--kind repo|issue|mr]",
64 passOpts{server: []string{"search"}}),
65 group("notifications", "your notification inbox", 62 group("notifications", "your notification inbox",
66 pass("list", "unread notifications, or [--all] [--limit n] [--cursor c]", 63 pass("list", passOpts{server: []string{"notifications", "list"}}),
67 passOpts{server: []string{"notifications", "list"}}), 64 pass("read", passOpts{server: []string{"notifications", "read"}}),
68 pass("read", "mark notifications read: <id>... | --all",
69 passOpts{server: []string{"notifications", "read"}}),
70 group("settings", "notification preferences", 65 group("settings", "notification preferences",
71 pass("show", "your notification preferences", passOpts{server: []string{"notifications", "settings", "show"}}), 66 pass("show", passOpts{server: []string{"notifications", "settings", "show"}}),
72 pass("mail", "activity by mail as well as the inbox: on|off", passOpts{server: []string{"notifications", "settings", "mail"}}), 67 pass("mail", passOpts{server: []string{"notifications", "settings", "mail"}}),
73 pass("watch", "every issue and merge request on repositories you can write to: on|off", passOpts{server: []string{"notifications", "settings", "watch"}}), 68 pass("watch", passOpts{server: []string{"notifications", "settings", "watch"}}),
74 pass("push", "activity on your registered devices: on|off", passOpts{server: []string{"notifications", "settings", "push"}}), 69 pass("push", passOpts{server: []string{"notifications", "settings", "push"}}),
75 ), 70 ),
76 group("device", "Apple devices registered for push", 71 group("device", "Apple devices registered for push",
77 pass("add", "register a device, token on stdin: [--label name]", 72 pass("add", passOpts{server: []string{"notifications", "device", "add"}, alwaysStdin: true, stdinWhat: "the device token"}),
78 passOpts{server: []string{"notifications", "device", "add"}, alwaysStdin: true, stdinWhat: "the device token"}), 73 pass("list", passOpts{server: []string{"notifications", "device", "list"}}),
79 pass("list", "your registered devices", 74 pass("remove", passOpts{server: []string{"notifications", "device", "remove"}}),
80 passOpts{server: []string{"notifications", "device", "list"}}),
81 pass("remove", "deregister a device: <id>",
82 passOpts{server: []string{"notifications", "device", "remove"}}),
83 ), 75 ),
84 ), 76 ),
85 group("wiki", "a repository's wiki pages", 77 group("wiki", "a repository's wiki pages",
86 pass("list", "list pages: [<owner/name>]", passOpts{server: []string{"wiki", "list"}, needsRepo: true}), 78 pass("list", passOpts{server: []string{"wiki", "list"}, needsRepo: true}),
87 pass("show", "print a page: [<owner/name>] [<page>]", passOpts{server: []string{"wiki", "show"}, needsRepo: true}), 79 pass("show", passOpts{server: []string{"wiki", "show"}, needsRepo: true}),
88 ), 80 ),
89 group("snippet", "shared text files, outside any repository", 81 group("snippet", "shared text files, outside any repository",
90 pass("create", "create from one file on stdin: <filename> [--description d] [--visibility public|unlisted|private] < file", 82 pass("create", passOpts{server: []string{"snippet", "create"}, alwaysStdin: true, stdinWhat: "the file's text"}),
91 passOpts{server: []string{"snippet", "create"}, alwaysStdin: true, stdinWhat: "the file's text"}), 83 pass("show", passOpts{server: []string{"snippet", "show"}}),
92 pass("show", "metadata and files: <id>", passOpts{server: []string{"snippet", "show"}}), 84 pass("list", passOpts{server: []string{"snippet", "list"}}),
93 pass("list", "your snippets, or an owner's public ones: [<owner>] [--limit n] [--cursor c]", 85 pass("edit", passOpts{server: []string{"snippet", "edit"}}),
94 passOpts{server: []string{"snippet", "list"}}), 86 pass("delete", passOpts{server: []string{"snippet", "delete"}}),
95 pass("edit", "change description or visibility: <id> [--description d] [--visibility v]",
96 passOpts{server: []string{"snippet", "edit"}}),
97 pass("delete", "delete a snippet: <id>", passOpts{server: []string{"snippet", "delete"}}),
98 group("file", "the files in a snippet", 87 group("file", "the files in a snippet",
99 pass("set", "add or replace a file from stdin: <id> <filename> < file", 88 pass("set", passOpts{server: []string{"snippet", "file", "set"}, alwaysStdin: true, stdinWhat: "the file's text"}),
100 passOpts{server: []string{"snippet", "file", "set"}, alwaysStdin: true, stdinWhat: "the file's text"}), 89 pass("get", passOpts{server: []string{"snippet", "file", "get"}}),
101 pass("get", "print a file: <id> <filename> > file", passOpts{server: []string{"snippet", "file", "get"}}), 90 pass("remove", passOpts{server: []string{"snippet", "file", "remove"}}),
102 pass("remove", "remove a file: <id> <filename>", passOpts{server: []string{"snippet", "file", "remove"}}),
103 ), 91 ),
104 ), 92 ),
105 repoCmd(), 93 repoCmd(),
@@ -111,52 +99,106 @@ func newRoot() *cobra.Command {
111 webCmd(), 99 webCmd(),
112 orgCmd(), 100 orgCmd(),
113 group("profile", "user and org profiles", 101 group("profile", "user and org profiles",
114 pass("show", "show a profile: [name]", passOpts{server: []string{"profile", "show"}}), 102 pass("show", passOpts{server: []string{"profile", "show"}}),
115 pass("set", "set your profile: [--description d] [--website url] [--link label|url]...", 103 pass("set", passOpts{server: []string{"profile", "set"}}),
116 passOpts{server: []string{"profile", "set"}}),
117 ), 104 ),
118 webhookCmd(), 105 webhookCmd(),
119 remoteCmd(), 106 remoteCmd(),
120 initCmd(), 107 initCmd(),
121 pass("register", "create an account on the default instance: gitbay register --username <n> --email <a> | --invite <code>", 108 withShort(pass("register", passOpts{server: []string{"register"}}), "create an account on this instance"),
122 passOpts{server: []string{"register"}}), 109 pass("audit", passOpts{server: []string{"audit"}}),
123 pass("audit", "instance audit log (admins): [--actor <user>|-] [--action <prefix>] [--since <duration|date>] [--limit <n>]", passOpts{server: []string{"audit"}}),
124 group("admin", "instance administration (admins)", 110 group("admin", "instance administration (admins)",
125 group("user", "accounts on this instance", 111 group("user", "accounts on this instance",
126 pass("list", "list accounts: [--state active|pending|disabled|admin] [--limit n] [--cursor c]", passOpts{server: []string{"admin", "user", "list"}}), 112 pass("list", passOpts{server: []string{"admin", "user", "list"}}),
127 pass("show", "show an account: <username>", passOpts{server: []string{"admin", "user", "show"}}), 113 pass("show", passOpts{server: []string{"admin", "user", "show"}}),
128 pass("promote", "make an account an instance admin: <username>", passOpts{server: []string{"admin", "user", "promote"}}), 114 pass("promote", passOpts{server: []string{"admin", "user", "promote"}}),
129 pass("demote", "remove instance admin (never the last one): <username>", passOpts{server: []string{"admin", "user", "demote"}}), 115 pass("demote", passOpts{server: []string{"admin", "user", "demote"}}),
130 pass("create", "create an account: <username> [--admin] [--email a [--verified]] [--key -] < key.pub", passOpts{server: []string{"admin", "user", "create"}, stdinOK: true}), 116 pass("create", passOpts{server: []string{"admin", "user", "create"}, stdinOK: true}),
131 pass("disable", "suspend an account: <username>", passOpts{server: []string{"admin", "user", "disable"}}), 117 pass("disable", passOpts{server: []string{"admin", "user", "disable"}}),
132 pass("enable", "restore a suspended account: <username>", passOpts{server: []string{"admin", "user", "enable"}}), 118 pass("enable", passOpts{server: []string{"admin", "user", "enable"}}),
133 pass("delete", "delete an account that anchors nothing: <username> --yes", passOpts{server: []string{"admin", "user", "delete"}}), 119 pass("delete", passOpts{server: []string{"admin", "user", "delete"}}),
134 pass("limits", "show or set repository and storage caps: <username> [--repos n|default] [--bytes n|default]", passOpts{server: []string{"admin", "user", "limits"}}), 120 pass("limits", passOpts{server: []string{"admin", "user", "limits"}}),
135 ), 121 ),
136 group("email", "addresses on any account", 122 group("email", "addresses on any account",
137 pass("verify", "mark an address verified by admin assertion: <username> <address>", passOpts{server: []string{"admin", "email", "verify"}}), 123 pass("verify", passOpts{server: []string{"admin", "email", "verify"}}),
138 ), 124 ),
139 pass("invite", "issue a registration invite and mail its code: --email <address>", passOpts{server: []string{"admin", "invite"}}), 125 pass("invite", passOpts{server: []string{"admin", "invite"}}),
140 pass("stats", "instance statistics: counts and per-repository disk usage", passOpts{server: []string{"admin", "stats"}}), 126 pass("stats", passOpts{server: []string{"admin", "stats"}}),
141 withSub(pass("runners", "the build queue and runner keys: last poll, scope, the build each holds", passOpts{server: []string{"admin", "runners"}}), 127 withSub(pass("runners", passOpts{server: []string{"admin", "runners"}}),
142 pass("remove", "drop a key's heartbeat row: <fingerprint>", passOpts{server: []string{"admin", "runners", "remove"}}), 128 pass("remove", passOpts{server: []string{"admin", "runners", "remove"}}),
143 pass("forget", "alias of remove: <fingerprint>", passOpts{server: []string{"admin", "runners", "forget"}})), 129 pass("forget", passOpts{server: []string{"admin", "runners", "forget"}})),
144 group("repo", "any repository, for moderation (audited)", 130 group("repo", "any repository, for moderation (audited)",
145 pass("list", "every repository with size and last push: [--owner o] [--visibility v] [--limit n] [--cursor c]", passOpts{server: []string{"admin", "repo", "list"}}), 131 pass("list", passOpts{server: []string{"admin", "repo", "list"}}),
146 pass("archive", "archive a repository: <owner/name>", passOpts{server: []string{"admin", "repo", "archive"}}), 132 pass("archive", passOpts{server: []string{"admin", "repo", "archive"}}),
147 pass("unarchive", "unarchive a repository: <owner/name>", passOpts{server: []string{"admin", "repo", "unarchive"}}), 133 pass("unarchive", passOpts{server: []string{"admin", "repo", "unarchive"}}),
148 pass("visibility", "set visibility: <owner/name> public|private", passOpts{server: []string{"admin", "repo", "visibility"}}), 134 pass("visibility", passOpts{server: []string{"admin", "repo", "visibility"}}),
149 pass("delete", "delete a repository: <owner/name> --yes", passOpts{server: []string{"admin", "repo", "delete"}}), 135 pass("delete", passOpts{server: []string{"admin", "repo", "delete"}}),
150 ), 136 ),
151 group("mr", "merge requests in any repository (audited)", 137 group("mr", "merge requests in any repository (audited)",
152 pass("prune", "drop merged or closed MRs' head refs and the objects only they kept: <owner/name> <n>... --yes", passOpts{server: []string{"admin", "mr", "prune"}}), 138 pass("prune", passOpts{server: []string{"admin", "mr", "prune"}}),
153 ), 139 ),
154 ), 140 ),
155 manCmd(root), 141 manCmd(root),
156 ) 142 )
143 defaultHelp := root.HelpFunc()
144 root.SetHelpFunc(func(cmd *cobra.Command, args []string) {
145 if cmd == root {
146 rootHelp(root)
147 return
148 }
149 defaultHelp(cmd, args)
150 })
157 return root 151 return root
158} 152}
159 153
154// rootSection is one heading in gitbay --help: a title and the root
155// commands that belong under it.
156type rootSection struct {
157 title string
158 names []string
159}
160
161var rootSections = []rootSection{
162 {"WORK", []string{"issue", "mr", "build", "release", "milestone", "label", "search"}},
163 {"REPOSITORIES", []string{"repo", "wiki", "status", "webhook", "init"}},
164 {"YOU", []string{"dashboard", "feed", "notifications", "auth", "profile", "snippet", "web"}},
165 {"INSTANCE", []string{"org", "explore", "register", "migrate", "remote", "admin", "audit", "man"}},
166}
167
168// rootHelp is gitbay --help: the nouns grouped by what they are for.
169func rootHelp(root *cobra.Command) {
170 byName := map[string]*cobra.Command{}
171 wide := 0
172 for _, c := range root.Commands() {
173 byName[c.Name()] = c
174 if !c.Hidden {
175 wide = max(wide, len(c.Name()))
176 }
177 }
178 fmt.Println("gitbay: command-line client for a gitbay forge")
179 fmt.Println()
180 fmt.Println("USAGE")
181 fmt.Println(" gitbay <command> [<owner/name>] [flags]")
182 for _, s := range rootSections {
183 var rows [][2]string
184 for _, n := range s.names {
185 if c := byName[n]; c != nil && !c.Hidden {
186 rows = append(rows, [2]string{n, c.Short})
187 }
188 }
189 if len(rows) == 0 {
190 continue
191 }
192 fmt.Println()
193 fmt.Println(s.title)
194 for _, r := range rows {
195 fmt.Printf(" %-*s %s\n", wide, r[0], r[1])
196 }
197 }
198 fmt.Println()
199 fmt.Println("gitbay <command> --help for its verbs; gitbay help <prefix> for the server reference.")
200}
201
160// serverPath is the annotation key holding a passthrough command's 202// serverPath is the annotation key holding a passthrough command's
161// server-side path, so the tree can be checked against the registry. 203// server-side path, so the tree can be checked against the registry.
162const serverPath = "gitbay.server_path" 204const serverPath = "gitbay.server_path"
@@ -197,10 +239,10 @@ func (o passOpts) stdinModeName() string {
197 return "none" 239 return "none"
198} 240}
199 241
200func pass(use, short string, o passOpts) *cobra.Command { 242func pass(use string, o passOpts) *cobra.Command {
201 return &cobra.Command{ 243 return &cobra.Command{
202 Use: use, 244 Use: use,
203 Short: short, 245 Short: summaries[strings.Join(o.server, " ")],
204 Annotations: map[string]string{ 246 Annotations: map[string]string{
205 serverPath: strings.Join(o.server, " "), 247 serverPath: strings.Join(o.server, " "),
206 stdinMode: o.stdinModeName(), 248 stdinMode: o.stdinModeName(),
@@ -222,6 +264,17 @@ func pass(use, short string, o passOpts) *cobra.Command {
222 } 264 }
223} 265}
224 266
267// withShort overrides a passthrough command's one-line summary. Used
268// where the registry's own Summary for a single-verb noun (dashboard,
269// explore, search, register) reads differently from nounSummaries, which
270// is what a grouped noun's header and the root help both show; the two
271// must agree (TestGroupsSayWhatTheServerSays), so these few take the
272// noun's wording instead of the command's.
273func withShort(cmd *cobra.Command, short string) *cobra.Command {
274 cmd.Short = short
275 return cmd
276}
277
225// runServerHelp prints the registry's usage for one command. 278// runServerHelp prints the registry's usage for one command.
226func runServerHelp(o passOpts) int { 279func runServerHelp(o passOpts) int {
227 t, err := resolveTarget() 280 t, err := resolveTarget()
@@ -335,12 +388,18 @@ func group(use, short string, subs ...*cobra.Command) *cobra.Command {
335} 388}
336 389
337// serverHelp prints the registry's usage for a prefix and reports whether 390// serverHelp prints the registry's usage for a prefix and reports whether
338// it did. 391// it did. At a terminal it goes through the terminal-aware path, so it
392// gets the same --term=<cols>[,color] treatment (and layout) as any other
393// command; piped, it stays a quiet capture, so a network or lookup
394// failure falls back to cobra's local help without noise.
339func serverHelp(prefix string) bool { 395func serverHelp(prefix string) bool {
340 t, err := resolveTarget() 396 t, err := resolveTarget()
341 if err != nil { 397 if err != nil {
342 return false 398 return false
343 } 399 }
400 if term.IsTerminal(int(os.Stdout.Fd())) {
401 return runSSH(t, []string{"help", prefix}, strings.NewReader("")) == 0
402 }
344 out, code := sshCapture(t, []string{"help", prefix}) 403 out, code := sshCapture(t, []string{"help", prefix})
345 if code != 0 || out == "" { 404 if code != 0 || out == "" {
346 return false 405 return false
@@ -368,8 +427,7 @@ func local(use, short string, fn func(args []string) int) *cobra.Command {
368} 427}
369 428
370func authCmd() *cobra.Command { 429func authCmd() *cobra.Command {
371 keysAdd := pass("add", "register an SSH public key (reads the key from stdin or --file -)", 430 keysAdd := pass("add", passOpts{server: []string{"keys", "add"}, alwaysStdin: true, stdinWhat: "an SSH public key"})
372 passOpts{server: []string{"keys", "add"}, alwaysStdin: true, stdinWhat: "an SSH public key"})
373 // keys add always reads stdin on the server; wire it through directly. 431 // keys add always reads stdin on the server; wire it through directly.
374 keysAdd.RunE = func(cmd *cobra.Command, args []string) error { 432 keysAdd.RunE = func(cmd *cobra.Command, args []string) error {
375 t, err := resolveTarget() 433 t, err := resolveTarget()
@@ -405,212 +463,197 @@ func authCmd() *cobra.Command {
405 }, 463 },
406 } 464 }
407 tokens := group("token", "API tokens (minted over SSH, used with the JSON API)", 465 tokens := group("token", "API tokens (minted over SSH, used with the JSON API)",
408 pass("create", "mint a token: --name <n> [--scope full|read] [--ttl 30d]", passOpts{server: []string{"token", "create"}}), 466 pass("create", passOpts{server: []string{"token", "create"}}),
409 pass("list", "list API tokens", passOpts{server: []string{"token", "list"}}), 467 pass("list", passOpts{server: []string{"token", "list"}}),
410 pass("revoke", "revoke a token by name", passOpts{server: []string{"token", "revoke"}}), 468 pass("revoke", passOpts{server: []string{"token", "revoke"}}),
411 ) 469 )
412 return group("auth", "identity: whoami, SSH and PGP keys", 470 return group("auth", "identity: whoami, SSH and PGP keys",
413 pass("export", "write your account bundle (a user-level backup) to stdout", 471 pass("export", passOpts{server: []string{"account", "export"}}),
414 passOpts{server: []string{"account", "export"}}),
415 tokens, 472 tokens,
416 pass("whoami", "show the authenticated account", passOpts{server: []string{"whoami"}}), 473 pass("whoami", passOpts{server: []string{"whoami"}}),
417 group("keys", "manage SSH keys", 474 group("keys", "manage SSH keys",
418 pass("list", "list registered SSH keys", passOpts{server: []string{"keys", "list"}}), 475 pass("list", passOpts{server: []string{"keys", "list"}}),
419 keysAdd, 476 keysAdd,
420 pass("label", "name a key: <fingerprint> [<text>]; no text clears it", passOpts{server: []string{"keys", "label"}}), 477 pass("label", passOpts{server: []string{"keys", "label"}}),
421 pass("remove", "remove an SSH key by fingerprint", passOpts{server: []string{"keys", "remove"}}), 478 pass("remove", passOpts{server: []string{"keys", "remove"}}),
422 ), 479 ),
423 group("email", "manage email addresses", 480 group("email", "manage email addresses",
424 pass("add", "add an address and get a verification code by mail", passOpts{server: []string{"email", "add"}}), 481 pass("add", passOpts{server: []string{"email", "add"}}),
425 pass("verify", "confirm a verification code", passOpts{server: []string{"email", "verify"}}), 482 pass("verify", passOpts{server: []string{"email", "verify"}}),
426 pass("list", "list the addresses on your account", passOpts{server: []string{"email", "list"}}), 483 pass("list", passOpts{server: []string{"email", "list"}}),
427 pass("remove", "remove an address; not the primary, nor the last verified one", passOpts{server: []string{"email", "remove"}}), 484 pass("remove", passOpts{server: []string{"email", "remove"}}),
428 pass("primary", "make a verified address the primary", passOpts{server: []string{"email", "primary"}}), 485 pass("primary", passOpts{server: []string{"email", "primary"}}),
429 ), 486 ),
430 group("pgp", "manage OpenPGP keys", 487 group("pgp", "manage OpenPGP keys",
431 pass("list", "list registered PGP keys", passOpts{server: []string{"pgp", "list"}}), 488 pass("list", passOpts{server: []string{"pgp", "list"}}),
432 pgpAdd, 489 pgpAdd,
433 pass("remove", "remove a PGP key by fingerprint", passOpts{server: []string{"pgp", "remove"}}), 490 pass("remove", passOpts{server: []string{"pgp", "remove"}}),
434 ), 491 ),
435 ) 492 )
436} 493}
437 494
438func repoCmd() *cobra.Command { 495func repoCmd() *cobra.Command {
439 return group("repo", "create and manage repositories", 496 return group("repo", "create and manage repositories",
440 pass("create", "create a repository: gitbay repo create <owner/name> [--private]", 497 pass("create", passOpts{server: []string{"repo", "create"}}),
441 passOpts{server: []string{"repo", "create"}}), 498 pass("list", passOpts{server: []string{"repo", "list"}}),
442 pass("list", "list repositories you own or can access [--limit n] [--cursor c]", passOpts{server: []string{"repo", "list"}}), 499 pass("show", passOpts{server: []string{"repo", "show"}, needsRepo: true}),
443 pass("show", "show repository details", passOpts{server: []string{"repo", "show"}, needsRepo: true}), 500 pass("log", passOpts{server: []string{"repo", "log"}, needsRepo: true}),
444 pass("log", "commit log with signature states", passOpts{server: []string{"repo", "log"}, needsRepo: true}), 501 pass("transfer", passOpts{server: []string{"repo", "transfer"}, needsRepo: true}),
445 pass("transfer", "move a repository to another owner: <new-owner>", passOpts{server: []string{"repo", "transfer"}, needsRepo: true}), 502 pass("rename", passOpts{server: []string{"repo", "rename"}, needsRepo: true}),
446 pass("rename", "rename a repository: <new-name> (clone URLs change)", passOpts{server: []string{"repo", "rename"}, needsRepo: true}), 503 pass("delete", passOpts{server: []string{"repo", "delete"}, needsRepo: true}),
447 pass("delete", "delete a repository (--yes)", passOpts{server: []string{"repo", "delete"}, needsRepo: true}), 504 pass("fork", passOpts{server: []string{"repo", "fork"}, needsRepo: true}),
448 pass("fork", "fork a repository to you or an organization", passOpts{server: []string{"repo", "fork"}, needsRepo: true}), 505 pass("search", passOpts{server: []string{"repo", "search"}}),
449 pass("search", "find repositories by name, description, or topic: <query>", passOpts{server: []string{"repo", "search"}}), 506 pass("grep", passOpts{server: []string{"repo", "grep"}, needsRepo: true}),
450 pass("grep", "search file contents: <query> [--ref <ref>]", passOpts{server: []string{"repo", "grep"}, needsRepo: true}), 507 pass("diff", passOpts{server: []string{"repo", "diff"}, needsRepo: true}),
451 pass("diff", "the patch between two refs: <base> <head>", passOpts{server: []string{"repo", "diff"}, needsRepo: true}), 508 pass("tree", passOpts{server: []string{"repo", "tree"}, needsRepo: true}),
452 pass("tree", "list a directory: [<path>] [--ref <ref>]", passOpts{server: []string{"repo", "tree"}, needsRepo: true}), 509 pass("cat", passOpts{server: []string{"repo", "cat"}, needsRepo: true}),
453 pass("cat", "read a file: <path> [--ref <ref>]", passOpts{server: []string{"repo", "cat"}, needsRepo: true}), 510 pass("blame", passOpts{server: []string{"repo", "blame"}, needsRepo: true}),
454 pass("blame", "attribute lines to commits: <path> [--ref <ref>] [--from <n>] [--to <n>]", 511 pass("commit", passOpts{server: []string{"repo", "commit"}, needsRepo: true}),
455 passOpts{server: []string{"repo", "blame"}, needsRepo: true}), 512 pass("commit-file", passOpts{server: []string{"repo", "commit-file"}, needsRepo: true, stdinOK: true}),
456 pass("commit", "show one commit with its patch: <sha>", 513 pass("refs", passOpts{server: []string{"repo", "refs"}, needsRepo: true}),
457 passOpts{server: []string{"repo", "commit"}, needsRepo: true}), 514 pass("download", passOpts{server: []string{"repo", "download"}, needsRepo: true}),
458 pass("commit-file", "write a file and commit it: <path> [--ref <ref>] [--message <m>] --file -", 515 pass("pin", passOpts{server: []string{"repo", "pin"}, needsRepo: true}),
459 passOpts{server: []string{"repo", "commit-file"}, needsRepo: true, stdinOK: true}), 516 pass("unpin", passOpts{server: []string{"repo", "unpin"}, needsRepo: true}),
460 pass("refs", "list branches and tags", passOpts{server: []string{"repo", "refs"}, needsRepo: true}), 517 pass("bookmark", passOpts{server: []string{"repo", "bookmark"}, needsRepo: true}),
461 pass("download", "write a tar.gz of a ref to stdout: [--ref <r>] > repo.tar.gz", 518 pass("unbookmark", passOpts{server: []string{"repo", "unbookmark"}, needsRepo: true}),
462 passOpts{server: []string{"repo", "download"}, needsRepo: true}), 519 pass("bookmarks", passOpts{server: []string{"repo", "bookmarks"}}),
463 pass("pin", "pin a repository to your dashboard", passOpts{server: []string{"repo", "pin"}, needsRepo: true}), 520 pass("watch", passOpts{server: []string{"repo", "watch"}, needsRepo: true}),
464 pass("unpin", "unpin a repository", passOpts{server: []string{"repo", "unpin"}, needsRepo: true}), 521 pass("unwatch", passOpts{server: []string{"repo", "unwatch"}, needsRepo: true}),
465 pass("bookmark", "bookmark a repository to come back to", passOpts{server: []string{"repo", "bookmark"}, needsRepo: true}), 522 pass("mute", passOpts{server: []string{"repo", "mute"}, needsRepo: true}),
466 pass("unbookmark", "remove a bookmark", passOpts{server: []string{"repo", "unbookmark"}, needsRepo: true}), 523 pass("archive", passOpts{server: []string{"repo", "archive"}, needsRepo: true}),
467 pass("bookmarks", "list the repositories you have bookmarked", passOpts{server: []string{"repo", "bookmarks"}}), 524 pass("unarchive", passOpts{server: []string{"repo", "unarchive"}, needsRepo: true}),
468 pass("watch", "hear about all activity on a repository", passOpts{server: []string{"repo", "watch"}, needsRepo: true}),
469 pass("unwatch", "stop watching a repository", passOpts{server: []string{"repo", "unwatch"}, needsRepo: true}),
470 pass("mute", "mute a repository, including work you are part of", passOpts{server: []string{"repo", "mute"}, needsRepo: true}),
471 pass("archive", "archive a repository (read-only)", passOpts{server: []string{"repo", "archive"}, needsRepo: true}),
472 pass("unarchive", "unarchive a repository", passOpts{server: []string{"repo", "unarchive"}, needsRepo: true}),
473 local("clone", "clone via ssh: gitbay repo clone <owner/name> [dir]", cmdRepoClone), 525 local("clone", "clone via ssh: gitbay repo clone <owner/name> [dir]", cmdRepoClone),
474 importCmd(), 526 importCmd(),
475 pass("import-issues", "import GitHub issue/PR history: --from <ghowner/ghrepo> [--token-stdin]", 527 pass("import-issues", passOpts{server: []string{"repo", "import-issues"}, needsRepo: true, stdinOK: true}),
476 passOpts{server: []string{"repo", "import-issues"}, needsRepo: true, stdinOK: true}),
477 group("deploy-key", "repository-bound CI keys", 528 group("deploy-key", "repository-bound CI keys",
478 pass("add", "bind a key: [--rw] < key.pub", passOpts{server: []string{"repo", "deploy-key", "add"}, needsRepo: true, alwaysStdin: true, stdinWhat: "an SSH public key"}), 529 pass("add", passOpts{server: []string{"repo", "deploy-key", "add"}, needsRepo: true, alwaysStdin: true, stdinWhat: "an SSH public key"}),
479 pass("list", "list deploy keys", passOpts{server: []string{"repo", "deploy-key", "list"}, needsRepo: true}), 530 pass("list", passOpts{server: []string{"repo", "deploy-key", "list"}, needsRepo: true}),
480 pass("remove", "remove a deploy key: <fingerprint>", passOpts{server: []string{"repo", "deploy-key", "remove"}, needsRepo: true}), 531 pass("remove", passOpts{server: []string{"repo", "deploy-key", "remove"}, needsRepo: true}),
481 ), 532 ),
482 group("runner", "runners attached to a repository", 533 group("runner", "runners attached to a repository",
483 pass("add", "attach a runner's public key: < key.pub", passOpts{server: []string{"repo", "runner", "add"}, needsRepo: true, alwaysStdin: true, stdinWhat: "an SSH public key"}), 534 pass("add", passOpts{server: []string{"repo", "runner", "add"}, needsRepo: true, alwaysStdin: true, stdinWhat: "an SSH public key"}),
484 pass("list", "list attached runners", passOpts{server: []string{"repo", "runner", "list"}, needsRepo: true}), 535 pass("list", passOpts{server: []string{"repo", "runner", "list"}, needsRepo: true}),
485 pass("remove", "detach a runner: <fingerprint>", passOpts{server: []string{"repo", "runner", "remove"}, needsRepo: true}), 536 pass("remove", passOpts{server: []string{"repo", "runner", "remove"}, needsRepo: true}),
486 ), 537 ),
487 group("mirror", "sync with a foreign remote", 538 group("mirror", "sync with a foreign remote",
488 pass("add", "add a mirror: <https-url> --direction push|pull [--username <u>] [--token-stdin]", 539 pass("add", passOpts{server: []string{"repo", "mirror", "add"}, needsRepo: true, stdinOK: true}),
489 passOpts{server: []string{"repo", "mirror", "add"}, needsRepo: true, stdinOK: true}), 540 pass("list", passOpts{server: []string{"repo", "mirror", "list"}, needsRepo: true}),
490 pass("list", "list mirrors with sync status", passOpts{server: []string{"repo", "mirror", "list"}, needsRepo: true}), 541 pass("remove", passOpts{server: []string{"repo", "mirror", "remove"}, needsRepo: true}),
491 pass("remove", "remove a mirror: <id>", passOpts{server: []string{"repo", "mirror", "remove"}, needsRepo: true}), 542 pass("sync", passOpts{server: []string{"repo", "mirror", "sync"}, needsRepo: true}),
492 pass("sync", "schedule an immediate sync", passOpts{server: []string{"repo", "mirror", "sync"}, needsRepo: true}),
493 ), 543 ),
494 group("deps", "check dependencies against upstream registries", 544 group("deps", "check dependencies against upstream registries",
495 pass("enable", "check this repo's dependencies for updates", passOpts{server: []string{"repo", "deps", "enable"}, needsRepo: true}), 545 pass("enable", passOpts{server: []string{"repo", "deps", "enable"}, needsRepo: true}),
496 pass("disable", "stop checking dependencies", passOpts{server: []string{"repo", "deps", "disable"}, needsRepo: true}), 546 pass("disable", passOpts{server: []string{"repo", "deps", "disable"}, needsRepo: true}),
497 pass("status", "show check state and what is behind", passOpts{server: []string{"repo", "deps", "status"}, needsRepo: true}), 547 pass("status", passOpts{server: []string{"repo", "deps", "status"}, needsRepo: true}),
498 ), 548 ),
499 group("secret", "build secrets (values on stdin, injected into build env)", 549 group("secret", "build secrets (values on stdin, injected into build env)",
500 pass("set", "set a secret: <NAME> (value on stdin)", passOpts{server: []string{"repo", "secret", "set"}, needsRepo: true, alwaysStdin: true, stdinWhat: "the secret value", stdinSecret: true}), 550 pass("set", passOpts{server: []string{"repo", "secret", "set"}, needsRepo: true, alwaysStdin: true, stdinWhat: "the secret value", stdinSecret: true}),
501 pass("list", "list secret names", passOpts{server: []string{"repo", "secret", "list"}, needsRepo: true}), 551 pass("list", passOpts{server: []string{"repo", "secret", "list"}, needsRepo: true}),
502 pass("remove", "remove a secret: <NAME>", passOpts{server: []string{"repo", "secret", "remove"}, needsRepo: true}), 552 pass("remove", passOpts{server: []string{"repo", "secret", "remove"}, needsRepo: true}),
503 ), 553 ),
504 group("domain", "custom domains for the pages branch", 554 group("domain", "custom domains for the pages branch",
505 pass("add", "claim a domain (verify with a DNS TXT record): <domain>", passOpts{server: []string{"repo", "domain", "add"}, needsRepo: true}), 555 pass("add", passOpts{server: []string{"repo", "domain", "add"}, needsRepo: true}),
506 pass("verify", "check the DNS challenge and activate a claim: <domain>", passOpts{server: []string{"repo", "domain", "verify"}, needsRepo: true}), 556 pass("verify", passOpts{server: []string{"repo", "domain", "verify"}, needsRepo: true}),
507 pass("list", "list custom pages domains", passOpts{server: []string{"repo", "domain", "list"}, needsRepo: true}), 557 pass("list", passOpts{server: []string{"repo", "domain", "list"}, needsRepo: true}),
508 pass("remove", "remove a custom pages domain: <domain>", passOpts{server: []string{"repo", "domain", "remove"}, needsRepo: true}), 558 pass("remove", passOpts{server: []string{"repo", "domain", "remove"}, needsRepo: true}),
509 ), 559 ),
510 group("topics", "free-form repository tags", 560 group("topics", "free-form repository tags",
511 pass("list", "list topics", passOpts{server: []string{"repo", "topics"}, needsRepo: true}), 561 pass("list", passOpts{server: []string{"repo", "topics"}, needsRepo: true}),
512 pass("add", "add topics: <topic>...", passOpts{server: []string{"repo", "topics", "add"}, needsRepo: true}), 562 pass("add", passOpts{server: []string{"repo", "topics", "add"}, needsRepo: true}),
513 pass("remove", "remove topics: <topic>...", passOpts{server: []string{"repo", "topics", "remove"}, needsRepo: true}), 563 pass("remove", passOpts{server: []string{"repo", "topics", "remove"}, needsRepo: true}),
514 ), 564 ),
515 group("access", "manage access grants", 565 group("access", "manage access grants",
516 pass("grant", "grant access: ... <user> read|write|admin", passOpts{server: []string{"repo", "access", "grant"}, needsRepo: true}), 566 pass("grant", passOpts{server: []string{"repo", "access", "grant"}, needsRepo: true}),
517 pass("revoke", "revoke access: ... <user>", passOpts{server: []string{"repo", "access", "revoke"}, needsRepo: true}), 567 pass("revoke", passOpts{server: []string{"repo", "access", "revoke"}, needsRepo: true}),
518 pass("list", "list access grants", passOpts{server: []string{"repo", "access", "list"}, needsRepo: true}), 568 pass("list", passOpts{server: []string{"repo", "access", "list"}, needsRepo: true}),
519 ), 569 ),
520 group("settings", "repository settings", 570 group("settings", "repository settings",
521 pass("show", "show settings", passOpts{server: []string{"repo", "settings", "show"}, needsRepo: true}), 571 pass("show", passOpts{server: []string{"repo", "settings", "show"}, needsRepo: true}),
522 pass("protect", "protect a branch", passOpts{server: []string{"repo", "settings", "protect"}, needsRepo: true}), 572 pass("protect", passOpts{server: []string{"repo", "settings", "protect"}, needsRepo: true}),
523 pass("unprotect", "unprotect a branch", passOpts{server: []string{"repo", "settings", "unprotect"}, needsRepo: true}), 573 pass("unprotect", passOpts{server: []string{"repo", "settings", "unprotect"}, needsRepo: true}),
524 pass("protect-tag", "protect tags matching a glob: <glob>", passOpts{server: []string{"repo", "settings", "protect-tag"}, needsRepo: true}), 574 pass("protect-tag", passOpts{server: []string{"repo", "settings", "protect-tag"}, needsRepo: true}),
525 pass("unprotect-tag", "drop a protected-tag glob: <glob>", passOpts{server: []string{"repo", "settings", "unprotect-tag"}, needsRepo: true}), 575 pass("unprotect-tag", passOpts{server: []string{"repo", "settings", "unprotect-tag"}, needsRepo: true}),
526 pass("default-branch", "set the default branch: <branch>", passOpts{server: []string{"repo", "settings", "default-branch"}, needsRepo: true}), 576 pass("default-branch", passOpts{server: []string{"repo", "settings", "default-branch"}, needsRepo: true}),
527 pass("require-approvals", "require N fresh approvals to merge: <n>", passOpts{server: []string{"repo", "settings", "require-approvals"}, needsRepo: true}), 577 pass("require-approvals", passOpts{server: []string{"repo", "settings", "require-approvals"}, needsRepo: true}),
528 pass("require-resolved", "require threads resolved to merge: on|off", passOpts{server: []string{"repo", "settings", "require-resolved"}, needsRepo: true}), 578 pass("require-resolved", passOpts{server: []string{"repo", "settings", "require-resolved"}, needsRepo: true}),
529 pass("require-codeowners", "require an owner's approval per covered file: on|off", passOpts{server: []string{"repo", "settings", "require-codeowners"}, needsRepo: true}), 579 pass("require-codeowners", passOpts{server: []string{"repo", "settings", "require-codeowners"}, needsRepo: true}),
530 pass("require-checks", "gate merges on green statuses: ... on|off", passOpts{server: []string{"repo", "settings", "require-checks"}, needsRepo: true}), 580 pass("require-checks", passOpts{server: []string{"repo", "settings", "require-checks"}, needsRepo: true}),
531 pass("visibility", "set repository visibility: public|private", passOpts{server: []string{"repo", "settings", "visibility"}, needsRepo: true}), 581 pass("visibility", passOpts{server: []string{"repo", "settings", "visibility"}, needsRepo: true}),
532 pass("require-signed", "require verified commit signatures: ... on|off", passOpts{server: []string{"repo", "settings", "require-signed"}, needsRepo: true}), 582 pass("require-signed", passOpts{server: []string{"repo", "settings", "require-signed"}, needsRepo: true}),
533 pass("require-mr", "protected branches take changes through merge requests only: on|off", passOpts{server: []string{"repo", "settings", "require-mr"}, needsRepo: true}), 583 pass("require-mr", passOpts{server: []string{"repo", "settings", "require-mr"}, needsRepo: true}),
534 pass("description", "set the repository description: <text>", passOpts{server: []string{"repo", "settings", "description"}, needsRepo: true}), 584 pass("description", passOpts{server: []string{"repo", "settings", "description"}, needsRepo: true}),
535 pass("website", "set the repository website: <url> ('' clears)", passOpts{server: []string{"repo", "settings", "website"}, needsRepo: true}), 585 pass("website", passOpts{server: []string{"repo", "settings", "website"}, needsRepo: true}),
536 pass("git-daemon", "expose over git://: ... on|off", passOpts{server: []string{"repo", "settings", "git-daemon"}, needsRepo: true}), 586 pass("git-daemon", passOpts{server: []string{"repo", "settings", "git-daemon"}, needsRepo: true}),
537 ), 587 ),
538 ) 588 )
539} 589}
540 590
541func issueCmd() *cobra.Command { 591func issueCmd() *cobra.Command {
542 return group("issue", "issues", 592 return group("issue", "issues",
543 pass("create", "open an issue: --title <t> [--body|--file -|$EDITOR]", 593 pass("create", passOpts{server: []string{"issue", "create"}, needsRepo: true, stdinOK: true, editor: "issue"}),
544 passOpts{server: []string{"issue", "create"}, needsRepo: true, stdinOK: true, editor: "issue"}), 594 pass("list", passOpts{server: []string{"issue", "list"}, needsRepo: true}),
545 pass("list", "list issues [--state open|closed|all] [--label l] [--assignee u] [--author u] [--milestone m|none] [--limit n] [--cursor c]", passOpts{server: []string{"issue", "list"}, needsRepo: true}), 595 pass("show", passOpts{server: []string{"issue", "show"}, needsRepo: true}),
546 pass("show", "show an issue with comments", passOpts{server: []string{"issue", "show"}, needsRepo: true}), 596 pass("comment", passOpts{server: []string{"issue", "comment"}, needsRepo: true, stdinOK: true, editor: "comment"}),
547 pass("comment", "comment on an issue [--message|--file -|$EDITOR]", 597 pass("close", passOpts{server: []string{"issue", "close"}, needsRepo: true}),
548 passOpts{server: []string{"issue", "comment"}, needsRepo: true, stdinOK: true, editor: "comment"}), 598 pass("reopen", passOpts{server: []string{"issue", "reopen"}, needsRepo: true}),
549 pass("close", "close an issue", passOpts{server: []string{"issue", "close"}, needsRepo: true}), 599 pass("label", passOpts{server: []string{"issue", "label"}, needsRepo: true}),
550 pass("reopen", "reopen an issue", passOpts{server: []string{"issue", "reopen"}, needsRepo: true}), 600 pass("assign", passOpts{server: []string{"issue", "assign"}, needsRepo: true}),
551 pass("label", "add or remove labels: [--add <l>]... [--remove <l>]...", passOpts{server: []string{"issue", "label"}, needsRepo: true}), 601 pass("edit", passOpts{server: []string{"issue", "edit"}, needsRepo: true, stdinOK: true}),
552 pass("assign", "assign users: [--add <u>]... [--remove <u>]...", passOpts{server: []string{"issue", "assign"}, needsRepo: true}), 602 pass("milestone", passOpts{server: []string{"issue", "milestone"}, needsRepo: true}),
553 pass("edit", "edit title or body: <n> [--title <t>] [--body <b>|--file -]", passOpts{server: []string{"issue", "edit"}, needsRepo: true, stdinOK: true}), 603 pass("templates", passOpts{server: []string{"issue", "templates"}, needsRepo: true}),
554 pass("milestone", "set or clear the milestone: <n> <title|none>", passOpts{server: []string{"issue", "milestone"}, needsRepo: true}),
555 pass("templates", "list issue templates (.gitbay/issue-template*.md)", passOpts{server: []string{"issue", "templates"}, needsRepo: true}),
556 ) 604 )
557} 605}
558 606
559func releaseCmd() *cobra.Command { 607func releaseCmd() *cobra.Command {
560 return group("release", "tag-anchored releases with notes and assets", 608 return group("release", "tag-anchored releases with notes and assets",
561 pass("create", "create a release on a pushed tag: <tag> [--title <t>] [--notes|--file -|$EDITOR]", 609 pass("create", passOpts{server: []string{"release", "create"}, needsRepo: true, stdinOK: true, editor: "release"}),
562 passOpts{server: []string{"release", "create"}, needsRepo: true, stdinOK: true, editor: "release"}), 610 pass("edit", passOpts{server: []string{"release", "edit"}, needsRepo: true, stdinOK: true}),
563 pass("edit", "update title and notes: <tag> [--title <t>] [--notes|--file -]", 611 pass("list", passOpts{server: []string{"release", "list"}, needsRepo: true}),
564 passOpts{server: []string{"release", "edit"}, needsRepo: true, stdinOK: true}), 612 pass("show", passOpts{server: []string{"release", "show"}, needsRepo: true}),
565 pass("list", "releases: <owner/name> [--limit <n>] [--cursor <c>]", passOpts{server: []string{"release", "list"}, needsRepo: true}), 613 pass("delete", passOpts{server: []string{"release", "delete"}, needsRepo: true}),
566 pass("show", "show a release with assets: <tag>", passOpts{server: []string{"release", "show"}, needsRepo: true}),
567 pass("delete", "delete a release and its assets: <tag> --yes", passOpts{server: []string{"release", "delete"}, needsRepo: true}),
568 group("asset", "binary assets on a release", 614 group("asset", "binary assets on a release",
569 pass("add", "upload from stdin: <tag> <filename> < file", passOpts{server: []string{"release", "asset", "add"}, needsRepo: true, alwaysStdin: true, stdinWhat: "the asset's bytes"}), 615 pass("add", passOpts{server: []string{"release", "asset", "add"}, needsRepo: true, alwaysStdin: true, stdinWhat: "the asset's bytes"}),
570 pass("get", "download to stdout: <tag> <filename> > file", passOpts{server: []string{"release", "asset", "get"}, needsRepo: true}), 616 pass("get", passOpts{server: []string{"release", "asset", "get"}, needsRepo: true}),
571 pass("remove", "remove an asset: <tag> <filename>", passOpts{server: []string{"release", "asset", "remove"}, needsRepo: true}), 617 pass("remove", passOpts{server: []string{"release", "asset", "remove"}, needsRepo: true}),
572 ), 618 ),
573 ) 619 )
574} 620}
575 621
576func milestoneCmd() *cobra.Command { 622func milestoneCmd() *cobra.Command {
577 return group("milestone", "group issues and MRs toward a release", 623 return group("milestone", "group issues and MRs toward a release",
578 pass("create", "create a milestone: <title> [--description <d>] [--due YYYY-MM-DD]", 624 pass("create", passOpts{server: []string{"milestone", "create"}, needsRepo: true}),
579 passOpts{server: []string{"milestone", "create"}, needsRepo: true}), 625 pass("list", passOpts{server: []string{"milestone", "list"}, needsRepo: true}),
580 pass("list", "list milestones with progress [--state open|closed|all]", 626 pass("close", passOpts{server: []string{"milestone", "close"}, needsRepo: true}),
581 passOpts{server: []string{"milestone", "list"}, needsRepo: true}), 627 pass("reopen", passOpts{server: []string{"milestone", "reopen"}, needsRepo: true}),
582 pass("close", "close a milestone: <title>", passOpts{server: []string{"milestone", "close"}, needsRepo: true}),
583 pass("reopen", "reopen a milestone: <title>", passOpts{server: []string{"milestone", "reopen"}, needsRepo: true}),
584 ) 628 )
585} 629}
586 630
587func mrCmd() *cobra.Command { 631func mrCmd() *cobra.Command {
588 review := pass("review", "submit a review: --approve|--request-changes|--comment, or --discard a pending batch", passOpts{server: []string{"mr", "review"}, needsRepo: true}) 632 review := pass("review", passOpts{server: []string{"mr", "review"}, needsRepo: true})
589 review.AddCommand(pass("request", "ask specific people for review: [--add <u>]... [--remove <u>]...", passOpts{server: []string{"mr", "review", "request"}, needsRepo: true})) 633 review.AddCommand(pass("request", passOpts{server: []string{"mr", "review", "request"}, needsRepo: true}))
590 return group("mr", "merge requests", 634 return group("mr", "merge requests",
591 pass("create", "open a merge request: --source <branch> --target <branch> --title <t>", 635 pass("create", passOpts{server: []string{"mr", "create"}, needsRepo: true, stdinOK: true, editor: "merge request", inferSource: true}),
592 passOpts{server: []string{"mr", "create"}, needsRepo: true, stdinOK: true, editor: "merge request", inferSource: true}), 636 pass("list", passOpts{server: []string{"mr", "list"}, needsRepo: true}),
593 pass("list", "list merge requests [--state ...] [--label l] [--author u] [--milestone m|none] [--limit n] [--cursor c]", passOpts{server: []string{"mr", "list"}, needsRepo: true}), 637 pass("show", passOpts{server: []string{"mr", "show"}, needsRepo: true}),
594 pass("show", "show a merge request", passOpts{server: []string{"mr", "show"}, needsRepo: true}), 638 pass("diff", passOpts{server: []string{"mr", "diff"}, needsRepo: true}),
595 pass("diff", "show the diff", passOpts{server: []string{"mr", "diff"}, needsRepo: true}),
596 local("checkout", "fetch and check out the MR head locally: gitbay mr checkout <n>", cmdMRCheckout), 639 local("checkout", "fetch and check out the MR head locally: gitbay mr checkout <n>", cmdMRCheckout),
597 local("rebase", "replay the MR's branch onto its target and re-push: gitbay mr rebase <n>", cmdMRRebase), 640 local("rebase", "replay the MR's branch onto its target and re-push: gitbay mr rebase <n>", cmdMRRebase),
598 pass("comment", "comment on a merge request", passOpts{server: []string{"mr", "comment"}, needsRepo: true, stdinOK: true, editor: "comment"}), 641 pass("comment", passOpts{server: []string{"mr", "comment"}, needsRepo: true, stdinOK: true, editor: "comment"}),
599 pass("diff-comment", "comment on a diff line: --path <f> --line <l> [--old] [--pending] [--reply <id>]", passOpts{server: []string{"mr", "diff-comment"}, needsRepo: true, stdinOK: true, editor: "comment"}), 642 pass("diff-comment", passOpts{server: []string{"mr", "diff-comment"}, needsRepo: true, stdinOK: true, editor: "comment"}),
600 pass("threads", "review threads on an MR", passOpts{server: []string{"mr", "threads"}, needsRepo: true}), 643 pass("threads", passOpts{server: []string{"mr", "threads"}, needsRepo: true}),
601 pass("resolve", "resolve a review thread: <n> <thread-id>", passOpts{server: []string{"mr", "resolve"}, needsRepo: true}), 644 pass("resolve", passOpts{server: []string{"mr", "resolve"}, needsRepo: true}),
602 pass("unresolve", "reopen a review thread: <n> <thread-id>", passOpts{server: []string{"mr", "unresolve"}, needsRepo: true}), 645 pass("unresolve", passOpts{server: []string{"mr", "unresolve"}, needsRepo: true}),
603 review, 646 review,
604 pass("merge", "merge: [--strategy ff|merge|squash|rebase]", passOpts{server: []string{"mr", "merge"}, needsRepo: true}), 647 pass("merge", passOpts{server: []string{"mr", "merge"}, needsRepo: true}),
605 pass("close", "close without merging", passOpts{server: []string{"mr", "close"}, needsRepo: true}), 648 pass("close", passOpts{server: []string{"mr", "close"}, needsRepo: true}),
606 pass("revisions", "the heads this merge request has had", passOpts{server: []string{"mr", "revisions"}, needsRepo: true}), 649 pass("revisions", passOpts{server: []string{"mr", "revisions"}, needsRepo: true}),
607 pass("range-diff", "what changed between two revisions: [--from <sha>] [--to <sha>]", passOpts{server: []string{"mr", "range-diff"}, needsRepo: true}), 650 pass("range-diff", passOpts{server: []string{"mr", "range-diff"}, needsRepo: true}),
608 pass("draft", "mark as work in progress", passOpts{server: []string{"mr", "draft"}, needsRepo: true}), 651 pass("draft", passOpts{server: []string{"mr", "draft"}, needsRepo: true}),
609 pass("ready", "take the draft mark off, so it can merge", passOpts{server: []string{"mr", "ready"}, needsRepo: true}), 652 pass("ready", passOpts{server: []string{"mr", "ready"}, needsRepo: true}),
610 pass("edit", "edit title or body: <n> [--title <t>] [--body <b>|--file -]", passOpts{server: []string{"mr", "edit"}, needsRepo: true, stdinOK: true}), 653 pass("edit", passOpts{server: []string{"mr", "edit"}, needsRepo: true, stdinOK: true}),
611 pass("label", "add or remove labels: [--add <l>]... [--remove <l>]...", passOpts{server: []string{"mr", "label"}, needsRepo: true}), 654 pass("label", passOpts{server: []string{"mr", "label"}, needsRepo: true}),
612 pass("milestone", "set or clear the milestone: <n> <title|none>", passOpts{server: []string{"mr", "milestone"}, needsRepo: true}), 655 pass("milestone", passOpts{server: []string{"mr", "milestone"}, needsRepo: true}),
613 pass("retarget", "retarget onto another branch: <n> <branch>", passOpts{server: []string{"mr", "retarget"}, needsRepo: true}), 656 pass("retarget", passOpts{server: []string{"mr", "retarget"}, needsRepo: true}),
614 ) 657 )
615} 658}
616 659
@@ -652,65 +695,64 @@ func usesTokenStdin(args []string) bool {
652 695
653func webCmd() *cobra.Command { 696func webCmd() *cobra.Command {
654 return group("web", "browser session", 697 return group("web", "browser session",
655 pass("login", "mint a one-time browser login URL over ssh", passOpts{server: []string{"web", "login"}}), 698 pass("login", passOpts{server: []string{"web", "login"}}),
656 group("sessions", "your browser sessions", 699 group("sessions", "your browser sessions",
657 pass("list", "list your browser sessions", passOpts{server: []string{"web", "sessions", "list"}}), 700 pass("list", passOpts{server: []string{"web", "sessions", "list"}}),
658 pass("revoke", "end a browser session: <id>|--all", passOpts{server: []string{"web", "sessions", "revoke"}}), 701 pass("revoke", passOpts{server: []string{"web", "sessions", "revoke"}}),
659 ), 702 ),
660 group("theme", "the colour scheme the web UI uses for you", 703 group("theme", "the colour scheme the web UI uses for you",
661 pass("show", "show your colour scheme", passOpts{server: []string{"web", "theme", "show"}}), 704 pass("show", passOpts{server: []string{"web", "theme", "show"}}),
662 pass("set", "follow the browser, or force one: system|light|dark", passOpts{server: []string{"web", "theme", "set"}}), 705 pass("set", passOpts{server: []string{"web", "theme", "set"}}),
663 ), 706 ),
664 ) 707 )
665} 708}
666 709
667func webhookCmd() *cobra.Command { 710func webhookCmd() *cobra.Command {
668 return group("webhook", "outbound event delivery", 711 return group("webhook", "outbound event delivery",
669 pass("add", "add a webhook: <url> [--secret s] [--events k1,k2|*]", passOpts{server: []string{"webhook", "add"}, needsRepo: true}), 712 pass("add", passOpts{server: []string{"webhook", "add"}, needsRepo: true}),
670 pass("list", "list webhooks", passOpts{server: []string{"webhook", "list"}, needsRepo: true}), 713 pass("list", passOpts{server: []string{"webhook", "list"}, needsRepo: true}),
671 pass("remove", "remove a webhook: <id>", passOpts{server: []string{"webhook", "remove"}, needsRepo: true}), 714 pass("remove", passOpts{server: []string{"webhook", "remove"}, needsRepo: true}),
672 pass("deliveries", "recent deliveries [--limit n]", passOpts{server: []string{"webhook", "deliveries"}, needsRepo: true}), 715 pass("deliveries", passOpts{server: []string{"webhook", "deliveries"}, needsRepo: true}),
673 pass("redeliver", "requeue a delivery: <delivery-id>", passOpts{server: []string{"webhook", "redeliver"}, needsRepo: true}), 716 pass("redeliver", passOpts{server: []string{"webhook", "redeliver"}, needsRepo: true}),
674 ) 717 )
675} 718}
676 719
677func orgCmd() *cobra.Command { 720func orgCmd() *cobra.Command {
678 return group("org", "organizations", 721 return group("org", "organizations",
679 pass("create", "create an organization", passOpts{server: []string{"org", "create"}}), 722 pass("create", passOpts{server: []string{"org", "create"}}),
680 pass("list", "list organizations you belong to", passOpts{server: []string{"org", "list"}}), 723 pass("list", passOpts{server: []string{"org", "list"}}),
681 pass("show", "show an organization and its members", passOpts{server: []string{"org", "show"}}), 724 pass("show", passOpts{server: []string{"org", "show"}}),
682 pass("rename", "rename an organization: <old> <new>", passOpts{server: []string{"org", "rename"}}), 725 pass("rename", passOpts{server: []string{"org", "rename"}}),
683 pass("delete", "delete an empty organization (--yes)", passOpts{server: []string{"org", "delete"}}), 726 pass("delete", passOpts{server: []string{"org", "delete"}}),
684 pass("profile", "show or set an org profile: <org> [--description d] [--website url] [--link label|url]...", 727 pass("profile", passOpts{server: []string{"org", "profile"}}),
685 passOpts{server: []string{"org", "profile"}}),
686 group("members", "manage members", 728 group("members", "manage members",
687 pass("add", "add or update a member: <org> <user> [--role member|admin]", passOpts{server: []string{"org", "members", "add"}}), 729 pass("add", passOpts{server: []string{"org", "members", "add"}}),
688 pass("remove", "remove a member: <org> <user>", passOpts{server: []string{"org", "members", "remove"}}), 730 pass("remove", passOpts{server: []string{"org", "members", "remove"}}),
689 pass("list", "list members: <org>", passOpts{server: []string{"org", "members", "list"}}), 731 pass("list", passOpts{server: []string{"org", "members", "list"}}),
690 ), 732 ),
691 group("label", "labels every org repository sees", 733 group("label", "labels every org repository sees",
692 pass("set", "create an org label or set its colour: <org> <label> [--color rrggbb|'']", passOpts{server: []string{"org", "label", "set"}}), 734 pass("set", passOpts{server: []string{"org", "label", "set"}}),
693 pass("list", "list org labels with use across readable repositories: <org>", passOpts{server: []string{"org", "label", "list"}}), 735 pass("list", passOpts{server: []string{"org", "label", "list"}}),
694 pass("remove", "remove an org label everywhere: <org> <label>", passOpts{server: []string{"org", "label", "remove"}}), 736 pass("remove", passOpts{server: []string{"org", "label", "remove"}}),
695 ), 737 ),
696 group("milestone", "milestones spanning an org's repositories", 738 group("milestone", "milestones spanning an org's repositories",
697 pass("create", "create an org milestone: <org> <title> [--description d] [--due YYYY-MM-DD]", passOpts{server: []string{"org", "milestone", "create"}}), 739 pass("create", passOpts{server: []string{"org", "milestone", "create"}}),
698 pass("list", "list org milestones with progress: <org> [--state open|closed|all]", passOpts{server: []string{"org", "milestone", "list"}}), 740 pass("list", passOpts{server: []string{"org", "milestone", "list"}}),
699 pass("close", "close an org milestone: <org> <title>", passOpts{server: []string{"org", "milestone", "close"}}), 741 pass("close", passOpts{server: []string{"org", "milestone", "close"}}),
700 pass("reopen", "reopen an org milestone: <org> <title>", passOpts{server: []string{"org", "milestone", "reopen"}}), 742 pass("reopen", passOpts{server: []string{"org", "milestone", "reopen"}}),
701 ), 743 ),
702 group("team", "scope repository access with teams", 744 group("team", "scope repository access with teams",
703 pass("create", "create a team: <org> <team>", passOpts{server: []string{"org", "team", "create"}}), 745 pass("create", passOpts{server: []string{"org", "team", "create"}}),
704 pass("delete", "delete a team: <org> <team>", passOpts{server: []string{"org", "team", "delete"}}), 746 pass("delete", passOpts{server: []string{"org", "team", "delete"}}),
705 pass("list", "list teams: <org>", passOpts{server: []string{"org", "team", "list"}}), 747 pass("list", passOpts{server: []string{"org", "team", "list"}}),
706 pass("show", "show members and grants: <org> <team>", passOpts{server: []string{"org", "team", "show"}}), 748 pass("show", passOpts{server: []string{"org", "team", "show"}}),
707 pass("add", "add org members: <org> <team> <user>...", passOpts{server: []string{"org", "team", "add"}}), 749 pass("add", passOpts{server: []string{"org", "team", "add"}}),
708 pass("remove", "remove members: <org> <team> <user>...", passOpts{server: []string{"org", "team", "remove"}}), 750 pass("remove", passOpts{server: []string{"org", "team", "remove"}}),
709 pass("grant", "grant a repo role: <org> <team> <owner/name> read|write|admin", passOpts{server: []string{"org", "team", "grant"}}), 751 pass("grant", passOpts{server: []string{"org", "team", "grant"}}),
710 pass("revoke", "revoke a repo grant: <org> <team> <owner/name>", passOpts{server: []string{"org", "team", "revoke"}}), 752 pass("revoke", passOpts{server: []string{"org", "team", "revoke"}}),
711 ), 753 ),
712 group("settings", "organization settings", 754 group("settings", "organization settings",
713 pass("members-role", "role plain membership implies: <org> write|read|none", passOpts{server: []string{"org", "settings", "members-role"}}), 755 pass("members-role", passOpts{server: []string{"org", "settings", "members-role"}}),
714 ), 756 ),
715 ) 757 )
716} 758}
@@ -724,7 +766,7 @@ func remoteCmd() *cobra.Command {
724} 766}
725 767
726func initCmd() *cobra.Command { 768func initCmd() *cobra.Command {
727 return local("init", "git init + repo create + set origin, in one step: gitbay init [name] [--private]", cmdInit) 769 return local("init [name] [--private]", "git init + repo create + set origin, in one step", cmdInit)
728} 770}
729 771
730// manCmd generates man pages; a CLI-first tool without man pages is not 772// manCmd generates man pages; a CLI-first tool without man pages is not
@@ -759,7 +801,8 @@ func helpCmd(root *cobra.Command) *cobra.Command {
759 DisableFlagParsing: true, 801 DisableFlagParsing: true,
760 RunE: func(cmd *cobra.Command, args []string) error { 802 RunE: func(cmd *cobra.Command, args []string) error {
761 if len(args) == 0 { 803 if len(args) == 0 {
762 return root.Help() 804 rootHelp(root)
805 return nil
763 } 806 }
764 t, err := resolveTarget() 807 t, err := resolveTarget()
765 if err != nil { 808 if err != nil {
cmd/gitbay/migrate.go +2 −2
@@ -18,8 +18,8 @@ func migrateCmd() *cobra.Command {
18 var from string 18 var from string
19 var fromPort int 19 var fromPort int
20 cmd := &cobra.Command{ 20 cmd := &cobra.Command{
21 Use: "migrate", 21 Use: "migrate --from <host>",
22 Short: "move your account here from another gitbay instance: gitbay migrate --from <host>", 22 Short: "move your account here from another gitbay instance",
23 Long: `Migrate authenticates to the source instance with your own SSH key, 23 Long: `Migrate authenticates to the source instance with your own SSH key,
24exports your account bundle (profile, emails, repos with settings, 24exports your account bundle (profile, emails, repos with settings,
25issues, MRs, comments), replays it on this instance, then mirrors each 25issues, MRs, comments), replays it on this instance, then mirrors each
cmd/gitbay/summaries_gen.go added +238
@@ -0,0 +1,238 @@
1// Code generated by TestSummariesAreCurrent -update; DO NOT EDIT.
2
3package main
4
5var summaries = map[string]string{
6 "account export": "write your account bundle (profile, repos, issues, MRs) as JSON",
7 "account import-bundle": "replay an account bundle (see gitbay migrate)",
8 "admin email verify": "mark an address verified by admin assertion",
9 "admin invite": "issue a registration invite and mail its code",
10 "admin mr prune": "drop merged or closed MRs' head refs and the objects only they kept, e.g. after a history rewrite (instance admins; audited)",
11 "admin repo archive": "archive any repository (instance admins; audited)",
12 "admin repo delete": "delete any repository (instance admins; audited)",
13 "admin repo list": "list every repository with size and last push (instance admins)",
14 "admin repo unarchive": "unarchive any repository (instance admins; audited)",
15 "admin repo visibility": "set any repository's visibility (instance admins; audited)",
16 "admin runners forget": "alias of admin runners remove",
17 "admin runners remove": "drop a key's runner heartbeat row, e.g. one that polled once by mistake (instance admins)",
18 "admin runners": "the build queue and runner accounts: last poll, scope, the build each holds (instance admins)",
19 "admin stats": "instance statistics: counts and per-repository disk usage",
20 "admin user create": "create an account, optionally with a key and a verified address (instance admins)",
21 "admin user delete": "delete an account that anchors nothing (keys, emails and sessions go with it)",
22 "admin user demote": "remove instance admin from an account (never the last one)",
23 "admin user disable": "suspend an account: SSH, web sessions and API tokens refused until re-enabled",
24 "admin user enable": "restore a suspended account",
25 "admin user limits": "show or set an account's repository and storage caps (instance admins)",
26 "admin user list": "list accounts (instance admins)",
27 "admin user promote": "make an account an instance admin",
28 "admin user show": "show an account: keys, emails, orgs, tokens, sessions (instance admins)",
29 "audit": "instance audit log (admins)",
30 "build cancel": "withdraw a queued build before a runner claims it",
31 "build jobs": "list the jobs a trigger can name",
32 "build list": "list recent builds",
33 "build log": "print a build's log, or follow it until the build ends",
34 "build show": "show one build",
35 "build trigger": "queue a job now (scheduled or not)",
36 "dashboard": "one read for the account dashboard: review queue, assigned and open work, pins, activity, builds",
37 "email add": "add an address and mail a verification code",
38 "email list": "list the addresses on your account",
39 "email primary": "make a verified address the primary",
40 "email remove": "remove an address; not the primary, nor the last verified one",
41 "email verify": "confirm a verification code",
42 "explore": "list public repositories",
43 "feed": "activity on repositories you can reach",
44 "help": "list available commands",
45 "issue assign": "assignees",
46 "issue close": "close an issue",
47 "issue comment": "comment",
48 "issue create": "open an issue",
49 "issue edit": "edit title or body",
50 "issue label": "labels",
51 "issue list": "list issues",
52 "issue milestone": "set or clear an issue's milestone",
53 "issue reopen": "reopen an issue",
54 "issue show": "show an issue with comments",
55 "issue templates": "list issue templates (.gitbay/issue-template*.md)",
56 "keys add": "register an SSH public key (authorized_keys format)",
57 "keys label": "name a key; an empty label clears it",
58 "keys list": "list registered SSH keys",
59 "keys remove": "remove an SSH key by fingerprint",
60 "label list": "list a repository's labels with colour and use",
61 "label remove": "remove a label from the repository and from every issue and merge request",
62 "label set": "create a label or set its colour",
63 "milestone close": "close a milestone",
64 "milestone create": "create a milestone",
65 "milestone list": "list milestones with progress",
66 "milestone reopen": "reopen a milestone",
67 "mr close": "close without merging",
68 "mr comment": "comment",
69 "mr create": "open a merge request",
70 "mr diff": "show the diff",
71 "mr diff-comment": "comment on a diff line",
72 "mr draft": "mark a merge request as work in progress",
73 "mr edit": "edit title or body",
74 "mr label": "labels",
75 "mr list": "list merge requests",
76 "mr merge": "merge",
77 "mr milestone": "set or clear an MR's milestone",
78 "mr range-diff": "what changed between two revisions of a merge request",
79 "mr ready": "take the draft mark off, so it can merge",
80 "mr resolve": "resolve a review thread",
81 "mr retarget": "retarget onto another branch",
82 "mr review request": "ask specific people for a review",
83 "mr review": "review",
84 "mr revisions": "the heads a merge request has had",
85 "mr show": "show a merge request",
86 "mr threads": "review threads on an MR",
87 "mr unresolve": "reopen a review thread",
88 "notifications device add": "register an Apple device for push, token on stdin",
89 "notifications device list": "your registered devices",
90 "notifications device remove": "deregister a device",
91 "notifications list": "your notification inbox, newest first",
92 "notifications read": "mark notifications read",
93 "notifications settings mail": "activity by mail as well as the inbox (login links are unaffected)",
94 "notifications settings push": "activity on your registered devices as well as the inbox",
95 "notifications settings show": "your notification preferences",
96 "notifications settings watch": "every issue and merge request on repositories you can write to",
97 "org create": "create an organization (you become its first admin)",
98 "org delete": "delete an empty organization",
99 "org label list": "list an org's labels with use across the repositories you can read",
100 "org label remove": "remove an org label from the org and from every issue under it",
101 "org label set": "create an org label every org repository sees, or set its colour; folds in same-named repo labels",
102 "org list": "list organizations you belong to",
103 "org members add": "add or update a member",
104 "org members list": "list members",
105 "org members remove": "remove a member",
106 "org milestone close": "close an org milestone",
107 "org milestone create": "create an org milestone spanning every org repository; folds in same-titled repo milestones",
108 "org milestone list": "list an org's milestones with progress across the repositories you can read",
109 "org milestone reopen": "reopen an org milestone",
110 "org profile": "show or set an org's profile",
111 "org rename": "rename an organization",
112 "org settings members-role": "role plain membership implies on every org repo",
113 "org show": "show an organization and its members",
114 "org team add": "add org members to a team",
115 "org team create": "create a team",
116 "org team delete": "delete a team (its grants with it)",
117 "org team grant": "grant a team a role on an org repo",
118 "org team list": "list an org's teams",
119 "org team remove": "remove members from a team",
120 "org team revoke": "revoke a team's grant",
121 "org team show": "show a team's members and grants",
122 "pgp add": "register an OpenPGP public key (armored)",
123 "pgp list": "list registered OpenPGP keys",
124 "pgp remove": "remove an OpenPGP key by fingerprint",
125 "profile set": "set your profile",
126 "profile show": "show a user's or org's profile",
127 "register": "create an account (only meaningful for unregistered keys)",
128 "release asset add": "upload an asset from stdin",
129 "release asset get": "write an asset to stdout",
130 "release asset remove": "remove an asset",
131 "release create": "create a release on a tag",
132 "release delete": "delete a release and its assets",
133 "release edit": "update a release's title and notes",
134 "release list": "list releases",
135 "release show": "show a release with assets",
136 "repo access grant": "grant access",
137 "repo access list": "list who can reach the repository, with the role and where it comes from",
138 "repo access revoke": "revoke access",
139 "repo archive": "archive a repository (read-only: pushes and issue/MR writes refused)",
140 "repo blame": "attribute lines to commits",
141 "repo bookmark": "bookmark a repository to come back to",
142 "repo bookmarks": "list the repositories you have bookmarked",
143 "repo cat": "read a file",
144 "repo commit": "show one commit with its patch",
145 "repo commit-file": "write a file and commit it",
146 "repo create": "create a repository",
147 "repo delete": "delete a repository",
148 "repo deploy-key add": "bind a read-only (or --rw) key to one repository",
149 "repo deploy-key list": "list deploy keys",
150 "repo deploy-key remove": "remove a deploy key",
151 "repo deps disable": "stop checking dependencies",
152 "repo deps enable": "check dependencies for updates",
153 "repo deps status": "show dependency check state",
154 "repo diff": "the patch between two refs, from their merge base",
155 "repo domain add": "claim a custom pages domain (verify with a DNS TXT record)",
156 "repo domain list": "list custom pages domains",
157 "repo domain remove": "remove a custom pages domain",
158 "repo domain verify": "check the DNS challenge and activate a claim",
159 "repo download": "write a tar.gz of a ref to stdout",
160 "repo fork": "fork a repository under your account",
161 "repo grep": "search file contents",
162 "repo import": "server-side mirror of a foreign repository",
163 "repo import-issues": "import issue and PR history from GitHub or Forgejo",
164 "repo list": "list repositories you own or can access",
165 "repo log": "commit log with signature states",
166 "repo mirror add": "mirror to or from a remote",
167 "repo mirror list": "list mirrors with sync status",
168 "repo mirror remove": "remove a mirror",
169 "repo mirror sync": "schedule an immediate sync",
170 "repo mute": "mute a repository, including work you are part of",
171 "repo pin": "pin a repository to your dashboard",
172 "repo refs": "list branches and tags",
173 "repo rename": "rename a repository",
174 "repo runner add": "attach a runner's public key to a repository",
175 "repo runner list": "list the runners attached to a repository",
176 "repo runner remove": "detach a runner from a repository",
177 "repo search": "find repositories by name, description, or topic",
178 "repo secret list": "list build secret names",
179 "repo secret remove": "remove a build secret",
180 "repo secret set": "set a build secret",
181 "repo settings default-branch": "set the default branch",
182 "repo settings description": "set the repository description",
183 "repo settings git-daemon": "expose over git://",
184 "repo settings protect": "protect a branch",
185 "repo settings protect-tag": "protect tags matching a glob (created once, never moved or deleted)",
186 "repo settings require-approvals": "require N fresh approvals to merge",
187 "repo settings require-checks": "gate merges on green statuses",
188 "repo settings require-codeowners": "require an owner's approval for every file CODEOWNERS covers",
189 "repo settings require-mr": "protected branches take changes through merge requests only",
190 "repo settings require-resolved": "require all review threads resolved to merge",
191 "repo settings require-signed": "require verified commit signatures",
192 "repo settings show": "show settings",
193 "repo settings unprotect": "unprotect a branch",
194 "repo settings unprotect-tag": "drop a protected-tag glob",
195 "repo settings visibility": "set repository visibility",
196 "repo settings website": "set the repository website",
197 "repo show": "show repository details",
198 "repo topics add": "add topics",
199 "repo topics remove": "remove topics",
200 "repo topics": "list topics",
201 "repo transfer": "move a repository to another owner",
202 "repo tree": "list a directory",
203 "repo unarchive": "unarchive a repository",
204 "repo unbookmark": "remove a bookmark",
205 "repo unpin": "unpin a repository",
206 "repo unwatch": "back to the default: only work you are part of",
207 "repo watch": "hear about all activity on a repository",
208 "runner done": "finish a build",
209 "runner log": "append a build's log from stdin",
210 "runner next": "claim the oldest pending build this key may run (runner protocol)",
211 "search": "find repositories, issues and merge requests across the instance",
212 "snippet create": "create a snippet from one file on stdin",
213 "snippet delete": "delete a snippet and its files",
214 "snippet edit": "change a snippet's description or visibility",
215 "snippet file get": "write a snippet file to stdout",
216 "snippet file remove": "remove a file from a snippet",
217 "snippet file set": "add a file to a snippet, or replace one, from stdin",
218 "snippet list": "list your snippets, or an owner's public ones",
219 "snippet show": "show a snippet's metadata and files",
220 "status list": "statuses on a commit",
221 "status set": "report a commit status (CI)",
222 "token create": "mint an API token (shown once)",
223 "token list": "list API tokens",
224 "token revoke": "revoke an API token by name",
225 "web login": "mint a one-time browser login URL",
226 "web sessions list": "list your browser sessions",
227 "web sessions revoke": "end a browser session, or all of them",
228 "web theme set": "follow the browser's scheme, or force light or dark",
229 "web theme show": "the colour scheme the web UI uses for you",
230 "webhook add": "add a webhook",
231 "webhook deliveries": "recent deliveries",
232 "webhook list": "list webhooks",
233 "webhook redeliver": "queue a delivery again",
234 "webhook remove": "remove a webhook",
235 "whoami": "show the authenticated account",
236 "wiki list": "list a repository's wiki pages",
237 "wiki show": "print a wiki page",
238}
cmd/gitbay/summaries_test.go added +62
@@ -0,0 +1,62 @@
1package main
2
3import (
4 "flag"
5 "fmt"
6 "os"
7 "slices"
8 "strings"
9 "testing"
10
11 "gitbay.org/gitbay/internal/control"
12)
13
14var updateSummaries = flag.Bool("update", false, "rewrite summaries_gen.go")
15
16// summaries_gen.go is the registry's one-line summaries, so the CLI's
17// command list and completions say what help says.
18func TestSummariesAreCurrent(t *testing.T) {
19 var b strings.Builder
20 b.WriteString("// Code generated by TestSummariesAreCurrent -update; DO NOT EDIT.\n\npackage main\n\nvar summaries = map[string]string{\n")
21 var lines []string
22 for _, cmd := range control.Commands() {
23 lines = append(lines, fmt.Sprintf("\t%q: %q,\n", strings.Join(cmd.Path, " "), cmd.Summary))
24 }
25 slices.Sort(lines)
26 b.WriteString(strings.Join(lines, ""))
27 b.WriteString("}\n")
28 if *updateSummaries {
29 os.WriteFile("summaries_gen.go", []byte(b.String()), 0o644)
30 }
31 got, _ := os.ReadFile("summaries_gen.go")
32 if string(got) != b.String() {
33 t.Fatal("summaries_gen.go is stale: go test ./cmd/gitbay -run TestSummariesAreCurrent -update")
34 }
35}
36
37func TestRootSectionsCoverEveryCommand(t *testing.T) {
38 seen := map[string]int{}
39 for _, s := range rootSections {
40 for _, n := range s.names {
41 seen[n]++
42 }
43 }
44 for _, c := range newRoot().Commands() {
45 name := c.Name()
46 if name == "help" || name == "completion" {
47 continue
48 }
49 if seen[name] != 1 {
50 t.Errorf("%s is in %d root sections", name, seen[name])
51 }
52 }
53}
54
55func TestGroupsSayWhatTheServerSays(t *testing.T) {
56 nouns := control.NounSummaries()
57 for _, c := range newRoot().Commands() {
58 if s, ok := nouns[c.Name()]; ok && c.Short != s {
59 t.Errorf("%s: CLI says %q, server says %q", c.Name(), c.Short, s)
60 }
61 }
62}
e2e/cli_test.go +7 −3
@@ -146,11 +146,15 @@ func TestCLI(t *testing.T) {
146 mustGit(t, dir, cliGitEnv, "add", ".") 146 mustGit(t, dir, cliGitEnv, "add", ".")
147 mustGit(t, dir, cliGitEnv, "commit", "-q", "-m", "feature work") 147 mustGit(t, dir, cliGitEnv, "commit", "-q", "-m", "feature work")
148 mustGit(t, dir, cliGitEnv, "push", "-q", "origin", "feature") 148 mustGit(t, dir, cliGitEnv, "push", "-q", "origin", "feature")
149 // A noun's --help is the server's reference for that noun, flags 149 // A noun's --help is the server's reference for that noun, not
150 // included, not cobra's flagless subcommand list (#130). 150 // cobra's flagless subcommand list (#130); a verb's --help carries
151 if out := c.must(t, dir, "", "issue", "--help"); !strings.Contains(out, "issue create <owner/name>") { 151 // its flags.
152 if out := c.must(t, dir, "", "issue", "--help"); !strings.Contains(out, "WRITE\n") || !strings.Contains(out, "issue <verb> --help for flags.") {
152 t.Fatalf("issue --help is not the server's reference:\n%s", out) 153 t.Fatalf("issue --help is not the server's reference:\n%s", out)
153 } 154 }
155 if out := c.must(t, dir, "", "issue", "create", "--help"); !strings.Contains(out, "issue create <owner/name>") || !strings.Contains(out, "--title <t>") {
156 t.Fatalf("issue create --help carries no flags:\n%s", out)
157 }
154 158
155 // Inside the clone on the branch, --source is the checked-out branch 159 // Inside the clone on the branch, --source is the checked-out branch
156 // and --target the default branch; neither needs typing (#101). 160 // and --target the default branch; neither needs typing (#101).
internal/control/admin.go +57 −27
@@ -16,59 +16,89 @@ import (
16 16
17func init() { 17func init() {
18 register(Command{Path: []string{"admin", "user", "list"}, 18 register(Command{Path: []string{"admin", "user", "list"},
19 Summary: "list accounts (instance admins)", 19 Summary: "list accounts (instance admins)",
20 Usage: "admin user list [--state active|pending|disabled|admin] [--limit <n>] [--cursor <c>]", 20 Usage: "admin user list [--state active|pending|disabled|admin] [--limit <n>] [--cursor <c>]",
21 Flags: []Flag{
22 {"--state", "active|pending|disabled|admin", "which accounts", ""},
23 {"--limit", "<n>", "rows per page", ""},
24 {"--cursor", "<c>", "continue from the previous page", ""},
25 },
26 Examples: []string{"admin user list --state pending"},
21 ReadOnly: true, Run: runAdminUserList}) 27 ReadOnly: true, Run: runAdminUserList})
22 register(Command{Path: []string{"admin", "user", "show"}, 28 register(Command{Path: []string{"admin", "user", "show"},
23 Summary: "show an account: keys, emails, orgs, tokens, sessions (instance admins)", 29 Summary: "show an account: keys, emails, orgs, tokens, sessions (instance admins)",
24 Usage: "admin user show <username>", 30 Usage: "admin user show <username>",
31 Examples: []string{"admin user show alice"},
25 ReadOnly: true, Run: runAdminUserShow}) 32 ReadOnly: true, Run: runAdminUserShow})
26 register(Command{Path: []string{"admin", "user", "promote"}, 33 register(Command{Path: []string{"admin", "user", "promote"},
27 Summary: "make an account an instance admin", 34 Summary: "make an account an instance admin",
28 Usage: "admin user promote <username>", 35 Usage: "admin user promote <username>",
29 Run: runAdminUserPromote}) 36 Examples: []string{"admin user promote alice"},
37 Run: runAdminUserPromote})
30 register(Command{Path: []string{"admin", "user", "demote"}, 38 register(Command{Path: []string{"admin", "user", "demote"},
31 Summary: "remove instance admin from an account (never the last one)", 39 Summary: "remove instance admin from an account (never the last one)",
32 Usage: "admin user demote <username>", 40 Usage: "admin user demote <username>",
33 Run: runAdminUserDemote}) 41 Examples: []string{"admin user demote alice"},
42 Run: runAdminUserDemote})
34 register(Command{Path: []string{"admin", "runners"}, 43 register(Command{Path: []string{"admin", "runners"},
35 Summary: "the build queue and runner accounts: last poll, scope, the build each holds (instance admins)", 44 Summary: "the build queue and runner accounts: last poll, scope, the build each holds (instance admins)",
36 Usage: "admin runners", 45 Usage: "admin runners",
46 Examples: []string{"admin runners"},
37 ReadOnly: true, Run: runAdminRunners}) 47 ReadOnly: true, Run: runAdminRunners})
38 register(Command{Path: []string{"admin", "runners", "remove"}, 48 register(Command{Path: []string{"admin", "runners", "remove"},
39 Summary: "drop a key's runner heartbeat row, e.g. one that polled once by mistake (instance admins)", 49 Summary: "drop a key's runner heartbeat row, e.g. one that polled once by mistake (instance admins)",
40 Usage: "admin runners remove <fingerprint>", 50 Usage: "admin runners remove <fingerprint>",
41 Run: runAdminRunnersForget}) 51 Examples: []string{"admin runners remove SHA256:abcd1234"},
52 Run: runAdminRunnersForget})
42 // forget is the name this shipped under in v1.18; remove is the verb 53 // forget is the name this shipped under in v1.18; remove is the verb
43 // every other noun uses. Both stay for one release. 54 // every other noun uses. Both stay for one release.
44 register(Command{Path: []string{"admin", "runners", "forget"}, 55 register(Command{Path: []string{"admin", "runners", "forget"},
45 Summary: "alias of admin runners remove", 56 Summary: "alias of admin runners remove",
46 Usage: "admin runners forget <fingerprint>", 57 Usage: "admin runners forget <fingerprint>",
47 Run: runAdminRunnersForget}) 58 Examples: []string{"admin runners forget SHA256:abcd1234"},
59 Run: runAdminRunnersForget})
48 register(Command{Path: []string{"admin", "repo", "list"}, 60 register(Command{Path: []string{"admin", "repo", "list"},
49 Summary: "list every repository with size and last push (instance admins)", 61 Summary: "list every repository with size and last push (instance admins)",
50 Usage: "admin repo list [--owner <name>] [--visibility public|private] [--limit <n>] [--cursor <c>]", 62 Usage: "admin repo list [--owner <name>] [--visibility public|private] [--limit <n>] [--cursor <c>]",
63 Flags: []Flag{
64 {"--owner", "<name>", "only this owner's repositories", ""},
65 {"--visibility", "public|private", "which repositories", ""},
66 {"--limit", "<n>", "rows per page", ""},
67 {"--cursor", "<c>", "continue from the previous page", ""},
68 },
69 Examples: []string{"admin repo list --owner alice"},
51 ReadOnly: true, Run: runAdminRepoList}) 70 ReadOnly: true, Run: runAdminRepoList})
52 register(Command{Path: []string{"admin", "repo", "archive"}, 71 register(Command{Path: []string{"admin", "repo", "archive"},
53 Summary: "archive any repository (instance admins; audited)", 72 Summary: "archive any repository (instance admins; audited)",
54 Usage: "admin repo archive <owner/name>", 73 Usage: "admin repo archive <owner/name>",
55 Run: runAdminRepoArchive}) 74 Examples: []string{"admin repo archive alice/old-project"},
75 Run: runAdminRepoArchive})
56 register(Command{Path: []string{"admin", "repo", "unarchive"}, 76 register(Command{Path: []string{"admin", "repo", "unarchive"},
57 Summary: "unarchive any repository (instance admins; audited)", 77 Summary: "unarchive any repository (instance admins; audited)",
58 Usage: "admin repo unarchive <owner/name>", 78 Usage: "admin repo unarchive <owner/name>",
59 Run: runAdminRepoUnarchive}) 79 Examples: []string{"admin repo unarchive alice/old-project"},
80 Run: runAdminRepoUnarchive})
60 register(Command{Path: []string{"admin", "repo", "visibility"}, 81 register(Command{Path: []string{"admin", "repo", "visibility"},
61 Summary: "set any repository's visibility (instance admins; audited)", 82 Summary: "set any repository's visibility (instance admins; audited)",
62 Usage: "admin repo visibility <owner/name> public|private", 83 Usage: "admin repo visibility <owner/name> public|private",
63 Run: runAdminRepoVisibility}) 84 Examples: []string{"admin repo visibility alice/secret private"},
85 Run: runAdminRepoVisibility})
64 register(Command{Path: []string{"admin", "repo", "delete"}, 86 register(Command{Path: []string{"admin", "repo", "delete"},
65 Summary: "delete any repository (instance admins; audited)", 87 Summary: "delete any repository (instance admins; audited)",
66 Usage: "admin repo delete <owner/name> --yes", 88 Usage: "admin repo delete <owner/name> --yes",
67 Run: runAdminRepoDelete}) 89 Flags: []Flag{
90 {"--yes", "", "confirm the permanent delete", ""},
91 },
92 Examples: []string{"admin repo delete alice/spam --yes"},
93 Run: runAdminRepoDelete})
68 register(Command{Path: []string{"admin", "mr", "prune"}, 94 register(Command{Path: []string{"admin", "mr", "prune"},
69 Summary: "drop merged or closed MRs' head refs and the objects only they kept, e.g. after a history rewrite (instance admins; audited)", 95 Summary: "drop merged or closed MRs' head refs and the objects only they kept, e.g. after a history rewrite (instance admins; audited)",
70 Usage: "admin mr prune <owner/name> <n> [<n>...] --yes", 96 Usage: "admin mr prune <owner/name> <n> [<n>...] --yes",
71 Run: runAdminMRPrune}) 97 Flags: []Flag{
98 {"--yes", "", "confirm the permanent prune", ""},
99 },
100 Examples: []string{"admin mr prune krz/gitbay 12 13 --yes"},
101 Run: runAdminMRPrune})
72} 102}
73 103
74// requireInstanceAdmin gates the admin noun. -1 means proceed. 104// requireInstanceAdmin gates the admin noun. -1 means proceed.
internal/control/adminhost.go +32 −13
@@ -22,32 +22,51 @@ import (
22 22
23func init() { 23func init() {
24 register(Command{Path: []string{"admin", "user", "create"}, 24 register(Command{Path: []string{"admin", "user", "create"},
25 Summary: "create an account, optionally with a key and a verified address (instance admins)", 25 Summary: "create an account, optionally with a key and a verified address (instance admins)",
26 Usage: "admin user create <username> [--admin] [--email <address> [--verified]] [--key -] < key.pub", 26 Usage: "admin user create <username> [--admin] [--email <address> [--verified]] [--key -] < key.pub",
27 Flags: []Flag{
28 {"--admin", "", "make the account an instance admin", ""},
29 {"--email", "<address>", "an address to add", ""},
30 {"--verified", "", "mark that address verified", ""},
31 {"--key", "-", "read a public key from stdin", ""},
32 },
33 Examples: []string{"admin user create alice --email alice@example.org --key - < key.pub"},
27 ReadsStdin: true, Run: runAdminUserCreate}) 34 ReadsStdin: true, Run: runAdminUserCreate})
28 register(Command{Path: []string{"admin", "user", "disable"}, 35 register(Command{Path: []string{"admin", "user", "disable"},
29 Summary: "suspend an account: SSH, web sessions and API tokens refused until re-enabled", 36 Summary: "suspend an account: SSH, web sessions and API tokens refused until re-enabled",
30 Usage: "admin user disable <username>", 37 Usage: "admin user disable <username>",
31 Run: runAdminUserDisable}) 38 Examples: []string{"admin user disable alice"},
39 Run: runAdminUserDisable})
32 register(Command{Path: []string{"admin", "user", "enable"}, 40 register(Command{Path: []string{"admin", "user", "enable"},
33 Summary: "restore a suspended account", 41 Summary: "restore a suspended account",
34 Usage: "admin user enable <username>", 42 Usage: "admin user enable <username>",
35 Run: runAdminUserEnable}) 43 Examples: []string{"admin user enable alice"},
44 Run: runAdminUserEnable})
36 register(Command{Path: []string{"admin", "user", "delete"}, 45 register(Command{Path: []string{"admin", "user", "delete"},
37 Summary: "delete an account that anchors nothing (keys, emails and sessions go with it)", 46 Summary: "delete an account that anchors nothing (keys, emails and sessions go with it)",
38 Usage: "admin user delete <username> --yes", 47 Usage: "admin user delete <username> --yes",
39 Run: runAdminUserDelete}) 48 Flags: []Flag{
49 {"--yes", "", "confirm the permanent delete", ""},
50 },
51 Examples: []string{"admin user delete alice --yes"},
52 Run: runAdminUserDelete})
40 register(Command{Path: []string{"admin", "email", "verify"}, 53 register(Command{Path: []string{"admin", "email", "verify"},
41 Summary: "mark an address verified by admin assertion", 54 Summary: "mark an address verified by admin assertion",
42 Usage: "admin email verify <username> <address>", 55 Usage: "admin email verify <username> <address>",
43 Run: runAdminEmailVerify}) 56 Examples: []string{"admin email verify alice alice@example.org"},
57 Run: runAdminEmailVerify})
44 register(Command{Path: []string{"admin", "invite"}, 58 register(Command{Path: []string{"admin", "invite"},
45 Summary: "issue a registration invite and mail its code", 59 Summary: "issue a registration invite and mail its code",
46 Usage: "admin invite --email <address>", 60 Usage: "admin invite --email <address>",
47 Run: runAdminInvite}) 61 Flags: []Flag{
62 {"--email", "<address>", "who the invite is for", ""},
63 },
64 Examples: []string{"admin invite --email alice@example.org"},
65 Run: runAdminInvite})
48 register(Command{Path: []string{"admin", "stats"}, 66 register(Command{Path: []string{"admin", "stats"},
49 Summary: "instance statistics: counts and per-repository disk usage", 67 Summary: "instance statistics: counts and per-repository disk usage",
50 Usage: "admin stats", 68 Usage: "admin stats",
69 Examples: []string{"admin stats"},
51 ReadOnly: true, Run: runAdminStats}) 70 ReadOnly: true, Run: runAdminStats})
52} 71}
53 72
internal/control/audit.go +9 −2
@@ -12,8 +12,15 @@ import (
12 12
13func init() { 13func init() {
14 register(Command{Path: []string{"audit"}, 14 register(Command{Path: []string{"audit"},
15 Summary: "instance audit log (admins)", 15 Summary: "instance audit log (admins)",
16 Usage: "audit [--actor <user>|-] [--action <prefix>] [--since <duration|date>] [--limit <n>]", 16 Usage: "audit [--actor <user>|-] [--action <prefix>] [--since <duration|date>] [--limit <n>]",
17 Flags: []Flag{
18 {"--actor", "<user>|-", "only entries by this user", ""},
19 {"--action", "<prefix>", "only actions starting with this", ""},
20 {"--since", "<duration|date>", "only entries after this", ""},
21 {"--limit", "<n>", "rows to show", "100"},
22 },
23 Examples: []string{"audit --actor alice --since 24h"},
17 ReadOnly: true, Run: runAudit}) 24 ReadOnly: true, Run: runAudit})
18} 25}
19 26
internal/control/build.go +55 −19
@@ -22,37 +22,64 @@ import (
22func init() { 22func init() {
23 register(Command{Path: []string{"build", "list"}, 23 register(Command{Path: []string{"build", "list"},
24 Summary: "list recent builds", 24 Summary: "list recent builds",
25 Usage: "build list <owner/name> [--ref <branch>] [--status <state>] [--job <name>] [--limit <n>] [--cursor <c>]", ReadOnly: true, Run: runBuildList}) 25 Usage: "build list <owner/name> [--ref <branch>] [--status <state>] [--job <name>] [--limit <n>] [--cursor <c>]",
26 Flags: []Flag{
27 {"--ref", "<branch>", "only builds on this branch", ""},
28 {"--status", "<state>", "only builds in this state", ""},
29 {"--job", "<name>", "only this job", ""},
30 {"--limit", "<n>", "rows per page", "50"},
31 {"--cursor", "<c>", "continue from the previous page", ""},
32 },
33 Examples: []string{"build list krz/gitbay --status failure"},
34 ReadOnly: true, Run: runBuildList})
26 register(Command{Path: []string{"build", "show"}, 35 register(Command{Path: []string{"build", "show"},
27 Summary: "show one build", 36 Summary: "show one build",
28 Usage: "build show <owner/name> <n>", ReadOnly: true, Run: runBuildShow}) 37 Usage: "build show <owner/name> <n>",
38 Examples: []string{"build show krz/gitbay 431"},
39 ReadOnly: true, Run: runBuildShow})
29 register(Command{Path: []string{"build", "log"}, 40 register(Command{Path: []string{"build", "log"},
30 Summary: "print a build's log, or follow it until the build ends", 41 Summary: "print a build's log, or follow it until the build ends",
31 Usage: "build log <owner/name> <n> [--follow]", ReadOnly: true, Run: runBuildLog}) 42 Usage: "build log <owner/name> <n> [--follow]",
43 Flags: []Flag{
44 {"--follow", "", "stream the log until the build ends", ""},
45 },
46 Examples: []string{"build log krz/gitbay 431 --follow"},
47 ReadOnly: true, Run: runBuildLog})
32 48
33 register(Command{Path: []string{"build", "jobs"}, 49 register(Command{Path: []string{"build", "jobs"},
34 Summary: "list the jobs a trigger can name", 50 Summary: "list the jobs a trigger can name",
35 Usage: "build jobs <owner/name>", ReadOnly: true, Run: runBuildJobs}) 51 Usage: "build jobs <owner/name>",
52 Examples: []string{"build jobs krz/gitbay"},
53 ReadOnly: true, Run: runBuildJobs})
36 54
37 register(Command{Path: []string{"build", "cancel"}, 55 register(Command{Path: []string{"build", "cancel"},
38 Summary: "withdraw a queued build before a runner claims it", 56 Summary: "withdraw a queued build before a runner claims it",
39 Usage: "build cancel <owner/name> <n>", Run: runBuildCancel}) 57 Usage: "build cancel <owner/name> <n>",
58 Examples: []string{"build cancel krz/gitbay 431"},
59 Run: runBuildCancel})
40 register(Command{Path: []string{"build", "trigger"}, 60 register(Command{Path: []string{"build", "trigger"},
41 Summary: "queue a job now (scheduled or not)", 61 Summary: "queue a job now (scheduled or not)",
42 Usage: "build trigger <owner/name> <job>", Run: runBuildTrigger}) 62 Usage: "build trigger <owner/name> <job>",
63 Examples: []string{"build trigger krz/gitbay vuln"},
64 Run: runBuildTrigger})
43 // Secrets: set over stdin, listed by name only, injected into the 65 // Secrets: set over stdin, listed by name only, injected into the
44 // repo's builds as environment variables. Same discipline as mirror 66 // repo's builds as environment variables. Same discipline as mirror
45 // tokens — the value never appears in argv, logs, or output. 67 // tokens — the value never appears in argv, logs, or output.
46 register(Command{Path: []string{"repo", "secret", "set"}, 68 register(Command{Path: []string{"repo", "secret", "set"},
47 Summary: "set a build secret", 69 Summary: "set a build secret",
48 Usage: "repo secret set <owner/name> <NAME> (value on stdin)", 70 Usage: "repo secret set <owner/name> <NAME> (value on stdin)",
71 Examples: []string{"repo secret set krz/gitbay DEPLOY_TOKEN"},
49 ReadsStdin: true, Run: runSecretSet}) 72 ReadsStdin: true, Run: runSecretSet})
50 register(Command{Path: []string{"repo", "secret", "remove"}, 73 register(Command{Path: []string{"repo", "secret", "remove"},
51 Summary: "remove a build secret", 74 Summary: "remove a build secret",
52 Usage: "repo secret remove <owner/name> <NAME>", Run: runSecretRemove}) 75 Usage: "repo secret remove <owner/name> <NAME>",
76 Examples: []string{"repo secret remove krz/gitbay DEPLOY_TOKEN"},
77 Run: runSecretRemove})
53 register(Command{Path: []string{"repo", "secret", "list"}, 78 register(Command{Path: []string{"repo", "secret", "list"},
54 Summary: "list build secret names", 79 Summary: "list build secret names",
55 Usage: "repo secret list <owner/name>", ReadOnly: true, Run: runSecretList}) 80 Usage: "repo secret list <owner/name>",
81 Examples: []string{"repo secret list krz/gitbay"},
82 ReadOnly: true, Run: runSecretList})
56 83
57 // Runner commands: the claim/report loop for gitbay-runner. A runner 84 // Runner commands: the claim/report loop for gitbay-runner. A runner
58 // executes arbitrary repo code, so handing out jobs is the instance 85 // executes arbitrary repo code, so handing out jobs is the instance
@@ -61,13 +88,22 @@ func init() {
61 // an admin key, which a runner host should not hold (#92). 88 // an admin key, which a runner host should not hold (#92).
62 register(Command{Path: []string{"runner", "next"}, 89 register(Command{Path: []string{"runner", "next"},
63 Summary: "claim the oldest pending build this key may run (runner protocol)", 90 Summary: "claim the oldest pending build this key may run (runner protocol)",
64 Usage: "runner next [--untrusted] [<owner/name>...]", Run: runRunnerNext}) 91 Usage: "runner next [--untrusted] [<owner/name>...]",
92 Flags: []Flag{
93 {"--untrusted", "", "this runner may build a fork's merge request head", ""},
94 },
95 Examples: []string{"runner next krz/gitbay"},
96 Run: runRunnerNext})
65 register(Command{Path: []string{"runner", "log"}, 97 register(Command{Path: []string{"runner", "log"},
66 Summary: "append a build's log from stdin", 98 Summary: "append a build's log from stdin",
67 Usage: "runner log <build-id>", ReadsStdin: true, Run: runRunnerLog}) 99 Usage: "runner log <build-id>",
100 Examples: []string{"runner log 431"},
101 ReadsStdin: true, Run: runRunnerLog})
68 register(Command{Path: []string{"runner", "done"}, 102 register(Command{Path: []string{"runner", "done"},
69 Summary: "finish a build", 103 Summary: "finish a build",
70 Usage: "runner done <build-id> success|failure", Run: runRunnerDone}) 104 Usage: "runner done <build-id> success|failure",
105 Examples: []string{"runner done 431 success"},
106 Run: runRunnerDone})
71} 107}
72 108
73type BuildOut struct { 109type BuildOut struct {
internal/control/commitfile.go +8
@@ -17,6 +17,14 @@ func init() {
17 Summary: "write a file and commit it", 17 Summary: "write a file and commit it",
18 Usage: "repo commit-file <owner/name> <path> " + 18 Usage: "repo commit-file <owner/name> <path> " +
19 "--ref <branch> [--message <m>] [--file -]", 19 "--ref <branch> [--message <m>] [--file -]",
20 Flags: []Flag{
21 {"--ref", "<branch>", "branch to commit to", ""},
22 {"--message", "<m>", "the commit message", ""},
23 {"--file", "-", "read the new content from stdin", ""},
24 },
25 Examples: []string{
26 `repo commit-file krz/gitbay CHANGELOG.org --ref main --message "note the release" --file - < CHANGELOG.org`,
27 },
20 ReadsStdin: true, 28 ReadsStdin: true,
21 Run: runCommitFile, 29 Run: runCommitFile,
22 }) 30 })
internal/control/control.go +10 −51
@@ -68,6 +68,14 @@ func (c *Ctx) usageWith(msg string) int {
68 return c.fail(protocol.ExitUsage, "%s\nusage: %s", msg, c.Cmd.Usage) 68 return c.fail(protocol.ExitUsage, "%s\nusage: %s", msg, c.Cmd.Usage)
69} 69}
70 70
71// Flag is one flag in a command's help.
72type Flag struct {
73 Name string `json:"name"` // "--state"
74 Arg string `json:"arg,omitempty"` // "open|closed|all"; empty for a switch
75 Desc string `json:"desc,omitempty"` // what it does, lower case, no full stop
76 Default string `json:"default,omitempty"` // empty for none
77}
78
71type Command struct { 79type Command struct {
72 Path []string // e.g. ["keys", "add"] 80 Path []string // e.g. ["keys", "add"]
73 // Summary is one line of prose: what the command does, no argument 81 // Summary is one line of prose: what the command does, no argument
@@ -75,6 +83,8 @@ type Command struct {
75 // help renders them separately, so neither may carry the other's job. 83 // help renders them separately, so neither may carry the other's job.
76 Summary string 84 Summary string
77 Usage string 85 Usage string
86 Flags []Flag
87 Examples []string // full argv after the program, repository named
78 ReadsStdin bool 88 ReadsStdin bool
79 ReadOnly bool // safe for read-scoped API tokens 89 ReadOnly bool // safe for read-scoped API tokens
80 Run func(c *Ctx, args []string) int 90 Run func(c *Ctx, args []string) int
@@ -315,57 +325,6 @@ func (c *Ctx) fail(code int, format string, args ...any) int {
315 return code 325 return code
316} 326}
317 327
318func init() {
319 register(Command{
320 Path: []string{"help"},
321 Summary: "list available commands",
322 Usage: "help [<prefix>...]",
323 ReadOnly: true,
324 Run: runHelp,
325 })
326}
327
328// helpEntry is one row of the registry as help reports it.
329type helpEntry struct {
330 Path string `json:"path"`
331 Summary string `json:"summary"`
332 Usage string `json:"usage"`
333}
334
335// runHelp lists the registry, sorted, so a noun's commands sit together.
336// A prefix narrows the listing and adds each command's argument syntax —
337// the only place flags are written down. The unfiltered listing stays one
338// line per command.
339func runHelp(c *Ctx, args []string) int {
340 prefix := joinPath(args)
341 var matched []helpEntry
342 for _, cmd := range registry {
343 p := joinPath(cmd.Path)
344 if prefix != "" && p != prefix && !strings.HasPrefix(p, prefix+" ") {
345 continue
346 }
347 matched = append(matched, helpEntry{Path: p, Summary: cmd.Summary, Usage: cmd.Usage})
348 }
349 if len(matched) == 0 {
350 return c.fail(protocol.ExitNotFound, "no command matches %q; try: help", prefix)
351 }
352 slices.SortFunc(matched, func(a, b helpEntry) int { return strings.Compare(a.Path, b.Path) })
353 return c.emit(matched, func(w io.Writer) {
354 for _, e := range matched {
355 summary := e.Summary
356 if c.Term.Cols > 0 {
357 if avail := c.Term.Cols - max(cells(e.Path), 24) - 1; avail > 0 {
358 summary = clip(summary, avail)
359 }
360 }
361 fmt.Fprintf(w, "%-24s %s\n", e.Path, summary)
362 if prefix != "" {
363 fmt.Fprintf(w, " %s\n", e.Usage)
364 }
365 }
366 })
367}
368
369func joinPath(p []string) string { 328func joinPath(p []string) string {
370 out := "" 329 out := ""
371 for i, s := range p { 330 for i, s := range p {
internal/control/control_test.go +8 −12
@@ -125,25 +125,21 @@ func TestEveryCommandDocumentsItsUsage(t *testing.T) {
125 } 125 }
126} 126}
127 127
128// TestHelpPrefixNarrowsAndShowsFlags covers the reason the command exists: 128// TestHelpPrefixNarrowsToTheNoun covers the reason the command exists:
129// before this, reading one command's flags meant reading all of them. 129// before this, reading one command's flags meant reading all of them. A
130func TestHelpPrefixNarrowsAndShowsFlags(t *testing.T) { 130// noun prefix now lists its verbs under READ/WRITE; a verb's own flags
131// are on `help <noun> <verb>` (TestHelpVerb, help_test.go).
132func TestHelpPrefixNarrowsToTheNoun(t *testing.T) {
131 var buf bytes.Buffer 133 var buf bytes.Buffer
132 c := &Ctx{Stdout: &buf, Stderr: io.Discard} 134 c := &Ctx{Stdout: &buf, Stderr: io.Discard}
133 if code := runHelp(c, []string{"issue"}); code != protocol.ExitOK { 135 if code := runHelp(c, []string{"issue"}); code != protocol.ExitOK {
134 t.Fatalf("help issue exited %d", code) 136 t.Fatalf("help issue exited %d", code)
135 } 137 }
136 out := buf.String() 138 out := buf.String()
137 for _, line := range strings.Split(strings.TrimSpace(out), "\n") { 139 for _, want := range []string{"READ\n", " list", "WRITE\n", " create", "issue <verb> --help for flags.\n"} {
138 if strings.HasPrefix(line, " ") { 140 if !strings.Contains(out, want) {
139 continue // the indented usage line 141 t.Errorf("missing %q in:\n%s", want, out)
140 } 142 }
141 if !strings.HasPrefix(line, "issue ") {
142 t.Errorf("help issue listed an unrelated command: %q", line)
143 }
144 }
145 if !strings.Contains(out, "--state open|closed|all") {
146 t.Error("help issue did not print issue list's flags")
147 } 143 }
148} 144}
149 145
internal/control/dashboard.go +8 −2
@@ -18,10 +18,16 @@ func init() {
18 register(Command{Path: []string{"dashboard"}, 18 register(Command{Path: []string{"dashboard"},
19 Summary: "one read for the account dashboard: review queue, assigned and open work, pins, activity, builds", 19 Summary: "one read for the account dashboard: review queue, assigned and open work, pins, activity, builds",
20 Usage: "dashboard", 20 Usage: "dashboard",
21 Examples: []string{"dashboard"},
21 ReadOnly: true, Run: runDashboard}) 22 ReadOnly: true, Run: runDashboard})
22 register(Command{Path: []string{"feed"}, 23 register(Command{Path: []string{"feed"},
23 Summary: "activity on repositories you can reach", 24 Summary: "activity on repositories you can reach",
24 Usage: "feed [--limit <n>] [--cursor <c>]", 25 Usage: "feed [--limit <n>] [--cursor <c>]",
26 Flags: []Flag{
27 {"--limit", "<n>", "rows per page", ""},
28 {"--cursor", "<c>", "continue from the previous page", ""},
29 },
30 Examples: []string{"feed --limit 20"},
25 ReadOnly: true, Run: runFeed}) 31 ReadOnly: true, Run: runFeed})
26} 32}
27 33
internal/control/deploykey.go +14 −6
@@ -14,15 +14,23 @@ import (
14 14
15func init() { 15func init() {
16 register(Command{Path: []string{"repo", "deploy-key", "add"}, 16 register(Command{Path: []string{"repo", "deploy-key", "add"},
17 Summary: "bind a read-only (or --rw) key to one repository", 17 Summary: "bind a read-only (or --rw) key to one repository",
18 Usage: "repo deploy-key add <owner/name> [--rw] < key.pub", 18 Usage: "repo deploy-key add <owner/name> [--rw] < key.pub",
19 Flags: []Flag{
20 {"--rw", "", "the key may push, not just fetch", ""},
21 },
22 Examples: []string{"repo deploy-key add krz/gitbay < key.pub"},
19 ReadsStdin: true, Run: runDeployKeyAdd}) 23 ReadsStdin: true, Run: runDeployKeyAdd})
20 register(Command{Path: []string{"repo", "deploy-key", "list"}, 24 register(Command{Path: []string{"repo", "deploy-key", "list"},
21 Summary: "list deploy keys", 25 Summary: "list deploy keys",
22 Usage: "repo deploy-key list <owner/name>", ReadOnly: true, Run: runDeployKeyList}) 26 Usage: "repo deploy-key list <owner/name>",
27 Examples: []string{"repo deploy-key list krz/gitbay"},
28 ReadOnly: true, Run: runDeployKeyList})
23 register(Command{Path: []string{"repo", "deploy-key", "remove"}, 29 register(Command{Path: []string{"repo", "deploy-key", "remove"},
24 Summary: "remove a deploy key", 30 Summary: "remove a deploy key",
25 Usage: "repo deploy-key remove <owner/name> <fingerprint>", Run: runDeployKeyRemove}) 31 Usage: "repo deploy-key remove <owner/name> <fingerprint>",
32 Examples: []string{"repo deploy-key remove krz/gitbay SHA256:abcd1234"},
33 Run: runDeployKeyRemove})
26} 34}
27 35
28func runDeployKeyAdd(c *Ctx, args []string) int { 36func runDeployKeyAdd(c *Ctx, args []string) int {
internal/control/deps.go +12 −6
@@ -15,14 +15,20 @@ func init() {
15 // public registry what the repository depends on, which is the owner's 15 // public registry what the repository depends on, which is the owner's
16 // disclosure to make, not the instance's. 16 // disclosure to make, not the instance's.
17 register(Command{Path: []string{"repo", "deps", "enable"}, 17 register(Command{Path: []string{"repo", "deps", "enable"},
18 Summary: "check dependencies for updates", 18 Summary: "check dependencies for updates",
19 Usage: "repo deps enable <owner/name>", Run: runDepsEnable}) 19 Usage: "repo deps enable <owner/name>",
20 Examples: []string{"repo deps enable krz/gitbay"},
21 Run: runDepsEnable})
20 register(Command{Path: []string{"repo", "deps", "disable"}, 22 register(Command{Path: []string{"repo", "deps", "disable"},
21 Summary: "stop checking dependencies", 23 Summary: "stop checking dependencies",
22 Usage: "repo deps disable <owner/name>", Run: runDepsDisable}) 24 Usage: "repo deps disable <owner/name>",
25 Examples: []string{"repo deps disable krz/gitbay"},
26 Run: runDepsDisable})
23 register(Command{Path: []string{"repo", "deps", "status"}, 27 register(Command{Path: []string{"repo", "deps", "status"},
24 Summary: "show dependency check state", 28 Summary: "show dependency check state",
25 Usage: "repo deps status <owner/name>", ReadOnly: true, Run: runDepsStatus}) 29 Usage: "repo deps status <owner/name>",
30 Examples: []string{"repo deps status krz/gitbay"},
31 ReadOnly: true, Run: runDepsStatus})
26} 32}
27 33
28// DepsOut is what `repo deps status` emits: the check's state and what 34// DepsOut is what `repo deps status` emits: the check's state and what
internal/control/diffcomment.go +27 −8
@@ -16,18 +16,37 @@ import (
16 16
17func init() { 17func init() {
18 register(Command{Path: []string{"mr", "diff-comment"}, 18 register(Command{Path: []string{"mr", "diff-comment"},
19 Summary: "comment on a diff line", 19 Summary: "comment on a diff line",
20 Usage: "mr diff-comment <owner/name> <n> --path <file> --line <l> [--old] [--pending] [--reply <id>] [--message <m> | --file -]", 20 Usage: "mr diff-comment <owner/name> <n> --path <file> --line <l> [--old] [--pending] [--reply <id>] [--message <m> | --file -]",
21 Flags: []Flag{
22 {"--path", "<file>", "the file the comment is on", ""},
23 {"--line", "<l>", "the line the comment is on", ""},
24 {"--old", "", "the line is on the old side of the diff", ""},
25 {"--pending", "", "hold the comment for `mr review --comment`", ""},
26 {"--reply", "<id>", "reply to this thread instead of opening one", ""},
27 {"--message", "<m>", "the comment's text", ""},
28 {"--file", "-", "read the comment from stdin", ""},
29 },
30 Examples: []string{
31 `mr diff-comment krz/gitbay 431 --path internal/control/build.go --line 42 --message "why is this a switch"`,
32 "mr diff-comment krz/gitbay 431 --reply 12 --file - < notes.md",
33 },
21 ReadsStdin: true, Run: runDiffComment}) 34 ReadsStdin: true, Run: runDiffComment})
22 register(Command{Path: []string{"mr", "threads"}, 35 register(Command{Path: []string{"mr", "threads"},
23 Summary: "review threads on an MR", 36 Summary: "review threads on an MR",
24 Usage: "mr threads <owner/name> <n>", ReadOnly: true, Run: runMRThreads}) 37 Usage: "mr threads <owner/name> <n>",
38 Examples: []string{"mr threads krz/gitbay 431"},
39 ReadOnly: true, Run: runMRThreads})
25 register(Command{Path: []string{"mr", "resolve"}, 40 register(Command{Path: []string{"mr", "resolve"},
26 Summary: "resolve a review thread", 41 Summary: "resolve a review thread",
27 Usage: "mr resolve <owner/name> <n> <thread-id>", Run: runMRResolve}) 42 Usage: "mr resolve <owner/name> <n> <thread-id>",
43 Examples: []string{"mr resolve krz/gitbay 431 12"},
44 Run: runMRResolve})
28 register(Command{Path: []string{"mr", "unresolve"}, 45 register(Command{Path: []string{"mr", "unresolve"},
29 Summary: "reopen a review thread", 46 Summary: "reopen a review thread",
30 Usage: "mr unresolve <owner/name> <n> <thread-id>", Run: runMRUnresolve}) 47 Usage: "mr unresolve <owner/name> <n> <thread-id>",
48 Examples: []string{"mr unresolve krz/gitbay 431 12"},
49 Run: runMRUnresolve})
31} 50}
32 51
33func runDiffComment(c *Ctx, args []string) int { 52func runDiffComment(c *Ctx, args []string) int {
internal/control/explore.go +14 −5
@@ -11,9 +11,14 @@ import (
11 11
12func init() { 12func init() {
13 register(Command{ 13 register(Command{
14 Path: []string{"explore"}, 14 Path: []string{"explore"},
15 Summary: "list public repositories", 15 Summary: "list public repositories",
16 Usage: "explore [--limit <n>] [--cursor <c>]", 16 Usage: "explore [--limit <n>] [--cursor <c>]",
17 Flags: []Flag{
18 {"--limit", "<n>", "rows per page", ""},
19 {"--cursor", "<c>", "continue from the previous page", ""},
20 },
21 Examples: []string{"explore --limit 20"},
17 ReadOnly: true, 22 ReadOnly: true,
18 Run: runExplore, 23 Run: runExplore,
19 }) 24 })
@@ -21,8 +26,12 @@ func init() {
21 Path: []string{"repo", "download"}, 26 Path: []string{"repo", "download"},
22 // Not "repo archive": that name is taken by the read-only flag, 27 // Not "repo archive": that name is taken by the read-only flag,
23 // and renaming it would break every script that sets it. 28 // and renaming it would break every script that sets it.
24 Summary: "write a tar.gz of a ref to stdout", 29 Summary: "write a tar.gz of a ref to stdout",
25 Usage: "repo download <owner/name> [--ref <r>] > repo.tar.gz", 30 Usage: "repo download <owner/name> [--ref <r>] > repo.tar.gz",
31 Flags: []Flag{
32 {"--ref", "<r>", "branch, tag or commit to archive", ""},
33 },
34 Examples: []string{"repo download krz/gitbay --ref main > gitbay.tar.gz"},
26 ReadOnly: true, 35 ReadOnly: true,
27 Run: runRepoDownload, 36 Run: runRepoDownload,
28 }) 37 })
internal/control/ghimport.go +8 −2
@@ -23,8 +23,14 @@ import (
23 23
24func init() { 24func init() {
25 register(Command{Path: []string{"repo", "import-issues"}, 25 register(Command{Path: []string{"repo", "import-issues"},
26 Summary: "import issue and PR history from GitHub or Forgejo", 26 Summary: "import issue and PR history from GitHub or Forgejo",
27 Usage: "repo import-issues <owner/name> --from <owner/repo> [--token-stdin] [--api-base <url>]", 27 Usage: "repo import-issues <owner/name> --from <owner/repo> [--token-stdin] [--api-base <url>]",
28 Flags: []Flag{
29 {"--from", "<owner/repo>", "the source repository", ""},
30 {"--token-stdin", "", "read an API token from stdin", ""},
31 {"--api-base", "<url>", "the API's base URL, for Forgejo", ""},
32 },
33 Examples: []string{"repo import-issues krz/gitbay --from krz/gitbay-old"},
28 ReadsStdin: true, Run: runImportIssues}) 34 ReadsStdin: true, Run: runImportIssues})
29} 35}
30 36
internal/control/help.go added +227
@@ -0,0 +1,227 @@
1package control
2
3import (
4 "fmt"
5 "io"
6 "slices"
7 "strings"
8
9 "gitbay.org/gitbay/internal/protocol"
10 "gitbay.org/gitbay/internal/termtext"
11)
12
13func init() {
14 register(Command{
15 Path: []string{"help"},
16 Summary: "list available commands",
17 Usage: "help [<prefix>...]",
18 Examples: []string{"help", "help repo"},
19 ReadOnly: true,
20 Run: runHelp,
21 })
22}
23
24// nounSummaries is one line per distinct first path element in the
25// registry, reusing the CLI's group short texts (cmd/gitbay/main.go)
26// where the noun matches, so the two agree.
27var nounSummaries = map[string]string{
28 "account": "export or import your account, for instance migration",
29 "admin": "instance administration (admins)",
30 "audit": "instance audit log (admins)",
31 "build": "CI builds",
32 "dashboard": "pinned repos, open MRs, assigned issues, recent builds",
33 "email": "manage email addresses",
34 "explore": "public repositories on this instance",
35 "feed": "activity on repositories you can reach",
36 "help": "list available commands",
37 "issue": "issues",
38 "keys": "manage SSH keys",
39 "label": "issue labels",
40 "milestone": "group issues and MRs toward a release",
41 "mr": "merge requests",
42 "notifications": "your notification inbox",
43 "org": "organizations",
44 "pgp": "manage OpenPGP keys",
45 "profile": "user and org profiles",
46 "register": "create an account on this instance",
47 "release": "tag-anchored releases with notes and assets",
48 "repo": "create and manage repositories",
49 "runner": "the claim/report loop CI runners use",
50 "search": "find repositories, issues and merge requests",
51 "snippet": "shared text files, outside any repository",
52 "status": "commit statuses (CI)",
53 "token": "API tokens (minted over SSH, used with the JSON API)",
54 "web": "browser session",
55 "webhook": "outbound event delivery",
56 "whoami": "show the authenticated account",
57 "wiki": "a repository's wiki pages",
58}
59
60// NounSummaries returns nounSummaries, for the CLI group-text agreement
61// test (task 4.5).
62func NounSummaries() map[string]string { return nounSummaries }
63
64// helpEntry is one row of the registry as help reports it.
65type helpEntry struct {
66 Path string `json:"path"`
67 Summary string `json:"summary"`
68 Usage string `json:"usage"`
69 Flags []Flag `json:"flags,omitempty"`
70 Examples []string `json:"examples,omitempty"`
71}
72
73// runHelp lists the registry, sorted, so a noun's commands sit together.
74// Bare `help` stays one line per command. A prefix that names one command
75// exactly renders its full USAGE/FLAGS/EXAMPLES; a prefix that names a
76// noun with several commands under it renders a READ/WRITE summary.
77func runHelp(c *Ctx, args []string) int {
78 prefix := joinPath(args)
79 var matched []Command
80 for _, cmd := range registry {
81 p := joinPath(cmd.Path)
82 if prefix == "" || p == prefix || strings.HasPrefix(p, prefix+" ") {
83 matched = append(matched, cmd)
84 }
85 }
86 if len(matched) == 0 {
87 return c.fail(protocol.ExitNotFound, "no command matches %q; try: help", prefix)
88 }
89 slices.SortFunc(matched, func(a, b Command) int { return strings.Compare(joinPath(a.Path), joinPath(b.Path)) })
90 entries := make([]helpEntry, len(matched))
91 for i, cmd := range matched {
92 entries[i] = helpEntry{Path: joinPath(cmd.Path), Summary: cmd.Summary, Usage: cmd.Usage, Flags: cmd.Flags, Examples: cmd.Examples}
93 }
94 return c.emit(entries, func(w io.Writer) {
95 switch {
96 case prefix == "":
97 for _, e := range entries {
98 summary := e.Summary
99 if c.Term.Cols > 0 {
100 if avail := c.Term.Cols - max(cells(e.Path), 24) - 1; avail > 0 {
101 summary = clip(summary, avail)
102 }
103 }
104 fmt.Fprintf(w, "%-24s %s\n", e.Path, summary)
105 }
106 case joinPath(matched[0].Path) == prefix:
107 c.helpVerb(w, matched[0], matched[1:])
108 default:
109 c.helpNoun(w, prefix, matched)
110 }
111 })
112}
113
114// program is how help spells the command it documents: the CLI at a
115// terminal (only the CLI sends GITBAY_TERM), ssh otherwise.
116func (c *Ctx) program() string {
117 if c.Term.Cols > 0 {
118 return "gitbay"
119 }
120 return "ssh git@" + hostOf(c.Cfg.Server.SiteURL)
121}
122
123func (c *Ctx) heading(w io.Writer, s string) {
124 fmt.Fprintln(w, c.Term.paint(sgrBold, s))
125}
126
127// wrapLine prints prefix+text, wrapping text at Term.Cols with a hanging
128// indent under prefix when it would overflow. Plain output never wraps.
129func (c *Ctx) wrapLine(w io.Writer, prefix, text string) {
130 if c.Term.Cols <= 0 || cells(prefix+text) <= c.Term.Cols {
131 fmt.Fprintln(w, prefix+text)
132 return
133 }
134 indent := cells(prefix)
135 width := c.Term.Cols - indent
136 for i, line := range termtext.Wrap(text, width) {
137 if i == 0 {
138 fmt.Fprintln(w, prefix+line)
139 } else {
140 fmt.Fprintln(w, strings.Repeat(" ", indent)+line)
141 }
142 }
143}
144
145func (c *Ctx) helpVerb(w io.Writer, cmd Command, below []Command) {
146 fmt.Fprintln(w, cmd.Summary)
147 fmt.Fprintln(w)
148 c.heading(w, "USAGE")
149 // Cutting at the first optional flag drops the rest of the usage
150 // syntax behind "[flags]" — safe only for what is actually optional.
151 // A required flag (repo delete --yes) or an alternative
152 // (notifications read <id>... | --all) has no " [--" to cut at, so
153 // the usage prints whole.
154 shape := cmd.Usage
155 if i := strings.Index(shape, " [--"); i >= 0 {
156 shape = shape[:i] + " [flags]"
157 }
158 fmt.Fprintf(w, " %s %s\n", c.program(), shape)
159 fmt.Fprintln(w)
160 c.heading(w, "FLAGS")
161 rows := make([][2]string, 0, len(cmd.Flags)+1)
162 for _, f := range cmd.Flags {
163 name := f.Name
164 if f.Arg != "" {
165 name += " " + f.Arg
166 }
167 desc := f.Desc
168 if f.Default != "" {
169 desc += " (default " + f.Default + ")"
170 }
171 rows = append(rows, [2]string{name, desc})
172 }
173 rows = append(rows, [2]string{"--json", "machine-readable output"})
174 wide := 0
175 for _, r := range rows {
176 wide = max(wide, cells(r[0]))
177 }
178 for _, r := range rows {
179 c.wrapLine(w, " "+pad(r[0], wide)+" ", r[1])
180 }
181 if len(cmd.Examples) > 0 {
182 fmt.Fprintln(w)
183 c.heading(w, "EXAMPLES")
184 for _, ex := range cmd.Examples {
185 c.wrapLine(w, " "+c.program()+" ", ex)
186 }
187 }
188 if len(below) > 0 {
189 fmt.Fprintln(w)
190 c.heading(w, "SEE ALSO")
191 for _, b := range below {
192 fmt.Fprintf(w, " %s %s\n", c.program(), joinPath(b.Path))
193 }
194 }
195}
196
197func (c *Ctx) helpNoun(w io.Writer, prefix string, cmds []Command) {
198 head := nounSummaries[strings.Fields(prefix)[0]]
199 fmt.Fprintln(w, head)
200 fmt.Fprintln(w)
201 c.heading(w, "USAGE")
202 fmt.Fprintf(w, " %s %s <verb> ...\n", c.program(), prefix)
203 wide := 0
204 for _, cmd := range cmds {
205 wide = max(wide, cells(strings.TrimPrefix(joinPath(cmd.Path), prefix+" ")))
206 }
207 for _, section := range []struct {
208 title string
209 read bool
210 }{{"READ", true}, {"WRITE", false}} {
211 first := true
212 for _, cmd := range cmds {
213 if cmd.ReadOnly != section.read {
214 continue
215 }
216 if first {
217 fmt.Fprintln(w)
218 c.heading(w, section.title)
219 first = false
220 }
221 verb := strings.TrimPrefix(joinPath(cmd.Path), prefix+" ")
222 fmt.Fprintf(w, " %s %s\n", pad(verb, wide), cmd.Summary)
223 }
224 }
225 fmt.Fprintln(w)
226 fmt.Fprintf(w, "%s %s <verb> --help for flags.\n", c.program(), prefix)
227}
internal/control/help_test.go added +144
@@ -0,0 +1,144 @@
1package control
2
3import (
4 "bytes"
5 "regexp"
6 "slices"
7 "strings"
8 "testing"
9
10 "gitbay.org/gitbay/internal/protocol"
11)
12
13func helpOut(t *testing.T, term Term, prefix ...string) string {
14 t.Helper()
15 var out, errOut bytes.Buffer
16 c := &Ctx{Stdout: &out, Stderr: &errOut, Term: term, Scope: "full"}
17 c.Cfg.Server.SiteURL = "https://forge.test"
18 if code := Dispatch(c, append([]string{"help"}, prefix...)); code != protocol.ExitOK {
19 t.Fatalf("help %v: exit %d: %s", prefix, code, errOut.String())
20 }
21 return out.String()
22}
23
24func TestHelpVerb(t *testing.T) {
25 out := helpOut(t, Term{Cols: 100}, "issue", "list")
26 for _, want := range []string{
27 "list issues\n",
28 "USAGE\n gitbay issue list <owner/name> [flags]\n",
29 "FLAGS\n",
30 " --state open|closed|all",
31 "which issues (default open)\n",
32 " --json",
33 "EXAMPLES\n gitbay issue list krz/gitbay --label bug --state all\n",
34 } {
35 if !strings.Contains(out, want) {
36 t.Errorf("missing %q in:\n%s", want, out)
37 }
38 }
39 plain := helpOut(t, Term{}, "issue", "list")
40 if !strings.Contains(plain, " ssh git@forge.test issue list krz/gitbay --label bug --state all\n") {
41 t.Errorf("plain examples not ssh:\n%s", plain)
42 }
43 if !strings.Contains(plain, "USAGE\n ssh git@forge.test issue list <owner/name> [flags]\n") {
44 t.Errorf("plain usage:\n%s", plain)
45 }
46}
47
48// TestHelpVerbUsageKeepsRequiredFlags covers a usage line with no
49// optional flag to cut at: it prints whole, not truncated to "[flags]"
50// as though --yes were optional.
51func TestHelpVerbUsageKeepsRequiredFlags(t *testing.T) {
52 out := helpOut(t, Term{Cols: 100}, "repo", "delete")
53 if !strings.Contains(out, "USAGE\n gitbay repo delete <owner/name> --yes\n") {
54 t.Errorf("missing required --yes in usage:\n%s", out)
55 }
56 if strings.Contains(out, "[flags]") {
57 t.Errorf("repo delete has no optional flags; should not print [flags]:\n%s", out)
58 }
59}
60
61// TestHelpVerbUsageKeepsAlternative covers a usage line whose flag is
62// one side of a "|" alternative, not an optional extra.
63func TestHelpVerbUsageKeepsAlternative(t *testing.T) {
64 out := helpOut(t, Term{Cols: 100}, "notifications", "read")
65 if !strings.Contains(out, "USAGE\n gitbay notifications read <id>... | --all\n") {
66 t.Errorf("missing whole alternative in usage:\n%s", out)
67 }
68}
69
70func TestHelpNoun(t *testing.T) {
71 out := helpOut(t, Term{Cols: 100}, "issue")
72 for _, want := range []string{"issues\n", "READ\n", "WRITE\n", " list ", " create ", "gitbay issue <verb> --help for flags.\n"} {
73 if !strings.Contains(out, want) {
74 t.Errorf("missing %q in:\n%s", want, out)
75 }
76 }
77 if strings.Index(out, " list ") > strings.Index(out, "WRITE") {
78 t.Errorf("list is not under READ:\n%s", out)
79 }
80}
81
82func TestEveryNounHasASummary(t *testing.T) {
83 for _, cmd := range Commands() {
84 if nounSummaries[cmd.Path[0]] == "" {
85 t.Errorf("no noun summary for %q", cmd.Path[0])
86 }
87 }
88}
89
90var usageFlag = regexp.MustCompile(`--[a-z][a-z0-9-]*`)
91
92// trailingRedirect strips a shell redirect an example ends with, the way a
93// person's shell would before gitbay ever sees argv: protocol.Tokenize
94// rejects a bare < or > as a shell metacharacter, so the redirected part
95// is not something the command's own tokenizer parses.
96var trailingRedirect = regexp.MustCompile(`^(.*?)\s+[<>]\s*\S+$`)
97
98// Help is written once, in the registry. Every flag in a usage line has
99// a description, every description names a flag in the usage line, and
100// every command has an example that runs it.
101func TestHelpIsComplete(t *testing.T) {
102 for _, cmd := range Commands() {
103 path := strings.Join(cmd.Path, " ")
104 inUsage := map[string]bool{}
105 for _, f := range usageFlag.FindAllString(cmd.Usage, -1) {
106 if f != "--json" {
107 inUsage[f] = true
108 }
109 }
110 described := map[string]bool{}
111 for _, f := range cmd.Flags {
112 described[f.Name] = true
113 if f.Desc == "" {
114 t.Errorf("%s: %s has no description", path, f.Name)
115 }
116 if !inUsage[f.Name] {
117 t.Errorf("%s: %s is described but not in the usage", path, f.Name)
118 }
119 }
120 for f := range inUsage {
121 if !described[f] {
122 t.Errorf("%s: %s is in the usage with no description", path, f)
123 }
124 }
125 if len(cmd.Examples) == 0 {
126 t.Errorf("%s: no example", path)
127 }
128 for _, ex := range cmd.Examples {
129 toParse := ex
130 if m := trailingRedirect.FindStringSubmatch(ex); m != nil {
131 toParse = m[1]
132 }
133 argv, err := protocol.Tokenize(toParse)
134 if err != nil {
135 t.Errorf("%s: example %q: %v", path, ex, err)
136 continue
137 }
138 got, _, ok := Lookup(argv)
139 if !ok || !slices.Equal(got.Path, cmd.Path) {
140 t.Errorf("%s: example %q runs %v", path, ex, got.Path)
141 }
142 }
143 }
144}
internal/control/identity.go +20 −11
@@ -18,6 +18,7 @@ func init() {
18 Path: []string{"whoami"}, 18 Path: []string{"whoami"},
19 Summary: "show the authenticated account", 19 Summary: "show the authenticated account",
20 Usage: "whoami", 20 Usage: "whoami",
21 Examples: []string{"whoami"},
21 ReadOnly: true, 22 ReadOnly: true,
22 Run: runWhoami, 23 Run: runWhoami,
23 }) 24 })
@@ -25,27 +26,35 @@ func init() {
25 Path: []string{"keys", "list"}, 26 Path: []string{"keys", "list"},
26 Summary: "list registered SSH keys", 27 Summary: "list registered SSH keys",
27 Usage: "keys list", 28 Usage: "keys list",
29 Examples: []string{"keys list"},
28 ReadOnly: true, 30 ReadOnly: true,
29 Run: runKeysList, 31 Run: runKeysList,
30 }) 32 })
31 register(Command{ 33 register(Command{
32 Path: []string{"keys", "add"}, 34 Path: []string{"keys", "add"},
33 Summary: "register an SSH public key (authorized_keys format)", 35 Summary: "register an SSH public key (authorized_keys format)",
34 Usage: "keys add [--scope full|git|runner] [--label <text>] < key.pub", 36 Usage: "keys add [--scope full|git|runner] [--label <text>] < key.pub",
37 Flags: []Flag{
38 {"--scope", "full|git|runner", "what the key may do", "full"},
39 {"--label", "<text>", "a name for the key", ""},
40 },
41 Examples: []string{"keys add --label laptop < key.pub"},
35 ReadsStdin: true, 42 ReadsStdin: true,
36 Run: runKeysAdd, 43 Run: runKeysAdd,
37 }) 44 })
38 register(Command{ 45 register(Command{
39 Path: []string{"keys", "label"}, 46 Path: []string{"keys", "label"},
40 Summary: "name a key; an empty label clears it", 47 Summary: "name a key; an empty label clears it",
41 Usage: "keys label <fingerprint> [<text>]", 48 Usage: "keys label <fingerprint> [<text>]",
42 Run: runKeysLabel, 49 Examples: []string{`keys label SHA256:abcd1234 "work laptop"`},
50 Run: runKeysLabel,
43 }) 51 })
44 register(Command{ 52 register(Command{
45 Path: []string{"keys", "remove"}, 53 Path: []string{"keys", "remove"},
46 Summary: "remove an SSH key by fingerprint", 54 Summary: "remove an SSH key by fingerprint",
47 Usage: "keys remove <fingerprint>", 55 Usage: "keys remove <fingerprint>",
48 Run: runKeysRemove, 56 Examples: []string{"keys remove SHA256:abcd1234"},
57 Run: runKeysRemove,
49 }) 58 })
50} 59}
51 60
internal/control/import.go +8 −2
@@ -17,8 +17,14 @@ import (
17 17
18func init() { 18func init() {
19 register(Command{Path: []string{"repo", "import"}, 19 register(Command{Path: []string{"repo", "import"},
20 Summary: "server-side mirror of a foreign repository", 20 Summary: "server-side mirror of a foreign repository",
21 Usage: "repo import <owner/name> --from <url> [--private] [--token-stdin]", 21 Usage: "repo import <owner/name> --from <url> [--private] [--token-stdin]",
22 Flags: []Flag{
23 {"--from", "<url>", "the repository to import", ""},
24 {"--private", "", "create it private", ""},
25 {"--token-stdin", "", "read a credential token from stdin", ""},
26 },
27 Examples: []string{"repo import krz/imported --from https://github.com/krz/old.git"},
22 ReadsStdin: true, Run: runRepoImport}) 28 ReadsStdin: true, Run: runRepoImport})
23} 29}
24 30
internal/control/issue.go +77 −15
@@ -16,35 +16,97 @@ const maxBodyBytes = 64 << 10
16 16
17func init() { 17func init() {
18 register(Command{Path: []string{"issue", "create"}, 18 register(Command{Path: []string{"issue", "create"},
19 Summary: "open an issue", 19 Summary: "open an issue",
20 Usage: "issue create <owner/name> --title <t> [--body <b> | --file -] [--format md|org]", 20 Usage: "issue create <owner/name> --title <t> [--body <b> | --file -] [--format md|org]",
21 Flags: []Flag{
22 {"--title", "<t>", "the issue's title", ""},
23 {"--body", "<b>", "the issue's body", ""},
24 {"--file", "-", "read the body from stdin", ""},
25 {"--format", "md|org", "the body's markup", "md"},
26 },
27 Examples: []string{
28 `issue create krz/gitbay --title "crash on empty repo" --body "steps to reproduce..."`,
29 "issue create krz/gitbay --title notes --file - < notes.md",
30 },
21 ReadsStdin: true, Run: runIssueCreate}) 31 ReadsStdin: true, Run: runIssueCreate})
22 register(Command{Path: []string{"issue", "list"}, 32 register(Command{Path: []string{"issue", "list"},
23 Summary: "list issues", 33 Summary: "list issues",
24 Usage: "issue list <owner/name> [--state open|closed|all] [--label <l>] [--assignee <user>] [--author <user>] [--milestone <title>|none] [--search <text>] [--limit <n>] [--cursor <c>]", ReadOnly: true, Run: runIssueList}) 34 Usage: "issue list <owner/name> [--state open|closed|all] [--label <l>] [--assignee <user>] [--author <user>] [--milestone <title>|none] [--search <text>] [--limit <n>] [--cursor <c>]",
35 Flags: []Flag{
36 {"--state", "open|closed|all", "which issues", "open"},
37 {"--label", "<l>", "only issues carrying this label", ""},
38 {"--assignee", "<user>", "only issues assigned to this user", ""},
39 {"--author", "<user>", "only issues opened by this user", ""},
40 {"--milestone", "<title>|none", "only issues in this milestone, or in none", ""},
41 {"--search", "<text>", "match title and body", ""},
42 {"--limit", "<n>", "rows per page", ""},
43 {"--cursor", "<c>", "continue from the previous page", ""},
44 },
45 Examples: []string{
46 "issue list krz/gitbay --label bug --state all",
47 "issue list krz/gitbay --assignee cmc",
48 },
49 ReadOnly: true, Run: runIssueList})
25 register(Command{Path: []string{"issue", "show"}, 50 register(Command{Path: []string{"issue", "show"},
26 Summary: "show an issue with comments", 51 Summary: "show an issue with comments",
27 Usage: "issue show <owner/name> <n>", ReadOnly: true, Run: runIssueShow}) 52 Usage: "issue show <owner/name> <n>",
53 Examples: []string{"issue show krz/gitbay 42"},
54 ReadOnly: true, Run: runIssueShow})
28 register(Command{Path: []string{"issue", "edit"}, 55 register(Command{Path: []string{"issue", "edit"},
29 Summary: "edit title or body", 56 Summary: "edit title or body",
30 Usage: "issue edit <owner/name> <n> [--title <t>] [--body <b> | --file -] [--format md|org]", 57 Usage: "issue edit <owner/name> <n> [--title <t>] [--body <b> | --file -] [--format md|org]",
58 Flags: []Flag{
59 {"--title", "<t>", "the issue's new title", ""},
60 {"--body", "<b>", "the issue's new body", ""},
61 {"--file", "-", "read the new body from stdin", ""},
62 {"--format", "md|org", "the body's markup", ""},
63 },
64 Examples: []string{
65 `issue edit krz/gitbay 42 --title "crash on empty repo, take two"`,
66 "issue edit krz/gitbay 42 --file - < notes.md",
67 },
31 ReadsStdin: true, Run: runIssueEdit}) 68 ReadsStdin: true, Run: runIssueEdit})
32 register(Command{Path: []string{"issue", "comment"}, 69 register(Command{Path: []string{"issue", "comment"},
33 Summary: "comment", 70 Summary: "comment",
34 Usage: "issue comment <owner/name> <n> [--message <m> | --file -] [--format md|org]", 71 Usage: "issue comment <owner/name> <n> [--message <m> | --file -] [--format md|org]",
72 Flags: []Flag{
73 {"--message", "<m>", "the comment's text", ""},
74 {"--file", "-", "read the comment from stdin", ""},
75 {"--format", "md|org", "the comment's markup", "md"},
76 },
77 Examples: []string{
78 `issue comment krz/gitbay 42 --message "can't reproduce on main"`,
79 "issue comment krz/gitbay 42 --file - < notes.md",
80 },
35 ReadsStdin: true, Run: runIssueComment}) 81 ReadsStdin: true, Run: runIssueComment})
36 register(Command{Path: []string{"issue", "close"}, 82 register(Command{Path: []string{"issue", "close"},
37 Summary: "close an issue", 83 Summary: "close an issue",
38 Usage: "issue close <owner/name> <n>", Run: runIssueClose}) 84 Usage: "issue close <owner/name> <n>",
85 Examples: []string{"issue close krz/gitbay 42"},
86 Run: runIssueClose})
39 register(Command{Path: []string{"issue", "reopen"}, 87 register(Command{Path: []string{"issue", "reopen"},
40 Summary: "reopen an issue", 88 Summary: "reopen an issue",
41 Usage: "issue reopen <owner/name> <n>", Run: runIssueReopen}) 89 Usage: "issue reopen <owner/name> <n>",
90 Examples: []string{"issue reopen krz/gitbay 42"},
91 Run: runIssueReopen})
42 register(Command{Path: []string{"issue", "label"}, 92 register(Command{Path: []string{"issue", "label"},
43 Summary: "labels", 93 Summary: "labels",
44 Usage: "issue label <owner/name> <n> [--add <l>]... [--remove <l>]...", Run: runIssueLabel}) 94 Usage: "issue label <owner/name> <n> [--add <l>]... [--remove <l>]...",
95 Flags: []Flag{
96 {"--add", "<l>", "label to add, may repeat", ""},
97 {"--remove", "<l>", "label to remove, may repeat", ""},
98 },
99 Examples: []string{"issue label krz/gitbay 42 --add bug --remove needs-triage"},
100 Run: runIssueLabel})
45 register(Command{Path: []string{"issue", "assign"}, 101 register(Command{Path: []string{"issue", "assign"},
46 Summary: "assignees", 102 Summary: "assignees",
47 Usage: "issue assign <owner/name> <n> [--add <user>]... [--remove <user>]...", Run: runIssueAssign}) 103 Usage: "issue assign <owner/name> <n> [--add <user>]... [--remove <user>]...",
104 Flags: []Flag{
105 {"--add", "<user>", "user to assign, may repeat", ""},
106 {"--remove", "<user>", "user to unassign, may repeat", ""},
107 },
108 Examples: []string{"issue assign krz/gitbay 42 --add cmc"},
109 Run: runIssueAssign})
48} 110}
49 111
50// issueArgs parses "<owner/name> <n>" plus flags handled by the caller. 112// issueArgs parses "<owner/name> <n>" plus flags handled by the caller.
internal/control/label.go +14 −5
@@ -14,14 +14,23 @@ import (
14 14
15func init() { 15func init() {
16 register(Command{Path: []string{"label", "list"}, 16 register(Command{Path: []string{"label", "list"},
17 Summary: "list a repository's labels with colour and use", 17 Summary: "list a repository's labels with colour and use",
18 Usage: "label list <owner/name>", ReadOnly: true, Run: runLabelList}) 18 Usage: "label list <owner/name>",
19 Examples: []string{"label list krz/gitbay"},
20 ReadOnly: true, Run: runLabelList})
19 register(Command{Path: []string{"label", "set"}, 21 register(Command{Path: []string{"label", "set"},
20 Summary: "create a label or set its colour", 22 Summary: "create a label or set its colour",
21 Usage: "label set <owner/name> <label> [--color rrggbb|'']", Run: runLabelSet}) 23 Usage: "label set <owner/name> <label> [--color rrggbb|'']",
24 Flags: []Flag{
25 {"--color", "rrggbb|''", "the label's colour, or '' to clear it", ""},
26 },
27 Examples: []string{"label set krz/gitbay bug --color d73a4a"},
28 Run: runLabelSet})
22 register(Command{Path: []string{"label", "remove"}, 29 register(Command{Path: []string{"label", "remove"},
23 Summary: "remove a label from the repository and from every issue and merge request", 30 Summary: "remove a label from the repository and from every issue and merge request",
24 Usage: "label remove <owner/name> <label>", Run: runLabelRemove}) 31 Usage: "label remove <owner/name> <label>",
32 Examples: []string{"label remove krz/gitbay wontfix"},
33 Run: runLabelRemove})
25} 34}
26 35
27// A colour is six hex digits, with or without the hash: over bare ssh a 36// A colour is six hex digits, with or without the hash: over bare ssh a
internal/control/migrate.go +7 −2
@@ -16,10 +16,15 @@ func init() {
16 register(Command{Path: []string{"account", "export"}, 16 register(Command{Path: []string{"account", "export"},
17 Summary: "write your account bundle (profile, repos, issues, MRs) as JSON", 17 Summary: "write your account bundle (profile, repos, issues, MRs) as JSON",
18 Usage: "account export > bundle.json", 18 Usage: "account export > bundle.json",
19 Examples: []string{"account export > bundle.json"},
19 ReadOnly: true, Run: runAccountExport}) 20 ReadOnly: true, Run: runAccountExport})
20 register(Command{Path: []string{"account", "import-bundle"}, 21 register(Command{Path: []string{"account", "import-bundle"},
21 Summary: "replay an account bundle (see gitbay migrate)", 22 Summary: "replay an account bundle (see gitbay migrate)",
22 Usage: "account import-bundle [--source <host>] < bundle.json", 23 Usage: "account import-bundle [--source <host>] < bundle.json",
24 Flags: []Flag{
25 {"--source", "<host>", "the previous instance, for attribution", ""},
26 },
27 Examples: []string{"account import-bundle --source old.example.org < bundle.json"},
23 ReadsStdin: true, Run: runAccountImportBundle}) 28 ReadsStdin: true, Run: runAccountImportBundle})
24} 29}
25 30
internal/control/milestone.go +33 −12
@@ -17,25 +17,46 @@ import (
17func init() { 17func init() {
18 register(Command{Path: []string{"milestone", "create"}, 18 register(Command{Path: []string{"milestone", "create"},
19 Summary: "create a milestone", 19 Summary: "create a milestone",
20 Usage: "milestone create <owner/name> <title> [--description <d>] [--due YYYY-MM-DD]", Run: runMilestoneCreate}) 20 Usage: "milestone create <owner/name> <title> [--description <d>] [--due YYYY-MM-DD]",
21 Flags: []Flag{
22 {"--description", "<d>", "what the milestone covers", ""},
23 {"--due", "YYYY-MM-DD", "target date", ""},
24 },
25 Examples: []string{`milestone create krz/gitbay v1.31.0 --due 2026-10-01`},
26 Run: runMilestoneCreate})
21 register(Command{Path: []string{"milestone", "list"}, 27 register(Command{Path: []string{"milestone", "list"},
22 Summary: "list milestones with progress", 28 Summary: "list milestones with progress",
23 Usage: "milestone list <owner/name> [--state open|closed|all]", ReadOnly: true, Run: runMilestoneList}) 29 Usage: "milestone list <owner/name> [--state open|closed|all]",
30 Flags: []Flag{
31 {"--state", "open|closed|all", "which milestones", "open"},
32 },
33 Examples: []string{"milestone list krz/gitbay --state all"},
34 ReadOnly: true, Run: runMilestoneList})
24 register(Command{Path: []string{"milestone", "close"}, 35 register(Command{Path: []string{"milestone", "close"},
25 Summary: "close a milestone", 36 Summary: "close a milestone",
26 Usage: "milestone close <owner/name> <title>", Run: runMilestoneClose}) 37 Usage: "milestone close <owner/name> <title>",
38 Examples: []string{"milestone close krz/gitbay v1.30.0"},
39 Run: runMilestoneClose})
27 register(Command{Path: []string{"milestone", "reopen"}, 40 register(Command{Path: []string{"milestone", "reopen"},
28 Summary: "reopen a milestone", 41 Summary: "reopen a milestone",
29 Usage: "milestone reopen <owner/name> <title>", Run: runMilestoneReopen}) 42 Usage: "milestone reopen <owner/name> <title>",
43 Examples: []string{"milestone reopen krz/gitbay v1.30.0"},
44 Run: runMilestoneReopen})
30 register(Command{Path: []string{"issue", "milestone"}, 45 register(Command{Path: []string{"issue", "milestone"},
31 Summary: "set or clear an issue's milestone", 46 Summary: "set or clear an issue's milestone",
32 Usage: "issue milestone <owner/name> <n> <title|none>", Run: runIssueMilestone}) 47 Usage: "issue milestone <owner/name> <n> <title|none>",
48 Examples: []string{"issue milestone krz/gitbay 42 v1.31.0"},
49 Run: runIssueMilestone})
33 register(Command{Path: []string{"mr", "milestone"}, 50 register(Command{Path: []string{"mr", "milestone"},
34 Summary: "set or clear an MR's milestone", 51 Summary: "set or clear an MR's milestone",
35 Usage: "mr milestone <owner/name> <n> <title|none>", Run: runMRMilestone}) 52 Usage: "mr milestone <owner/name> <n> <title|none>",
53 Examples: []string{"mr milestone krz/gitbay 431 v1.31.0"},
54 Run: runMRMilestone})
36 register(Command{Path: []string{"issue", "templates"}, 55 register(Command{Path: []string{"issue", "templates"},
37 Summary: "list issue templates (.gitbay/issue-template*.md)", 56 Summary: "list issue templates (.gitbay/issue-template*.md)",
38 Usage: "issue templates <owner/name>", ReadOnly: true, Run: runIssueTemplates}) 57 Usage: "issue templates <owner/name>",
58 Examples: []string{"issue templates krz/gitbay"},
59 ReadOnly: true, Run: runIssueTemplates})
39} 60}
40 61
41var duePat = regexp.MustCompile(`^\d{4}-\d{2}-\d{2}$`) 62var duePat = regexp.MustCompile(`^\d{4}-\d{2}-\d{2}$`)
internal/control/mirrorcmd.go +20 −8
@@ -16,18 +16,30 @@ import (
16 16
17func init() { 17func init() {
18 register(Command{Path: []string{"repo", "mirror", "add"}, 18 register(Command{Path: []string{"repo", "mirror", "add"},
19 Summary: "mirror to or from a remote", 19 Summary: "mirror to or from a remote",
20 Usage: "repo mirror add <owner/name> <https-url> --direction push|pull [--username <u>] [--token-stdin]", 20 Usage: "repo mirror add <owner/name> <https-url> --direction push|pull [--username <u>] [--token-stdin]",
21 Flags: []Flag{
22 {"--direction", "push|pull", "which way the mirror syncs", ""},
23 {"--username", "<u>", "the remote's username", ""},
24 {"--token-stdin", "", "read a credential token from stdin", ""},
25 },
26 Examples: []string{"repo mirror add krz/gitbay https://github.com/krazywarez/gitbay.git --direction push"},
21 ReadsStdin: true, Run: runMirrorAdd}) 27 ReadsStdin: true, Run: runMirrorAdd})
22 register(Command{Path: []string{"repo", "mirror", "list"}, 28 register(Command{Path: []string{"repo", "mirror", "list"},
23 Summary: "list mirrors with sync status", 29 Summary: "list mirrors with sync status",
24 Usage: "repo mirror list <owner/name>", ReadOnly: true, Run: runMirrorList}) 30 Usage: "repo mirror list <owner/name>",
31 Examples: []string{"repo mirror list krz/gitbay"},
32 ReadOnly: true, Run: runMirrorList})
25 register(Command{Path: []string{"repo", "mirror", "remove"}, 33 register(Command{Path: []string{"repo", "mirror", "remove"},
26 Summary: "remove a mirror", 34 Summary: "remove a mirror",
27 Usage: "repo mirror remove <owner/name> <id>", Run: runMirrorRemove}) 35 Usage: "repo mirror remove <owner/name> <id>",
36 Examples: []string{"repo mirror remove krz/gitbay 3"},
37 Run: runMirrorRemove})
28 register(Command{Path: []string{"repo", "mirror", "sync"}, 38 register(Command{Path: []string{"repo", "mirror", "sync"},
29 Summary: "schedule an immediate sync", 39 Summary: "schedule an immediate sync",
30 Usage: "repo mirror sync <owner/name>", Run: runMirrorSync}) 40 Usage: "repo mirror sync <owner/name>",
41 Examples: []string{"repo mirror sync krz/gitbay"},
42 Run: runMirrorSync})
31} 43}
32 44
33func runMirrorAdd(c *Ctx, args []string) int { 45func runMirrorAdd(c *Ctx, args []string) int {
internal/control/mr.go +142 −37
@@ -20,78 +20,183 @@ import (
20func init() { 20func init() {
21 register(Command{Path: []string{"repo", "fork"}, 21 register(Command{Path: []string{"repo", "fork"},
22 Summary: "fork a repository under your account", 22 Summary: "fork a repository under your account",
23 Usage: "repo fork <owner/name> [--owner <o>] [--name <n>]", Run: runRepoFork}) 23 Usage: "repo fork <owner/name> [--owner <o>] [--name <n>]",
24 Flags: []Flag{
25 {"--owner", "<o>", "fork under this user or org, default your account", ""},
26 {"--name", "<n>", "name the fork", "the source's name"},
27 },
28 Examples: []string{"repo fork krz/gitbay"},
29 Run: runRepoFork})
24 register(Command{Path: []string{"repo", "settings", "require-approvals"}, 30 register(Command{Path: []string{"repo", "settings", "require-approvals"},
25 Summary: "require N fresh approvals to merge", 31 Summary: "require N fresh approvals to merge",
26 Usage: "repo settings require-approvals <owner/name> <n> (0 = off)", Run: runRequireApprovals}) 32 Usage: "repo settings require-approvals <owner/name> <n> (0 = off)",
33 Examples: []string{"repo settings require-approvals krz/gitbay 1"},
34 Run: runRequireApprovals})
27 register(Command{Path: []string{"repo", "settings", "require-resolved"}, 35 register(Command{Path: []string{"repo", "settings", "require-resolved"},
28 Summary: "require all review threads resolved to merge", 36 Summary: "require all review threads resolved to merge",
29 Usage: "repo settings require-resolved <owner/name> on|off", Run: runRequireResolved}) 37 Usage: "repo settings require-resolved <owner/name> on|off",
38 Examples: []string{"repo settings require-resolved krz/gitbay on"},
39 Run: runRequireResolved})
30 register(Command{Path: []string{"repo", "settings", "require-codeowners"}, 40 register(Command{Path: []string{"repo", "settings", "require-codeowners"},
31 Summary: "require an owner's approval for every file CODEOWNERS covers", 41 Summary: "require an owner's approval for every file CODEOWNERS covers",
32 Usage: "repo settings require-codeowners <owner/name> on|off", Run: runRequireCodeowners}) 42 Usage: "repo settings require-codeowners <owner/name> on|off",
43 Examples: []string{"repo settings require-codeowners krz/gitbay on"},
44 Run: runRequireCodeowners})
33 register(Command{Path: []string{"repo", "settings", "require-checks"}, 45 register(Command{Path: []string{"repo", "settings", "require-checks"},
34 Summary: "gate merges on green statuses", 46 Summary: "gate merges on green statuses",
35 Usage: "repo settings require-checks <owner/name> on|off", Run: runRequireChecks}) 47 Usage: "repo settings require-checks <owner/name> on|off",
48 Examples: []string{"repo settings require-checks krz/gitbay on"},
49 Run: runRequireChecks})
36 register(Command{Path: []string{"repo", "settings", "require-mr"}, 50 register(Command{Path: []string{"repo", "settings", "require-mr"},
37 Summary: "protected branches take changes through merge requests only", 51 Summary: "protected branches take changes through merge requests only",
38 Usage: "repo settings require-mr <owner/name> on|off", Run: runRequireMR}) 52 Usage: "repo settings require-mr <owner/name> on|off",
53 Examples: []string{"repo settings require-mr krz/gitbay on"},
54 Run: runRequireMR})
39 register(Command{Path: []string{"repo", "settings", "require-signed"}, 55 register(Command{Path: []string{"repo", "settings", "require-signed"},
40 Summary: "require verified commit signatures", 56 Summary: "require verified commit signatures",
41 Usage: "repo settings require-signed <owner/name> on|off", Run: runRequireSigned}) 57 Usage: "repo settings require-signed <owner/name> on|off",
58 Examples: []string{"repo settings require-signed krz/gitbay on"},
59 Run: runRequireSigned})
42 register(Command{Path: []string{"mr", "create"}, 60 register(Command{Path: []string{"mr", "create"},
43 Summary: "open a merge request", 61 Summary: "open a merge request",
44 Usage: "mr create <target owner/name> --source [owner/name:]<branch> --target <branch> --title <t> [--body <b> | --file -] [--format md|org] [--draft]", 62 Usage: "mr create <target owner/name> --source [owner/name:]<branch> --target <branch> --title <t> [--body <b> | --file -] [--format md|org] [--draft]",
63 Flags: []Flag{
64 {"--source", "[owner/name:]<branch>", "the branch to merge, from a fork with owner/name:", ""},
65 {"--target", "<branch>", "the branch to merge into", ""},
66 {"--title", "<t>", "the merge request's title", ""},
67 {"--body", "<b>", "the merge request's body", ""},
68 {"--file", "-", "read the body from stdin", ""},
69 {"--format", "md|org", "the body's markup", "md"},
70 {"--draft", "", "open it as work in progress", ""},
71 },
72 Examples: []string{
73 `mr create krz/gitbay --source cli-output-help --target main --title "control: flag help"`,
74 "mr create krz/gitbay --source cli-output-help --target main --title notes --file - < notes.md",
75 },
45 ReadsStdin: true, Run: runMRCreate}) 76 ReadsStdin: true, Run: runMRCreate})
46 register(Command{Path: []string{"mr", "range-diff"}, 77 register(Command{Path: []string{"mr", "range-diff"},
47 Summary: "what changed between two revisions of a merge request", 78 Summary: "what changed between two revisions of a merge request",
48 Usage: "mr range-diff <owner/name> <n> [--from <sha>] [--to <sha>]", 79 Usage: "mr range-diff <owner/name> <n> [--from <sha>] [--to <sha>]",
80 Flags: []Flag{
81 {"--from", "<sha>", "earlier revision, default the one before --to", ""},
82 {"--to", "<sha>", "later revision, default the head", ""},
83 },
84 Examples: []string{"mr range-diff krz/gitbay 431"},
49 ReadOnly: true, Run: runMRRangeDiff}) 85 ReadOnly: true, Run: runMRRangeDiff})
50 register(Command{Path: []string{"mr", "revisions"}, 86 register(Command{Path: []string{"mr", "revisions"},
51 Summary: "the heads a merge request has had", 87 Summary: "the heads a merge request has had",
52 Usage: "mr revisions <owner/name> <n>", 88 Usage: "mr revisions <owner/name> <n>",
89 Examples: []string{"mr revisions krz/gitbay 431"},
53 ReadOnly: true, Run: runMRRevisions}) 90 ReadOnly: true, Run: runMRRevisions})
54 register(Command{Path: []string{"mr", "draft"}, 91 register(Command{Path: []string{"mr", "draft"},
55 Summary: "mark a merge request as work in progress", 92 Summary: "mark a merge request as work in progress",
56 Usage: "mr draft <owner/name> <n>", Run: runMRDraft}) 93 Usage: "mr draft <owner/name> <n>",
94 Examples: []string{"mr draft krz/gitbay 431"},
95 Run: runMRDraft})
57 register(Command{Path: []string{"mr", "ready"}, 96 register(Command{Path: []string{"mr", "ready"},
58 Summary: "take the draft mark off, so it can merge", 97 Summary: "take the draft mark off, so it can merge",
59 Usage: "mr ready <owner/name> <n>", Run: runMRReady}) 98 Usage: "mr ready <owner/name> <n>",
99 Examples: []string{"mr ready krz/gitbay 431"},
100 Run: runMRReady})
60 register(Command{Path: []string{"mr", "list"}, 101 register(Command{Path: []string{"mr", "list"},
61 Summary: "list merge requests", 102 Summary: "list merge requests",
62 Usage: "mr list <owner/name> [--state open|merged|closed|source_gone|all] [--label <l>] [--author <user>] [--milestone <title>|none] [--search <text>] [--limit <n>] [--cursor <c>]", ReadOnly: true, Run: runMRList}) 103 Usage: "mr list <owner/name> [--state open|merged|closed|source_gone|all] [--label <l>] [--author <user>] [--milestone <title>|none] [--search <text>] [--limit <n>] [--cursor <c>]",
104 Flags: []Flag{
105 {"--state", "open|merged|closed|source_gone|all", "which merge requests", "open"},
106 {"--label", "<l>", "only MRs carrying this label", ""},
107 {"--author", "<user>", "only MRs opened by this user", ""},
108 {"--milestone", "<title>|none", "only MRs in this milestone, or in none", ""},
109 {"--search", "<text>", "match title and body", ""},
110 {"--limit", "<n>", "rows per page", ""},
111 {"--cursor", "<c>", "continue from the previous page", ""},
112 },
113 Examples: []string{
114 "mr list krz/gitbay --state open",
115 "mr list krz/gitbay --author cmc --state all",
116 },
117 ReadOnly: true, Run: runMRList})
63 register(Command{Path: []string{"mr", "show"}, 118 register(Command{Path: []string{"mr", "show"},
64 Summary: "show a merge request", 119 Summary: "show a merge request",
65 Usage: "mr show <owner/name> <n>", ReadOnly: true, Run: runMRShow}) 120 Usage: "mr show <owner/name> <n>",
121 Examples: []string{"mr show krz/gitbay 431"},
122 ReadOnly: true, Run: runMRShow})
66 register(Command{Path: []string{"mr", "diff"}, 123 register(Command{Path: []string{"mr", "diff"},
67 Summary: "show the diff", 124 Summary: "show the diff",
68 Usage: "mr diff <owner/name> <n>", ReadOnly: true, Run: runMRDiff}) 125 Usage: "mr diff <owner/name> <n>",
126 Examples: []string{"mr diff krz/gitbay 431"},
127 ReadOnly: true, Run: runMRDiff})
69 register(Command{Path: []string{"mr", "edit"}, 128 register(Command{Path: []string{"mr", "edit"},
70 Summary: "edit title or body", 129 Summary: "edit title or body",
71 Usage: "mr edit <owner/name> <n> [--title <t>] [--body <b> | --file -] [--format md|org] [--superseded-by <m>|none]", 130 Usage: "mr edit <owner/name> <n> [--title <t>] [--body <b> | --file -] [--format md|org] [--superseded-by <m>|none]",
131 Flags: []Flag{
132 {"--title", "<t>", "the merge request's new title", ""},
133 {"--body", "<b>", "the merge request's new body", ""},
134 {"--file", "-", "read the new body from stdin", ""},
135 {"--format", "md|org", "the body's markup", ""},
136 {"--superseded-by", "<m>|none", "the MR replacing this one, or none to clear", ""},
137 },
138 Examples: []string{`mr edit krz/gitbay 431 --title "control: flag help, take two"`},
72 ReadsStdin: true, Run: runMREdit}) 139 ReadsStdin: true, Run: runMREdit})
73 register(Command{Path: []string{"mr", "retarget"}, 140 register(Command{Path: []string{"mr", "retarget"},
74 Summary: "retarget onto another branch", 141 Summary: "retarget onto another branch",
75 Usage: "mr retarget <owner/name> <n> <branch>", Run: runMRRetarget}) 142 Usage: "mr retarget <owner/name> <n> <branch>",
143 Examples: []string{"mr retarget krz/gitbay 431 main"},
144 Run: runMRRetarget})
76 register(Command{Path: []string{"mr", "comment"}, 145 register(Command{Path: []string{"mr", "comment"},
77 Summary: "comment", 146 Summary: "comment",
78 Usage: "mr comment <owner/name> <n> [--message <m> | --file -] [--format md|org]", 147 Usage: "mr comment <owner/name> <n> [--message <m> | --file -] [--format md|org]",
148 Flags: []Flag{
149 {"--message", "<m>", "the comment's text", ""},
150 {"--file", "-", "read the comment from stdin", ""},
151 {"--format", "md|org", "the comment's markup", "md"},
152 },
153 Examples: []string{`mr comment krz/gitbay 431 --message "looks good"`},
79 ReadsStdin: true, Run: runMRComment}) 154 ReadsStdin: true, Run: runMRComment})
80 register(Command{Path: []string{"mr", "review"}, 155 register(Command{Path: []string{"mr", "review"},
81 Summary: "review", 156 Summary: "review",
82 Usage: "mr review <owner/name> <n> --approve|--request-changes|--comment|--discard", Run: runMRReview}) 157 Usage: "mr review <owner/name> <n> --approve|--request-changes|--comment|--discard",
158 Flags: []Flag{
159 {"--approve", "", "approve the merge request", ""},
160 {"--request-changes", "", "ask for changes", ""},
161 {"--comment", "", "submit pending diff comments without a verdict", ""},
162 {"--discard", "", "throw away pending diff comments", ""},
163 },
164 Examples: []string{"mr review krz/gitbay 431 --approve"},
165 Run: runMRReview})
83 register(Command{Path: []string{"mr", "review", "request"}, 166 register(Command{Path: []string{"mr", "review", "request"},
84 Summary: "ask specific people for a review", 167 Summary: "ask specific people for a review",
85 Usage: "mr review request <owner/name> <n> [--add <user>]... [--remove <user>]...", Run: runMRReviewRequest}) 168 Usage: "mr review request <owner/name> <n> [--add <user>]... [--remove <user>]...",
169 Flags: []Flag{
170 {"--add", "<user>", "reviewer to add, may repeat", ""},
171 {"--remove", "<user>", "reviewer to remove, may repeat", ""},
172 },
173 Examples: []string{"mr review request krz/gitbay 431 --add cmc"},
174 Run: runMRReviewRequest})
86 register(Command{Path: []string{"mr", "label"}, 175 register(Command{Path: []string{"mr", "label"},
87 Summary: "labels", 176 Summary: "labels",
88 Usage: "mr label <owner/name> <n> [--add <l>]... [--remove <l>]...", Run: runMRLabel}) 177 Usage: "mr label <owner/name> <n> [--add <l>]... [--remove <l>]...",
178 Flags: []Flag{
179 {"--add", "<l>", "label to add, may repeat", ""},
180 {"--remove", "<l>", "label to remove, may repeat", ""},
181 },
182 Examples: []string{"mr label krz/gitbay 431 --add needs-review"},
183 Run: runMRLabel})
89 register(Command{Path: []string{"mr", "merge"}, 184 register(Command{Path: []string{"mr", "merge"},
90 Summary: "merge", 185 Summary: "merge",
91 Usage: "mr merge <owner/name> <n> [--strategy ff|merge|squash|rebase]", Run: runMRMerge}) 186 Usage: "mr merge <owner/name> <n> [--strategy ff|merge|squash|rebase]",
187 Flags: []Flag{
188 {"--strategy", "ff|merge|squash|rebase", "how to merge", ""},
189 },
190 Examples: []string{"mr merge krz/gitbay 431 --strategy ff"},
191 Run: runMRMerge})
92 register(Command{Path: []string{"mr", "close"}, 192 register(Command{Path: []string{"mr", "close"},
93 Summary: "close without merging", 193 Summary: "close without merging",
94 Usage: "mr close <owner/name> <n> [--by <m>]", Run: runMRClose}) 194 Usage: "mr close <owner/name> <n> [--by <m>]",
195 Flags: []Flag{
196 {"--by", "<m>", "the MR that supersedes this one", ""},
197 },
198 Examples: []string{"mr close krz/gitbay 431"},
199 Run: runMRClose})
95} 200}
96 201
97// ForkOut is what `repo fork` emits: where the fork landed, and what it 202// ForkOut is what `repo fork` emits: where the fork landed, and what it
internal/control/notifications.go +48 −17
@@ -15,47 +15,78 @@ import (
15 15
16func init() { 16func init() {
17 register(Command{Path: []string{"notifications", "list"}, 17 register(Command{Path: []string{"notifications", "list"},
18 Summary: "your notification inbox, newest first", 18 Summary: "your notification inbox, newest first",
19 Usage: "notifications list [--all] [--limit <n>] [--cursor <c>]", 19 Usage: "notifications list [--all] [--limit <n>] [--cursor <c>]",
20 Flags: []Flag{
21 {"--all", "", "include already-read notifications", ""},
22 {"--limit", "<n>", "rows per page", ""},
23 {"--cursor", "<c>", "continue from the previous page", ""},
24 },
25 Examples: []string{"notifications list", "notifications list --all --limit 50"},
20 ReadOnly: true, Run: runNotificationsList}) 26 ReadOnly: true, Run: runNotificationsList})
21 register(Command{Path: []string{"notifications", "read"}, 27 register(Command{Path: []string{"notifications", "read"},
22 Summary: "mark notifications read", 28 Summary: "mark notifications read",
23 Usage: "notifications read <id>... | --all", Run: runNotificationsRead}) 29 Usage: "notifications read <id>... | --all",
30 Flags: []Flag{
31 {"--all", "", "mark every unread notification read", ""},
32 },
33 Examples: []string{"notifications read 12 13", "notifications read --all"},
34 Run: runNotificationsRead})
24 register(Command{Path: []string{"notifications", "settings", "show"}, 35 register(Command{Path: []string{"notifications", "settings", "show"},
25 Summary: "your notification preferences", 36 Summary: "your notification preferences",
26 Usage: "notifications settings show", 37 Usage: "notifications settings show",
38 Examples: []string{"notifications settings show"},
27 ReadOnly: true, Run: runNotificationsSettingsShow}) 39 ReadOnly: true, Run: runNotificationsSettingsShow})
28 register(Command{Path: []string{"notifications", "settings", "mail"}, 40 register(Command{Path: []string{"notifications", "settings", "mail"},
29 Summary: "activity by mail as well as the inbox (login links are unaffected)", 41 Summary: "activity by mail as well as the inbox (login links are unaffected)",
30 Usage: "notifications settings mail on|off", Run: runNotificationsSettingsMail}) 42 Usage: "notifications settings mail on|off",
43 Examples: []string{"notifications settings mail on"},
44 Run: runNotificationsSettingsMail})
31 register(Command{Path: []string{"notifications", "settings", "watch"}, 45 register(Command{Path: []string{"notifications", "settings", "watch"},
32 Summary: "every issue and merge request on repositories you can write to", 46 Summary: "every issue and merge request on repositories you can write to",
33 Usage: "notifications settings watch on|off", Run: runNotificationsSettingsWatch}) 47 Usage: "notifications settings watch on|off",
48 Examples: []string{"notifications settings watch on"},
49 Run: runNotificationsSettingsWatch})
34 register(Command{Path: []string{"notifications", "device", "add"}, 50 register(Command{Path: []string{"notifications", "device", "add"},
35 Summary: "register an Apple device for push, token on stdin", 51 Summary: "register an Apple device for push, token on stdin",
36 Usage: "notifications device add [--label <name>] < token", 52 Usage: "notifications device add [--label <name>] < token",
53 Flags: []Flag{
54 {"--label", "<name>", "a name for the device", ""},
55 },
56 Examples: []string{"notifications device add --label iphone < token"},
37 // Mandatory: without it control.go swaps in an empty reader and 57 // Mandatory: without it control.go swaps in an empty reader and
38 // this command stores an empty token without erroring. 58 // this command stores an empty token without erroring.
39 ReadsStdin: true, Run: runNotificationsDeviceAdd}) 59 ReadsStdin: true, Run: runNotificationsDeviceAdd})
40 register(Command{Path: []string{"notifications", "device", "list"}, 60 register(Command{Path: []string{"notifications", "device", "list"},
41 Summary: "your registered devices", 61 Summary: "your registered devices",
42 Usage: "notifications device list", 62 Usage: "notifications device list",
63 Examples: []string{"notifications device list"},
43 ReadOnly: true, Run: runNotificationsDeviceList}) 64 ReadOnly: true, Run: runNotificationsDeviceList})
44 register(Command{Path: []string{"notifications", "device", "remove"}, 65 register(Command{Path: []string{"notifications", "device", "remove"},
45 Summary: "deregister a device", 66 Summary: "deregister a device",
46 Usage: "notifications device remove <id>", Run: runNotificationsDeviceRemove}) 67 Usage: "notifications device remove <id>",
68 Examples: []string{"notifications device remove 4"},
69 Run: runNotificationsDeviceRemove})
47 register(Command{Path: []string{"notifications", "settings", "push"}, 70 register(Command{Path: []string{"notifications", "settings", "push"},
48 Summary: "activity on your registered devices as well as the inbox", 71 Summary: "activity on your registered devices as well as the inbox",
49 Usage: "notifications settings push on|off", Run: runNotificationsSettingsPush}) 72 Usage: "notifications settings push on|off",
73 Examples: []string{"notifications settings push on"},
74 Run: runNotificationsSettingsPush})
50 register(Command{Path: []string{"repo", "watch"}, 75 register(Command{Path: []string{"repo", "watch"},
51 Summary: "hear about all activity on a repository", 76 Summary: "hear about all activity on a repository",
52 Usage: "repo watch <owner/name>", Run: runRepoWatch}) 77 Usage: "repo watch <owner/name>",
78 Examples: []string{"repo watch krz/gitbay"},
79 Run: runRepoWatch})
53 register(Command{Path: []string{"repo", "unwatch"}, 80 register(Command{Path: []string{"repo", "unwatch"},
54 Summary: "back to the default: only work you are part of", 81 Summary: "back to the default: only work you are part of",
55 Usage: "repo unwatch <owner/name>", Run: runRepoUnwatch}) 82 Usage: "repo unwatch <owner/name>",
83 Examples: []string{"repo unwatch krz/gitbay"},
84 Run: runRepoUnwatch})
56 register(Command{Path: []string{"repo", "mute"}, 85 register(Command{Path: []string{"repo", "mute"},
57 Summary: "mute a repository, including work you are part of", 86 Summary: "mute a repository, including work you are part of",
58 Usage: "repo mute <owner/name>", Run: runRepoMute}) 87 Usage: "repo mute <owner/name>",
88 Examples: []string{"repo mute krz/gitbay"},
89 Run: runRepoMute})
59} 90}
60 91
61// notice is one thing that happened, in the shape both delivery routes 92// notice is one thing that happened, in the shape both delivery routes
internal/control/org.go +28 −14
@@ -14,29 +14,43 @@ import (
14 14
15func init() { 15func init() {
16 register(Command{Path: []string{"org", "create"}, 16 register(Command{Path: []string{"org", "create"},
17 Summary: "create an organization (you become its first admin)", 17 Summary: "create an organization (you become its first admin)",
18 Usage: "org create <name>", Run: runOrgCreate}) 18 Usage: "org create <name>",
19 Examples: []string{"org create krz"}, Run: runOrgCreate})
19 register(Command{Path: []string{"org", "list"}, 20 register(Command{Path: []string{"org", "list"},
20 Summary: "list organizations you belong to", 21 Summary: "list organizations you belong to",
21 Usage: "org list", ReadOnly: true, Run: runOrgList}) 22 Usage: "org list",
23 Examples: []string{"org list"}, ReadOnly: true, Run: runOrgList})
22 register(Command{Path: []string{"org", "show"}, 24 register(Command{Path: []string{"org", "show"},
23 Summary: "show an organization and its members", 25 Summary: "show an organization and its members",
24 Usage: "org show <name>", ReadOnly: true, Run: runOrgShow}) 26 Usage: "org show <name>",
27 Examples: []string{"org show krz"}, ReadOnly: true, Run: runOrgShow})
25 register(Command{Path: []string{"org", "rename"}, 28 register(Command{Path: []string{"org", "rename"},
26 Summary: "rename an organization", 29 Summary: "rename an organization",
27 Usage: "org rename <old> <new> (clone URLs change)", Run: runOrgRename}) 30 Usage: "org rename <old> <new> (clone URLs change)",
31 Examples: []string{"org rename krz krazywarez"}, Run: runOrgRename})
28 register(Command{Path: []string{"org", "delete"}, 32 register(Command{Path: []string{"org", "delete"},
29 Summary: "delete an empty organization", 33 Summary: "delete an empty organization",
30 Usage: "org delete <name> --yes", Run: runOrgDelete}) 34 Usage: "org delete <name> --yes",
35 Flags: []Flag{
36 {"--yes", "", "confirm the permanent delete", ""},
37 },
38 Examples: []string{"org delete krz --yes"}, Run: runOrgDelete})
31 register(Command{Path: []string{"org", "members", "add"}, 39 register(Command{Path: []string{"org", "members", "add"},
32 Summary: "add or update a member", 40 Summary: "add or update a member",
33 Usage: "org members add <org> <user> [--role member|admin]", Run: runOrgMembersAdd}) 41 Usage: "org members add <org> <user> [--role member|admin]",
42 Flags: []Flag{
43 {"--role", "member|admin", "the member's role", "member"},
44 },
45 Examples: []string{"org members add krz cmc --role admin"}, Run: runOrgMembersAdd})
34 register(Command{Path: []string{"org", "members", "remove"}, 46 register(Command{Path: []string{"org", "members", "remove"},
35 Summary: "remove a member", 47 Summary: "remove a member",
36 Usage: "org members remove <org> <user>", Run: runOrgMembersRemove}) 48 Usage: "org members remove <org> <user>",
49 Examples: []string{"org members remove krz cmc"}, Run: runOrgMembersRemove})
37 register(Command{Path: []string{"org", "members", "list"}, 50 register(Command{Path: []string{"org", "members", "list"},
38 Summary: "list members", 51 Summary: "list members",
39 Usage: "org members list <org>", ReadOnly: true, Run: runOrgMembersList}) 52 Usage: "org members list <org>",
53 Examples: []string{"org members list krz"}, ReadOnly: true, Run: runOrgMembersList})
40} 54}
41 55
42// orgAdmin loads an org and requires the caller to be one of its admins. 56// orgAdmin loads an org and requires the caller to be one of its admins.
internal/control/orglabel.go +35 −11
@@ -13,25 +13,49 @@ import (
13func init() { 13func init() {
14 register(Command{Path: []string{"org", "label", "set"}, 14 register(Command{Path: []string{"org", "label", "set"},
15 Summary: "create an org label every org repository sees, or set its colour; folds in same-named repo labels", 15 Summary: "create an org label every org repository sees, or set its colour; folds in same-named repo labels",
16 Usage: "org label set <org> <label> [--color rrggbb|'']", Run: runOrgLabelSet}) 16 Usage: "org label set <org> <label> [--color rrggbb|'']",
17 Flags: []Flag{
18 {"--color", "rrggbb|''", "the label's colour, or '' to clear it", ""},
19 },
20 Examples: []string{"org label set krz bug --color d73a4a"},
21 Run: runOrgLabelSet})
17 register(Command{Path: []string{"org", "label", "list"}, 22 register(Command{Path: []string{"org", "label", "list"},
18 Summary: "list an org's labels with use across the repositories you can read", 23 Summary: "list an org's labels with use across the repositories you can read",
19 Usage: "org label list <org>", ReadOnly: true, Run: runOrgLabelList}) 24 Usage: "org label list <org>",
25 Examples: []string{"org label list krz"},
26 ReadOnly: true, Run: runOrgLabelList})
20 register(Command{Path: []string{"org", "label", "remove"}, 27 register(Command{Path: []string{"org", "label", "remove"},
21 Summary: "remove an org label from the org and from every issue under it", 28 Summary: "remove an org label from the org and from every issue under it",
22 Usage: "org label remove <org> <label>", Run: runOrgLabelRemove}) 29 Usage: "org label remove <org> <label>",
30 Examples: []string{"org label remove krz wontfix"},
31 Run: runOrgLabelRemove})
23 register(Command{Path: []string{"org", "milestone", "create"}, 32 register(Command{Path: []string{"org", "milestone", "create"},
24 Summary: "create an org milestone spanning every org repository; folds in same-titled repo milestones", 33 Summary: "create an org milestone spanning every org repository; folds in same-titled repo milestones",
25 Usage: "org milestone create <org> <title> [--description <d>] [--due YYYY-MM-DD]", Run: runOrgMilestoneCreate}) 34 Usage: "org milestone create <org> <title> [--description <d>] [--due YYYY-MM-DD]",
35 Flags: []Flag{
36 {"--description", "<d>", "what the milestone covers", ""},
37 {"--due", "YYYY-MM-DD", "target date", ""},
38 },
39 Examples: []string{"org milestone create krz mobile --due 2026-12-01"},
40 Run: runOrgMilestoneCreate})
26 register(Command{Path: []string{"org", "milestone", "list"}, 41 register(Command{Path: []string{"org", "milestone", "list"},
27 Summary: "list an org's milestones with progress across the repositories you can read", 42 Summary: "list an org's milestones with progress across the repositories you can read",
28 Usage: "org milestone list <org> [--state open|closed|all]", ReadOnly: true, Run: runOrgMilestoneList}) 43 Usage: "org milestone list <org> [--state open|closed|all]",
44 Flags: []Flag{
45 {"--state", "open|closed|all", "which milestones", "open"},
46 },
47 Examples: []string{"org milestone list krz --state all"},
48 ReadOnly: true, Run: runOrgMilestoneList})
29 register(Command{Path: []string{"org", "milestone", "close"}, 49 register(Command{Path: []string{"org", "milestone", "close"},
30 Summary: "close an org milestone", 50 Summary: "close an org milestone",
31 Usage: "org milestone close <org> <title>", Run: runOrgMilestoneClose}) 51 Usage: "org milestone close <org> <title>",
52 Examples: []string{"org milestone close krz mobile"},
53 Run: runOrgMilestoneClose})
32 register(Command{Path: []string{"org", "milestone", "reopen"}, 54 register(Command{Path: []string{"org", "milestone", "reopen"},
33 Summary: "reopen an org milestone", 55 Summary: "reopen an org milestone",
34 Usage: "org milestone reopen <org> <title>", Run: runOrgMilestoneReopen}) 56 Usage: "org milestone reopen <org> <title>",
57 Examples: []string{"org milestone reopen krz mobile"},
58 Run: runOrgMilestoneReopen})
35} 59}
36 60
37// orgReader resolves an org for a read of its labels or milestones. 61// orgReader resolves an org for a read of its labels or milestones.
internal/control/pagescmd.go +16 −8
@@ -20,17 +20,25 @@ import (
20 20
21func init() { 21func init() {
22 register(Command{Path: []string{"repo", "domain", "add"}, 22 register(Command{Path: []string{"repo", "domain", "add"},
23 Summary: "claim a custom pages domain (verify with a DNS TXT record)", 23 Summary: "claim a custom pages domain (verify with a DNS TXT record)",
24 Usage: "repo domain add <owner/name> <domain>", Run: runDomainAdd}) 24 Usage: "repo domain add <owner/name> <domain>",
25 Examples: []string{"repo domain add krz/gitbay gitbay.org"},
26 Run: runDomainAdd})
25 register(Command{Path: []string{"repo", "domain", "verify"}, 27 register(Command{Path: []string{"repo", "domain", "verify"},
26 Summary: "check the DNS challenge and activate a claim", 28 Summary: "check the DNS challenge and activate a claim",
27 Usage: "repo domain verify <owner/name> <domain>", Run: runDomainVerify}) 29 Usage: "repo domain verify <owner/name> <domain>",
30 Examples: []string{"repo domain verify krz/gitbay gitbay.org"},
31 Run: runDomainVerify})
28 register(Command{Path: []string{"repo", "domain", "remove"}, 32 register(Command{Path: []string{"repo", "domain", "remove"},
29 Summary: "remove a custom pages domain", 33 Summary: "remove a custom pages domain",
30 Usage: "repo domain remove <owner/name> <domain>", Run: runDomainRemove}) 34 Usage: "repo domain remove <owner/name> <domain>",
35 Examples: []string{"repo domain remove krz/gitbay gitbay.org"},
36 Run: runDomainRemove})
31 register(Command{Path: []string{"repo", "domain", "list"}, 37 register(Command{Path: []string{"repo", "domain", "list"},
32 Summary: "list custom pages domains", 38 Summary: "list custom pages domains",
33 Usage: "repo domain list <owner/name>", ReadOnly: true, Run: runDomainList}) 39 Usage: "repo domain list <owner/name>",
40 Examples: []string{"repo domain list krz/gitbay"},
41 ReadOnly: true, Run: runDomainList})
34} 42}
35 43
36// challengeLabel prefixes the domain for the ownership TXT record. 44// challengeLabel prefixes the domain for the ownership TXT record.
internal/control/profile.go +17 −4
@@ -16,14 +16,27 @@ import (
16 16
17func init() { 17func init() {
18 register(Command{Path: []string{"profile", "show"}, 18 register(Command{Path: []string{"profile", "show"},
19 Summary: "show a user's or org's profile", 19 Summary: "show a user's or org's profile",
20 Usage: "profile show [name]", ReadOnly: true, Run: runProfileShow}) 20 Usage: "profile show [name]",
21 Examples: []string{"profile show cmc"}, ReadOnly: true, Run: runProfileShow})
21 register(Command{Path: []string{"profile", "set"}, 22 register(Command{Path: []string{"profile", "set"},
22 Summary: "set your profile", 23 Summary: "set your profile",
23 Usage: "profile set [--description <d>] [--website <url>] [--link <label|url>]... ('' clears)", Run: runProfileSet}) 24 Usage: "profile set [--description <d>] [--website <url>] [--link <label|url>]... ('' clears)",
25 Flags: []Flag{
26 {"--description", "<d>", "one line about you", ""},
27 {"--website", "<url>", "your website", ""},
28 {"--link", "<label|url>", "a profile link, may repeat", ""},
29 },
30 Examples: []string{`profile set --description "gitbay's author" --website https://cleberg.net`}, Run: runProfileSet})
24 register(Command{Path: []string{"org", "profile"}, 31 register(Command{Path: []string{"org", "profile"},
25 Summary: "show or set an org's profile", 32 Summary: "show or set an org's profile",
26 Usage: "org profile <org> [--description <d>] [--website <url>] [--link <label|url>]...", Run: runOrgProfile}) 33 Usage: "org profile <org> [--description <d>] [--website <url>] [--link <label|url>]...",
34 Flags: []Flag{
35 {"--description", "<d>", "one line about the org", ""},
36 {"--website", "<url>", "the org's website", ""},
37 {"--link", "<label|url>", "a profile link, may repeat", ""},
38 },
39 Examples: []string{"org profile krz", `org profile krz --description "a self-hosted forge"`}, Run: runOrgProfile})
27} 40}
28 41
29// maxProfileLinks caps the free-form link list. A profile is a header, 42// maxProfileLinks caps the free-form link list. A profile is a header,
internal/control/quota.go +6 −1
@@ -85,7 +85,12 @@ func init() {
85 register(Command{Path: []string{"admin", "user", "limits"}, 85 register(Command{Path: []string{"admin", "user", "limits"},
86 Summary: "show or set an account's repository and storage caps (instance admins)", 86 Summary: "show or set an account's repository and storage caps (instance admins)",
87 Usage: "admin user limits <username> [--repos <n>|default] [--bytes <n>|default]", 87 Usage: "admin user limits <username> [--repos <n>|default] [--bytes <n>|default]",
88 Run: runAdminUserLimits}) 88 Flags: []Flag{
89 {"--repos", "<n>|default", "the account's repository cap", ""},
90 {"--bytes", "<n>|default", "the account's storage cap", ""},
91 },
92 Examples: []string{"admin user limits alice", "admin user limits alice --repos 50"},
93 Run: runAdminUserLimits})
89} 94}
90 95
91func runAdminUserLimits(c *Ctx, args []string) int { 96func runAdminUserLimits(c *Ctx, args []string) int {
internal/control/read.go +24 −9
@@ -17,23 +17,37 @@ import (
17 17
18func init() { 18func init() {
19 register(Command{ 19 register(Command{
20 Path: []string{"repo", "tree"}, 20 Path: []string{"repo", "tree"},
21 Summary: "list a directory", 21 Summary: "list a directory",
22 Usage: "repo tree <owner/name> [<path>] [--ref <ref>]", 22 Usage: "repo tree <owner/name> [<path>] [--ref <ref>]",
23 Flags: []Flag{
24 {"--ref", "<ref>", "branch, tag or commit to read", "the default branch"},
25 },
26 Examples: []string{"repo tree krz/gitbay internal/control"},
23 ReadOnly: true, 27 ReadOnly: true,
24 Run: runRepoTree, 28 Run: runRepoTree,
25 }) 29 })
26 register(Command{ 30 register(Command{
27 Path: []string{"repo", "cat"}, 31 Path: []string{"repo", "cat"},
28 Summary: "read a file", 32 Summary: "read a file",
29 Usage: "repo cat <owner/name> <path> [--ref <ref>]", 33 Usage: "repo cat <owner/name> <path> [--ref <ref>]",
34 Flags: []Flag{
35 {"--ref", "<ref>", "branch, tag or commit to read", "the default branch"},
36 },
37 Examples: []string{"repo cat krz/gitbay internal/control/control.go"},
30 ReadOnly: true, 38 ReadOnly: true,
31 Run: runRepoCat, 39 Run: runRepoCat,
32 }) 40 })
33 register(Command{ 41 register(Command{
34 Path: []string{"repo", "blame"}, 42 Path: []string{"repo", "blame"},
35 Summary: "attribute lines to commits", 43 Summary: "attribute lines to commits",
36 Usage: "repo blame <owner/name> <path> [--ref <ref>] [--from <n>] [--to <n>]", 44 Usage: "repo blame <owner/name> <path> [--ref <ref>] [--from <n>] [--to <n>]",
45 Flags: []Flag{
46 {"--ref", "<ref>", "branch, tag or commit to read", "the default branch"},
47 {"--from", "<n>", "first line to attribute", "1"},
48 {"--to", "<n>", "last line to attribute", "the file's last line"},
49 },
50 Examples: []string{"repo blame krz/gitbay internal/control/control.go --from 1 --to 40"},
37 ReadOnly: true, 51 ReadOnly: true,
38 Run: runRepoBlame, 52 Run: runRepoBlame,
39 }) 53 })
@@ -41,6 +55,7 @@ func init() {
41 Path: []string{"repo", "refs"}, 55 Path: []string{"repo", "refs"},
42 Summary: "list branches and tags", 56 Summary: "list branches and tags",
43 Usage: "repo refs <owner/name>", 57 Usage: "repo refs <owner/name>",
58 Examples: []string{"repo refs krz/gitbay"},
44 ReadOnly: true, 59 ReadOnly: true,
45 Run: runRepoRefs, 60 Run: runRepoRefs,
46 }) 61 })
internal/control/register.go +19 −8
@@ -20,27 +20,38 @@ func init() {
20 register(Command{Path: []string{"register"}, 20 register(Command{Path: []string{"register"},
21 Summary: "create an account (only meaningful for unregistered keys)", 21 Summary: "create an account (only meaningful for unregistered keys)",
22 Usage: "register --username <name> [--email <address> | --invite <code>]", 22 Usage: "register --username <name> [--email <address> | --invite <code>]",
23 Flags: []Flag{
24 {"--username", "<name>", "the account's username", ""},
25 {"--email", "<address>", "for open registration", ""},
26 {"--invite", "<code>", "for invite-only registration", ""},
27 },
28 Examples: []string{"register --username cmc --email cmc@example.org"},
23 Run: func(c *Ctx, args []string) int { 29 Run: func(c *Ctx, args []string) int {
24 return c.fail(protocol.ExitUsage, 30 return c.fail(protocol.ExitUsage,
25 "this SSH key already belongs to %s. To register a new account, connect with the key it should use:\n ssh -F /dev/null -i <newkey> git@<host> register ...", 31 "this SSH key already belongs to %s. To register a new account, connect with the key it should use:\n ssh -F /dev/null -i <newkey> git@<host> register ...",
26 c.User.Username) 32 c.User.Username)
27 }}) 33 }})
28 register(Command{Path: []string{"email", "add"}, 34 register(Command{Path: []string{"email", "add"},
29 Summary: "add an address and mail a verification code", 35 Summary: "add an address and mail a verification code",
30 Usage: "email add <address>", Run: runEmailAdd}) 36 Usage: "email add <address>",
37 Examples: []string{"email add cmc@example.org"}, Run: runEmailAdd})
31 register(Command{Path: []string{"email", "verify"}, 38 register(Command{Path: []string{"email", "verify"},
32 Summary: "confirm a verification code", 39 Summary: "confirm a verification code",
33 Usage: "email verify <code>", Run: runEmailVerify}) 40 Usage: "email verify <code>",
41 Examples: []string{"email verify abc123"}, Run: runEmailVerify})
34 register(Command{Path: []string{"email", "list"}, 42 register(Command{Path: []string{"email", "list"},
35 Summary: "list the addresses on your account", 43 Summary: "list the addresses on your account",
36 Usage: "email list", 44 Usage: "email list",
45 Examples: []string{"email list"},
37 ReadOnly: true, Run: runEmailList}) 46 ReadOnly: true, Run: runEmailList})
38 register(Command{Path: []string{"email", "remove"}, 47 register(Command{Path: []string{"email", "remove"},
39 Summary: "remove an address; not the primary, nor the last verified one", 48 Summary: "remove an address; not the primary, nor the last verified one",
40 Usage: "email remove <address>", Run: runEmailRemove}) 49 Usage: "email remove <address>",
50 Examples: []string{"email remove old@example.org"}, Run: runEmailRemove})
41 register(Command{Path: []string{"email", "primary"}, 51 register(Command{Path: []string{"email", "primary"},
42 Summary: "make a verified address the primary", 52 Summary: "make a verified address the primary",
43 Usage: "email primary <address>", Run: runEmailPrimary}) 53 Usage: "email primary <address>",
54 Examples: []string{"email primary cmc@example.org"}, Run: runEmailPrimary})
44} 55}
45 56
46func runEmailList(c *Ctx, args []string) int { 57func runEmailList(c *Ctx, args []string) int {
internal/control/release.go +44 −10
@@ -20,33 +20,67 @@ import (
20 20
21func init() { 21func init() {
22 register(Command{Path: []string{"release", "create"}, 22 register(Command{Path: []string{"release", "create"},
23 Summary: "create a release on a tag", 23 Summary: "create a release on a tag",
24 Usage: "release create <owner/name> <tag> [--title <t>] [--notes <n> | --file -] [--format md|org]", 24 Usage: "release create <owner/name> <tag> [--title <t>] [--notes <n> | --file -] [--format md|org]",
25 Flags: []Flag{
26 {"--title", "<t>", "the release's title", "the tag"},
27 {"--notes", "<n>", "the release notes", ""},
28 {"--file", "-", "read the release notes from stdin", ""},
29 {"--format", "md|org", "the notes' markup", "md"},
30 },
31 Examples: []string{
32 `release create krz/gitbay v1.31.0 --title "v1.31.0" --notes "flag help"`,
33 "release create krz/gitbay v1.31.0 --file - < notes.md",
34 },
25 ReadsStdin: true, Run: runReleaseCreate}) 35 ReadsStdin: true, Run: runReleaseCreate})
26 register(Command{Path: []string{"release", "edit"}, 36 register(Command{Path: []string{"release", "edit"},
27 Summary: "update a release's title and notes", 37 Summary: "update a release's title and notes",
28 Usage: "release edit <owner/name> <tag> [--title <t>] [--notes <n> | --file -] [--format md|org]", 38 Usage: "release edit <owner/name> <tag> [--title <t>] [--notes <n> | --file -] [--format md|org]",
39 Flags: []Flag{
40 {"--title", "<t>", "the release's new title", ""},
41 {"--notes", "<n>", "the release's new notes", ""},
42 {"--file", "-", "read the new release notes from stdin", ""},
43 {"--format", "md|org", "the notes' markup", ""},
44 },
45 Examples: []string{`release edit krz/gitbay v1.31.0 --title "v1.31.0"`},
29 ReadsStdin: true, Run: runReleaseEdit}) 46 ReadsStdin: true, Run: runReleaseEdit})
30 register(Command{Path: []string{"release", "list"}, 47 register(Command{Path: []string{"release", "list"},
31 Summary: "list releases", 48 Summary: "list releases",
32 Usage: "release list <owner/name> [--limit <n>] [--cursor <c>]", ReadOnly: true, Run: runReleaseList}) 49 Usage: "release list <owner/name> [--limit <n>] [--cursor <c>]",
50 Flags: []Flag{
51 {"--limit", "<n>", "rows per page", ""},
52 {"--cursor", "<c>", "continue from the previous page", ""},
53 },
54 Examples: []string{"release list krz/gitbay --limit 10"},
55 ReadOnly: true, Run: runReleaseList})
33 register(Command{Path: []string{"release", "show"}, 56 register(Command{Path: []string{"release", "show"},
34 Summary: "show a release with assets", 57 Summary: "show a release with assets",
35 Usage: "release show <owner/name> <tag>", ReadOnly: true, Run: runReleaseShow}) 58 Usage: "release show <owner/name> <tag>",
59 Examples: []string{"release show krz/gitbay v1.30.0"},
60 ReadOnly: true, Run: runReleaseShow})
36 register(Command{Path: []string{"release", "delete"}, 61 register(Command{Path: []string{"release", "delete"},
37 Summary: "delete a release and its assets", 62 Summary: "delete a release and its assets",
38 Usage: "release delete <owner/name> <tag> --yes", Run: runReleaseDelete}) 63 Usage: "release delete <owner/name> <tag> --yes",
64 Flags: []Flag{
65 {"--yes", "", "confirm the permanent delete", ""},
66 },
67 Examples: []string{"release delete krz/gitbay v1.30.0 --yes"},
68 Run: runReleaseDelete})
39 register(Command{Path: []string{"release", "asset", "add"}, 69 register(Command{Path: []string{"release", "asset", "add"},
40 Summary: "upload an asset from stdin", 70 Summary: "upload an asset from stdin",
41 Usage: "release asset add <owner/name> <tag> <filename> < file", 71 Usage: "release asset add <owner/name> <tag> <filename> < file",
72 Examples: []string{"release asset add krz/gitbay v1.30.0 gitbay-darwin-arm64 < gitbay-darwin-arm64"},
42 ReadsStdin: true, Run: runAssetAdd}) 73 ReadsStdin: true, Run: runAssetAdd})
43 register(Command{Path: []string{"release", "asset", "get"}, 74 register(Command{Path: []string{"release", "asset", "get"},
44 Summary: "write an asset to stdout", 75 Summary: "write an asset to stdout",
45 Usage: "release asset get <owner/name> <tag> <filename> > file", 76 Usage: "release asset get <owner/name> <tag> <filename> > file",
77 Examples: []string{"release asset get krz/gitbay v1.30.0 gitbay-darwin-arm64 > gitbay-darwin-arm64"},
46 ReadOnly: true, Run: runAssetGet}) 78 ReadOnly: true, Run: runAssetGet})
47 register(Command{Path: []string{"release", "asset", "remove"}, 79 register(Command{Path: []string{"release", "asset", "remove"},
48 Summary: "remove an asset", 80 Summary: "remove an asset",
49 Usage: "release asset remove <owner/name> <tag> <filename>", Run: runAssetRemove}) 81 Usage: "release asset remove <owner/name> <tag> <filename>",
82 Examples: []string{"release asset remove krz/gitbay v1.30.0 gitbay-darwin-arm64"},
83 Run: runAssetRemove})
50} 84}
51 85
52var assetNamePat = regexp.MustCompile(`^[A-Za-z0-9][A-Za-z0-9._+-]{0,199}$`) 86var assetNamePat = regexp.MustCompile(`^[A-Za-z0-9][A-Za-z0-9._+-]{0,199}$`)
internal/control/repo.go +137 −60
@@ -28,100 +28,177 @@ func HooksDir(root string) string { return filepath.Join(root, "hooks") }
28func init() { 28func init() {
29 register(Command{Path: []string{"repo", "create"}, 29 register(Command{Path: []string{"repo", "create"},
30 Summary: "create a repository", 30 Summary: "create a repository",
31 Usage: "repo create <owner/name> [--private]", Run: runRepoCreate}) 31 Usage: "repo create <owner/name> [--private]",
32 Flags: []Flag{
33 {"--private", "", "create it private", ""},
34 },
35 Examples: []string{"repo create krz/newthing --private"},
36 Run: runRepoCreate})
32 register(Command{Path: []string{"repo", "list"}, 37 register(Command{Path: []string{"repo", "list"},
33 Summary: "list repositories you own or can access", 38 Summary: "list repositories you own or can access",
34 Usage: "repo list [--limit <n>] [--cursor <c>]", ReadOnly: true, Run: runRepoList}) 39 Usage: "repo list [--limit <n>] [--cursor <c>]",
40 Flags: []Flag{
41 {"--limit", "<n>", "rows per page", ""},
42 {"--cursor", "<c>", "continue from the previous page", ""},
43 },
44 Examples: []string{"repo list --limit 20"},
45 ReadOnly: true, Run: runRepoList})
35 register(Command{Path: []string{"repo", "show"}, 46 register(Command{Path: []string{"repo", "show"},
36 Summary: "show repository details", 47 Summary: "show repository details",
37 Usage: "repo show <owner/name>", ReadOnly: true, Run: runRepoShow}) 48 Usage: "repo show <owner/name>",
49 Examples: []string{"repo show krz/gitbay"},
50 ReadOnly: true, Run: runRepoShow})
38 register(Command{Path: []string{"repo", "transfer"}, 51 register(Command{Path: []string{"repo", "transfer"},
39 Summary: "move a repository to another owner", 52 Summary: "move a repository to another owner",
40 Usage: "repo transfer <owner/name> <new-owner> (clone URLs change)", Run: runRepoTransfer}) 53 Usage: "repo transfer <owner/name> <new-owner> (clone URLs change)",
54 Examples: []string{"repo transfer krz/gitbay krazywarez"},
55 Run: runRepoTransfer})
41 register(Command{Path: []string{"repo", "rename"}, 56 register(Command{Path: []string{"repo", "rename"},
42 Summary: "rename a repository", 57 Summary: "rename a repository",
43 Usage: "repo rename <owner/name> <new-name> (clone URLs change)", Run: runRepoRename}) 58 Usage: "repo rename <owner/name> <new-name> (clone URLs change)",
59 Examples: []string{"repo rename krz/gitbay forge"},
60 Run: runRepoRename})
44 register(Command{Path: []string{"repo", "delete"}, 61 register(Command{Path: []string{"repo", "delete"},
45 Summary: "delete a repository", 62 Summary: "delete a repository",
46 Usage: "repo delete <owner/name> --yes", Run: runRepoDelete}) 63 Usage: "repo delete <owner/name> --yes",
64 Flags: []Flag{
65 {"--yes", "", "confirm the permanent delete", ""},
66 },
67 Examples: []string{"repo delete cmc/scratch --yes"},
68 Run: runRepoDelete})
47 register(Command{Path: []string{"repo", "access", "grant"}, 69 register(Command{Path: []string{"repo", "access", "grant"},
48 Summary: "grant access", 70 Summary: "grant access",
49 Usage: "repo access grant <owner/name> <user> read|write|admin", Run: runAccessGrant}) 71 Usage: "repo access grant <owner/name> <user> read|write|admin",
72 Examples: []string{"repo access grant krz/gitbay cmc write"},
73 Run: runAccessGrant})
50 register(Command{Path: []string{"repo", "access", "revoke"}, 74 register(Command{Path: []string{"repo", "access", "revoke"},
51 Summary: "revoke access", 75 Summary: "revoke access",
52 Usage: "repo access revoke <owner/name> <user>", Run: runAccessRevoke}) 76 Usage: "repo access revoke <owner/name> <user>",
77 Examples: []string{"repo access revoke krz/gitbay cmc"},
78 Run: runAccessRevoke})
53 register(Command{Path: []string{"repo", "access", "list"}, 79 register(Command{Path: []string{"repo", "access", "list"},
54 Summary: "list who can reach the repository, with the role and where it comes from", 80 Summary: "list who can reach the repository, with the role and where it comes from",
55 Usage: "repo access list <owner/name>", ReadOnly: true, Run: runAccessList}) 81 Usage: "repo access list <owner/name>",
82 Examples: []string{"repo access list krz/gitbay"},
83 ReadOnly: true, Run: runAccessList})
56 register(Command{Path: []string{"repo", "settings", "show"}, 84 register(Command{Path: []string{"repo", "settings", "show"},
57 Summary: "show settings", 85 Summary: "show settings",
58 Usage: "repo settings show <owner/name>", ReadOnly: true, Run: runSettingsShow}) 86 Usage: "repo settings show <owner/name>",
87 Examples: []string{"repo settings show krz/gitbay"},
88 ReadOnly: true, Run: runSettingsShow})
59 register(Command{Path: []string{"repo", "settings", "protect"}, 89 register(Command{Path: []string{"repo", "settings", "protect"},
60 Summary: "protect a branch", 90 Summary: "protect a branch",
61 Usage: "repo settings protect <owner/name> <branch>", Run: runProtect}) 91 Usage: "repo settings protect <owner/name> <branch>",
92 Examples: []string{"repo settings protect krz/gitbay main"},
93 Run: runProtect})
62 register(Command{Path: []string{"repo", "settings", "unprotect"}, 94 register(Command{Path: []string{"repo", "settings", "unprotect"},
63 Summary: "unprotect a branch", 95 Summary: "unprotect a branch",
64 Usage: "repo settings unprotect <owner/name> <branch>", Run: runUnprotect}) 96 Usage: "repo settings unprotect <owner/name> <branch>",
97 Examples: []string{"repo settings unprotect krz/gitbay main"},
98 Run: runUnprotect})
65 register(Command{Path: []string{"repo", "settings", "protect-tag"}, 99 register(Command{Path: []string{"repo", "settings", "protect-tag"},
66 Summary: "protect tags matching a glob (created once, never moved or deleted)", 100 Summary: "protect tags matching a glob (created once, never moved or deleted)",
67 Usage: "repo settings protect-tag <owner/name> <glob>", Run: runProtectTag}) 101 Usage: "repo settings protect-tag <owner/name> <glob>",
102 Examples: []string{"repo settings protect-tag krz/gitbay 'v*'"},
103 Run: runProtectTag})
68 register(Command{Path: []string{"repo", "settings", "unprotect-tag"}, 104 register(Command{Path: []string{"repo", "settings", "unprotect-tag"},
69 Summary: "drop a protected-tag glob", 105 Summary: "drop a protected-tag glob",
70 Usage: "repo settings unprotect-tag <owner/name> <glob>", Run: runUnprotectTag}) 106 Usage: "repo settings unprotect-tag <owner/name> <glob>",
107 Examples: []string{"repo settings unprotect-tag krz/gitbay 'v*'"},
108 Run: runUnprotectTag})
71 register(Command{Path: []string{"repo", "settings", "description"}, 109 register(Command{Path: []string{"repo", "settings", "description"},
72 Summary: "set the repository description", 110 Summary: "set the repository description",
73 Usage: "repo settings description <owner/name> <text> ('' clears)", Run: runSetDescription}) 111 Usage: "repo settings description <owner/name> <text> ('' clears)",
112 Examples: []string{`repo settings description krz/gitbay "a CLI-first git forge"`},
113 Run: runSetDescription})
74 register(Command{Path: []string{"repo", "settings", "visibility"}, 114 register(Command{Path: []string{"repo", "settings", "visibility"},
75 Summary: "set repository visibility", 115 Summary: "set repository visibility",
76 Usage: "repo settings visibility <owner/name> public|private", Run: runSetVisibility}) 116 Usage: "repo settings visibility <owner/name> public|private",
117 Examples: []string{"repo settings visibility krz/gitbay public"},
118 Run: runSetVisibility})
77 register(Command{Path: []string{"repo", "settings", "website"}, 119 register(Command{Path: []string{"repo", "settings", "website"},
78 Summary: "set the repository website", 120 Summary: "set the repository website",
79 Usage: "repo settings website <owner/name> <url> ('' clears)", Run: runSetWebsite}) 121 Usage: "repo settings website <owner/name> <url> ('' clears)",
122 Examples: []string{"repo settings website krz/gitbay https://gitbay.org"},
123 Run: runSetWebsite})
80 register(Command{Path: []string{"repo", "settings", "default-branch"}, 124 register(Command{Path: []string{"repo", "settings", "default-branch"},
81 Summary: "set the default branch", 125 Summary: "set the default branch",
82 Usage: "repo settings default-branch <owner/name> <branch>", Run: runSetDefaultBranch}) 126 Usage: "repo settings default-branch <owner/name> <branch>",
127 Examples: []string{"repo settings default-branch krz/gitbay main"},
128 Run: runSetDefaultBranch})
83 register(Command{Path: []string{"repo", "settings", "git-daemon"}, 129 register(Command{Path: []string{"repo", "settings", "git-daemon"},
84 Summary: "expose over git://", 130 Summary: "expose over git://",
85 Usage: "repo settings git-daemon <owner/name> on|off", Run: runGitDaemon}) 131 Usage: "repo settings git-daemon <owner/name> on|off",
132 Examples: []string{"repo settings git-daemon krz/gitbay on"},
133 Run: runGitDaemon})
86 register(Command{Path: []string{"repo", "archive"}, 134 register(Command{Path: []string{"repo", "archive"},
87 Summary: "archive a repository (read-only: pushes and issue/MR writes refused)", 135 Summary: "archive a repository (read-only: pushes and issue/MR writes refused)",
88 Usage: "repo archive <owner/name>", Run: runArchive}) 136 Usage: "repo archive <owner/name>",
137 Examples: []string{"repo archive krz/gitbay"},
138 Run: runArchive})
89 register(Command{Path: []string{"repo", "unarchive"}, 139 register(Command{Path: []string{"repo", "unarchive"},
90 Summary: "unarchive a repository", 140 Summary: "unarchive a repository",
91 Usage: "repo unarchive <owner/name>", Run: runUnarchive}) 141 Usage: "repo unarchive <owner/name>",
142 Examples: []string{"repo unarchive krz/gitbay"},
143 Run: runUnarchive})
92 register(Command{Path: []string{"repo", "topics"}, 144 register(Command{Path: []string{"repo", "topics"},
93 Summary: "list topics", 145 Summary: "list topics",
94 Usage: "repo topics <owner/name>", ReadOnly: true, Run: runTopicsList}) 146 Usage: "repo topics <owner/name>",
147 Examples: []string{"repo topics krz/gitbay"},
148 ReadOnly: true, Run: runTopicsList})
95 register(Command{Path: []string{"repo", "topics", "add"}, 149 register(Command{Path: []string{"repo", "topics", "add"},
96 Summary: "add topics", 150 Summary: "add topics",
97 Usage: "repo topics add <owner/name> <topic>...", Run: runTopicsAdd}) 151 Usage: "repo topics add <owner/name> <topic>...",
152 Examples: []string{"repo topics add krz/gitbay git forge cli"},
153 Run: runTopicsAdd})
98 register(Command{Path: []string{"repo", "topics", "remove"}, 154 register(Command{Path: []string{"repo", "topics", "remove"},
99 Summary: "remove topics", 155 Summary: "remove topics",
100 Usage: "repo topics remove <owner/name> <topic>...", Run: runTopicsRemove}) 156 Usage: "repo topics remove <owner/name> <topic>...",
157 Examples: []string{"repo topics remove krz/gitbay cli"},
158 Run: runTopicsRemove})
101 register(Command{Path: []string{"repo", "search"}, 159 register(Command{Path: []string{"repo", "search"},
102 Summary: "find repositories by name, description, or topic", 160 Summary: "find repositories by name, description, or topic",
103 Usage: "repo search <query>", ReadOnly: true, Run: runRepoSearch}) 161 Usage: "repo search <query>",
162 Examples: []string{"repo search forge"},
163 ReadOnly: true, Run: runRepoSearch})
104 register(Command{Path: []string{"repo", "grep"}, 164 register(Command{Path: []string{"repo", "grep"},
105 Summary: "search file contents", 165 Summary: "search file contents",
106 Usage: "repo grep <owner/name> <query> [--ref <ref>]", ReadOnly: true, Run: runRepoGrep}) 166 Usage: "repo grep <owner/name> <query> [--ref <ref>]",
167 Flags: []Flag{
168 {"--ref", "<ref>", "branch, tag or commit to search", "the default branch"},
169 },
170 Examples: []string{"repo grep krz/gitbay TODO"},
171 ReadOnly: true, Run: runRepoGrep})
107 register(Command{Path: []string{"repo", "diff"}, 172 register(Command{Path: []string{"repo", "diff"},
108 Summary: "the patch between two refs, from their merge base", 173 Summary: "the patch between two refs, from their merge base",
109 Usage: "repo diff <owner/name> <base> <head>", ReadOnly: true, Run: runRepoDiff}) 174 Usage: "repo diff <owner/name> <base> <head>",
175 Examples: []string{"repo diff krz/gitbay main cli-output-help"},
176 ReadOnly: true, Run: runRepoDiff})
110 register(Command{Path: []string{"repo", "pin"}, 177 register(Command{Path: []string{"repo", "pin"},
111 Summary: "pin a repository to your dashboard", 178 Summary: "pin a repository to your dashboard",
112 Usage: "repo pin <owner/name>", Run: runRepoPin}) 179 Usage: "repo pin <owner/name>",
180 Examples: []string{"repo pin krz/gitbay"},
181 Run: runRepoPin})
113 register(Command{Path: []string{"repo", "unpin"}, 182 register(Command{Path: []string{"repo", "unpin"},
114 Summary: "unpin a repository", 183 Summary: "unpin a repository",
115 Usage: "repo unpin <owner/name>", Run: runRepoUnpin}) 184 Usage: "repo unpin <owner/name>",
185 Examples: []string{"repo unpin krz/gitbay"},
186 Run: runRepoUnpin})
116 register(Command{Path: []string{"repo", "bookmark"}, 187 register(Command{Path: []string{"repo", "bookmark"},
117 Summary: "bookmark a repository to come back to", 188 Summary: "bookmark a repository to come back to",
118 Usage: "repo bookmark <owner/name>", Run: runRepoBookmark}) 189 Usage: "repo bookmark <owner/name>",
190 Examples: []string{"repo bookmark krz/gitbay"},
191 Run: runRepoBookmark})
119 register(Command{Path: []string{"repo", "unbookmark"}, 192 register(Command{Path: []string{"repo", "unbookmark"},
120 Summary: "remove a bookmark", 193 Summary: "remove a bookmark",
121 Usage: "repo unbookmark <owner/name>", Run: runRepoUnbookmark}) 194 Usage: "repo unbookmark <owner/name>",
195 Examples: []string{"repo unbookmark krz/gitbay"},
196 Run: runRepoUnbookmark})
122 register(Command{Path: []string{"repo", "bookmarks"}, 197 register(Command{Path: []string{"repo", "bookmarks"},
123 Summary: "list the repositories you have bookmarked", 198 Summary: "list the repositories you have bookmarked",
124 Usage: "repo bookmarks", ReadOnly: true, Run: runRepoBookmarks}) 199 Usage: "repo bookmarks",
200 Examples: []string{"repo bookmarks"},
201 ReadOnly: true, Run: runRepoBookmarks})
125} 202}
126 203
127const ( 204const (
internal/control/runnerrepo.go +9 −4
@@ -21,13 +21,18 @@ func init() {
21 register(Command{Path: []string{"repo", "runner", "add"}, 21 register(Command{Path: []string{"repo", "runner", "add"},
22 Summary: "attach a runner's public key to a repository", 22 Summary: "attach a runner's public key to a repository",
23 Usage: "repo runner add <owner/name> < key.pub", 23 Usage: "repo runner add <owner/name> < key.pub",
24 Examples: []string{"repo runner add krz/gitbay < key.pub"},
24 ReadsStdin: true, Run: runRepoRunnerAdd}) 25 ReadsStdin: true, Run: runRepoRunnerAdd})
25 register(Command{Path: []string{"repo", "runner", "list"}, 26 register(Command{Path: []string{"repo", "runner", "list"},
26 Summary: "list the runners attached to a repository", 27 Summary: "list the runners attached to a repository",
27 Usage: "repo runner list <owner/name>", ReadOnly: true, Run: runRepoRunnerList}) 28 Usage: "repo runner list <owner/name>",
29 Examples: []string{"repo runner list krz/gitbay"},
30 ReadOnly: true, Run: runRepoRunnerList})
28 register(Command{Path: []string{"repo", "runner", "remove"}, 31 register(Command{Path: []string{"repo", "runner", "remove"},
29 Summary: "detach a runner from a repository", 32 Summary: "detach a runner from a repository",
30 Usage: "repo runner remove <owner/name> <fingerprint>", Run: runRepoRunnerRemove}) 33 Usage: "repo runner remove <owner/name> <fingerprint>",
34 Examples: []string{"repo runner remove krz/gitbay SHA256:abcd1234"},
35 Run: runRepoRunnerRemove})
31} 36}
32 37
33func runRepoRunnerAdd(c *Ctx, args []string) int { 38func runRepoRunnerAdd(c *Ctx, args []string) int {
internal/control/search.go +9 −2
@@ -12,8 +12,15 @@ import (
12 12
13func init() { 13func init() {
14 register(Command{Path: []string{"search"}, 14 register(Command{Path: []string{"search"},
15 Summary: "find repositories, issues and merge requests across the instance", 15 Summary: "find repositories, issues and merge requests across the instance",
16 Usage: "search <query> [--kind repo|issue|mr]", 16 Usage: "search <query> [--kind repo|issue|mr]",
17 Flags: []Flag{
18 {"--kind", "repo|issue|mr", "only this kind of result, may repeat", ""},
19 },
20 Examples: []string{
21 `search "auth bug"`,
22 `search "auth bug" --kind issue`,
23 },
17 ReadOnly: true, Run: runSearch}) 24 ReadOnly: true, Run: runSearch})
18} 25}
19 26
internal/control/sig.go +19 −7
@@ -18,21 +18,33 @@ import (
18 18
19func init() { 19func init() {
20 register(Command{Path: []string{"pgp", "add"}, 20 register(Command{Path: []string{"pgp", "add"},
21 Summary: "register an OpenPGP public key (armored)", 21 Summary: "register an OpenPGP public key (armored)",
22 Usage: "pgp add < key.asc", ReadsStdin: true, Run: runPGPAdd}) 22 Usage: "pgp add < key.asc",
23 Examples: []string{"pgp add < key.asc"},
24 ReadsStdin: true, Run: runPGPAdd})
23 register(Command{Path: []string{"pgp", "list"}, 25 register(Command{Path: []string{"pgp", "list"},
24 Summary: "list registered OpenPGP keys", 26 Summary: "list registered OpenPGP keys",
25 Usage: "pgp list", ReadOnly: true, Run: runPGPList}) 27 Usage: "pgp list",
28 Examples: []string{"pgp list"}, ReadOnly: true, Run: runPGPList})
26 register(Command{Path: []string{"pgp", "remove"}, 29 register(Command{Path: []string{"pgp", "remove"},
27 Summary: "remove an OpenPGP key by fingerprint", 30 Summary: "remove an OpenPGP key by fingerprint",
28 Usage: "pgp remove <fingerprint>", Run: runPGPRemove}) 31 Usage: "pgp remove <fingerprint>",
32 Examples: []string{"pgp remove ABCD1234ABCD1234ABCD1234ABCD1234ABCD1234"}, Run: runPGPRemove})
29 register(Command{Path: []string{"repo", "commit"}, 33 register(Command{Path: []string{"repo", "commit"},
30 Summary: "show one commit with its patch", 34 Summary: "show one commit with its patch",
31 Usage: "repo commit <owner/name> <sha>", 35 Usage: "repo commit <owner/name> <sha>",
36 Examples: []string{"repo commit krz/gitbay a1b2c3d"},
32 ReadOnly: true, Run: runRepoCommit}) 37 ReadOnly: true, Run: runRepoCommit})
33 register(Command{Path: []string{"repo", "log"}, 38 register(Command{Path: []string{"repo", "log"},
34 Summary: "commit log with signature states", 39 Summary: "commit log with signature states",
35 Usage: "repo log <owner/name> [--ref <r>] [--limit n] [--path <file>]", ReadOnly: true, Run: runRepoLog}) 40 Usage: "repo log <owner/name> [--ref <r>] [--limit n] [--path <file>]",
41 Flags: []Flag{
42 {"--ref", "<r>", "branch, tag or commit to start from", "the default branch"},
43 {"--limit", "n", "rows to show", "30"},
44 {"--path", "<file>", "only commits touching this path", ""},
45 },
46 Examples: []string{"repo log krz/gitbay --limit 10"},
47 ReadOnly: true, Run: runRepoLog})
36} 48}
37 49
38func runPGPAdd(c *Ctx, args []string) int { 50func runPGPAdd(c *Ctx, args []string) int {
internal/control/snippet.go +38 −12
@@ -19,31 +19,57 @@ const maxSnippetFiles = 64
19 19
20func init() { 20func init() {
21 register(Command{Path: []string{"snippet", "create"}, 21 register(Command{Path: []string{"snippet", "create"},
22 Summary: "create a snippet from one file on stdin", 22 Summary: "create a snippet from one file on stdin",
23 Usage: "snippet create <filename> [--description <d>] [--visibility public|unlisted|private] < file", 23 Usage: "snippet create <filename> [--description <d>] [--visibility public|unlisted|private] < file",
24 Flags: []Flag{
25 {"--description", "<d>", "one line about the snippet", ""},
26 {"--visibility", "public|unlisted|private", "who can find it", "unlisted"},
27 },
28 Examples: []string{"snippet create notes.md --visibility private < notes.md"},
24 ReadsStdin: true, Run: runSnippetCreate}) 29 ReadsStdin: true, Run: runSnippetCreate})
25 register(Command{Path: []string{"snippet", "show"}, 30 register(Command{Path: []string{"snippet", "show"},
26 Summary: "show a snippet's metadata and files", 31 Summary: "show a snippet's metadata and files",
27 Usage: "snippet show <id>", ReadOnly: true, Run: runSnippetShow}) 32 Usage: "snippet show <id>",
33 Examples: []string{"snippet show a1b2c3"},
34 ReadOnly: true, Run: runSnippetShow})
28 register(Command{Path: []string{"snippet", "list"}, 35 register(Command{Path: []string{"snippet", "list"},
29 Summary: "list your snippets, or an owner's public ones", 36 Summary: "list your snippets, or an owner's public ones",
30 Usage: "snippet list [<owner>] [--limit n] [--cursor c]", ReadOnly: true, Run: runSnippetList}) 37 Usage: "snippet list [<owner>] [--limit n] [--cursor c]",
38 Flags: []Flag{
39 {"--limit", "n", "rows per page", ""},
40 {"--cursor", "c", "continue from the previous page", ""},
41 },
42 Examples: []string{"snippet list cmc"},
43 ReadOnly: true, Run: runSnippetList})
31 register(Command{Path: []string{"snippet", "edit"}, 44 register(Command{Path: []string{"snippet", "edit"},
32 Summary: "change a snippet's description or visibility", 45 Summary: "change a snippet's description or visibility",
33 Usage: "snippet edit <id> [--description <d>] [--visibility public|unlisted|private]", Run: runSnippetEdit}) 46 Usage: "snippet edit <id> [--description <d>] [--visibility public|unlisted|private]",
47 Flags: []Flag{
48 {"--description", "<d>", "one line about the snippet", ""},
49 {"--visibility", "public|unlisted|private", "who can find it", ""},
50 },
51 Examples: []string{"snippet edit a1b2c3 --visibility public"},
52 Run: runSnippetEdit})
34 register(Command{Path: []string{"snippet", "delete"}, 53 register(Command{Path: []string{"snippet", "delete"},
35 Summary: "delete a snippet and its files", 54 Summary: "delete a snippet and its files",
36 Usage: "snippet delete <id>", Run: runSnippetDelete}) 55 Usage: "snippet delete <id>",
56 Examples: []string{"snippet delete a1b2c3"},
57 Run: runSnippetDelete})
37 register(Command{Path: []string{"snippet", "file", "set"}, 58 register(Command{Path: []string{"snippet", "file", "set"},
38 Summary: "add a file to a snippet, or replace one, from stdin", 59 Summary: "add a file to a snippet, or replace one, from stdin",
39 Usage: "snippet file set <id> <filename> < file", 60 Usage: "snippet file set <id> <filename> < file",
61 Examples: []string{"snippet file set a1b2c3 notes.md < notes.md"},
40 ReadsStdin: true, Run: runSnippetFileSet}) 62 ReadsStdin: true, Run: runSnippetFileSet})
41 register(Command{Path: []string{"snippet", "file", "get"}, 63 register(Command{Path: []string{"snippet", "file", "get"},
42 Summary: "write a snippet file to stdout", 64 Summary: "write a snippet file to stdout",
43 Usage: "snippet file get <id> <filename> > file", ReadOnly: true, Run: runSnippetFileGet}) 65 Usage: "snippet file get <id> <filename> > file",
66 Examples: []string{"snippet file get a1b2c3 notes.md > notes.md"},
67 ReadOnly: true, Run: runSnippetFileGet})
44 register(Command{Path: []string{"snippet", "file", "remove"}, 68 register(Command{Path: []string{"snippet", "file", "remove"},
45 Summary: "remove a file from a snippet", 69 Summary: "remove a file from a snippet",
46 Usage: "snippet file remove <id> <filename>", Run: runSnippetFileRemove}) 70 Usage: "snippet file remove <id> <filename>",
71 Examples: []string{"snippet file remove a1b2c3 notes.md"},
72 Run: runSnippetFileRemove})
47} 73}
48 74
49type SnippetFileOut struct { 75type SnippetFileOut struct {
internal/control/status.go +14 −3
@@ -15,10 +15,21 @@ func init() {
15 register(Command{Path: []string{"status", "set"}, 15 register(Command{Path: []string{"status", "set"},
16 Summary: "report a commit status (CI)", 16 Summary: "report a commit status (CI)",
17 Usage: "status set <owner/name> <sha> --context <c> --state pending|success|failure|error [--description <d>] [--url <u>]", 17 Usage: "status set <owner/name> <sha> --context <c> --state pending|success|failure|error [--description <d>] [--url <u>]",
18 Run: runStatusSet}) 18 Flags: []Flag{
19 {"--context", "<c>", "the check this status reports for", ""},
20 {"--state", "pending|success|failure|error", "the check's outcome", ""},
21 {"--description", "<d>", "short text shown beside the state", ""},
22 {"--url", "<u>", "link to the check's own output", ""},
23 },
24 Examples: []string{
25 "status set krz/gitbay a1b2c3d --context ci/build --state success",
26 },
27 Run: runStatusSet})
19 register(Command{Path: []string{"status", "list"}, 28 register(Command{Path: []string{"status", "list"},
20 Summary: "statuses on a commit", 29 Summary: "statuses on a commit",
21 Usage: "status list <owner/name> <sha>", ReadOnly: true, Run: runStatusList}) 30 Usage: "status list <owner/name> <sha>",
31 Examples: []string{"status list krz/gitbay a1b2c3d"},
32 ReadOnly: true, Run: runStatusList})
22} 33}
23 34
24var validStatusState = map[string]bool{"pending": true, "success": true, "failure": true, "error": true} 35var validStatusState = map[string]bool{"pending": true, "success": true, "failure": true, "error": true}
internal/control/teams.go +27 −18
@@ -14,32 +14,41 @@ import (
14 14
15func init() { 15func init() {
16 register(Command{Path: []string{"org", "team", "create"}, 16 register(Command{Path: []string{"org", "team", "create"},
17 Summary: "create a team", 17 Summary: "create a team",
18 Usage: "org team create <org> <team>", Run: runTeamCreate}) 18 Usage: "org team create <org> <team>",
19 Examples: []string{"org team create krz maintainers"}, Run: runTeamCreate})
19 register(Command{Path: []string{"org", "team", "delete"}, 20 register(Command{Path: []string{"org", "team", "delete"},
20 Summary: "delete a team (its grants with it)", 21 Summary: "delete a team (its grants with it)",
21 Usage: "org team delete <org> <team>", Run: runTeamDelete}) 22 Usage: "org team delete <org> <team>",
23 Examples: []string{"org team delete krz maintainers"}, Run: runTeamDelete})
22 register(Command{Path: []string{"org", "team", "list"}, 24 register(Command{Path: []string{"org", "team", "list"},
23 Summary: "list an org's teams", 25 Summary: "list an org's teams",
24 Usage: "org team list <org>", ReadOnly: true, Run: runTeamList}) 26 Usage: "org team list <org>",
27 Examples: []string{"org team list krz"}, ReadOnly: true, Run: runTeamList})
25 register(Command{Path: []string{"org", "team", "show"}, 28 register(Command{Path: []string{"org", "team", "show"},
26 Summary: "show a team's members and grants", 29 Summary: "show a team's members and grants",
27 Usage: "org team show <org> <team>", ReadOnly: true, Run: runTeamShow}) 30 Usage: "org team show <org> <team>",
31 Examples: []string{"org team show krz maintainers"}, ReadOnly: true, Run: runTeamShow})
28 register(Command{Path: []string{"org", "team", "add"}, 32 register(Command{Path: []string{"org", "team", "add"},
29 Summary: "add org members to a team", 33 Summary: "add org members to a team",
30 Usage: "org team add <org> <team> <user>...", Run: runTeamAdd}) 34 Usage: "org team add <org> <team> <user>...",
35 Examples: []string{"org team add krz maintainers cmc"}, Run: runTeamAdd})
31 register(Command{Path: []string{"org", "team", "remove"}, 36 register(Command{Path: []string{"org", "team", "remove"},
32 Summary: "remove members from a team", 37 Summary: "remove members from a team",
33 Usage: "org team remove <org> <team> <user>...", Run: runTeamRemove}) 38 Usage: "org team remove <org> <team> <user>...",
39 Examples: []string{"org team remove krz maintainers cmc"}, Run: runTeamRemove})
34 register(Command{Path: []string{"org", "team", "grant"}, 40 register(Command{Path: []string{"org", "team", "grant"},
35 Summary: "grant a team a role on an org repo", 41 Summary: "grant a team a role on an org repo",
36 Usage: "org team grant <org> <team> <owner/name> read|write|admin", Run: runTeamGrant}) 42 Usage: "org team grant <org> <team> <owner/name> read|write|admin",
43 Examples: []string{"org team grant krz maintainers krz/gitbay write"}, Run: runTeamGrant})
37 register(Command{Path: []string{"org", "team", "revoke"}, 44 register(Command{Path: []string{"org", "team", "revoke"},
38 Summary: "revoke a team's grant", 45 Summary: "revoke a team's grant",
39 Usage: "org team revoke <org> <team> <owner/name>", Run: runTeamRevoke}) 46 Usage: "org team revoke <org> <team> <owner/name>",
47 Examples: []string{"org team revoke krz maintainers krz/gitbay"}, Run: runTeamRevoke})
40 register(Command{Path: []string{"org", "settings", "members-role"}, 48 register(Command{Path: []string{"org", "settings", "members-role"},
41 Summary: "role plain membership implies on every org repo", 49 Summary: "role plain membership implies on every org repo",
42 Usage: "org settings members-role <org> write|read|none (default write)", Run: runOrgMembersRole}) 50 Usage: "org settings members-role <org> write|read|none (default write)",
51 Examples: []string{"org settings members-role krz read"}, Run: runOrgMembersRole})
43} 52}
44 53
45// orgAdminRef resolves an org and requires the caller to admin it. 54// orgAdminRef resolves an org and requires the caller to admin it.
internal/control/theme.go +4 −2
@@ -11,10 +11,12 @@ func init() {
11 register(Command{Path: []string{"web", "theme", "show"}, 11 register(Command{Path: []string{"web", "theme", "show"},
12 Summary: "the colour scheme the web UI uses for you", 12 Summary: "the colour scheme the web UI uses for you",
13 Usage: "web theme show", 13 Usage: "web theme show",
14 Examples: []string{"web theme show"},
14 ReadOnly: true, Run: runWebThemeShow}) 15 ReadOnly: true, Run: runWebThemeShow})
15 register(Command{Path: []string{"web", "theme", "set"}, 16 register(Command{Path: []string{"web", "theme", "set"},
16 Summary: "follow the browser's scheme, or force light or dark", 17 Summary: "follow the browser's scheme, or force light or dark",
17 Usage: "web theme set system|light|dark", Run: runWebThemeSet}) 18 Usage: "web theme set system|light|dark",
19 Examples: []string{"web theme set dark"}, Run: runWebThemeSet})
18} 20}
19 21
20// themes are the values the layout knows how to stamp. system is the 22// themes are the values the layout knows how to stamp. system is the
internal/control/token.go +14 −6
@@ -16,14 +16,22 @@ func init() {
16 register(Command{Path: []string{"token", "create"}, 16 register(Command{Path: []string{"token", "create"},
17 Summary: "mint an API token (shown once)", 17 Summary: "mint an API token (shown once)",
18 Usage: "token create --name <n> [--scope full|read] [--ttl 30d|720h]", 18 Usage: "token create --name <n> [--scope full|read] [--ttl 30d|720h]",
19 Run: runTokenCreate}) 19 Flags: []Flag{
20 {"--name", "<n>", "the token's name", ""},
21 {"--scope", "full|read", "what the token may do", "full"},
22 {"--ttl", "30d|720h", "how long the token is valid", "never expires"},
23 },
24 Examples: []string{"token create --name laptop --scope read --ttl 30d"},
25 Run: runTokenCreate})
20 register(Command{Path: []string{"token", "list"}, 26 register(Command{Path: []string{"token", "list"},
21 Summary: "list API tokens", 27 Summary: "list API tokens",
22 Usage: "token list", ReadOnly: true, Run: runTokenList}) 28 Usage: "token list",
29 Examples: []string{"token list"}, ReadOnly: true, Run: runTokenList})
23 register(Command{Path: []string{"token", "revoke"}, 30 register(Command{Path: []string{"token", "revoke"},
24 Summary: "revoke an API token by name", 31 Summary: "revoke an API token by name",
25 Usage: "token revoke <name>", 32 Usage: "token revoke <name>",
26 Run: runTokenRevoke}) 33 Examples: []string{"token revoke laptop"},
34 Run: runTokenRevoke})
27} 35}
28 36
29// parseTTL accepts Go durations plus a day suffix ("30d"). 37// parseTTL accepts Go durations plus a day suffix ("30d").
internal/control/web.go +11 −5
@@ -14,14 +14,20 @@ func newStoredToken() (token, hash string, err error) { return store.NewToken()
14 14
15func init() { 15func init() {
16 register(Command{Path: []string{"web", "login"}, 16 register(Command{Path: []string{"web", "login"},
17 Summary: "mint a one-time browser login URL", 17 Summary: "mint a one-time browser login URL",
18 Usage: "web login", Run: runWebLogin}) 18 Usage: "web login",
19 Examples: []string{"web login"}, Run: runWebLogin})
19 register(Command{Path: []string{"web", "sessions", "list"}, 20 register(Command{Path: []string{"web", "sessions", "list"},
20 Summary: "list your browser sessions", 21 Summary: "list your browser sessions",
21 Usage: "web sessions list", ReadOnly: true, Run: runWebSessionsList}) 22 Usage: "web sessions list",
23 Examples: []string{"web sessions list"}, ReadOnly: true, Run: runWebSessionsList})
22 register(Command{Path: []string{"web", "sessions", "revoke"}, 24 register(Command{Path: []string{"web", "sessions", "revoke"},
23 Summary: "end a browser session, or all of them", 25 Summary: "end a browser session, or all of them",
24 Usage: "web sessions revoke <id>|--all", Run: runWebSessionsRevoke}) 26 Usage: "web sessions revoke <id>|--all",
27 Flags: []Flag{
28 {"--all", "", "revoke every browser session", ""},
29 },
30 Examples: []string{"web sessions revoke --all"}, Run: runWebSessionsRevoke})
25} 31}
26 32
27func runWebSessionsList(c *Ctx, args []string) int { 33func runWebSessionsList(c *Ctx, args []string) int {
internal/control/webhook.go +22 −8
@@ -15,19 +15,33 @@ import (
15func init() { 15func init() {
16 register(Command{Path: []string{"webhook", "add"}, 16 register(Command{Path: []string{"webhook", "add"},
17 Summary: "add a webhook", 17 Summary: "add a webhook",
18 Usage: "webhook add <owner/name> <url> [--secret <s>] [--events push,issue.created|*]", Run: runWebhookAdd}) 18 Usage: "webhook add <owner/name> <url> [--secret <s>] [--events push,issue.created|*]",
19 Flags: []Flag{
20 {"--secret", "<s>", "signs deliveries so the receiver can verify them", ""},
21 {"--events", "push,issue.created|*", "which events to send", "*"},
22 },
23 Examples: []string{"webhook add krz/gitbay https://ci.example.org/hook --events push"},
24 Run: runWebhookAdd})
19 register(Command{Path: []string{"webhook", "list"}, 25 register(Command{Path: []string{"webhook", "list"},
20 Summary: "list webhooks", 26 Summary: "list webhooks",
21 Usage: "webhook list <owner/name>", ReadOnly: true, Run: runWebhookList}) 27 Usage: "webhook list <owner/name>",
28 Examples: []string{"webhook list krz/gitbay"}, ReadOnly: true, Run: runWebhookList})
22 register(Command{Path: []string{"webhook", "remove"}, 29 register(Command{Path: []string{"webhook", "remove"},
23 Summary: "remove a webhook", 30 Summary: "remove a webhook",
24 Usage: "webhook remove <owner/name> <id>", Run: runWebhookRemove}) 31 Usage: "webhook remove <owner/name> <id>",
32 Examples: []string{"webhook remove krz/gitbay 3"}, Run: runWebhookRemove})
25 register(Command{Path: []string{"webhook", "deliveries"}, 33 register(Command{Path: []string{"webhook", "deliveries"},
26 Summary: "recent deliveries", 34 Summary: "recent deliveries",
27 Usage: "webhook deliveries <owner/name> [--limit n]", ReadOnly: true, Run: runWebhookDeliveries}) 35 Usage: "webhook deliveries <owner/name> [--limit n]",
36 Flags: []Flag{
37 {"--limit", "n", "rows to show", "20"},
38 },
39 Examples: []string{"webhook deliveries krz/gitbay --limit 50"},
40 ReadOnly: true, Run: runWebhookDeliveries})
28 register(Command{Path: []string{"webhook", "redeliver"}, 41 register(Command{Path: []string{"webhook", "redeliver"},
29 Summary: "queue a delivery again", 42 Summary: "queue a delivery again",
30 Usage: "webhook redeliver <owner/name> <delivery-id>", Run: runWebhookRedeliver}) 43 Usage: "webhook redeliver <owner/name> <delivery-id>",
44 Examples: []string{"webhook redeliver krz/gitbay 42"}, Run: runWebhookRedeliver})
31} 45}
32 46
33func runWebhookAdd(c *Ctx, args []string) int { 47func runWebhookAdd(c *Ctx, args []string) int {
internal/control/wiki.go +2
@@ -18,6 +18,7 @@ func init() {
18 Path: []string{"wiki", "list"}, 18 Path: []string{"wiki", "list"},
19 Summary: "list a repository's wiki pages", 19 Summary: "list a repository's wiki pages",
20 Usage: "wiki list <owner/name>", 20 Usage: "wiki list <owner/name>",
21 Examples: []string{"wiki list krz/gitbay"},
21 ReadOnly: true, 22 ReadOnly: true,
22 Run: runWikiList, 23 Run: runWikiList,
23 }) 24 })
@@ -25,6 +26,7 @@ func init() {
25 Path: []string{"wiki", "show"}, 26 Path: []string{"wiki", "show"},
26 Summary: "print a wiki page", 27 Summary: "print a wiki page",
27 Usage: "wiki show <owner/name> [<page>]", 28 Usage: "wiki show <owner/name> [<page>]",
29 Examples: []string{"wiki show krz/gitbay Quickstart"},
28 ReadOnly: true, 30 ReadOnly: true,
29 Run: runWikiShow, 31 Run: runWikiShow,
30 }) 32 })