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 (
1212
1313 "github.com/spf13/cobra"
1414 "github.com/spf13/cobra/doc"
15 "golang.org/x/term"
1516
1617 "gitbay.org/gitbay/internal/protocol"
1718)
@@ -38,68 +39,55 @@ func newRoot() *cobra.Command {
3839 root.AddCommand(
3940 authCmd(),
4041 group("label", "issue labels",
41 pass("list", "labels with colour and use", 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("remove", "remove a label everywhere: <label>", passOpts{server: []string{"label", "remove"}, needsRepo: true}),
42 pass("list", passOpts{server: []string{"label", "list"}, needsRepo: true}),
43 pass("set", passOpts{server: []string{"label", "set"}, needsRepo: true}),
44 pass("remove", passOpts{server: []string{"label", "remove"}, needsRepo: true}),
4445 ),
4546 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("list", "statuses on a commit: <sha>", passOpts{server: []string{"status", "list"}, needsRepo: true}),
47 pass("set", passOpts{server: []string{"status", "set"}, needsRepo: true}),
48 pass("list", passOpts{server: []string{"status", "list"}, needsRepo: true}),
4849 ),
4950 group("build", "CI builds",
50 pass("list", "recent builds: <owner/name>", passOpts{server: []string{"build", "list"}, needsRepo: true}),
51 pass("show", "one build: <owner/name> <n>", 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("jobs", "list the jobs a trigger can name", passOpts{server: []string{"build", "jobs"}, needsRepo: true}),
54 pass("trigger", "queue a job now: <job>", passOpts{server: []string{"build", "trigger"}, needsRepo: true}),
55 pass("cancel", "withdraw a queued build: <n>", passOpts{server: []string{"build", "cancel"}, needsRepo: true}),
51 pass("list", passOpts{server: []string{"build", "list"}, needsRepo: true}),
52 pass("show", passOpts{server: []string{"build", "show"}, needsRepo: true}),
53 pass("log", passOpts{server: []string{"build", "log"}, needsRepo: true}),
54 pass("jobs", passOpts{server: []string{"build", "jobs"}, needsRepo: true}),
55 pass("trigger", passOpts{server: []string{"build", "trigger"}, needsRepo: true}),
56 pass("cancel", passOpts{server: []string{"build", "cancel"}, needsRepo: true}),
5657 ),
57 pass("dashboard", "one read for the account dashboard: pinned repos, open MRs, assigned issues, recent builds",
58 passOpts{server: []string{"dashboard"}}),
59 pass("feed", "activity on repositories you can reach [--limit n] [--cursor c]",
60 passOpts{server: []string{"feed"}}),
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"}}),
58 withShort(pass("dashboard", passOpts{server: []string{"dashboard"}}), "pinned repos, open MRs, assigned issues, recent builds"),
59 pass("feed", passOpts{server: []string{"feed"}}),
60 withShort(pass("explore", passOpts{server: []string{"explore"}}), "public repositories on this instance"),
61 withShort(pass("search", passOpts{server: []string{"search"}}), "find repositories, issues and merge requests"),
6562 group("notifications", "your notification inbox",
66 pass("list", "unread notifications, or [--all] [--limit n] [--cursor c]",
67 passOpts{server: []string{"notifications", "list"}}),
68 pass("read", "mark notifications read: <id>... | --all",
69 passOpts{server: []string{"notifications", "read"}}),
63 pass("list", passOpts{server: []string{"notifications", "list"}}),
64 pass("read", passOpts{server: []string{"notifications", "read"}}),
7065 group("settings", "notification preferences",
71 pass("show", "your notification preferences", 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"}}),
73 pass("watch", "every issue and merge request on repositories you can write to: on|off", passOpts{server: []string{"notifications", "settings", "watch"}}),
74 pass("push", "activity on your registered devices: on|off", passOpts{server: []string{"notifications", "settings", "push"}}),
66 pass("show", passOpts{server: []string{"notifications", "settings", "show"}}),
67 pass("mail", passOpts{server: []string{"notifications", "settings", "mail"}}),
68 pass("watch", passOpts{server: []string{"notifications", "settings", "watch"}}),
69 pass("push", passOpts{server: []string{"notifications", "settings", "push"}}),
7570 ),
7671 group("device", "Apple devices registered for push",
77 pass("add", "register a device, token on stdin: [--label name]",
78 passOpts{server: []string{"notifications", "device", "add"}, alwaysStdin: true, stdinWhat: "the device token"}),
79 pass("list", "your registered devices",
80 passOpts{server: []string{"notifications", "device", "list"}}),
81 pass("remove", "deregister a device: <id>",
82 passOpts{server: []string{"notifications", "device", "remove"}}),
72 pass("add", passOpts{server: []string{"notifications", "device", "add"}, alwaysStdin: true, stdinWhat: "the device token"}),
73 pass("list", passOpts{server: []string{"notifications", "device", "list"}}),
74 pass("remove", passOpts{server: []string{"notifications", "device", "remove"}}),
8375 ),
8476 ),
8577 group("wiki", "a repository's wiki pages",
86 pass("list", "list pages: [<owner/name>]", passOpts{server: []string{"wiki", "list"}, needsRepo: true}),
87 pass("show", "print a page: [<owner/name>] [<page>]", passOpts{server: []string{"wiki", "show"}, needsRepo: true}),
78 pass("list", passOpts{server: []string{"wiki", "list"}, needsRepo: true}),
79 pass("show", passOpts{server: []string{"wiki", "show"}, needsRepo: true}),
8880 ),
8981 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",
91 passOpts{server: []string{"snippet", "create"}, alwaysStdin: true, stdinWhat: "the file's text"}),
92 pass("show", "metadata and files: <id>", passOpts{server: []string{"snippet", "show"}}),
93 pass("list", "your snippets, or an owner's public ones: [<owner>] [--limit n] [--cursor c]",
94 passOpts{server: []string{"snippet", "list"}}),
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"}}),
82 pass("create", passOpts{server: []string{"snippet", "create"}, alwaysStdin: true, stdinWhat: "the file's text"}),
83 pass("show", passOpts{server: []string{"snippet", "show"}}),
84 pass("list", passOpts{server: []string{"snippet", "list"}}),
85 pass("edit", passOpts{server: []string{"snippet", "edit"}}),
86 pass("delete", passOpts{server: []string{"snippet", "delete"}}),
9887 group("file", "the files in a snippet",
99 pass("set", "add or replace a file from stdin: <id> <filename> < file",
100 passOpts{server: []string{"snippet", "file", "set"}, alwaysStdin: true, stdinWhat: "the file's text"}),
101 pass("get", "print a file: <id> <filename> > file", passOpts{server: []string{"snippet", "file", "get"}}),
102 pass("remove", "remove a file: <id> <filename>", passOpts{server: []string{"snippet", "file", "remove"}}),
88 pass("set", passOpts{server: []string{"snippet", "file", "set"}, alwaysStdin: true, stdinWhat: "the file's text"}),
89 pass("get", passOpts{server: []string{"snippet", "file", "get"}}),
90 pass("remove", passOpts{server: []string{"snippet", "file", "remove"}}),
10391 ),
10492 ),
10593 repoCmd(),
@@ -111,52 +99,106 @@ func newRoot() *cobra.Command {
11199 webCmd(),
112100 orgCmd(),
113101 group("profile", "user and org profiles",
114 pass("show", "show a profile: [name]", passOpts{server: []string{"profile", "show"}}),
115 pass("set", "set your profile: [--description d] [--website url] [--link label|url]...",
116 passOpts{server: []string{"profile", "set"}}),
102 pass("show", passOpts{server: []string{"profile", "show"}}),
103 pass("set", passOpts{server: []string{"profile", "set"}}),
117104 ),
118105 webhookCmd(),
119106 remoteCmd(),
120107 initCmd(),
121 pass("register", "create an account on the default instance: gitbay register --username <n> --email <a> | --invite <code>",
122 passOpts{server: []string{"register"}}),
123 pass("audit", "instance audit log (admins): [--actor <user>|-] [--action <prefix>] [--since <duration|date>] [--limit <n>]", passOpts{server: []string{"audit"}}),
108 withShort(pass("register", passOpts{server: []string{"register"}}), "create an account on this instance"),
109 pass("audit", passOpts{server: []string{"audit"}}),
124110 group("admin", "instance administration (admins)",
125111 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"}}),
127 pass("show", "show an account: <username>", passOpts{server: []string{"admin", "user", "show"}}),
128 pass("promote", "make an account an instance admin: <username>", passOpts{server: []string{"admin", "user", "promote"}}),
129 pass("demote", "remove instance admin (never the last one): <username>", 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}),
131 pass("disable", "suspend an account: <username>", passOpts{server: []string{"admin", "user", "disable"}}),
132 pass("enable", "restore a suspended account: <username>", passOpts{server: []string{"admin", "user", "enable"}}),
133 pass("delete", "delete an account that anchors nothing: <username> --yes", 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"}}),
112 pass("list", passOpts{server: []string{"admin", "user", "list"}}),
113 pass("show", passOpts{server: []string{"admin", "user", "show"}}),
114 pass("promote", passOpts{server: []string{"admin", "user", "promote"}}),
115 pass("demote", passOpts{server: []string{"admin", "user", "demote"}}),
116 pass("create", passOpts{server: []string{"admin", "user", "create"}, stdinOK: true}),
117 pass("disable", passOpts{server: []string{"admin", "user", "disable"}}),
118 pass("enable", passOpts{server: []string{"admin", "user", "enable"}}),
119 pass("delete", passOpts{server: []string{"admin", "user", "delete"}}),
120 pass("limits", passOpts{server: []string{"admin", "user", "limits"}}),
135121 ),
136122 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"}}),
138124 ),
139 pass("invite", "issue a registration invite and mail its code: --email <address>", passOpts{server: []string{"admin", "invite"}}),
140 pass("stats", "instance statistics: counts and per-repository disk usage", 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"}}),
142 pass("remove", "drop a key's heartbeat row: <fingerprint>", passOpts{server: []string{"admin", "runners", "remove"}}),
143 pass("forget", "alias of remove: <fingerprint>", passOpts{server: []string{"admin", "runners", "forget"}})),
125 pass("invite", passOpts{server: []string{"admin", "invite"}}),
126 pass("stats", passOpts{server: []string{"admin", "stats"}}),
127 withSub(pass("runners", passOpts{server: []string{"admin", "runners"}}),
128 pass("remove", passOpts{server: []string{"admin", "runners", "remove"}}),
129 pass("forget", passOpts{server: []string{"admin", "runners", "forget"}})),
144130 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"}}),
146 pass("archive", "archive a repository: <owner/name>", passOpts{server: []string{"admin", "repo", "archive"}}),
147 pass("unarchive", "unarchive a repository: <owner/name>", passOpts{server: []string{"admin", "repo", "unarchive"}}),
148 pass("visibility", "set visibility: <owner/name> public|private", passOpts{server: []string{"admin", "repo", "visibility"}}),
149 pass("delete", "delete a repository: <owner/name> --yes", passOpts{server: []string{"admin", "repo", "delete"}}),
131 pass("list", passOpts{server: []string{"admin", "repo", "list"}}),
132 pass("archive", passOpts{server: []string{"admin", "repo", "archive"}}),
133 pass("unarchive", passOpts{server: []string{"admin", "repo", "unarchive"}}),
134 pass("visibility", passOpts{server: []string{"admin", "repo", "visibility"}}),
135 pass("delete", passOpts{server: []string{"admin", "repo", "delete"}}),
150136 ),
151137 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"}}),
153139 ),
154140 ),
155141 manCmd(root),
156142 )
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 })
157151 return root
158152}
159153
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
160202// serverPath is the annotation key holding a passthrough command's
161203// server-side path, so the tree can be checked against the registry.
162204const serverPath = "gitbay.server_path"
@@ -197,10 +239,10 @@ func (o passOpts) stdinModeName() string {
197239 return "none"
198240}
199241
200func pass(use, short string, o passOpts) *cobra.Command {
242func pass(use string, o passOpts) *cobra.Command {
201243 return &cobra.Command{
202244 Use: use,
203 Short: short,
245 Short: summaries[strings.Join(o.server, " ")],
204246 Annotations: map[string]string{
205247 serverPath: strings.Join(o.server, " "),
206248 stdinMode: o.stdinModeName(),
@@ -222,6 +264,17 @@ func pass(use, short string, o passOpts) *cobra.Command {
222264 }
223265}
224266
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
225278// runServerHelp prints the registry's usage for one command.
226279func runServerHelp(o passOpts) int {
227280 t, err := resolveTarget()
@@ -335,12 +388,18 @@ func group(use, short string, subs ...*cobra.Command) *cobra.Command {
335388}
336389
337390// 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.
339395func serverHelp(prefix string) bool {
340396 t, err := resolveTarget()
341397 if err != nil {
342398 return false
343399 }
400 if term.IsTerminal(int(os.Stdout.Fd())) {
401 return runSSH(t, []string{"help", prefix}, strings.NewReader("")) == 0
402 }
344403 out, code := sshCapture(t, []string{"help", prefix})
345404 if code != 0 || out == "" {
346405 return false
@@ -368,8 +427,7 @@ func local(use, short string, fn func(args []string) int) *cobra.Command {
368427}
369428
370429func authCmd() *cobra.Command {
371 keysAdd := pass("add", "register an SSH public key (reads the key from stdin or --file -)",
372 passOpts{server: []string{"keys", "add"}, alwaysStdin: true, stdinWhat: "an SSH public key"})
430 keysAdd := pass("add", passOpts{server: []string{"keys", "add"}, alwaysStdin: true, stdinWhat: "an SSH public key"})
373431 // keys add always reads stdin on the server; wire it through directly.
374432 keysAdd.RunE = func(cmd *cobra.Command, args []string) error {
375433 t, err := resolveTarget()
@@ -405,212 +463,197 @@ func authCmd() *cobra.Command {
405463 },
406464 }
407465 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"}}),
409 pass("list", "list API tokens", passOpts{server: []string{"token", "list"}}),
410 pass("revoke", "revoke a token by name", passOpts{server: []string{"token", "revoke"}}),
466 pass("create", passOpts{server: []string{"token", "create"}}),
467 pass("list", passOpts{server: []string{"token", "list"}}),
468 pass("revoke", passOpts{server: []string{"token", "revoke"}}),
411469 )
412470 return group("auth", "identity: whoami, SSH and PGP keys",
413 pass("export", "write your account bundle (a user-level backup) to stdout",
414 passOpts{server: []string{"account", "export"}}),
471 pass("export", passOpts{server: []string{"account", "export"}}),
415472 tokens,
416 pass("whoami", "show the authenticated account", passOpts{server: []string{"whoami"}}),
473 pass("whoami", passOpts{server: []string{"whoami"}}),
417474 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"}}),
419476 keysAdd,
420 pass("label", "name a key: <fingerprint> [<text>]; no text clears it", passOpts{server: []string{"keys", "label"}}),
421 pass("remove", "remove an SSH key by fingerprint", passOpts{server: []string{"keys", "remove"}}),
477 pass("label", passOpts{server: []string{"keys", "label"}}),
478 pass("remove", passOpts{server: []string{"keys", "remove"}}),
422479 ),
423480 group("email", "manage email addresses",
424 pass("add", "add an address and get a verification code by mail", passOpts{server: []string{"email", "add"}}),
425 pass("verify", "confirm a verification code", passOpts{server: []string{"email", "verify"}}),
426 pass("list", "list the addresses on your account", passOpts{server: []string{"email", "list"}}),
427 pass("remove", "remove an address; not the primary, nor the last verified one", passOpts{server: []string{"email", "remove"}}),
428 pass("primary", "make a verified address the primary", passOpts{server: []string{"email", "primary"}}),
481 pass("add", passOpts{server: []string{"email", "add"}}),
482 pass("verify", passOpts{server: []string{"email", "verify"}}),
483 pass("list", passOpts{server: []string{"email", "list"}}),
484 pass("remove", passOpts{server: []string{"email", "remove"}}),
485 pass("primary", passOpts{server: []string{"email", "primary"}}),
429486 ),
430487 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"}}),
432489 pgpAdd,
433 pass("remove", "remove a PGP key by fingerprint", passOpts{server: []string{"pgp", "remove"}}),
490 pass("remove", passOpts{server: []string{"pgp", "remove"}}),
434491 ),
435492 )
436493}
437494
438495func repoCmd() *cobra.Command {
439496 return group("repo", "create and manage repositories",
440 pass("create", "create a repository: gitbay repo create <owner/name> [--private]",
441 passOpts{server: []string{"repo", "create"}}),
442 pass("list", "list repositories you own or can access [--limit n] [--cursor c]", passOpts{server: []string{"repo", "list"}}),
443 pass("show", "show repository details", passOpts{server: []string{"repo", "show"}, needsRepo: true}),
444 pass("log", "commit log with signature states", passOpts{server: []string{"repo", "log"}, needsRepo: true}),
445 pass("transfer", "move a repository to another owner: <new-owner>", passOpts{server: []string{"repo", "transfer"}, needsRepo: true}),
446 pass("rename", "rename a repository: <new-name> (clone URLs change)", passOpts{server: []string{"repo", "rename"}, needsRepo: true}),
447 pass("delete", "delete a repository (--yes)", passOpts{server: []string{"repo", "delete"}, needsRepo: true}),
448 pass("fork", "fork a repository to you or an organization", passOpts{server: []string{"repo", "fork"}, needsRepo: true}),
449 pass("search", "find repositories by name, description, or topic: <query>", passOpts{server: []string{"repo", "search"}}),
450 pass("grep", "search file contents: <query> [--ref <ref>]", passOpts{server: []string{"repo", "grep"}, needsRepo: true}),
451 pass("diff", "the patch between two refs: <base> <head>", passOpts{server: []string{"repo", "diff"}, needsRepo: true}),
452 pass("tree", "list a directory: [<path>] [--ref <ref>]", passOpts{server: []string{"repo", "tree"}, needsRepo: true}),
453 pass("cat", "read a file: <path> [--ref <ref>]", passOpts{server: []string{"repo", "cat"}, needsRepo: true}),
454 pass("blame", "attribute lines to commits: <path> [--ref <ref>] [--from <n>] [--to <n>]",
455 passOpts{server: []string{"repo", "blame"}, needsRepo: true}),
456 pass("commit", "show one commit with its patch: <sha>",
457 passOpts{server: []string{"repo", "commit"}, needsRepo: true}),
458 pass("commit-file", "write a file and commit it: <path> [--ref <ref>] [--message <m>] --file -",
459 passOpts{server: []string{"repo", "commit-file"}, needsRepo: true, stdinOK: true}),
460 pass("refs", "list branches and tags", passOpts{server: []string{"repo", "refs"}, needsRepo: true}),
461 pass("download", "write a tar.gz of a ref to stdout: [--ref <r>] > repo.tar.gz",
462 passOpts{server: []string{"repo", "download"}, needsRepo: true}),
463 pass("pin", "pin a repository to your dashboard", passOpts{server: []string{"repo", "pin"}, needsRepo: true}),
464 pass("unpin", "unpin a repository", passOpts{server: []string{"repo", "unpin"}, needsRepo: true}),
465 pass("bookmark", "bookmark a repository to come back to", passOpts{server: []string{"repo", "bookmark"}, needsRepo: true}),
466 pass("unbookmark", "remove a bookmark", passOpts{server: []string{"repo", "unbookmark"}, needsRepo: true}),
467 pass("bookmarks", "list the repositories you have bookmarked", passOpts{server: []string{"repo", "bookmarks"}}),
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}),
497 pass("create", passOpts{server: []string{"repo", "create"}}),
498 pass("list", passOpts{server: []string{"repo", "list"}}),
499 pass("show", passOpts{server: []string{"repo", "show"}, needsRepo: true}),
500 pass("log", passOpts{server: []string{"repo", "log"}, needsRepo: true}),
501 pass("transfer", passOpts{server: []string{"repo", "transfer"}, needsRepo: true}),
502 pass("rename", passOpts{server: []string{"repo", "rename"}, needsRepo: true}),
503 pass("delete", passOpts{server: []string{"repo", "delete"}, needsRepo: true}),
504 pass("fork", passOpts{server: []string{"repo", "fork"}, needsRepo: true}),
505 pass("search", passOpts{server: []string{"repo", "search"}}),
506 pass("grep", passOpts{server: []string{"repo", "grep"}, needsRepo: true}),
507 pass("diff", passOpts{server: []string{"repo", "diff"}, needsRepo: true}),
508 pass("tree", passOpts{server: []string{"repo", "tree"}, needsRepo: true}),
509 pass("cat", passOpts{server: []string{"repo", "cat"}, needsRepo: true}),
510 pass("blame", passOpts{server: []string{"repo", "blame"}, needsRepo: true}),
511 pass("commit", passOpts{server: []string{"repo", "commit"}, needsRepo: true}),
512 pass("commit-file", passOpts{server: []string{"repo", "commit-file"}, needsRepo: true, stdinOK: true}),
513 pass("refs", passOpts{server: []string{"repo", "refs"}, needsRepo: true}),
514 pass("download", passOpts{server: []string{"repo", "download"}, needsRepo: true}),
515 pass("pin", passOpts{server: []string{"repo", "pin"}, needsRepo: true}),
516 pass("unpin", passOpts{server: []string{"repo", "unpin"}, needsRepo: true}),
517 pass("bookmark", passOpts{server: []string{"repo", "bookmark"}, needsRepo: true}),
518 pass("unbookmark", passOpts{server: []string{"repo", "unbookmark"}, needsRepo: true}),
519 pass("bookmarks", passOpts{server: []string{"repo", "bookmarks"}}),
520 pass("watch", passOpts{server: []string{"repo", "watch"}, needsRepo: true}),
521 pass("unwatch", passOpts{server: []string{"repo", "unwatch"}, needsRepo: true}),
522 pass("mute", passOpts{server: []string{"repo", "mute"}, needsRepo: true}),
523 pass("archive", passOpts{server: []string{"repo", "archive"}, needsRepo: true}),
524 pass("unarchive", passOpts{server: []string{"repo", "unarchive"}, needsRepo: true}),
473525 local("clone", "clone via ssh: gitbay repo clone <owner/name> [dir]", cmdRepoClone),
474526 importCmd(),
475 pass("import-issues", "import GitHub issue/PR history: --from <ghowner/ghrepo> [--token-stdin]",
476 passOpts{server: []string{"repo", "import-issues"}, needsRepo: true, stdinOK: true}),
527 pass("import-issues", passOpts{server: []string{"repo", "import-issues"}, needsRepo: true, stdinOK: true}),
477528 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"}),
479 pass("list", "list deploy keys", 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}),
529 pass("add", passOpts{server: []string{"repo", "deploy-key", "add"}, needsRepo: true, alwaysStdin: true, stdinWhat: "an SSH public key"}),
530 pass("list", passOpts{server: []string{"repo", "deploy-key", "list"}, needsRepo: true}),
531 pass("remove", passOpts{server: []string{"repo", "deploy-key", "remove"}, needsRepo: true}),
481532 ),
482533 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"}),
484 pass("list", "list attached runners", passOpts{server: []string{"repo", "runner", "list"}, needsRepo: true}),
485 pass("remove", "detach a runner: <fingerprint>", passOpts{server: []string{"repo", "runner", "remove"}, needsRepo: true}),
534 pass("add", passOpts{server: []string{"repo", "runner", "add"}, needsRepo: true, alwaysStdin: true, stdinWhat: "an SSH public key"}),
535 pass("list", passOpts{server: []string{"repo", "runner", "list"}, needsRepo: true}),
536 pass("remove", passOpts{server: []string{"repo", "runner", "remove"}, needsRepo: true}),
486537 ),
487538 group("mirror", "sync with a foreign remote",
488 pass("add", "add a mirror: <https-url> --direction push|pull [--username <u>] [--token-stdin]",
489 passOpts{server: []string{"repo", "mirror", "add"}, needsRepo: true, stdinOK: true}),
490 pass("list", "list mirrors with sync status", passOpts{server: []string{"repo", "mirror", "list"}, needsRepo: true}),
491 pass("remove", "remove a mirror: <id>", passOpts{server: []string{"repo", "mirror", "remove"}, needsRepo: true}),
492 pass("sync", "schedule an immediate sync", passOpts{server: []string{"repo", "mirror", "sync"}, needsRepo: true}),
539 pass("add", passOpts{server: []string{"repo", "mirror", "add"}, needsRepo: true, stdinOK: true}),
540 pass("list", passOpts{server: []string{"repo", "mirror", "list"}, needsRepo: true}),
541 pass("remove", passOpts{server: []string{"repo", "mirror", "remove"}, needsRepo: true}),
542 pass("sync", passOpts{server: []string{"repo", "mirror", "sync"}, needsRepo: true}),
493543 ),
494544 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}),
496 pass("disable", "stop checking dependencies", 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}),
545 pass("enable", passOpts{server: []string{"repo", "deps", "enable"}, needsRepo: true}),
546 pass("disable", passOpts{server: []string{"repo", "deps", "disable"}, needsRepo: true}),
547 pass("status", passOpts{server: []string{"repo", "deps", "status"}, needsRepo: true}),
498548 ),
499549 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}),
501 pass("list", "list secret names", passOpts{server: []string{"repo", "secret", "list"}, needsRepo: true}),
502 pass("remove", "remove a secret: <NAME>", passOpts{server: []string{"repo", "secret", "remove"}, needsRepo: true}),
550 pass("set", passOpts{server: []string{"repo", "secret", "set"}, needsRepo: true, alwaysStdin: true, stdinWhat: "the secret value", stdinSecret: true}),
551 pass("list", passOpts{server: []string{"repo", "secret", "list"}, needsRepo: true}),
552 pass("remove", passOpts{server: []string{"repo", "secret", "remove"}, needsRepo: true}),
503553 ),
504554 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}),
506 pass("verify", "check the DNS challenge and activate a claim: <domain>", passOpts{server: []string{"repo", "domain", "verify"}, needsRepo: true}),
507 pass("list", "list custom pages domains", 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}),
555 pass("add", passOpts{server: []string{"repo", "domain", "add"}, needsRepo: true}),
556 pass("verify", passOpts{server: []string{"repo", "domain", "verify"}, needsRepo: true}),
557 pass("list", passOpts{server: []string{"repo", "domain", "list"}, needsRepo: true}),
558 pass("remove", passOpts{server: []string{"repo", "domain", "remove"}, needsRepo: true}),
509559 ),
510560 group("topics", "free-form repository tags",
511 pass("list", "list topics", passOpts{server: []string{"repo", "topics"}, needsRepo: true}),
512 pass("add", "add topics: <topic>...", passOpts{server: []string{"repo", "topics", "add"}, needsRepo: true}),
513 pass("remove", "remove topics: <topic>...", passOpts{server: []string{"repo", "topics", "remove"}, needsRepo: true}),
561 pass("list", passOpts{server: []string{"repo", "topics"}, needsRepo: true}),
562 pass("add", passOpts{server: []string{"repo", "topics", "add"}, needsRepo: true}),
563 pass("remove", passOpts{server: []string{"repo", "topics", "remove"}, needsRepo: true}),
514564 ),
515565 group("access", "manage access grants",
516 pass("grant", "grant access: ... <user> read|write|admin", passOpts{server: []string{"repo", "access", "grant"}, needsRepo: true}),
517 pass("revoke", "revoke access: ... <user>", passOpts{server: []string{"repo", "access", "revoke"}, needsRepo: true}),
518 pass("list", "list access grants", passOpts{server: []string{"repo", "access", "list"}, needsRepo: true}),
566 pass("grant", passOpts{server: []string{"repo", "access", "grant"}, needsRepo: true}),
567 pass("revoke", passOpts{server: []string{"repo", "access", "revoke"}, needsRepo: true}),
568 pass("list", passOpts{server: []string{"repo", "access", "list"}, needsRepo: true}),
519569 ),
520570 group("settings", "repository settings",
521 pass("show", "show settings", passOpts{server: []string{"repo", "settings", "show"}, needsRepo: true}),
522 pass("protect", "protect a branch", passOpts{server: []string{"repo", "settings", "protect"}, needsRepo: true}),
523 pass("unprotect", "unprotect a branch", 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}),
525 pass("unprotect-tag", "drop a protected-tag glob: <glob>", 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}),
527 pass("require-approvals", "require N fresh approvals to merge: <n>", 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}),
529 pass("require-codeowners", "require an owner's approval per covered file: on|off", 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}),
531 pass("visibility", "set repository visibility: public|private", 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}),
533 pass("require-mr", "protected branches take changes through merge requests only: on|off", 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}),
535 pass("website", "set the repository website: <url> ('' clears)", 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}),
571 pass("show", passOpts{server: []string{"repo", "settings", "show"}, needsRepo: true}),
572 pass("protect", passOpts{server: []string{"repo", "settings", "protect"}, needsRepo: true}),
573 pass("unprotect", passOpts{server: []string{"repo", "settings", "unprotect"}, needsRepo: true}),
574 pass("protect-tag", passOpts{server: []string{"repo", "settings", "protect-tag"}, needsRepo: true}),
575 pass("unprotect-tag", passOpts{server: []string{"repo", "settings", "unprotect-tag"}, needsRepo: true}),
576 pass("default-branch", passOpts{server: []string{"repo", "settings", "default-branch"}, needsRepo: true}),
577 pass("require-approvals", passOpts{server: []string{"repo", "settings", "require-approvals"}, needsRepo: true}),
578 pass("require-resolved", passOpts{server: []string{"repo", "settings", "require-resolved"}, needsRepo: true}),
579 pass("require-codeowners", passOpts{server: []string{"repo", "settings", "require-codeowners"}, needsRepo: true}),
580 pass("require-checks", passOpts{server: []string{"repo", "settings", "require-checks"}, needsRepo: true}),
581 pass("visibility", passOpts{server: []string{"repo", "settings", "visibility"}, needsRepo: true}),
582 pass("require-signed", passOpts{server: []string{"repo", "settings", "require-signed"}, needsRepo: true}),
583 pass("require-mr", passOpts{server: []string{"repo", "settings", "require-mr"}, needsRepo: true}),
584 pass("description", passOpts{server: []string{"repo", "settings", "description"}, needsRepo: true}),
585 pass("website", passOpts{server: []string{"repo", "settings", "website"}, needsRepo: true}),
586 pass("git-daemon", passOpts{server: []string{"repo", "settings", "git-daemon"}, needsRepo: true}),
537587 ),
538588 )
539589}
540590
541591func issueCmd() *cobra.Command {
542592 return group("issue", "issues",
543 pass("create", "open an issue: --title <t> [--body|--file -|$EDITOR]",
544 passOpts{server: []string{"issue", "create"}, needsRepo: true, stdinOK: true, editor: "issue"}),
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}),
546 pass("show", "show an issue with comments", passOpts{server: []string{"issue", "show"}, needsRepo: true}),
547 pass("comment", "comment on an issue [--message|--file -|$EDITOR]",
548 passOpts{server: []string{"issue", "comment"}, needsRepo: true, stdinOK: true, editor: "comment"}),
549 pass("close", "close an issue", passOpts{server: []string{"issue", "close"}, needsRepo: true}),
550 pass("reopen", "reopen an issue", passOpts{server: []string{"issue", "reopen"}, needsRepo: true}),
551 pass("label", "add or remove labels: [--add <l>]... [--remove <l>]...", passOpts{server: []string{"issue", "label"}, needsRepo: true}),
552 pass("assign", "assign users: [--add <u>]... [--remove <u>]...", passOpts{server: []string{"issue", "assign"}, needsRepo: true}),
553 pass("edit", "edit title or body: <n> [--title <t>] [--body <b>|--file -]", passOpts{server: []string{"issue", "edit"}, needsRepo: true, stdinOK: 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}),
593 pass("create", passOpts{server: []string{"issue", "create"}, needsRepo: true, stdinOK: true, editor: "issue"}),
594 pass("list", passOpts{server: []string{"issue", "list"}, needsRepo: true}),
595 pass("show", passOpts{server: []string{"issue", "show"}, needsRepo: true}),
596 pass("comment", passOpts{server: []string{"issue", "comment"}, needsRepo: true, stdinOK: true, editor: "comment"}),
597 pass("close", passOpts{server: []string{"issue", "close"}, needsRepo: true}),
598 pass("reopen", passOpts{server: []string{"issue", "reopen"}, needsRepo: true}),
599 pass("label", passOpts{server: []string{"issue", "label"}, needsRepo: true}),
600 pass("assign", passOpts{server: []string{"issue", "assign"}, needsRepo: true}),
601 pass("edit", passOpts{server: []string{"issue", "edit"}, needsRepo: true, stdinOK: true}),
602 pass("milestone", passOpts{server: []string{"issue", "milestone"}, needsRepo: true}),
603 pass("templates", passOpts{server: []string{"issue", "templates"}, needsRepo: true}),
556604 )
557605}
558606
559607func releaseCmd() *cobra.Command {
560608 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]",
562 passOpts{server: []string{"release", "create"}, needsRepo: true, stdinOK: true, editor: "release"}),
563 pass("edit", "update title and notes: <tag> [--title <t>] [--notes|--file -]",
564 passOpts{server: []string{"release", "edit"}, needsRepo: true, stdinOK: true}),
565 pass("list", "releases: <owner/name> [--limit <n>] [--cursor <c>]", passOpts{server: []string{"release", "list"}, 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}),
609 pass("create", passOpts{server: []string{"release", "create"}, needsRepo: true, stdinOK: true, editor: "release"}),
610 pass("edit", passOpts{server: []string{"release", "edit"}, needsRepo: true, stdinOK: true}),
611 pass("list", passOpts{server: []string{"release", "list"}, needsRepo: true}),
612 pass("show", passOpts{server: []string{"release", "show"}, needsRepo: true}),
613 pass("delete", passOpts{server: []string{"release", "delete"}, needsRepo: true}),
568614 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"}),
570 pass("get", "download to stdout: <tag> <filename> > file", passOpts{server: []string{"release", "asset", "get"}, needsRepo: true}),
571 pass("remove", "remove an asset: <tag> <filename>", passOpts{server: []string{"release", "asset", "remove"}, needsRepo: true}),
615 pass("add", passOpts{server: []string{"release", "asset", "add"}, needsRepo: true, alwaysStdin: true, stdinWhat: "the asset's bytes"}),
616 pass("get", passOpts{server: []string{"release", "asset", "get"}, needsRepo: true}),
617 pass("remove", passOpts{server: []string{"release", "asset", "remove"}, needsRepo: true}),
572618 ),
573619 )
574620}
575621
576622func milestoneCmd() *cobra.Command {
577623 return group("milestone", "group issues and MRs toward a release",
578 pass("create", "create a milestone: <title> [--description <d>] [--due YYYY-MM-DD]",
579 passOpts{server: []string{"milestone", "create"}, needsRepo: true}),
580 pass("list", "list milestones with progress [--state open|closed|all]",
581 passOpts{server: []string{"milestone", "list"}, 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}),
624 pass("create", passOpts{server: []string{"milestone", "create"}, needsRepo: true}),
625 pass("list", passOpts{server: []string{"milestone", "list"}, needsRepo: true}),
626 pass("close", passOpts{server: []string{"milestone", "close"}, needsRepo: true}),
627 pass("reopen", passOpts{server: []string{"milestone", "reopen"}, needsRepo: true}),
584628 )
585629}
586630
587631func 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})
589 review.AddCommand(pass("request", "ask specific people for review: [--add <u>]... [--remove <u>]...", passOpts{server: []string{"mr", "review", "request"}, needsRepo: true}))
632 review := pass("review", passOpts{server: []string{"mr", "review"}, needsRepo: true})
633 review.AddCommand(pass("request", passOpts{server: []string{"mr", "review", "request"}, needsRepo: true}))
590634 return group("mr", "merge requests",
591 pass("create", "open a merge request: --source <branch> --target <branch> --title <t>",
592 passOpts{server: []string{"mr", "create"}, needsRepo: true, stdinOK: true, editor: "merge request", inferSource: 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}),
594 pass("show", "show a merge request", passOpts{server: []string{"mr", "show"}, needsRepo: true}),
595 pass("diff", "show the diff", passOpts{server: []string{"mr", "diff"}, needsRepo: true}),
635 pass("create", passOpts{server: []string{"mr", "create"}, needsRepo: true, stdinOK: true, editor: "merge request", inferSource: true}),
636 pass("list", passOpts{server: []string{"mr", "list"}, needsRepo: true}),
637 pass("show", passOpts{server: []string{"mr", "show"}, needsRepo: true}),
638 pass("diff", passOpts{server: []string{"mr", "diff"}, needsRepo: true}),
596639 local("checkout", "fetch and check out the MR head locally: gitbay mr checkout <n>", cmdMRCheckout),
597640 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"}),
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"}),
600 pass("threads", "review threads on an MR", passOpts{server: []string{"mr", "threads"}, needsRepo: true}),
601 pass("resolve", "resolve a review thread: <n> <thread-id>", passOpts{server: []string{"mr", "resolve"}, needsRepo: true}),
602 pass("unresolve", "reopen a review thread: <n> <thread-id>", passOpts{server: []string{"mr", "unresolve"}, needsRepo: true}),
641 pass("comment", passOpts{server: []string{"mr", "comment"}, needsRepo: true, stdinOK: true, editor: "comment"}),
642 pass("diff-comment", passOpts{server: []string{"mr", "diff-comment"}, needsRepo: true, stdinOK: true, editor: "comment"}),
643 pass("threads", passOpts{server: []string{"mr", "threads"}, needsRepo: true}),
644 pass("resolve", passOpts{server: []string{"mr", "resolve"}, needsRepo: true}),
645 pass("unresolve", passOpts{server: []string{"mr", "unresolve"}, needsRepo: true}),
603646 review,
604 pass("merge", "merge: [--strategy ff|merge|squash|rebase]", passOpts{server: []string{"mr", "merge"}, needsRepo: true}),
605 pass("close", "close without merging", passOpts{server: []string{"mr", "close"}, needsRepo: true}),
606 pass("revisions", "the heads this merge request has had", 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}),
608 pass("draft", "mark as work in progress", 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}),
610 pass("edit", "edit title or body: <n> [--title <t>] [--body <b>|--file -]", 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}),
612 pass("milestone", "set or clear the milestone: <n> <title|none>", passOpts{server: []string{"mr", "milestone"}, needsRepo: true}),
613 pass("retarget", "retarget onto another branch: <n> <branch>", passOpts{server: []string{"mr", "retarget"}, needsRepo: true}),
647 pass("merge", passOpts{server: []string{"mr", "merge"}, needsRepo: true}),
648 pass("close", passOpts{server: []string{"mr", "close"}, needsRepo: true}),
649 pass("revisions", passOpts{server: []string{"mr", "revisions"}, needsRepo: true}),
650 pass("range-diff", passOpts{server: []string{"mr", "range-diff"}, needsRepo: true}),
651 pass("draft", passOpts{server: []string{"mr", "draft"}, needsRepo: true}),
652 pass("ready", passOpts{server: []string{"mr", "ready"}, needsRepo: true}),
653 pass("edit", passOpts{server: []string{"mr", "edit"}, needsRepo: true, stdinOK: true}),
654 pass("label", passOpts{server: []string{"mr", "label"}, needsRepo: true}),
655 pass("milestone", passOpts{server: []string{"mr", "milestone"}, needsRepo: true}),
656 pass("retarget", passOpts{server: []string{"mr", "retarget"}, needsRepo: true}),
614657 )
615658}
616659
@@ -652,65 +695,64 @@ func usesTokenStdin(args []string) bool {
652695
653696func webCmd() *cobra.Command {
654697 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"}}),
656699 group("sessions", "your browser sessions",
657 pass("list", "list your browser sessions", passOpts{server: []string{"web", "sessions", "list"}}),
658 pass("revoke", "end a browser session: <id>|--all", passOpts{server: []string{"web", "sessions", "revoke"}}),
700 pass("list", passOpts{server: []string{"web", "sessions", "list"}}),
701 pass("revoke", passOpts{server: []string{"web", "sessions", "revoke"}}),
659702 ),
660703 group("theme", "the colour scheme the web UI uses for you",
661 pass("show", "show your colour scheme", passOpts{server: []string{"web", "theme", "show"}}),
662 pass("set", "follow the browser, or force one: system|light|dark", passOpts{server: []string{"web", "theme", "set"}}),
704 pass("show", passOpts{server: []string{"web", "theme", "show"}}),
705 pass("set", passOpts{server: []string{"web", "theme", "set"}}),
663706 ),
664707 )
665708}
666709
667710func webhookCmd() *cobra.Command {
668711 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}),
670 pass("list", "list webhooks", passOpts{server: []string{"webhook", "list"}, needsRepo: true}),
671 pass("remove", "remove a webhook: <id>", passOpts{server: []string{"webhook", "remove"}, needsRepo: true}),
672 pass("deliveries", "recent deliveries [--limit n]", passOpts{server: []string{"webhook", "deliveries"}, needsRepo: true}),
673 pass("redeliver", "requeue a delivery: <delivery-id>", passOpts{server: []string{"webhook", "redeliver"}, needsRepo: true}),
712 pass("add", passOpts{server: []string{"webhook", "add"}, needsRepo: true}),
713 pass("list", passOpts{server: []string{"webhook", "list"}, needsRepo: true}),
714 pass("remove", passOpts{server: []string{"webhook", "remove"}, needsRepo: true}),
715 pass("deliveries", passOpts{server: []string{"webhook", "deliveries"}, needsRepo: true}),
716 pass("redeliver", passOpts{server: []string{"webhook", "redeliver"}, needsRepo: true}),
674717 )
675718}
676719
677720func orgCmd() *cobra.Command {
678721 return group("org", "organizations",
679 pass("create", "create an organization", passOpts{server: []string{"org", "create"}}),
680 pass("list", "list organizations you belong to", passOpts{server: []string{"org", "list"}}),
681 pass("show", "show an organization and its members", passOpts{server: []string{"org", "show"}}),
682 pass("rename", "rename an organization: <old> <new>", passOpts{server: []string{"org", "rename"}}),
683 pass("delete", "delete an empty organization (--yes)", passOpts{server: []string{"org", "delete"}}),
684 pass("profile", "show or set an org profile: <org> [--description d] [--website url] [--link label|url]...",
685 passOpts{server: []string{"org", "profile"}}),
722 pass("create", passOpts{server: []string{"org", "create"}}),
723 pass("list", passOpts{server: []string{"org", "list"}}),
724 pass("show", passOpts{server: []string{"org", "show"}}),
725 pass("rename", passOpts{server: []string{"org", "rename"}}),
726 pass("delete", passOpts{server: []string{"org", "delete"}}),
727 pass("profile", passOpts{server: []string{"org", "profile"}}),
686728 group("members", "manage members",
687 pass("add", "add or update a member: <org> <user> [--role member|admin]", passOpts{server: []string{"org", "members", "add"}}),
688 pass("remove", "remove a member: <org> <user>", passOpts{server: []string{"org", "members", "remove"}}),
689 pass("list", "list members: <org>", passOpts{server: []string{"org", "members", "list"}}),
729 pass("add", passOpts{server: []string{"org", "members", "add"}}),
730 pass("remove", passOpts{server: []string{"org", "members", "remove"}}),
731 pass("list", passOpts{server: []string{"org", "members", "list"}}),
690732 ),
691733 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"}}),
693 pass("list", "list org labels with use across readable repositories: <org>", passOpts{server: []string{"org", "label", "list"}}),
694 pass("remove", "remove an org label everywhere: <org> <label>", passOpts{server: []string{"org", "label", "remove"}}),
734 pass("set", passOpts{server: []string{"org", "label", "set"}}),
735 pass("list", passOpts{server: []string{"org", "label", "list"}}),
736 pass("remove", passOpts{server: []string{"org", "label", "remove"}}),
695737 ),
696738 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"}}),
698 pass("list", "list org milestones with progress: <org> [--state open|closed|all]", passOpts{server: []string{"org", "milestone", "list"}}),
699 pass("close", "close an org milestone: <org> <title>", passOpts{server: []string{"org", "milestone", "close"}}),
700 pass("reopen", "reopen an org milestone: <org> <title>", passOpts{server: []string{"org", "milestone", "reopen"}}),
739 pass("create", passOpts{server: []string{"org", "milestone", "create"}}),
740 pass("list", passOpts{server: []string{"org", "milestone", "list"}}),
741 pass("close", passOpts{server: []string{"org", "milestone", "close"}}),
742 pass("reopen", passOpts{server: []string{"org", "milestone", "reopen"}}),
701743 ),
702744 group("team", "scope repository access with teams",
703 pass("create", "create a team: <org> <team>", passOpts{server: []string{"org", "team", "create"}}),
704 pass("delete", "delete a team: <org> <team>", passOpts{server: []string{"org", "team", "delete"}}),
705 pass("list", "list teams: <org>", passOpts{server: []string{"org", "team", "list"}}),
706 pass("show", "show members and grants: <org> <team>", passOpts{server: []string{"org", "team", "show"}}),
707 pass("add", "add org members: <org> <team> <user>...", passOpts{server: []string{"org", "team", "add"}}),
708 pass("remove", "remove members: <org> <team> <user>...", 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"}}),
710 pass("revoke", "revoke a repo grant: <org> <team> <owner/name>", passOpts{server: []string{"org", "team", "revoke"}}),
745 pass("create", passOpts{server: []string{"org", "team", "create"}}),
746 pass("delete", passOpts{server: []string{"org", "team", "delete"}}),
747 pass("list", passOpts{server: []string{"org", "team", "list"}}),
748 pass("show", passOpts{server: []string{"org", "team", "show"}}),
749 pass("add", passOpts{server: []string{"org", "team", "add"}}),
750 pass("remove", passOpts{server: []string{"org", "team", "remove"}}),
751 pass("grant", passOpts{server: []string{"org", "team", "grant"}}),
752 pass("revoke", passOpts{server: []string{"org", "team", "revoke"}}),
711753 ),
712754 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"}}),
714756 ),
715757 )
716758}
@@ -724,7 +766,7 @@ func remoteCmd() *cobra.Command {
724766}
725767
726768func 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)
728770}
729771
730772// manCmd generates man pages; a CLI-first tool without man pages is not
@@ -759,7 +801,8 @@ func helpCmd(root *cobra.Command) *cobra.Command {
759801 DisableFlagParsing: true,
760802 RunE: func(cmd *cobra.Command, args []string) error {
761803 if len(args) == 0 {
762 return root.Help()
804 rootHelp(root)
805 return nil
763806 }
764807 t, err := resolveTarget()
765808 if err != nil {
cmd/gitbay/migrate.go +2 −2
@@ -18,8 +18,8 @@ func migrateCmd() *cobra.Command {
1818 var from string
1919 var fromPort int
2020 cmd := &cobra.Command{
21 Use: "migrate",
22 Short: "move your account here from another gitbay instance: gitbay migrate --from <host>",
21 Use: "migrate --from <host>",
22 Short: "move your account here from another gitbay instance",
2323 Long: `Migrate authenticates to the source instance with your own SSH key,
2424exports your account bundle (profile, emails, repos with settings,
2525issues, 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) {
146146 mustGit(t, dir, cliGitEnv, "add", ".")
147147 mustGit(t, dir, cliGitEnv, "commit", "-q", "-m", "feature work")
148148 mustGit(t, dir, cliGitEnv, "push", "-q", "origin", "feature")
149 // A noun's --help is the server's reference for that noun, flags
150 // included, not cobra's flagless subcommand list (#130).
151 if out := c.must(t, dir, "", "issue", "--help"); !strings.Contains(out, "issue create <owner/name>") {
149 // A noun's --help is the server's reference for that noun, not
150 // cobra's flagless subcommand list (#130); a verb's --help carries
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.") {
152153 t.Fatalf("issue --help is not the server's reference:\n%s", out)
153154 }
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 }
154158
155159 // Inside the clone on the branch, --source is the checked-out branch
156160 // and --target the default branch; neither needs typing (#101).
internal/control/admin.go +57 −27
@@ -16,59 +16,89 @@ import (
1616
1717func init() {
1818 register(Command{Path: []string{"admin", "user", "list"},
19 Summary: "list accounts (instance admins)",
20 Usage: "admin user list [--state active|pending|disabled|admin] [--limit <n>] [--cursor <c>]",
19 Summary: "list accounts (instance admins)",
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"},
2127 ReadOnly: true, Run: runAdminUserList})
2228 register(Command{Path: []string{"admin", "user", "show"},
2329 Summary: "show an account: keys, emails, orgs, tokens, sessions (instance admins)",
2430 Usage: "admin user show <username>",
31 Examples: []string{"admin user show alice"},
2532 ReadOnly: true, Run: runAdminUserShow})
2633 register(Command{Path: []string{"admin", "user", "promote"},
27 Summary: "make an account an instance admin",
28 Usage: "admin user promote <username>",
29 Run: runAdminUserPromote})
34 Summary: "make an account an instance admin",
35 Usage: "admin user promote <username>",
36 Examples: []string{"admin user promote alice"},
37 Run: runAdminUserPromote})
3038 register(Command{Path: []string{"admin", "user", "demote"},
31 Summary: "remove instance admin from an account (never the last one)",
32 Usage: "admin user demote <username>",
33 Run: runAdminUserDemote})
39 Summary: "remove instance admin from an account (never the last one)",
40 Usage: "admin user demote <username>",
41 Examples: []string{"admin user demote alice"},
42 Run: runAdminUserDemote})
3443 register(Command{Path: []string{"admin", "runners"},
3544 Summary: "the build queue and runner accounts: last poll, scope, the build each holds (instance admins)",
3645 Usage: "admin runners",
46 Examples: []string{"admin runners"},
3747 ReadOnly: true, Run: runAdminRunners})
3848 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)",
40 Usage: "admin runners remove <fingerprint>",
41 Run: runAdminRunnersForget})
49 Summary: "drop a key's runner heartbeat row, e.g. one that polled once by mistake (instance admins)",
50 Usage: "admin runners remove <fingerprint>",
51 Examples: []string{"admin runners remove SHA256:abcd1234"},
52 Run: runAdminRunnersForget})
4253 // forget is the name this shipped under in v1.18; remove is the verb
4354 // every other noun uses. Both stay for one release.
4455 register(Command{Path: []string{"admin", "runners", "forget"},
45 Summary: "alias of admin runners remove",
46 Usage: "admin runners forget <fingerprint>",
47 Run: runAdminRunnersForget})
56 Summary: "alias of admin runners remove",
57 Usage: "admin runners forget <fingerprint>",
58 Examples: []string{"admin runners forget SHA256:abcd1234"},
59 Run: runAdminRunnersForget})
4860 register(Command{Path: []string{"admin", "repo", "list"},
49 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>]",
61 Summary: "list every repository with size and last push (instance admins)",
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"},
5170 ReadOnly: true, Run: runAdminRepoList})
5271 register(Command{Path: []string{"admin", "repo", "archive"},
53 Summary: "archive any repository (instance admins; audited)",
54 Usage: "admin repo archive <owner/name>",
55 Run: runAdminRepoArchive})
72 Summary: "archive any repository (instance admins; audited)",
73 Usage: "admin repo archive <owner/name>",
74 Examples: []string{"admin repo archive alice/old-project"},
75 Run: runAdminRepoArchive})
5676 register(Command{Path: []string{"admin", "repo", "unarchive"},
57 Summary: "unarchive any repository (instance admins; audited)",
58 Usage: "admin repo unarchive <owner/name>",
59 Run: runAdminRepoUnarchive})
77 Summary: "unarchive any repository (instance admins; audited)",
78 Usage: "admin repo unarchive <owner/name>",
79 Examples: []string{"admin repo unarchive alice/old-project"},
80 Run: runAdminRepoUnarchive})
6081 register(Command{Path: []string{"admin", "repo", "visibility"},
61 Summary: "set any repository's visibility (instance admins; audited)",
62 Usage: "admin repo visibility <owner/name> public|private",
63 Run: runAdminRepoVisibility})
82 Summary: "set any repository's visibility (instance admins; audited)",
83 Usage: "admin repo visibility <owner/name> public|private",
84 Examples: []string{"admin repo visibility alice/secret private"},
85 Run: runAdminRepoVisibility})
6486 register(Command{Path: []string{"admin", "repo", "delete"},
6587 Summary: "delete any repository (instance admins; audited)",
6688 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})
6894 register(Command{Path: []string{"admin", "mr", "prune"},
6995 Summary: "drop merged or closed MRs' head refs and the objects only they kept, e.g. after a history rewrite (instance admins; audited)",
7096 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})
72102}
73103
74104// requireInstanceAdmin gates the admin noun. -1 means proceed.
internal/control/adminhost.go +32 −13
@@ -22,32 +22,51 @@ import (
2222
2323func init() {
2424 register(Command{Path: []string{"admin", "user", "create"},
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",
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",
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"},
2734 ReadsStdin: true, Run: runAdminUserCreate})
2835 register(Command{Path: []string{"admin", "user", "disable"},
29 Summary: "suspend an account: SSH, web sessions and API tokens refused until re-enabled",
30 Usage: "admin user disable <username>",
31 Run: runAdminUserDisable})
36 Summary: "suspend an account: SSH, web sessions and API tokens refused until re-enabled",
37 Usage: "admin user disable <username>",
38 Examples: []string{"admin user disable alice"},
39 Run: runAdminUserDisable})
3240 register(Command{Path: []string{"admin", "user", "enable"},
33 Summary: "restore a suspended account",
34 Usage: "admin user enable <username>",
35 Run: runAdminUserEnable})
41 Summary: "restore a suspended account",
42 Usage: "admin user enable <username>",
43 Examples: []string{"admin user enable alice"},
44 Run: runAdminUserEnable})
3645 register(Command{Path: []string{"admin", "user", "delete"},
3746 Summary: "delete an account that anchors nothing (keys, emails and sessions go with it)",
3847 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})
4053 register(Command{Path: []string{"admin", "email", "verify"},
41 Summary: "mark an address verified by admin assertion",
42 Usage: "admin email verify <username> <address>",
43 Run: runAdminEmailVerify})
54 Summary: "mark an address verified by admin assertion",
55 Usage: "admin email verify <username> <address>",
56 Examples: []string{"admin email verify alice alice@example.org"},
57 Run: runAdminEmailVerify})
4458 register(Command{Path: []string{"admin", "invite"},
4559 Summary: "issue a registration invite and mail its code",
4660 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})
4866 register(Command{Path: []string{"admin", "stats"},
4967 Summary: "instance statistics: counts and per-repository disk usage",
5068 Usage: "admin stats",
69 Examples: []string{"admin stats"},
5170 ReadOnly: true, Run: runAdminStats})
5271}
5372
internal/control/audit.go +9 −2
@@ -12,8 +12,15 @@ import (
1212
1313func init() {
1414 register(Command{Path: []string{"audit"},
15 Summary: "instance audit log (admins)",
16 Usage: "audit [--actor <user>|-] [--action <prefix>] [--since <duration|date>] [--limit <n>]",
15 Summary: "instance audit log (admins)",
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"},
1724 ReadOnly: true, Run: runAudit})
1825}
1926
internal/control/build.go +55 −19
@@ -22,37 +22,64 @@ import (
2222func init() {
2323 register(Command{Path: []string{"build", "list"},
2424 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})
2635 register(Command{Path: []string{"build", "show"},
27 Summary: "show one build",
28 Usage: "build show <owner/name> <n>", ReadOnly: true, Run: runBuildShow})
36 Summary: "show one build",
37 Usage: "build show <owner/name> <n>",
38 Examples: []string{"build show krz/gitbay 431"},
39 ReadOnly: true, Run: runBuildShow})
2940 register(Command{Path: []string{"build", "log"},
3041 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})
3248
3349 register(Command{Path: []string{"build", "jobs"},
34 Summary: "list the jobs a trigger can name",
35 Usage: "build jobs <owner/name>", ReadOnly: true, Run: runBuildJobs})
50 Summary: "list the jobs a trigger can name",
51 Usage: "build jobs <owner/name>",
52 Examples: []string{"build jobs krz/gitbay"},
53 ReadOnly: true, Run: runBuildJobs})
3654
3755 register(Command{Path: []string{"build", "cancel"},
38 Summary: "withdraw a queued build before a runner claims it",
39 Usage: "build cancel <owner/name> <n>", Run: runBuildCancel})
56 Summary: "withdraw a queued build before a runner claims it",
57 Usage: "build cancel <owner/name> <n>",
58 Examples: []string{"build cancel krz/gitbay 431"},
59 Run: runBuildCancel})
4060 register(Command{Path: []string{"build", "trigger"},
41 Summary: "queue a job now (scheduled or not)",
42 Usage: "build trigger <owner/name> <job>", Run: runBuildTrigger})
61 Summary: "queue a job now (scheduled or not)",
62 Usage: "build trigger <owner/name> <job>",
63 Examples: []string{"build trigger krz/gitbay vuln"},
64 Run: runBuildTrigger})
4365 // Secrets: set over stdin, listed by name only, injected into the
4466 // repo's builds as environment variables. Same discipline as mirror
4567 // tokens — the value never appears in argv, logs, or output.
4668 register(Command{Path: []string{"repo", "secret", "set"},
4769 Summary: "set a build secret",
4870 Usage: "repo secret set <owner/name> <NAME> (value on stdin)",
71 Examples: []string{"repo secret set krz/gitbay DEPLOY_TOKEN"},
4972 ReadsStdin: true, Run: runSecretSet})
5073 register(Command{Path: []string{"repo", "secret", "remove"},
51 Summary: "remove a build secret",
52 Usage: "repo secret remove <owner/name> <NAME>", Run: runSecretRemove})
74 Summary: "remove a build secret",
75 Usage: "repo secret remove <owner/name> <NAME>",
76 Examples: []string{"repo secret remove krz/gitbay DEPLOY_TOKEN"},
77 Run: runSecretRemove})
5378 register(Command{Path: []string{"repo", "secret", "list"},
54 Summary: "list build secret names",
55 Usage: "repo secret list <owner/name>", ReadOnly: true, Run: runSecretList})
79 Summary: "list build secret names",
80 Usage: "repo secret list <owner/name>",
81 Examples: []string{"repo secret list krz/gitbay"},
82 ReadOnly: true, Run: runSecretList})
5683
5784 // Runner commands: the claim/report loop for gitbay-runner. A runner
5885 // executes arbitrary repo code, so handing out jobs is the instance
@@ -61,13 +88,22 @@ func init() {
6188 // an admin key, which a runner host should not hold (#92).
6289 register(Command{Path: []string{"runner", "next"},
6390 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})
6597 register(Command{Path: []string{"runner", "log"},
66 Summary: "append a build's log from stdin",
67 Usage: "runner log <build-id>", ReadsStdin: true, Run: runRunnerLog})
98 Summary: "append a build's log from stdin",
99 Usage: "runner log <build-id>",
100 Examples: []string{"runner log 431"},
101 ReadsStdin: true, Run: runRunnerLog})
68102 register(Command{Path: []string{"runner", "done"},
69 Summary: "finish a build",
70 Usage: "runner done <build-id> success|failure", Run: runRunnerDone})
103 Summary: "finish a build",
104 Usage: "runner done <build-id> success|failure",
105 Examples: []string{"runner done 431 success"},
106 Run: runRunnerDone})
71107}
72108
73109type BuildOut struct {
internal/control/commitfile.go +8
@@ -17,6 +17,14 @@ func init() {
1717 Summary: "write a file and commit it",
1818 Usage: "repo commit-file <owner/name> <path> " +
1919 "--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 },
2028 ReadsStdin: true,
2129 Run: runCommitFile,
2230 })
internal/control/control.go +10 −51
@@ -68,6 +68,14 @@ func (c *Ctx) usageWith(msg string) int {
6868 return c.fail(protocol.ExitUsage, "%s\nusage: %s", msg, c.Cmd.Usage)
6969}
7070
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
7179type Command struct {
7280 Path []string // e.g. ["keys", "add"]
7381 // Summary is one line of prose: what the command does, no argument
@@ -75,6 +83,8 @@ type Command struct {
7583 // help renders them separately, so neither may carry the other's job.
7684 Summary string
7785 Usage string
86 Flags []Flag
87 Examples []string // full argv after the program, repository named
7888 ReadsStdin bool
7989 ReadOnly bool // safe for read-scoped API tokens
8090 Run func(c *Ctx, args []string) int
@@ -315,57 +325,6 @@ func (c *Ctx) fail(code int, format string, args ...any) int {
315325 return code
316326}
317327
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
369328func joinPath(p []string) string {
370329 out := ""
371330 for i, s := range p {
internal/control/control_test.go +8 −12
@@ -125,25 +125,21 @@ func TestEveryCommandDocumentsItsUsage(t *testing.T) {
125125 }
126126}
127127
128// TestHelpPrefixNarrowsAndShowsFlags covers the reason the command exists:
129// before this, reading one command's flags meant reading all of them.
130func TestHelpPrefixNarrowsAndShowsFlags(t *testing.T) {
128// TestHelpPrefixNarrowsToTheNoun covers the reason the command exists:
129// before this, reading one command's flags meant reading all of them. A
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) {
131133 var buf bytes.Buffer
132134 c := &Ctx{Stdout: &buf, Stderr: io.Discard}
133135 if code := runHelp(c, []string{"issue"}); code != protocol.ExitOK {
134136 t.Fatalf("help issue exited %d", code)
135137 }
136138 out := buf.String()
137 for _, line := range strings.Split(strings.TrimSpace(out), "\n") {
138 if strings.HasPrefix(line, " ") {
139 continue // the indented usage line
139 for _, want := range []string{"READ\n", " list", "WRITE\n", " create", "issue <verb> --help for flags.\n"} {
140 if !strings.Contains(out, want) {
141 t.Errorf("missing %q in:\n%s", want, out)
140142 }
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")
147143 }
148144}
149145
internal/control/dashboard.go +8 −2
@@ -18,10 +18,16 @@ func init() {
1818 register(Command{Path: []string{"dashboard"},
1919 Summary: "one read for the account dashboard: review queue, assigned and open work, pins, activity, builds",
2020 Usage: "dashboard",
21 Examples: []string{"dashboard"},
2122 ReadOnly: true, Run: runDashboard})
2223 register(Command{Path: []string{"feed"},
23 Summary: "activity on repositories you can reach",
24 Usage: "feed [--limit <n>] [--cursor <c>]",
24 Summary: "activity on repositories you can reach",
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"},
2531 ReadOnly: true, Run: runFeed})
2632}
2733
internal/control/deploykey.go +14 −6
@@ -14,15 +14,23 @@ import (
1414
1515func init() {
1616 register(Command{Path: []string{"repo", "deploy-key", "add"},
17 Summary: "bind a read-only (or --rw) key to one repository",
18 Usage: "repo deploy-key add <owner/name> [--rw] < key.pub",
17 Summary: "bind a read-only (or --rw) key to one repository",
18 Usage: "repo deploy-key add <owner/name> [--rw] < key.pub",
19 Flags: []Flag{
20 {"--rw", "", "the key may push, not just fetch", ""},
21 },
22 Examples: []string{"repo deploy-key add krz/gitbay < key.pub"},
1923 ReadsStdin: true, Run: runDeployKeyAdd})
2024 register(Command{Path: []string{"repo", "deploy-key", "list"},
21 Summary: "list deploy keys",
22 Usage: "repo deploy-key list <owner/name>", ReadOnly: true, Run: runDeployKeyList})
25 Summary: "list deploy keys",
26 Usage: "repo deploy-key list <owner/name>",
27 Examples: []string{"repo deploy-key list krz/gitbay"},
28 ReadOnly: true, Run: runDeployKeyList})
2329 register(Command{Path: []string{"repo", "deploy-key", "remove"},
24 Summary: "remove a deploy key",
25 Usage: "repo deploy-key remove <owner/name> <fingerprint>", Run: runDeployKeyRemove})
30 Summary: "remove a deploy key",
31 Usage: "repo deploy-key remove <owner/name> <fingerprint>",
32 Examples: []string{"repo deploy-key remove krz/gitbay SHA256:abcd1234"},
33 Run: runDeployKeyRemove})
2634}
2735
2836func runDeployKeyAdd(c *Ctx, args []string) int {
internal/control/deps.go +12 −6
@@ -15,14 +15,20 @@ func init() {
1515 // public registry what the repository depends on, which is the owner's
1616 // disclosure to make, not the instance's.
1717 register(Command{Path: []string{"repo", "deps", "enable"},
18 Summary: "check dependencies for updates",
19 Usage: "repo deps enable <owner/name>", Run: runDepsEnable})
18 Summary: "check dependencies for updates",
19 Usage: "repo deps enable <owner/name>",
20 Examples: []string{"repo deps enable krz/gitbay"},
21 Run: runDepsEnable})
2022 register(Command{Path: []string{"repo", "deps", "disable"},
21 Summary: "stop checking dependencies",
22 Usage: "repo deps disable <owner/name>", Run: runDepsDisable})
23 Summary: "stop checking dependencies",
24 Usage: "repo deps disable <owner/name>",
25 Examples: []string{"repo deps disable krz/gitbay"},
26 Run: runDepsDisable})
2327 register(Command{Path: []string{"repo", "deps", "status"},
24 Summary: "show dependency check state",
25 Usage: "repo deps status <owner/name>", ReadOnly: true, Run: runDepsStatus})
28 Summary: "show dependency check state",
29 Usage: "repo deps status <owner/name>",
30 Examples: []string{"repo deps status krz/gitbay"},
31 ReadOnly: true, Run: runDepsStatus})
2632}
2733
2834// DepsOut is what `repo deps status` emits: the check's state and what
internal/control/diffcomment.go +27 −8
@@ -16,18 +16,37 @@ import (
1616
1717func init() {
1818 register(Command{Path: []string{"mr", "diff-comment"},
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 -]",
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 -]",
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 },
2134 ReadsStdin: true, Run: runDiffComment})
2235 register(Command{Path: []string{"mr", "threads"},
23 Summary: "review threads on an MR",
24 Usage: "mr threads <owner/name> <n>", ReadOnly: true, Run: runMRThreads})
36 Summary: "review threads on an MR",
37 Usage: "mr threads <owner/name> <n>",
38 Examples: []string{"mr threads krz/gitbay 431"},
39 ReadOnly: true, Run: runMRThreads})
2540 register(Command{Path: []string{"mr", "resolve"},
26 Summary: "resolve a review thread",
27 Usage: "mr resolve <owner/name> <n> <thread-id>", Run: runMRResolve})
41 Summary: "resolve a review thread",
42 Usage: "mr resolve <owner/name> <n> <thread-id>",
43 Examples: []string{"mr resolve krz/gitbay 431 12"},
44 Run: runMRResolve})
2845 register(Command{Path: []string{"mr", "unresolve"},
29 Summary: "reopen a review thread",
30 Usage: "mr unresolve <owner/name> <n> <thread-id>", Run: runMRUnresolve})
46 Summary: "reopen a review thread",
47 Usage: "mr unresolve <owner/name> <n> <thread-id>",
48 Examples: []string{"mr unresolve krz/gitbay 431 12"},
49 Run: runMRUnresolve})
3150}
3251
3352func runDiffComment(c *Ctx, args []string) int {
internal/control/explore.go +14 −5
@@ -11,9 +11,14 @@ import (
1111
1212func init() {
1313 register(Command{
14 Path: []string{"explore"},
15 Summary: "list public repositories",
16 Usage: "explore [--limit <n>] [--cursor <c>]",
14 Path: []string{"explore"},
15 Summary: "list public repositories",
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"},
1722 ReadOnly: true,
1823 Run: runExplore,
1924 })
@@ -21,8 +26,12 @@ func init() {
2126 Path: []string{"repo", "download"},
2227 // Not "repo archive": that name is taken by the read-only flag,
2328 // and renaming it would break every script that sets it.
24 Summary: "write a tar.gz of a ref to stdout",
25 Usage: "repo download <owner/name> [--ref <r>] > repo.tar.gz",
29 Summary: "write a tar.gz of a ref to stdout",
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"},
2635 ReadOnly: true,
2736 Run: runRepoDownload,
2837 })
internal/control/ghimport.go +8 −2
@@ -23,8 +23,14 @@ import (
2323
2424func init() {
2525 register(Command{Path: []string{"repo", "import-issues"},
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>]",
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>]",
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"},
2834 ReadsStdin: true, Run: runImportIssues})
2935}
3036
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() {
1818 Path: []string{"whoami"},
1919 Summary: "show the authenticated account",
2020 Usage: "whoami",
21 Examples: []string{"whoami"},
2122 ReadOnly: true,
2223 Run: runWhoami,
2324 })
@@ -25,27 +26,35 @@ func init() {
2526 Path: []string{"keys", "list"},
2627 Summary: "list registered SSH keys",
2728 Usage: "keys list",
29 Examples: []string{"keys list"},
2830 ReadOnly: true,
2931 Run: runKeysList,
3032 })
3133 register(Command{
32 Path: []string{"keys", "add"},
33 Summary: "register an SSH public key (authorized_keys format)",
34 Usage: "keys add [--scope full|git|runner] [--label <text>] < key.pub",
34 Path: []string{"keys", "add"},
35 Summary: "register an SSH public key (authorized_keys format)",
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"},
3542 ReadsStdin: true,
3643 Run: runKeysAdd,
3744 })
3845 register(Command{
39 Path: []string{"keys", "label"},
40 Summary: "name a key; an empty label clears it",
41 Usage: "keys label <fingerprint> [<text>]",
42 Run: runKeysLabel,
46 Path: []string{"keys", "label"},
47 Summary: "name a key; an empty label clears it",
48 Usage: "keys label <fingerprint> [<text>]",
49 Examples: []string{`keys label SHA256:abcd1234 "work laptop"`},
50 Run: runKeysLabel,
4351 })
4452 register(Command{
45 Path: []string{"keys", "remove"},
46 Summary: "remove an SSH key by fingerprint",
47 Usage: "keys remove <fingerprint>",
48 Run: runKeysRemove,
53 Path: []string{"keys", "remove"},
54 Summary: "remove an SSH key by fingerprint",
55 Usage: "keys remove <fingerprint>",
56 Examples: []string{"keys remove SHA256:abcd1234"},
57 Run: runKeysRemove,
4958 })
5059}
5160
internal/control/import.go +8 −2
@@ -17,8 +17,14 @@ import (
1717
1818func init() {
1919 register(Command{Path: []string{"repo", "import"},
20 Summary: "server-side mirror of a foreign repository",
21 Usage: "repo import <owner/name> --from <url> [--private] [--token-stdin]",
20 Summary: "server-side mirror of a foreign repository",
21 Usage: "repo import <owner/name> --from <url> [--private] [--token-stdin]",
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"},
2228 ReadsStdin: true, Run: runRepoImport})
2329}
2430
internal/control/issue.go +77 −15
@@ -16,35 +16,97 @@ const maxBodyBytes = 64 << 10
1616
1717func init() {
1818 register(Command{Path: []string{"issue", "create"},
19 Summary: "open an issue",
20 Usage: "issue create <owner/name> --title <t> [--body <b> | --file -] [--format md|org]",
19 Summary: "open an issue",
20 Usage: "issue create <owner/name> --title <t> [--body <b> | --file -] [--format md|org]",
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 },
2131 ReadsStdin: true, Run: runIssueCreate})
2232 register(Command{Path: []string{"issue", "list"},
2333 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})
2550 register(Command{Path: []string{"issue", "show"},
26 Summary: "show an issue with comments",
27 Usage: "issue show <owner/name> <n>", ReadOnly: true, Run: runIssueShow})
51 Summary: "show an issue with comments",
52 Usage: "issue show <owner/name> <n>",
53 Examples: []string{"issue show krz/gitbay 42"},
54 ReadOnly: true, Run: runIssueShow})
2855 register(Command{Path: []string{"issue", "edit"},
29 Summary: "edit title or body",
30 Usage: "issue edit <owner/name> <n> [--title <t>] [--body <b> | --file -] [--format md|org]",
56 Summary: "edit title or body",
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 },
3168 ReadsStdin: true, Run: runIssueEdit})
3269 register(Command{Path: []string{"issue", "comment"},
33 Summary: "comment",
34 Usage: "issue comment <owner/name> <n> [--message <m> | --file -] [--format md|org]",
70 Summary: "comment",
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 },
3581 ReadsStdin: true, Run: runIssueComment})
3682 register(Command{Path: []string{"issue", "close"},
37 Summary: "close an issue",
38 Usage: "issue close <owner/name> <n>", Run: runIssueClose})
83 Summary: "close an issue",
84 Usage: "issue close <owner/name> <n>",
85 Examples: []string{"issue close krz/gitbay 42"},
86 Run: runIssueClose})
3987 register(Command{Path: []string{"issue", "reopen"},
40 Summary: "reopen an issue",
41 Usage: "issue reopen <owner/name> <n>", Run: runIssueReopen})
88 Summary: "reopen an issue",
89 Usage: "issue reopen <owner/name> <n>",
90 Examples: []string{"issue reopen krz/gitbay 42"},
91 Run: runIssueReopen})
4292 register(Command{Path: []string{"issue", "label"},
4393 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})
45101 register(Command{Path: []string{"issue", "assign"},
46102 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})
48110}
49111
50112// issueArgs parses "<owner/name> <n>" plus flags handled by the caller.
internal/control/label.go +14 −5
@@ -14,14 +14,23 @@ import (
1414
1515func init() {
1616 register(Command{Path: []string{"label", "list"},
17 Summary: "list a repository's labels with colour and use",
18 Usage: "label list <owner/name>", ReadOnly: true, Run: runLabelList})
17 Summary: "list a repository's labels with colour and use",
18 Usage: "label list <owner/name>",
19 Examples: []string{"label list krz/gitbay"},
20 ReadOnly: true, Run: runLabelList})
1921 register(Command{Path: []string{"label", "set"},
2022 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})
2229 register(Command{Path: []string{"label", "remove"},
23 Summary: "remove a label from the repository and from every issue and merge request",
24 Usage: "label remove <owner/name> <label>", Run: runLabelRemove})
30 Summary: "remove a label from the repository and from every issue and merge request",
31 Usage: "label remove <owner/name> <label>",
32 Examples: []string{"label remove krz/gitbay wontfix"},
33 Run: runLabelRemove})
2534}
2635
2736// 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() {
1616 register(Command{Path: []string{"account", "export"},
1717 Summary: "write your account bundle (profile, repos, issues, MRs) as JSON",
1818 Usage: "account export > bundle.json",
19 Examples: []string{"account export > bundle.json"},
1920 ReadOnly: true, Run: runAccountExport})
2021 register(Command{Path: []string{"account", "import-bundle"},
21 Summary: "replay an account bundle (see gitbay migrate)",
22 Usage: "account import-bundle [--source <host>] < bundle.json",
22 Summary: "replay an account bundle (see gitbay migrate)",
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"},
2328 ReadsStdin: true, Run: runAccountImportBundle})
2429}
2530
internal/control/milestone.go +33 −12
@@ -17,25 +17,46 @@ import (
1717func init() {
1818 register(Command{Path: []string{"milestone", "create"},
1919 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})
2127 register(Command{Path: []string{"milestone", "list"},
2228 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})
2435 register(Command{Path: []string{"milestone", "close"},
25 Summary: "close a milestone",
26 Usage: "milestone close <owner/name> <title>", Run: runMilestoneClose})
36 Summary: "close a milestone",
37 Usage: "milestone close <owner/name> <title>",
38 Examples: []string{"milestone close krz/gitbay v1.30.0"},
39 Run: runMilestoneClose})
2740 register(Command{Path: []string{"milestone", "reopen"},
28 Summary: "reopen a milestone",
29 Usage: "milestone reopen <owner/name> <title>", Run: runMilestoneReopen})
41 Summary: "reopen a milestone",
42 Usage: "milestone reopen <owner/name> <title>",
43 Examples: []string{"milestone reopen krz/gitbay v1.30.0"},
44 Run: runMilestoneReopen})
3045 register(Command{Path: []string{"issue", "milestone"},
31 Summary: "set or clear an issue's milestone",
32 Usage: "issue milestone <owner/name> <n> <title|none>", Run: runIssueMilestone})
46 Summary: "set or clear an issue's milestone",
47 Usage: "issue milestone <owner/name> <n> <title|none>",
48 Examples: []string{"issue milestone krz/gitbay 42 v1.31.0"},
49 Run: runIssueMilestone})
3350 register(Command{Path: []string{"mr", "milestone"},
34 Summary: "set or clear an MR's milestone",
35 Usage: "mr milestone <owner/name> <n> <title|none>", Run: runMRMilestone})
51 Summary: "set or clear an MR's milestone",
52 Usage: "mr milestone <owner/name> <n> <title|none>",
53 Examples: []string{"mr milestone krz/gitbay 431 v1.31.0"},
54 Run: runMRMilestone})
3655 register(Command{Path: []string{"issue", "templates"},
37 Summary: "list issue templates (.gitbay/issue-template*.md)",
38 Usage: "issue templates <owner/name>", ReadOnly: true, Run: runIssueTemplates})
56 Summary: "list issue templates (.gitbay/issue-template*.md)",
57 Usage: "issue templates <owner/name>",
58 Examples: []string{"issue templates krz/gitbay"},
59 ReadOnly: true, Run: runIssueTemplates})
3960}
4061
4162var duePat = regexp.MustCompile(`^\d{4}-\d{2}-\d{2}$`)
internal/control/mirrorcmd.go +20 −8
@@ -16,18 +16,30 @@ import (
1616
1717func init() {
1818 register(Command{Path: []string{"repo", "mirror", "add"},
19 Summary: "mirror to or from a remote",
20 Usage: "repo mirror add <owner/name> <https-url> --direction push|pull [--username <u>] [--token-stdin]",
19 Summary: "mirror to or from a remote",
20 Usage: "repo mirror add <owner/name> <https-url> --direction push|pull [--username <u>] [--token-stdin]",
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"},
2127 ReadsStdin: true, Run: runMirrorAdd})
2228 register(Command{Path: []string{"repo", "mirror", "list"},
23 Summary: "list mirrors with sync status",
24 Usage: "repo mirror list <owner/name>", ReadOnly: true, Run: runMirrorList})
29 Summary: "list mirrors with sync status",
30 Usage: "repo mirror list <owner/name>",
31 Examples: []string{"repo mirror list krz/gitbay"},
32 ReadOnly: true, Run: runMirrorList})
2533 register(Command{Path: []string{"repo", "mirror", "remove"},
26 Summary: "remove a mirror",
27 Usage: "repo mirror remove <owner/name> <id>", Run: runMirrorRemove})
34 Summary: "remove a mirror",
35 Usage: "repo mirror remove <owner/name> <id>",
36 Examples: []string{"repo mirror remove krz/gitbay 3"},
37 Run: runMirrorRemove})
2838 register(Command{Path: []string{"repo", "mirror", "sync"},
29 Summary: "schedule an immediate sync",
30 Usage: "repo mirror sync <owner/name>", Run: runMirrorSync})
39 Summary: "schedule an immediate sync",
40 Usage: "repo mirror sync <owner/name>",
41 Examples: []string{"repo mirror sync krz/gitbay"},
42 Run: runMirrorSync})
3143}
3244
3345func runMirrorAdd(c *Ctx, args []string) int {
internal/control/mr.go +142 −37
@@ -20,78 +20,183 @@ import (
2020func init() {
2121 register(Command{Path: []string{"repo", "fork"},
2222 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})
2430 register(Command{Path: []string{"repo", "settings", "require-approvals"},
25 Summary: "require N fresh approvals to merge",
26 Usage: "repo settings require-approvals <owner/name> <n> (0 = off)", Run: runRequireApprovals})
31 Summary: "require N fresh approvals to merge",
32 Usage: "repo settings require-approvals <owner/name> <n> (0 = off)",
33 Examples: []string{"repo settings require-approvals krz/gitbay 1"},
34 Run: runRequireApprovals})
2735 register(Command{Path: []string{"repo", "settings", "require-resolved"},
28 Summary: "require all review threads resolved to merge",
29 Usage: "repo settings require-resolved <owner/name> on|off", Run: runRequireResolved})
36 Summary: "require all review threads resolved to merge",
37 Usage: "repo settings require-resolved <owner/name> on|off",
38 Examples: []string{"repo settings require-resolved krz/gitbay on"},
39 Run: runRequireResolved})
3040 register(Command{Path: []string{"repo", "settings", "require-codeowners"},
31 Summary: "require an owner's approval for every file CODEOWNERS covers",
32 Usage: "repo settings require-codeowners <owner/name> on|off", Run: runRequireCodeowners})
41 Summary: "require an owner's approval for every file CODEOWNERS covers",
42 Usage: "repo settings require-codeowners <owner/name> on|off",
43 Examples: []string{"repo settings require-codeowners krz/gitbay on"},
44 Run: runRequireCodeowners})
3345 register(Command{Path: []string{"repo", "settings", "require-checks"},
34 Summary: "gate merges on green statuses",
35 Usage: "repo settings require-checks <owner/name> on|off", Run: runRequireChecks})
46 Summary: "gate merges on green statuses",
47 Usage: "repo settings require-checks <owner/name> on|off",
48 Examples: []string{"repo settings require-checks krz/gitbay on"},
49 Run: runRequireChecks})
3650 register(Command{Path: []string{"repo", "settings", "require-mr"},
37 Summary: "protected branches take changes through merge requests only",
38 Usage: "repo settings require-mr <owner/name> on|off", Run: runRequireMR})
51 Summary: "protected branches take changes through merge requests only",
52 Usage: "repo settings require-mr <owner/name> on|off",
53 Examples: []string{"repo settings require-mr krz/gitbay on"},
54 Run: runRequireMR})
3955 register(Command{Path: []string{"repo", "settings", "require-signed"},
40 Summary: "require verified commit signatures",
41 Usage: "repo settings require-signed <owner/name> on|off", Run: runRequireSigned})
56 Summary: "require verified commit signatures",
57 Usage: "repo settings require-signed <owner/name> on|off",
58 Examples: []string{"repo settings require-signed krz/gitbay on"},
59 Run: runRequireSigned})
4260 register(Command{Path: []string{"mr", "create"},
43 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]",
61 Summary: "open a merge request",
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 },
4576 ReadsStdin: true, Run: runMRCreate})
4677 register(Command{Path: []string{"mr", "range-diff"},
47 Summary: "what changed between two revisions of a merge request",
48 Usage: "mr range-diff <owner/name> <n> [--from <sha>] [--to <sha>]",
78 Summary: "what changed between two revisions of a merge request",
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"},
4985 ReadOnly: true, Run: runMRRangeDiff})
5086 register(Command{Path: []string{"mr", "revisions"},
5187 Summary: "the heads a merge request has had",
5288 Usage: "mr revisions <owner/name> <n>",
89 Examples: []string{"mr revisions krz/gitbay 431"},
5390 ReadOnly: true, Run: runMRRevisions})
5491 register(Command{Path: []string{"mr", "draft"},
55 Summary: "mark a merge request as work in progress",
56 Usage: "mr draft <owner/name> <n>", Run: runMRDraft})
92 Summary: "mark a merge request as work in progress",
93 Usage: "mr draft <owner/name> <n>",
94 Examples: []string{"mr draft krz/gitbay 431"},
95 Run: runMRDraft})
5796 register(Command{Path: []string{"mr", "ready"},
58 Summary: "take the draft mark off, so it can merge",
59 Usage: "mr ready <owner/name> <n>", Run: runMRReady})
97 Summary: "take the draft mark off, so it can merge",
98 Usage: "mr ready <owner/name> <n>",
99 Examples: []string{"mr ready krz/gitbay 431"},
100 Run: runMRReady})
60101 register(Command{Path: []string{"mr", "list"},
61102 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})
63118 register(Command{Path: []string{"mr", "show"},
64 Summary: "show a merge request",
65 Usage: "mr show <owner/name> <n>", ReadOnly: true, Run: runMRShow})
119 Summary: "show a merge request",
120 Usage: "mr show <owner/name> <n>",
121 Examples: []string{"mr show krz/gitbay 431"},
122 ReadOnly: true, Run: runMRShow})
66123 register(Command{Path: []string{"mr", "diff"},
67 Summary: "show the diff",
68 Usage: "mr diff <owner/name> <n>", ReadOnly: true, Run: runMRDiff})
124 Summary: "show the diff",
125 Usage: "mr diff <owner/name> <n>",
126 Examples: []string{"mr diff krz/gitbay 431"},
127 ReadOnly: true, Run: runMRDiff})
69128 register(Command{Path: []string{"mr", "edit"},
70 Summary: "edit title or body",
71 Usage: "mr edit <owner/name> <n> [--title <t>] [--body <b> | --file -] [--format md|org] [--superseded-by <m>|none]",
129 Summary: "edit title or body",
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"`},
72139 ReadsStdin: true, Run: runMREdit})
73140 register(Command{Path: []string{"mr", "retarget"},
74 Summary: "retarget onto another branch",
75 Usage: "mr retarget <owner/name> <n> <branch>", Run: runMRRetarget})
141 Summary: "retarget onto another branch",
142 Usage: "mr retarget <owner/name> <n> <branch>",
143 Examples: []string{"mr retarget krz/gitbay 431 main"},
144 Run: runMRRetarget})
76145 register(Command{Path: []string{"mr", "comment"},
77 Summary: "comment",
78 Usage: "mr comment <owner/name> <n> [--message <m> | --file -] [--format md|org]",
146 Summary: "comment",
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"`},
79154 ReadsStdin: true, Run: runMRComment})
80155 register(Command{Path: []string{"mr", "review"},
81156 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})
83166 register(Command{Path: []string{"mr", "review", "request"},
84167 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})
86175 register(Command{Path: []string{"mr", "label"},
87176 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})
89184 register(Command{Path: []string{"mr", "merge"},
90185 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})
92192 register(Command{Path: []string{"mr", "close"},
93193 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})
95200}
96201
97202// ForkOut is what `repo fork` emits: where the fork landed, and what it
internal/control/notifications.go +48 −17
@@ -15,47 +15,78 @@ import (
1515
1616func init() {
1717 register(Command{Path: []string{"notifications", "list"},
18 Summary: "your notification inbox, newest first",
19 Usage: "notifications list [--all] [--limit <n>] [--cursor <c>]",
18 Summary: "your notification inbox, newest first",
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"},
2026 ReadOnly: true, Run: runNotificationsList})
2127 register(Command{Path: []string{"notifications", "read"},
2228 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})
2435 register(Command{Path: []string{"notifications", "settings", "show"},
2536 Summary: "your notification preferences",
2637 Usage: "notifications settings show",
38 Examples: []string{"notifications settings show"},
2739 ReadOnly: true, Run: runNotificationsSettingsShow})
2840 register(Command{Path: []string{"notifications", "settings", "mail"},
29 Summary: "activity by mail as well as the inbox (login links are unaffected)",
30 Usage: "notifications settings mail on|off", Run: runNotificationsSettingsMail})
41 Summary: "activity by mail as well as the inbox (login links are unaffected)",
42 Usage: "notifications settings mail on|off",
43 Examples: []string{"notifications settings mail on"},
44 Run: runNotificationsSettingsMail})
3145 register(Command{Path: []string{"notifications", "settings", "watch"},
32 Summary: "every issue and merge request on repositories you can write to",
33 Usage: "notifications settings watch on|off", Run: runNotificationsSettingsWatch})
46 Summary: "every issue and merge request on repositories you can write to",
47 Usage: "notifications settings watch on|off",
48 Examples: []string{"notifications settings watch on"},
49 Run: runNotificationsSettingsWatch})
3450 register(Command{Path: []string{"notifications", "device", "add"},
3551 Summary: "register an Apple device for push, token on stdin",
3652 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"},
3757 // Mandatory: without it control.go swaps in an empty reader and
3858 // this command stores an empty token without erroring.
3959 ReadsStdin: true, Run: runNotificationsDeviceAdd})
4060 register(Command{Path: []string{"notifications", "device", "list"},
4161 Summary: "your registered devices",
4262 Usage: "notifications device list",
63 Examples: []string{"notifications device list"},
4364 ReadOnly: true, Run: runNotificationsDeviceList})
4465 register(Command{Path: []string{"notifications", "device", "remove"},
45 Summary: "deregister a device",
46 Usage: "notifications device remove <id>", Run: runNotificationsDeviceRemove})
66 Summary: "deregister a device",
67 Usage: "notifications device remove <id>",
68 Examples: []string{"notifications device remove 4"},
69 Run: runNotificationsDeviceRemove})
4770 register(Command{Path: []string{"notifications", "settings", "push"},
48 Summary: "activity on your registered devices as well as the inbox",
49 Usage: "notifications settings push on|off", Run: runNotificationsSettingsPush})
71 Summary: "activity on your registered devices as well as the inbox",
72 Usage: "notifications settings push on|off",
73 Examples: []string{"notifications settings push on"},
74 Run: runNotificationsSettingsPush})
5075 register(Command{Path: []string{"repo", "watch"},
51 Summary: "hear about all activity on a repository",
52 Usage: "repo watch <owner/name>", Run: runRepoWatch})
76 Summary: "hear about all activity on a repository",
77 Usage: "repo watch <owner/name>",
78 Examples: []string{"repo watch krz/gitbay"},
79 Run: runRepoWatch})
5380 register(Command{Path: []string{"repo", "unwatch"},
54 Summary: "back to the default: only work you are part of",
55 Usage: "repo unwatch <owner/name>", Run: runRepoUnwatch})
81 Summary: "back to the default: only work you are part of",
82 Usage: "repo unwatch <owner/name>",
83 Examples: []string{"repo unwatch krz/gitbay"},
84 Run: runRepoUnwatch})
5685 register(Command{Path: []string{"repo", "mute"},
57 Summary: "mute a repository, including work you are part of",
58 Usage: "repo mute <owner/name>", Run: runRepoMute})
86 Summary: "mute a repository, including work you are part of",
87 Usage: "repo mute <owner/name>",
88 Examples: []string{"repo mute krz/gitbay"},
89 Run: runRepoMute})
5990}
6091
6192// notice is one thing that happened, in the shape both delivery routes
internal/control/org.go +28 −14
@@ -14,29 +14,43 @@ import (
1414
1515func init() {
1616 register(Command{Path: []string{"org", "create"},
17 Summary: "create an organization (you become its first admin)",
18 Usage: "org create <name>", Run: runOrgCreate})
17 Summary: "create an organization (you become its first admin)",
18 Usage: "org create <name>",
19 Examples: []string{"org create krz"}, Run: runOrgCreate})
1920 register(Command{Path: []string{"org", "list"},
20 Summary: "list organizations you belong to",
21 Usage: "org list", ReadOnly: true, Run: runOrgList})
21 Summary: "list organizations you belong to",
22 Usage: "org list",
23 Examples: []string{"org list"}, ReadOnly: true, Run: runOrgList})
2224 register(Command{Path: []string{"org", "show"},
23 Summary: "show an organization and its members",
24 Usage: "org show <name>", ReadOnly: true, Run: runOrgShow})
25 Summary: "show an organization and its members",
26 Usage: "org show <name>",
27 Examples: []string{"org show krz"}, ReadOnly: true, Run: runOrgShow})
2528 register(Command{Path: []string{"org", "rename"},
26 Summary: "rename an organization",
27 Usage: "org rename <old> <new> (clone URLs change)", Run: runOrgRename})
29 Summary: "rename an organization",
30 Usage: "org rename <old> <new> (clone URLs change)",
31 Examples: []string{"org rename krz krazywarez"}, Run: runOrgRename})
2832 register(Command{Path: []string{"org", "delete"},
2933 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})
3139 register(Command{Path: []string{"org", "members", "add"},
3240 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})
3446 register(Command{Path: []string{"org", "members", "remove"},
35 Summary: "remove a member",
36 Usage: "org members remove <org> <user>", Run: runOrgMembersRemove})
47 Summary: "remove a member",
48 Usage: "org members remove <org> <user>",
49 Examples: []string{"org members remove krz cmc"}, Run: runOrgMembersRemove})
3750 register(Command{Path: []string{"org", "members", "list"},
38 Summary: "list members",
39 Usage: "org members list <org>", ReadOnly: true, Run: runOrgMembersList})
51 Summary: "list members",
52 Usage: "org members list <org>",
53 Examples: []string{"org members list krz"}, ReadOnly: true, Run: runOrgMembersList})
4054}
4155
4256// 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 (
1313func init() {
1414 register(Command{Path: []string{"org", "label", "set"},
1515 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})
1722 register(Command{Path: []string{"org", "label", "list"},
18 Summary: "list an org's labels with use across the repositories you can read",
19 Usage: "org label list <org>", ReadOnly: true, Run: runOrgLabelList})
23 Summary: "list an org's labels with use across the repositories you can read",
24 Usage: "org label list <org>",
25 Examples: []string{"org label list krz"},
26 ReadOnly: true, Run: runOrgLabelList})
2027 register(Command{Path: []string{"org", "label", "remove"},
21 Summary: "remove an org label from the org and from every issue under it",
22 Usage: "org label remove <org> <label>", Run: runOrgLabelRemove})
28 Summary: "remove an org label from the org and from every issue under it",
29 Usage: "org label remove <org> <label>",
30 Examples: []string{"org label remove krz wontfix"},
31 Run: runOrgLabelRemove})
2332 register(Command{Path: []string{"org", "milestone", "create"},
2433 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})
2641 register(Command{Path: []string{"org", "milestone", "list"},
2742 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})
2949 register(Command{Path: []string{"org", "milestone", "close"},
30 Summary: "close an org milestone",
31 Usage: "org milestone close <org> <title>", Run: runOrgMilestoneClose})
50 Summary: "close an org milestone",
51 Usage: "org milestone close <org> <title>",
52 Examples: []string{"org milestone close krz mobile"},
53 Run: runOrgMilestoneClose})
3254 register(Command{Path: []string{"org", "milestone", "reopen"},
33 Summary: "reopen an org milestone",
34 Usage: "org milestone reopen <org> <title>", Run: runOrgMilestoneReopen})
55 Summary: "reopen an org milestone",
56 Usage: "org milestone reopen <org> <title>",
57 Examples: []string{"org milestone reopen krz mobile"},
58 Run: runOrgMilestoneReopen})
3559}
3660
3761// orgReader resolves an org for a read of its labels or milestones.
internal/control/pagescmd.go +16 −8
@@ -20,17 +20,25 @@ import (
2020
2121func init() {
2222 register(Command{Path: []string{"repo", "domain", "add"},
23 Summary: "claim a custom pages domain (verify with a DNS TXT record)",
24 Usage: "repo domain add <owner/name> <domain>", Run: runDomainAdd})
23 Summary: "claim a custom pages domain (verify with a DNS TXT record)",
24 Usage: "repo domain add <owner/name> <domain>",
25 Examples: []string{"repo domain add krz/gitbay gitbay.org"},
26 Run: runDomainAdd})
2527 register(Command{Path: []string{"repo", "domain", "verify"},
26 Summary: "check the DNS challenge and activate a claim",
27 Usage: "repo domain verify <owner/name> <domain>", Run: runDomainVerify})
28 Summary: "check the DNS challenge and activate a claim",
29 Usage: "repo domain verify <owner/name> <domain>",
30 Examples: []string{"repo domain verify krz/gitbay gitbay.org"},
31 Run: runDomainVerify})
2832 register(Command{Path: []string{"repo", "domain", "remove"},
29 Summary: "remove a custom pages domain",
30 Usage: "repo domain remove <owner/name> <domain>", Run: runDomainRemove})
33 Summary: "remove a custom pages domain",
34 Usage: "repo domain remove <owner/name> <domain>",
35 Examples: []string{"repo domain remove krz/gitbay gitbay.org"},
36 Run: runDomainRemove})
3137 register(Command{Path: []string{"repo", "domain", "list"},
32 Summary: "list custom pages domains",
33 Usage: "repo domain list <owner/name>", ReadOnly: true, Run: runDomainList})
38 Summary: "list custom pages domains",
39 Usage: "repo domain list <owner/name>",
40 Examples: []string{"repo domain list krz/gitbay"},
41 ReadOnly: true, Run: runDomainList})
3442}
3543
3644// challengeLabel prefixes the domain for the ownership TXT record.
internal/control/profile.go +17 −4
@@ -16,14 +16,27 @@ import (
1616
1717func init() {
1818 register(Command{Path: []string{"profile", "show"},
19 Summary: "show a user's or org's profile",
20 Usage: "profile show [name]", ReadOnly: true, Run: runProfileShow})
19 Summary: "show a user's or org's profile",
20 Usage: "profile show [name]",
21 Examples: []string{"profile show cmc"}, ReadOnly: true, Run: runProfileShow})
2122 register(Command{Path: []string{"profile", "set"},
2223 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})
2431 register(Command{Path: []string{"org", "profile"},
2532 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})
2740}
2841
2942// maxProfileLinks caps the free-form link list. A profile is a header,
internal/control/quota.go +6 −1
@@ -85,7 +85,12 @@ func init() {
8585 register(Command{Path: []string{"admin", "user", "limits"},
8686 Summary: "show or set an account's repository and storage caps (instance admins)",
8787 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})
8994}
9095
9196func runAdminUserLimits(c *Ctx, args []string) int {
internal/control/read.go +24 −9
@@ -17,23 +17,37 @@ import (
1717
1818func init() {
1919 register(Command{
20 Path: []string{"repo", "tree"},
21 Summary: "list a directory",
22 Usage: "repo tree <owner/name> [<path>] [--ref <ref>]",
20 Path: []string{"repo", "tree"},
21 Summary: "list a directory",
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"},
2327 ReadOnly: true,
2428 Run: runRepoTree,
2529 })
2630 register(Command{
27 Path: []string{"repo", "cat"},
28 Summary: "read a file",
29 Usage: "repo cat <owner/name> <path> [--ref <ref>]",
31 Path: []string{"repo", "cat"},
32 Summary: "read a file",
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"},
3038 ReadOnly: true,
3139 Run: runRepoCat,
3240 })
3341 register(Command{
34 Path: []string{"repo", "blame"},
35 Summary: "attribute lines to commits",
36 Usage: "repo blame <owner/name> <path> [--ref <ref>] [--from <n>] [--to <n>]",
42 Path: []string{"repo", "blame"},
43 Summary: "attribute lines to commits",
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"},
3751 ReadOnly: true,
3852 Run: runRepoBlame,
3953 })
@@ -41,6 +55,7 @@ func init() {
4155 Path: []string{"repo", "refs"},
4256 Summary: "list branches and tags",
4357 Usage: "repo refs <owner/name>",
58 Examples: []string{"repo refs krz/gitbay"},
4459 ReadOnly: true,
4560 Run: runRepoRefs,
4661 })
internal/control/register.go +19 −8
@@ -20,27 +20,38 @@ func init() {
2020 register(Command{Path: []string{"register"},
2121 Summary: "create an account (only meaningful for unregistered keys)",
2222 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"},
2329 Run: func(c *Ctx, args []string) int {
2430 return c.fail(protocol.ExitUsage,
2531 "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 ...",
2632 c.User.Username)
2733 }})
2834 register(Command{Path: []string{"email", "add"},
29 Summary: "add an address and mail a verification code",
30 Usage: "email add <address>", Run: runEmailAdd})
35 Summary: "add an address and mail a verification code",
36 Usage: "email add <address>",
37 Examples: []string{"email add cmc@example.org"}, Run: runEmailAdd})
3138 register(Command{Path: []string{"email", "verify"},
32 Summary: "confirm a verification code",
33 Usage: "email verify <code>", Run: runEmailVerify})
39 Summary: "confirm a verification code",
40 Usage: "email verify <code>",
41 Examples: []string{"email verify abc123"}, Run: runEmailVerify})
3442 register(Command{Path: []string{"email", "list"},
3543 Summary: "list the addresses on your account",
3644 Usage: "email list",
45 Examples: []string{"email list"},
3746 ReadOnly: true, Run: runEmailList})
3847 register(Command{Path: []string{"email", "remove"},
39 Summary: "remove an address; not the primary, nor the last verified one",
40 Usage: "email remove <address>", Run: runEmailRemove})
48 Summary: "remove an address; not the primary, nor the last verified one",
49 Usage: "email remove <address>",
50 Examples: []string{"email remove old@example.org"}, Run: runEmailRemove})
4151 register(Command{Path: []string{"email", "primary"},
42 Summary: "make a verified address the primary",
43 Usage: "email primary <address>", Run: runEmailPrimary})
52 Summary: "make a verified address the primary",
53 Usage: "email primary <address>",
54 Examples: []string{"email primary cmc@example.org"}, Run: runEmailPrimary})
4455}
4556
4657func runEmailList(c *Ctx, args []string) int {
internal/control/release.go +44 −10
@@ -20,33 +20,67 @@ import (
2020
2121func init() {
2222 register(Command{Path: []string{"release", "create"},
23 Summary: "create a release on a tag",
24 Usage: "release create <owner/name> <tag> [--title <t>] [--notes <n> | --file -] [--format md|org]",
23 Summary: "create a release on a tag",
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 },
2535 ReadsStdin: true, Run: runReleaseCreate})
2636 register(Command{Path: []string{"release", "edit"},
27 Summary: "update a release's title and notes",
28 Usage: "release edit <owner/name> <tag> [--title <t>] [--notes <n> | --file -] [--format md|org]",
37 Summary: "update a release's title and notes",
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"`},
2946 ReadsStdin: true, Run: runReleaseEdit})
3047 register(Command{Path: []string{"release", "list"},
3148 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})
3356 register(Command{Path: []string{"release", "show"},
34 Summary: "show a release with assets",
35 Usage: "release show <owner/name> <tag>", ReadOnly: true, Run: runReleaseShow})
57 Summary: "show a release with assets",
58 Usage: "release show <owner/name> <tag>",
59 Examples: []string{"release show krz/gitbay v1.30.0"},
60 ReadOnly: true, Run: runReleaseShow})
3661 register(Command{Path: []string{"release", "delete"},
3762 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})
3969 register(Command{Path: []string{"release", "asset", "add"},
4070 Summary: "upload an asset from stdin",
4171 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"},
4273 ReadsStdin: true, Run: runAssetAdd})
4374 register(Command{Path: []string{"release", "asset", "get"},
4475 Summary: "write an asset to stdout",
4576 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"},
4678 ReadOnly: true, Run: runAssetGet})
4779 register(Command{Path: []string{"release", "asset", "remove"},
48 Summary: "remove an asset",
49 Usage: "release asset remove <owner/name> <tag> <filename>", Run: runAssetRemove})
80 Summary: "remove an asset",
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})
5084}
5185
5286var 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") }
2828func init() {
2929 register(Command{Path: []string{"repo", "create"},
3030 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})
3237 register(Command{Path: []string{"repo", "list"},
3338 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})
3546 register(Command{Path: []string{"repo", "show"},
36 Summary: "show repository details",
37 Usage: "repo show <owner/name>", ReadOnly: true, Run: runRepoShow})
47 Summary: "show repository details",
48 Usage: "repo show <owner/name>",
49 Examples: []string{"repo show krz/gitbay"},
50 ReadOnly: true, Run: runRepoShow})
3851 register(Command{Path: []string{"repo", "transfer"},
39 Summary: "move a repository to another owner",
40 Usage: "repo transfer <owner/name> <new-owner> (clone URLs change)", Run: runRepoTransfer})
52 Summary: "move a repository to another owner",
53 Usage: "repo transfer <owner/name> <new-owner> (clone URLs change)",
54 Examples: []string{"repo transfer krz/gitbay krazywarez"},
55 Run: runRepoTransfer})
4156 register(Command{Path: []string{"repo", "rename"},
42 Summary: "rename a repository",
43 Usage: "repo rename <owner/name> <new-name> (clone URLs change)", Run: runRepoRename})
57 Summary: "rename a repository",
58 Usage: "repo rename <owner/name> <new-name> (clone URLs change)",
59 Examples: []string{"repo rename krz/gitbay forge"},
60 Run: runRepoRename})
4461 register(Command{Path: []string{"repo", "delete"},
4562 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})
4769 register(Command{Path: []string{"repo", "access", "grant"},
48 Summary: "grant access",
49 Usage: "repo access grant <owner/name> <user> read|write|admin", Run: runAccessGrant})
70 Summary: "grant access",
71 Usage: "repo access grant <owner/name> <user> read|write|admin",
72 Examples: []string{"repo access grant krz/gitbay cmc write"},
73 Run: runAccessGrant})
5074 register(Command{Path: []string{"repo", "access", "revoke"},
51 Summary: "revoke access",
52 Usage: "repo access revoke <owner/name> <user>", Run: runAccessRevoke})
75 Summary: "revoke access",
76 Usage: "repo access revoke <owner/name> <user>",
77 Examples: []string{"repo access revoke krz/gitbay cmc"},
78 Run: runAccessRevoke})
5379 register(Command{Path: []string{"repo", "access", "list"},
54 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})
80 Summary: "list who can reach the repository, with the role and where it comes from",
81 Usage: "repo access list <owner/name>",
82 Examples: []string{"repo access list krz/gitbay"},
83 ReadOnly: true, Run: runAccessList})
5684 register(Command{Path: []string{"repo", "settings", "show"},
57 Summary: "show settings",
58 Usage: "repo settings show <owner/name>", ReadOnly: true, Run: runSettingsShow})
85 Summary: "show settings",
86 Usage: "repo settings show <owner/name>",
87 Examples: []string{"repo settings show krz/gitbay"},
88 ReadOnly: true, Run: runSettingsShow})
5989 register(Command{Path: []string{"repo", "settings", "protect"},
60 Summary: "protect a branch",
61 Usage: "repo settings protect <owner/name> <branch>", Run: runProtect})
90 Summary: "protect a branch",
91 Usage: "repo settings protect <owner/name> <branch>",
92 Examples: []string{"repo settings protect krz/gitbay main"},
93 Run: runProtect})
6294 register(Command{Path: []string{"repo", "settings", "unprotect"},
63 Summary: "unprotect a branch",
64 Usage: "repo settings unprotect <owner/name> <branch>", Run: runUnprotect})
95 Summary: "unprotect a branch",
96 Usage: "repo settings unprotect <owner/name> <branch>",
97 Examples: []string{"repo settings unprotect krz/gitbay main"},
98 Run: runUnprotect})
6599 register(Command{Path: []string{"repo", "settings", "protect-tag"},
66 Summary: "protect tags matching a glob (created once, never moved or deleted)",
67 Usage: "repo settings protect-tag <owner/name> <glob>", Run: runProtectTag})
100 Summary: "protect tags matching a glob (created once, never moved or deleted)",
101 Usage: "repo settings protect-tag <owner/name> <glob>",
102 Examples: []string{"repo settings protect-tag krz/gitbay 'v*'"},
103 Run: runProtectTag})
68104 register(Command{Path: []string{"repo", "settings", "unprotect-tag"},
69 Summary: "drop a protected-tag glob",
70 Usage: "repo settings unprotect-tag <owner/name> <glob>", Run: runUnprotectTag})
105 Summary: "drop a protected-tag glob",
106 Usage: "repo settings unprotect-tag <owner/name> <glob>",
107 Examples: []string{"repo settings unprotect-tag krz/gitbay 'v*'"},
108 Run: runUnprotectTag})
71109 register(Command{Path: []string{"repo", "settings", "description"},
72 Summary: "set the repository description",
73 Usage: "repo settings description <owner/name> <text> ('' clears)", Run: runSetDescription})
110 Summary: "set the repository description",
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})
74114 register(Command{Path: []string{"repo", "settings", "visibility"},
75 Summary: "set repository visibility",
76 Usage: "repo settings visibility <owner/name> public|private", Run: runSetVisibility})
115 Summary: "set repository visibility",
116 Usage: "repo settings visibility <owner/name> public|private",
117 Examples: []string{"repo settings visibility krz/gitbay public"},
118 Run: runSetVisibility})
77119 register(Command{Path: []string{"repo", "settings", "website"},
78 Summary: "set the repository website",
79 Usage: "repo settings website <owner/name> <url> ('' clears)", Run: runSetWebsite})
120 Summary: "set the repository website",
121 Usage: "repo settings website <owner/name> <url> ('' clears)",
122 Examples: []string{"repo settings website krz/gitbay https://gitbay.org"},
123 Run: runSetWebsite})
80124 register(Command{Path: []string{"repo", "settings", "default-branch"},
81 Summary: "set the default branch",
82 Usage: "repo settings default-branch <owner/name> <branch>", Run: runSetDefaultBranch})
125 Summary: "set the default branch",
126 Usage: "repo settings default-branch <owner/name> <branch>",
127 Examples: []string{"repo settings default-branch krz/gitbay main"},
128 Run: runSetDefaultBranch})
83129 register(Command{Path: []string{"repo", "settings", "git-daemon"},
84 Summary: "expose over git://",
85 Usage: "repo settings git-daemon <owner/name> on|off", Run: runGitDaemon})
130 Summary: "expose over git://",
131 Usage: "repo settings git-daemon <owner/name> on|off",
132 Examples: []string{"repo settings git-daemon krz/gitbay on"},
133 Run: runGitDaemon})
86134 register(Command{Path: []string{"repo", "archive"},
87 Summary: "archive a repository (read-only: pushes and issue/MR writes refused)",
88 Usage: "repo archive <owner/name>", Run: runArchive})
135 Summary: "archive a repository (read-only: pushes and issue/MR writes refused)",
136 Usage: "repo archive <owner/name>",
137 Examples: []string{"repo archive krz/gitbay"},
138 Run: runArchive})
89139 register(Command{Path: []string{"repo", "unarchive"},
90 Summary: "unarchive a repository",
91 Usage: "repo unarchive <owner/name>", Run: runUnarchive})
140 Summary: "unarchive a repository",
141 Usage: "repo unarchive <owner/name>",
142 Examples: []string{"repo unarchive krz/gitbay"},
143 Run: runUnarchive})
92144 register(Command{Path: []string{"repo", "topics"},
93 Summary: "list topics",
94 Usage: "repo topics <owner/name>", ReadOnly: true, Run: runTopicsList})
145 Summary: "list topics",
146 Usage: "repo topics <owner/name>",
147 Examples: []string{"repo topics krz/gitbay"},
148 ReadOnly: true, Run: runTopicsList})
95149 register(Command{Path: []string{"repo", "topics", "add"},
96 Summary: "add topics",
97 Usage: "repo topics add <owner/name> <topic>...", Run: runTopicsAdd})
150 Summary: "add topics",
151 Usage: "repo topics add <owner/name> <topic>...",
152 Examples: []string{"repo topics add krz/gitbay git forge cli"},
153 Run: runTopicsAdd})
98154 register(Command{Path: []string{"repo", "topics", "remove"},
99 Summary: "remove topics",
100 Usage: "repo topics remove <owner/name> <topic>...", Run: runTopicsRemove})
155 Summary: "remove topics",
156 Usage: "repo topics remove <owner/name> <topic>...",
157 Examples: []string{"repo topics remove krz/gitbay cli"},
158 Run: runTopicsRemove})
101159 register(Command{Path: []string{"repo", "search"},
102 Summary: "find repositories by name, description, or topic",
103 Usage: "repo search <query>", ReadOnly: true, Run: runRepoSearch})
160 Summary: "find repositories by name, description, or topic",
161 Usage: "repo search <query>",
162 Examples: []string{"repo search forge"},
163 ReadOnly: true, Run: runRepoSearch})
104164 register(Command{Path: []string{"repo", "grep"},
105165 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})
107172 register(Command{Path: []string{"repo", "diff"},
108 Summary: "the patch between two refs, from their merge base",
109 Usage: "repo diff <owner/name> <base> <head>", ReadOnly: true, Run: runRepoDiff})
173 Summary: "the patch between two refs, from their merge base",
174 Usage: "repo diff <owner/name> <base> <head>",
175 Examples: []string{"repo diff krz/gitbay main cli-output-help"},
176 ReadOnly: true, Run: runRepoDiff})
110177 register(Command{Path: []string{"repo", "pin"},
111 Summary: "pin a repository to your dashboard",
112 Usage: "repo pin <owner/name>", Run: runRepoPin})
178 Summary: "pin a repository to your dashboard",
179 Usage: "repo pin <owner/name>",
180 Examples: []string{"repo pin krz/gitbay"},
181 Run: runRepoPin})
113182 register(Command{Path: []string{"repo", "unpin"},
114 Summary: "unpin a repository",
115 Usage: "repo unpin <owner/name>", Run: runRepoUnpin})
183 Summary: "unpin a repository",
184 Usage: "repo unpin <owner/name>",
185 Examples: []string{"repo unpin krz/gitbay"},
186 Run: runRepoUnpin})
116187 register(Command{Path: []string{"repo", "bookmark"},
117 Summary: "bookmark a repository to come back to",
118 Usage: "repo bookmark <owner/name>", Run: runRepoBookmark})
188 Summary: "bookmark a repository to come back to",
189 Usage: "repo bookmark <owner/name>",
190 Examples: []string{"repo bookmark krz/gitbay"},
191 Run: runRepoBookmark})
119192 register(Command{Path: []string{"repo", "unbookmark"},
120 Summary: "remove a bookmark",
121 Usage: "repo unbookmark <owner/name>", Run: runRepoUnbookmark})
193 Summary: "remove a bookmark",
194 Usage: "repo unbookmark <owner/name>",
195 Examples: []string{"repo unbookmark krz/gitbay"},
196 Run: runRepoUnbookmark})
122197 register(Command{Path: []string{"repo", "bookmarks"},
123 Summary: "list the repositories you have bookmarked",
124 Usage: "repo bookmarks", ReadOnly: true, Run: runRepoBookmarks})
198 Summary: "list the repositories you have bookmarked",
199 Usage: "repo bookmarks",
200 Examples: []string{"repo bookmarks"},
201 ReadOnly: true, Run: runRepoBookmarks})
125202}
126203
127204const (
internal/control/runnerrepo.go +9 −4
@@ -21,13 +21,18 @@ func init() {
2121 register(Command{Path: []string{"repo", "runner", "add"},
2222 Summary: "attach a runner's public key to a repository",
2323 Usage: "repo runner add <owner/name> < key.pub",
24 Examples: []string{"repo runner add krz/gitbay < key.pub"},
2425 ReadsStdin: true, Run: runRepoRunnerAdd})
2526 register(Command{Path: []string{"repo", "runner", "list"},
26 Summary: "list the runners attached to a repository",
27 Usage: "repo runner list <owner/name>", ReadOnly: true, Run: runRepoRunnerList})
27 Summary: "list the runners attached to a repository",
28 Usage: "repo runner list <owner/name>",
29 Examples: []string{"repo runner list krz/gitbay"},
30 ReadOnly: true, Run: runRepoRunnerList})
2831 register(Command{Path: []string{"repo", "runner", "remove"},
29 Summary: "detach a runner from a repository",
30 Usage: "repo runner remove <owner/name> <fingerprint>", Run: runRepoRunnerRemove})
32 Summary: "detach a runner from a repository",
33 Usage: "repo runner remove <owner/name> <fingerprint>",
34 Examples: []string{"repo runner remove krz/gitbay SHA256:abcd1234"},
35 Run: runRepoRunnerRemove})
3136}
3237
3338func runRepoRunnerAdd(c *Ctx, args []string) int {
internal/control/search.go +9 −2
@@ -12,8 +12,15 @@ import (
1212
1313func init() {
1414 register(Command{Path: []string{"search"},
15 Summary: "find repositories, issues and merge requests across the instance",
16 Usage: "search <query> [--kind repo|issue|mr]",
15 Summary: "find repositories, issues and merge requests across the instance",
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 },
1724 ReadOnly: true, Run: runSearch})
1825}
1926
internal/control/sig.go +19 −7
@@ -18,21 +18,33 @@ import (
1818
1919func init() {
2020 register(Command{Path: []string{"pgp", "add"},
21 Summary: "register an OpenPGP public key (armored)",
22 Usage: "pgp add < key.asc", ReadsStdin: true, Run: runPGPAdd})
21 Summary: "register an OpenPGP public key (armored)",
22 Usage: "pgp add < key.asc",
23 Examples: []string{"pgp add < key.asc"},
24 ReadsStdin: true, Run: runPGPAdd})
2325 register(Command{Path: []string{"pgp", "list"},
24 Summary: "list registered OpenPGP keys",
25 Usage: "pgp list", ReadOnly: true, Run: runPGPList})
26 Summary: "list registered OpenPGP keys",
27 Usage: "pgp list",
28 Examples: []string{"pgp list"}, ReadOnly: true, Run: runPGPList})
2629 register(Command{Path: []string{"pgp", "remove"},
27 Summary: "remove an OpenPGP key by fingerprint",
28 Usage: "pgp remove <fingerprint>", Run: runPGPRemove})
30 Summary: "remove an OpenPGP key by fingerprint",
31 Usage: "pgp remove <fingerprint>",
32 Examples: []string{"pgp remove ABCD1234ABCD1234ABCD1234ABCD1234ABCD1234"}, Run: runPGPRemove})
2933 register(Command{Path: []string{"repo", "commit"},
3034 Summary: "show one commit with its patch",
3135 Usage: "repo commit <owner/name> <sha>",
36 Examples: []string{"repo commit krz/gitbay a1b2c3d"},
3237 ReadOnly: true, Run: runRepoCommit})
3338 register(Command{Path: []string{"repo", "log"},
3439 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})
3648}
3749
3850func runPGPAdd(c *Ctx, args []string) int {
internal/control/snippet.go +38 −12
@@ -19,31 +19,57 @@ const maxSnippetFiles = 64
1919
2020func init() {
2121 register(Command{Path: []string{"snippet", "create"},
22 Summary: "create a snippet from one file on stdin",
23 Usage: "snippet create <filename> [--description <d>] [--visibility public|unlisted|private] < file",
22 Summary: "create a snippet from one file on stdin",
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"},
2429 ReadsStdin: true, Run: runSnippetCreate})
2530 register(Command{Path: []string{"snippet", "show"},
26 Summary: "show a snippet's metadata and files",
27 Usage: "snippet show <id>", ReadOnly: true, Run: runSnippetShow})
31 Summary: "show a snippet's metadata and files",
32 Usage: "snippet show <id>",
33 Examples: []string{"snippet show a1b2c3"},
34 ReadOnly: true, Run: runSnippetShow})
2835 register(Command{Path: []string{"snippet", "list"},
2936 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})
3144 register(Command{Path: []string{"snippet", "edit"},
3245 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})
3453 register(Command{Path: []string{"snippet", "delete"},
35 Summary: "delete a snippet and its files",
36 Usage: "snippet delete <id>", Run: runSnippetDelete})
54 Summary: "delete a snippet and its files",
55 Usage: "snippet delete <id>",
56 Examples: []string{"snippet delete a1b2c3"},
57 Run: runSnippetDelete})
3758 register(Command{Path: []string{"snippet", "file", "set"},
3859 Summary: "add a file to a snippet, or replace one, from stdin",
3960 Usage: "snippet file set <id> <filename> < file",
61 Examples: []string{"snippet file set a1b2c3 notes.md < notes.md"},
4062 ReadsStdin: true, Run: runSnippetFileSet})
4163 register(Command{Path: []string{"snippet", "file", "get"},
42 Summary: "write a snippet file to stdout",
43 Usage: "snippet file get <id> <filename> > file", ReadOnly: true, Run: runSnippetFileGet})
64 Summary: "write a snippet file to stdout",
65 Usage: "snippet file get <id> <filename> > file",
66 Examples: []string{"snippet file get a1b2c3 notes.md > notes.md"},
67 ReadOnly: true, Run: runSnippetFileGet})
4468 register(Command{Path: []string{"snippet", "file", "remove"},
45 Summary: "remove a file from a snippet",
46 Usage: "snippet file remove <id> <filename>", Run: runSnippetFileRemove})
69 Summary: "remove a file from a snippet",
70 Usage: "snippet file remove <id> <filename>",
71 Examples: []string{"snippet file remove a1b2c3 notes.md"},
72 Run: runSnippetFileRemove})
4773}
4874
4975type SnippetFileOut struct {
internal/control/status.go +14 −3
@@ -15,10 +15,21 @@ func init() {
1515 register(Command{Path: []string{"status", "set"},
1616 Summary: "report a commit status (CI)",
1717 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})
1928 register(Command{Path: []string{"status", "list"},
20 Summary: "statuses on a commit",
21 Usage: "status list <owner/name> <sha>", ReadOnly: true, Run: runStatusList})
29 Summary: "statuses on a commit",
30 Usage: "status list <owner/name> <sha>",
31 Examples: []string{"status list krz/gitbay a1b2c3d"},
32 ReadOnly: true, Run: runStatusList})
2233}
2334
2435var validStatusState = map[string]bool{"pending": true, "success": true, "failure": true, "error": true}
internal/control/teams.go +27 −18
@@ -14,32 +14,41 @@ import (
1414
1515func init() {
1616 register(Command{Path: []string{"org", "team", "create"},
17 Summary: "create a team",
18 Usage: "org team create <org> <team>", Run: runTeamCreate})
17 Summary: "create a team",
18 Usage: "org team create <org> <team>",
19 Examples: []string{"org team create krz maintainers"}, Run: runTeamCreate})
1920 register(Command{Path: []string{"org", "team", "delete"},
20 Summary: "delete a team (its grants with it)",
21 Usage: "org team delete <org> <team>", Run: runTeamDelete})
21 Summary: "delete a team (its grants with it)",
22 Usage: "org team delete <org> <team>",
23 Examples: []string{"org team delete krz maintainers"}, Run: runTeamDelete})
2224 register(Command{Path: []string{"org", "team", "list"},
23 Summary: "list an org's teams",
24 Usage: "org team list <org>", ReadOnly: true, Run: runTeamList})
25 Summary: "list an org's teams",
26 Usage: "org team list <org>",
27 Examples: []string{"org team list krz"}, ReadOnly: true, Run: runTeamList})
2528 register(Command{Path: []string{"org", "team", "show"},
26 Summary: "show a team's members and grants",
27 Usage: "org team show <org> <team>", ReadOnly: true, Run: runTeamShow})
29 Summary: "show a team's members and grants",
30 Usage: "org team show <org> <team>",
31 Examples: []string{"org team show krz maintainers"}, ReadOnly: true, Run: runTeamShow})
2832 register(Command{Path: []string{"org", "team", "add"},
29 Summary: "add org members to a team",
30 Usage: "org team add <org> <team> <user>...", Run: runTeamAdd})
33 Summary: "add org members to a team",
34 Usage: "org team add <org> <team> <user>...",
35 Examples: []string{"org team add krz maintainers cmc"}, Run: runTeamAdd})
3136 register(Command{Path: []string{"org", "team", "remove"},
32 Summary: "remove members from a team",
33 Usage: "org team remove <org> <team> <user>...", Run: runTeamRemove})
37 Summary: "remove members from a team",
38 Usage: "org team remove <org> <team> <user>...",
39 Examples: []string{"org team remove krz maintainers cmc"}, Run: runTeamRemove})
3440 register(Command{Path: []string{"org", "team", "grant"},
35 Summary: "grant a team a role on an org repo",
36 Usage: "org team grant <org> <team> <owner/name> read|write|admin", Run: runTeamGrant})
41 Summary: "grant a team a role on an org repo",
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})
3744 register(Command{Path: []string{"org", "team", "revoke"},
38 Summary: "revoke a team's grant",
39 Usage: "org team revoke <org> <team> <owner/name>", Run: runTeamRevoke})
45 Summary: "revoke a team's grant",
46 Usage: "org team revoke <org> <team> <owner/name>",
47 Examples: []string{"org team revoke krz maintainers krz/gitbay"}, Run: runTeamRevoke})
4048 register(Command{Path: []string{"org", "settings", "members-role"},
41 Summary: "role plain membership implies on every org repo",
42 Usage: "org settings members-role <org> write|read|none (default write)", Run: runOrgMembersRole})
49 Summary: "role plain membership implies on every org repo",
50 Usage: "org settings members-role <org> write|read|none (default write)",
51 Examples: []string{"org settings members-role krz read"}, Run: runOrgMembersRole})
4352}
4453
4554// orgAdminRef resolves an org and requires the caller to admin it.
internal/control/theme.go +4 −2
@@ -11,10 +11,12 @@ func init() {
1111 register(Command{Path: []string{"web", "theme", "show"},
1212 Summary: "the colour scheme the web UI uses for you",
1313 Usage: "web theme show",
14 Examples: []string{"web theme show"},
1415 ReadOnly: true, Run: runWebThemeShow})
1516 register(Command{Path: []string{"web", "theme", "set"},
16 Summary: "follow the browser's scheme, or force light or dark",
17 Usage: "web theme set system|light|dark", Run: runWebThemeSet})
17 Summary: "follow the browser's scheme, or force light or dark",
18 Usage: "web theme set system|light|dark",
19 Examples: []string{"web theme set dark"}, Run: runWebThemeSet})
1820}
1921
2022// 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() {
1616 register(Command{Path: []string{"token", "create"},
1717 Summary: "mint an API token (shown once)",
1818 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})
2026 register(Command{Path: []string{"token", "list"},
21 Summary: "list API tokens",
22 Usage: "token list", ReadOnly: true, Run: runTokenList})
27 Summary: "list API tokens",
28 Usage: "token list",
29 Examples: []string{"token list"}, ReadOnly: true, Run: runTokenList})
2330 register(Command{Path: []string{"token", "revoke"},
24 Summary: "revoke an API token by name",
25 Usage: "token revoke <name>",
26 Run: runTokenRevoke})
31 Summary: "revoke an API token by name",
32 Usage: "token revoke <name>",
33 Examples: []string{"token revoke laptop"},
34 Run: runTokenRevoke})
2735}
2836
2937// 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()
1414
1515func init() {
1616 register(Command{Path: []string{"web", "login"},
17 Summary: "mint a one-time browser login URL",
18 Usage: "web login", Run: runWebLogin})
17 Summary: "mint a one-time browser login URL",
18 Usage: "web login",
19 Examples: []string{"web login"}, Run: runWebLogin})
1920 register(Command{Path: []string{"web", "sessions", "list"},
20 Summary: "list your browser sessions",
21 Usage: "web sessions list", ReadOnly: true, Run: runWebSessionsList})
21 Summary: "list your browser sessions",
22 Usage: "web sessions list",
23 Examples: []string{"web sessions list"}, ReadOnly: true, Run: runWebSessionsList})
2224 register(Command{Path: []string{"web", "sessions", "revoke"},
2325 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})
2531}
2632
2733func runWebSessionsList(c *Ctx, args []string) int {
internal/control/webhook.go +22 −8
@@ -15,19 +15,33 @@ import (
1515func init() {
1616 register(Command{Path: []string{"webhook", "add"},
1717 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})
1925 register(Command{Path: []string{"webhook", "list"},
20 Summary: "list webhooks",
21 Usage: "webhook list <owner/name>", ReadOnly: true, Run: runWebhookList})
26 Summary: "list webhooks",
27 Usage: "webhook list <owner/name>",
28 Examples: []string{"webhook list krz/gitbay"}, ReadOnly: true, Run: runWebhookList})
2229 register(Command{Path: []string{"webhook", "remove"},
23 Summary: "remove a webhook",
24 Usage: "webhook remove <owner/name> <id>", Run: runWebhookRemove})
30 Summary: "remove a webhook",
31 Usage: "webhook remove <owner/name> <id>",
32 Examples: []string{"webhook remove krz/gitbay 3"}, Run: runWebhookRemove})
2533 register(Command{Path: []string{"webhook", "deliveries"},
2634 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})
2841 register(Command{Path: []string{"webhook", "redeliver"},
29 Summary: "queue a delivery again",
30 Usage: "webhook redeliver <owner/name> <delivery-id>", Run: runWebhookRedeliver})
42 Summary: "queue a delivery again",
43 Usage: "webhook redeliver <owner/name> <delivery-id>",
44 Examples: []string{"webhook redeliver krz/gitbay 42"}, Run: runWebhookRedeliver})
3145}
3246
3347func runWebhookAdd(c *Ctx, args []string) int {
internal/control/wiki.go +2
@@ -18,6 +18,7 @@ func init() {
1818 Path: []string{"wiki", "list"},
1919 Summary: "list a repository's wiki pages",
2020 Usage: "wiki list <owner/name>",
21 Examples: []string{"wiki list krz/gitbay"},
2122 ReadOnly: true,
2223 Run: runWikiList,
2324 })
@@ -25,6 +26,7 @@ func init() {
2526 Path: []string{"wiki", "show"},
2627 Summary: "print a wiki page",
2728 Usage: "wiki show <owner/name> [<page>]",
29 Examples: []string{"wiki show krz/gitbay Quickstart"},
2830 ReadOnly: true,
2931 Run: runWikiShow,
3032 })