CLI output: help from the registry !470
49 files changed, +2111 −721
Layout: unified · split
cmd/gitbay/main.go +305 −262
| @@ -12,6 +12,7 @@ import ( | ||
| 12 | 12 | |
| 13 | 13 | "github.com/spf13/cobra" |
| 14 | 14 | "github.com/spf13/cobra/doc" |
| 15 | "golang.org/x/term" | |
| 15 | 16 | |
| 16 | 17 | "gitbay.org/gitbay/internal/protocol" |
| 17 | 18 | ) |
| @@ -38,68 +39,55 @@ func newRoot() *cobra.Command { | ||
| 38 | 39 | root.AddCommand( |
| 39 | 40 | authCmd(), |
| 40 | 41 | 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}), | |
| 44 | 45 | ), |
| 45 | 46 | group("status", "commit statuses (CI)", |
| 46 | pass("set", "report a status: <sha> --context <c> --state <s> [--description d] [--url u]", passOpts{server: []string{"status", "set"}, needsRepo: true}), | |
| 47 | pass("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}), | |
| 48 | 49 | ), |
| 49 | 50 | 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}), | |
| 56 | 57 | ), |
| 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"), | |
| 65 | 62 | 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"}}), | |
| 70 | 65 | 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"}}), | |
| 75 | 70 | ), |
| 76 | 71 | 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"}}), | |
| 83 | 75 | ), |
| 84 | 76 | ), |
| 85 | 77 | 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}), | |
| 88 | 80 | ), |
| 89 | 81 | group("snippet", "shared text files, outside any repository", |
| 90 | pass("create", "create from one file on stdin: <filename> [--description d] [--visibility public|unlisted|private] < file", | |
| 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"}}), | |
| 98 | 87 | 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"}}), | |
| 103 | 91 | ), |
| 104 | 92 | ), |
| 105 | 93 | repoCmd(), |
| @@ -111,52 +99,106 @@ func newRoot() *cobra.Command { | ||
| 111 | 99 | webCmd(), |
| 112 | 100 | orgCmd(), |
| 113 | 101 | 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"}}), | |
| 117 | 104 | ), |
| 118 | 105 | webhookCmd(), |
| 119 | 106 | remoteCmd(), |
| 120 | 107 | 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"}}), | |
| 124 | 110 | group("admin", "instance administration (admins)", |
| 125 | 111 | group("user", "accounts on this instance", |
| 126 | pass("list", "list accounts: [--state active|pending|disabled|admin] [--limit n] [--cursor c]", passOpts{server: []string{"admin", "user", "list"}}), | |
| 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"}}), | |
| 135 | 121 | ), |
| 136 | 122 | group("email", "addresses on any account", |
| 137 | pass("verify", "mark an address verified by admin assertion: <username> <address>", passOpts{server: []string{"admin", "email", "verify"}}), | |
| 123 | pass("verify", passOpts{server: []string{"admin", "email", "verify"}}), | |
| 138 | 124 | ), |
| 139 | pass("invite", "issue a registration invite and mail its code: --email <address>", passOpts{server: []string{"admin", "invite"}}), | |
| 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"}})), | |
| 144 | 130 | group("repo", "any repository, for moderation (audited)", |
| 145 | pass("list", "every repository with size and last push: [--owner o] [--visibility v] [--limit n] [--cursor c]", passOpts{server: []string{"admin", "repo", "list"}}), | |
| 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"}}), | |
| 150 | 136 | ), |
| 151 | 137 | group("mr", "merge requests in any repository (audited)", |
| 152 | pass("prune", "drop merged or closed MRs' head refs and the objects only they kept: <owner/name> <n>... --yes", passOpts{server: []string{"admin", "mr", "prune"}}), | |
| 138 | pass("prune", passOpts{server: []string{"admin", "mr", "prune"}}), | |
| 153 | 139 | ), |
| 154 | 140 | ), |
| 155 | 141 | manCmd(root), |
| 156 | 142 | ) |
| 143 | defaultHelp := root.HelpFunc() | |
| 144 | root.SetHelpFunc(func(cmd *cobra.Command, args []string) { | |
| 145 | if cmd == root { | |
| 146 | rootHelp(root) | |
| 147 | return | |
| 148 | } | |
| 149 | defaultHelp(cmd, args) | |
| 150 | }) | |
| 157 | 151 | return root |
| 158 | 152 | } |
| 159 | 153 | |
| 154 | // rootSection is one heading in gitbay --help: a title and the root | |
| 155 | // commands that belong under it. | |
| 156 | type rootSection struct { | |
| 157 | title string | |
| 158 | names []string | |
| 159 | } | |
| 160 | ||
| 161 | var 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. | |
| 169 | func rootHelp(root *cobra.Command) { | |
| 170 | byName := map[string]*cobra.Command{} | |
| 171 | wide := 0 | |
| 172 | for _, c := range root.Commands() { | |
| 173 | byName[c.Name()] = c | |
| 174 | if !c.Hidden { | |
| 175 | wide = max(wide, len(c.Name())) | |
| 176 | } | |
| 177 | } | |
| 178 | fmt.Println("gitbay: command-line client for a gitbay forge") | |
| 179 | fmt.Println() | |
| 180 | fmt.Println("USAGE") | |
| 181 | fmt.Println(" gitbay <command> [<owner/name>] [flags]") | |
| 182 | for _, s := range rootSections { | |
| 183 | var rows [][2]string | |
| 184 | for _, n := range s.names { | |
| 185 | if c := byName[n]; c != nil && !c.Hidden { | |
| 186 | rows = append(rows, [2]string{n, c.Short}) | |
| 187 | } | |
| 188 | } | |
| 189 | if len(rows) == 0 { | |
| 190 | continue | |
| 191 | } | |
| 192 | fmt.Println() | |
| 193 | fmt.Println(s.title) | |
| 194 | for _, r := range rows { | |
| 195 | fmt.Printf(" %-*s %s\n", wide, r[0], r[1]) | |
| 196 | } | |
| 197 | } | |
| 198 | fmt.Println() | |
| 199 | fmt.Println("gitbay <command> --help for its verbs; gitbay help <prefix> for the server reference.") | |
| 200 | } | |
| 201 | ||
| 160 | 202 | // serverPath is the annotation key holding a passthrough command's |
| 161 | 203 | // server-side path, so the tree can be checked against the registry. |
| 162 | 204 | const serverPath = "gitbay.server_path" |
| @@ -197,10 +239,10 @@ func (o passOpts) stdinModeName() string { | ||
| 197 | 239 | return "none" |
| 198 | 240 | } |
| 199 | 241 | |
| 200 | func pass(use, short string, o passOpts) *cobra.Command { | |
| 242 | func pass(use string, o passOpts) *cobra.Command { | |
| 201 | 243 | return &cobra.Command{ |
| 202 | 244 | Use: use, |
| 203 | Short: short, | |
| 245 | Short: summaries[strings.Join(o.server, " ")], | |
| 204 | 246 | Annotations: map[string]string{ |
| 205 | 247 | serverPath: strings.Join(o.server, " "), |
| 206 | 248 | stdinMode: o.stdinModeName(), |
| @@ -222,6 +264,17 @@ func pass(use, short string, o passOpts) *cobra.Command { | ||
| 222 | 264 | } |
| 223 | 265 | } |
| 224 | 266 | |
| 267 | // withShort overrides a passthrough command's one-line summary. Used | |
| 268 | // where the registry's own Summary for a single-verb noun (dashboard, | |
| 269 | // explore, search, register) reads differently from nounSummaries, which | |
| 270 | // is what a grouped noun's header and the root help both show; the two | |
| 271 | // must agree (TestGroupsSayWhatTheServerSays), so these few take the | |
| 272 | // noun's wording instead of the command's. | |
| 273 | func withShort(cmd *cobra.Command, short string) *cobra.Command { | |
| 274 | cmd.Short = short | |
| 275 | return cmd | |
| 276 | } | |
| 277 | ||
| 225 | 278 | // runServerHelp prints the registry's usage for one command. |
| 226 | 279 | func runServerHelp(o passOpts) int { |
| 227 | 280 | t, err := resolveTarget() |
| @@ -335,12 +388,18 @@ func group(use, short string, subs ...*cobra.Command) *cobra.Command { | ||
| 335 | 388 | } |
| 336 | 389 | |
| 337 | 390 | // serverHelp prints the registry's usage for a prefix and reports whether |
| 338 | // it did. | |
| 391 | // it did. At a terminal it goes through the terminal-aware path, so it | |
| 392 | // gets the same --term=<cols>[,color] treatment (and layout) as any other | |
| 393 | // command; piped, it stays a quiet capture, so a network or lookup | |
| 394 | // failure falls back to cobra's local help without noise. | |
| 339 | 395 | func serverHelp(prefix string) bool { |
| 340 | 396 | t, err := resolveTarget() |
| 341 | 397 | if err != nil { |
| 342 | 398 | return false |
| 343 | 399 | } |
| 400 | if term.IsTerminal(int(os.Stdout.Fd())) { | |
| 401 | return runSSH(t, []string{"help", prefix}, strings.NewReader("")) == 0 | |
| 402 | } | |
| 344 | 403 | out, code := sshCapture(t, []string{"help", prefix}) |
| 345 | 404 | if code != 0 || out == "" { |
| 346 | 405 | return false |
| @@ -368,8 +427,7 @@ func local(use, short string, fn func(args []string) int) *cobra.Command { | ||
| 368 | 427 | } |
| 369 | 428 | |
| 370 | 429 | func 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"}) | |
| 373 | 431 | // keys add always reads stdin on the server; wire it through directly. |
| 374 | 432 | keysAdd.RunE = func(cmd *cobra.Command, args []string) error { |
| 375 | 433 | t, err := resolveTarget() |
| @@ -405,212 +463,197 @@ func authCmd() *cobra.Command { | ||
| 405 | 463 | }, |
| 406 | 464 | } |
| 407 | 465 | tokens := group("token", "API tokens (minted over SSH, used with the JSON API)", |
| 408 | pass("create", "mint a token: --name <n> [--scope full|read] [--ttl 30d]", passOpts{server: []string{"token", "create"}}), | |
| 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"}}), | |
| 411 | 469 | ) |
| 412 | 470 | 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"}}), | |
| 415 | 472 | tokens, |
| 416 | pass("whoami", "show the authenticated account", passOpts{server: []string{"whoami"}}), | |
| 473 | pass("whoami", passOpts{server: []string{"whoami"}}), | |
| 417 | 474 | group("keys", "manage SSH keys", |
| 418 | pass("list", "list registered SSH keys", passOpts{server: []string{"keys", "list"}}), | |
| 475 | pass("list", passOpts{server: []string{"keys", "list"}}), | |
| 419 | 476 | 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"}}), | |
| 422 | 479 | ), |
| 423 | 480 | 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"}}), | |
| 429 | 486 | ), |
| 430 | 487 | group("pgp", "manage OpenPGP keys", |
| 431 | pass("list", "list registered PGP keys", passOpts{server: []string{"pgp", "list"}}), | |
| 488 | pass("list", passOpts{server: []string{"pgp", "list"}}), | |
| 432 | 489 | pgpAdd, |
| 433 | pass("remove", "remove a PGP key by fingerprint", passOpts{server: []string{"pgp", "remove"}}), | |
| 490 | pass("remove", passOpts{server: []string{"pgp", "remove"}}), | |
| 434 | 491 | ), |
| 435 | 492 | ) |
| 436 | 493 | } |
| 437 | 494 | |
| 438 | 495 | func repoCmd() *cobra.Command { |
| 439 | 496 | 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}), | |
| 473 | 525 | local("clone", "clone via ssh: gitbay repo clone <owner/name> [dir]", cmdRepoClone), |
| 474 | 526 | 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}), | |
| 477 | 528 | group("deploy-key", "repository-bound CI keys", |
| 478 | pass("add", "bind a key: [--rw] < key.pub", passOpts{server: []string{"repo", "deploy-key", "add"}, needsRepo: true, alwaysStdin: true, stdinWhat: "an SSH public key"}), | |
| 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}), | |
| 481 | 532 | ), |
| 482 | 533 | group("runner", "runners attached to a repository", |
| 483 | pass("add", "attach a runner's public key: < key.pub", passOpts{server: []string{"repo", "runner", "add"}, needsRepo: true, alwaysStdin: true, stdinWhat: "an SSH public key"}), | |
| 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}), | |
| 486 | 537 | ), |
| 487 | 538 | 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}), | |
| 493 | 543 | ), |
| 494 | 544 | group("deps", "check dependencies against upstream registries", |
| 495 | pass("enable", "check this repo's dependencies for updates", passOpts{server: []string{"repo", "deps", "enable"}, needsRepo: true}), | |
| 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}), | |
| 498 | 548 | ), |
| 499 | 549 | group("secret", "build secrets (values on stdin, injected into build env)", |
| 500 | pass("set", "set a secret: <NAME> (value on stdin)", passOpts{server: []string{"repo", "secret", "set"}, needsRepo: true, alwaysStdin: true, stdinWhat: "the secret value", stdinSecret: true}), | |
| 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}), | |
| 503 | 553 | ), |
| 504 | 554 | group("domain", "custom domains for the pages branch", |
| 505 | pass("add", "claim a domain (verify with a DNS TXT record): <domain>", passOpts{server: []string{"repo", "domain", "add"}, needsRepo: true}), | |
| 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}), | |
| 509 | 559 | ), |
| 510 | 560 | 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}), | |
| 514 | 564 | ), |
| 515 | 565 | 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}), | |
| 519 | 569 | ), |
| 520 | 570 | 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}), | |
| 537 | 587 | ), |
| 538 | 588 | ) |
| 539 | 589 | } |
| 540 | 590 | |
| 541 | 591 | func issueCmd() *cobra.Command { |
| 542 | 592 | 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}), | |
| 556 | 604 | ) |
| 557 | 605 | } |
| 558 | 606 | |
| 559 | 607 | func releaseCmd() *cobra.Command { |
| 560 | 608 | return group("release", "tag-anchored releases with notes and assets", |
| 561 | pass("create", "create a release on a pushed tag: <tag> [--title <t>] [--notes|--file -|$EDITOR]", | |
| 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}), | |
| 568 | 614 | group("asset", "binary assets on a release", |
| 569 | pass("add", "upload from stdin: <tag> <filename> < file", passOpts{server: []string{"release", "asset", "add"}, needsRepo: true, alwaysStdin: true, stdinWhat: "the asset's bytes"}), | |
| 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}), | |
| 572 | 618 | ), |
| 573 | 619 | ) |
| 574 | 620 | } |
| 575 | 621 | |
| 576 | 622 | func milestoneCmd() *cobra.Command { |
| 577 | 623 | 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}), | |
| 584 | 628 | ) |
| 585 | 629 | } |
| 586 | 630 | |
| 587 | 631 | func 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})) | |
| 590 | 634 | 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}), | |
| 596 | 639 | local("checkout", "fetch and check out the MR head locally: gitbay mr checkout <n>", cmdMRCheckout), |
| 597 | 640 | local("rebase", "replay the MR's branch onto its target and re-push: gitbay mr rebase <n>", cmdMRRebase), |
| 598 | pass("comment", "comment on a merge request", passOpts{server: []string{"mr", "comment"}, needsRepo: true, stdinOK: true, editor: "comment"}), | |
| 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}), | |
| 603 | 646 | 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}), | |
| 614 | 657 | ) |
| 615 | 658 | } |
| 616 | 659 | |
| @@ -652,65 +695,64 @@ func usesTokenStdin(args []string) bool { | ||
| 652 | 695 | |
| 653 | 696 | func webCmd() *cobra.Command { |
| 654 | 697 | return group("web", "browser session", |
| 655 | pass("login", "mint a one-time browser login URL over ssh", passOpts{server: []string{"web", "login"}}), | |
| 698 | pass("login", passOpts{server: []string{"web", "login"}}), | |
| 656 | 699 | 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"}}), | |
| 659 | 702 | ), |
| 660 | 703 | group("theme", "the colour scheme the web UI uses for you", |
| 661 | pass("show", "show your colour scheme", passOpts{server: []string{"web", "theme", "show"}}), | |
| 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"}}), | |
| 663 | 706 | ), |
| 664 | 707 | ) |
| 665 | 708 | } |
| 666 | 709 | |
| 667 | 710 | func webhookCmd() *cobra.Command { |
| 668 | 711 | return group("webhook", "outbound event delivery", |
| 669 | pass("add", "add a webhook: <url> [--secret s] [--events k1,k2|*]", passOpts{server: []string{"webhook", "add"}, needsRepo: true}), | |
| 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}), | |
| 674 | 717 | ) |
| 675 | 718 | } |
| 676 | 719 | |
| 677 | 720 | func orgCmd() *cobra.Command { |
| 678 | 721 | 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"}}), | |
| 686 | 728 | 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"}}), | |
| 690 | 732 | ), |
| 691 | 733 | group("label", "labels every org repository sees", |
| 692 | pass("set", "create an org label or set its colour: <org> <label> [--color rrggbb|'']", passOpts{server: []string{"org", "label", "set"}}), | |
| 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"}}), | |
| 695 | 737 | ), |
| 696 | 738 | group("milestone", "milestones spanning an org's repositories", |
| 697 | pass("create", "create an org milestone: <org> <title> [--description d] [--due YYYY-MM-DD]", passOpts{server: []string{"org", "milestone", "create"}}), | |
| 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"}}), | |
| 701 | 743 | ), |
| 702 | 744 | 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"}}), | |
| 711 | 753 | ), |
| 712 | 754 | group("settings", "organization settings", |
| 713 | pass("members-role", "role plain membership implies: <org> write|read|none", passOpts{server: []string{"org", "settings", "members-role"}}), | |
| 755 | pass("members-role", passOpts{server: []string{"org", "settings", "members-role"}}), | |
| 714 | 756 | ), |
| 715 | 757 | ) |
| 716 | 758 | } |
| @@ -724,7 +766,7 @@ func remoteCmd() *cobra.Command { | ||
| 724 | 766 | } |
| 725 | 767 | |
| 726 | 768 | func initCmd() *cobra.Command { |
| 727 | return local("init", "git init + repo create + set origin, in one step: gitbay init [name] [--private]", cmdInit) | |
| 769 | return local("init [name] [--private]", "git init + repo create + set origin, in one step", cmdInit) | |
| 728 | 770 | } |
| 729 | 771 | |
| 730 | 772 | // manCmd generates man pages; a CLI-first tool without man pages is not |
| @@ -759,7 +801,8 @@ func helpCmd(root *cobra.Command) *cobra.Command { | ||
| 759 | 801 | DisableFlagParsing: true, |
| 760 | 802 | RunE: func(cmd *cobra.Command, args []string) error { |
| 761 | 803 | if len(args) == 0 { |
| 762 | return root.Help() | |
| 804 | rootHelp(root) | |
| 805 | return nil | |
| 763 | 806 | } |
| 764 | 807 | t, err := resolveTarget() |
| 765 | 808 | if err != nil { |
cmd/gitbay/migrate.go +2 −2
| @@ -18,8 +18,8 @@ func migrateCmd() *cobra.Command { | ||
| 18 | 18 | var from string |
| 19 | 19 | var fromPort int |
| 20 | 20 | 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", | |
| 23 | 23 | Long: `Migrate authenticates to the source instance with your own SSH key, |
| 24 | 24 | exports your account bundle (profile, emails, repos with settings, |
| 25 | 25 | issues, 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 | ||
| 3 | package main | |
| 4 | ||
| 5 | var 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 @@ | ||
| 1 | package main | |
| 2 | ||
| 3 | import ( | |
| 4 | "flag" | |
| 5 | "fmt" | |
| 6 | "os" | |
| 7 | "slices" | |
| 8 | "strings" | |
| 9 | "testing" | |
| 10 | ||
| 11 | "gitbay.org/gitbay/internal/control" | |
| 12 | ) | |
| 13 | ||
| 14 | var 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. | |
| 18 | func 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 | ||
| 37 | func 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 | ||
| 55 | func TestGroupsSayWhatTheServerSays(t *testing.T) { | |
| 56 | nouns := control.NounSummaries() | |
| 57 | for _, c := range newRoot().Commands() { | |
| 58 | if s, ok := nouns[c.Name()]; ok && c.Short != s { | |
| 59 | t.Errorf("%s: CLI says %q, server says %q", c.Name(), c.Short, s) | |
| 60 | } | |
| 61 | } | |
| 62 | } | |
e2e/cli_test.go +7 −3
| @@ -146,11 +146,15 @@ func TestCLI(t *testing.T) { | ||
| 146 | 146 | mustGit(t, dir, cliGitEnv, "add", ".") |
| 147 | 147 | mustGit(t, dir, cliGitEnv, "commit", "-q", "-m", "feature work") |
| 148 | 148 | 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.") { | |
| 152 | 153 | t.Fatalf("issue --help is not the server's reference:\n%s", out) |
| 153 | 154 | } |
| 155 | if out := c.must(t, dir, "", "issue", "create", "--help"); !strings.Contains(out, "issue create <owner/name>") || !strings.Contains(out, "--title <t>") { | |
| 156 | t.Fatalf("issue create --help carries no flags:\n%s", out) | |
| 157 | } | |
| 154 | 158 | |
| 155 | 159 | // Inside the clone on the branch, --source is the checked-out branch |
| 156 | 160 | // and --target the default branch; neither needs typing (#101). |
internal/control/admin.go +57 −27
| @@ -16,59 +16,89 @@ import ( | ||
| 16 | 16 | |
| 17 | 17 | func init() { |
| 18 | 18 | 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"}, | |
| 21 | 27 | ReadOnly: true, Run: runAdminUserList}) |
| 22 | 28 | register(Command{Path: []string{"admin", "user", "show"}, |
| 23 | 29 | Summary: "show an account: keys, emails, orgs, tokens, sessions (instance admins)", |
| 24 | 30 | Usage: "admin user show <username>", |
| 31 | Examples: []string{"admin user show alice"}, | |
| 25 | 32 | ReadOnly: true, Run: runAdminUserShow}) |
| 26 | 33 | 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}) | |
| 30 | 38 | 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}) | |
| 34 | 43 | register(Command{Path: []string{"admin", "runners"}, |
| 35 | 44 | Summary: "the build queue and runner accounts: last poll, scope, the build each holds (instance admins)", |
| 36 | 45 | Usage: "admin runners", |
| 46 | Examples: []string{"admin runners"}, | |
| 37 | 47 | ReadOnly: true, Run: runAdminRunners}) |
| 38 | 48 | register(Command{Path: []string{"admin", "runners", "remove"}, |
| 39 | Summary: "drop a key's runner heartbeat row, e.g. one that polled once by mistake (instance admins)", | |
| 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}) | |
| 42 | 53 | // forget is the name this shipped under in v1.18; remove is the verb |
| 43 | 54 | // every other noun uses. Both stay for one release. |
| 44 | 55 | 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}) | |
| 48 | 60 | 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"}, | |
| 51 | 70 | ReadOnly: true, Run: runAdminRepoList}) |
| 52 | 71 | 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}) | |
| 56 | 76 | 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}) | |
| 60 | 81 | 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}) | |
| 64 | 86 | register(Command{Path: []string{"admin", "repo", "delete"}, |
| 65 | 87 | Summary: "delete any repository (instance admins; audited)", |
| 66 | 88 | Usage: "admin repo delete <owner/name> --yes", |
| 67 | Run: runAdminRepoDelete}) | |
| 89 | Flags: []Flag{ | |
| 90 | {"--yes", "", "confirm the permanent delete", ""}, | |
| 91 | }, | |
| 92 | Examples: []string{"admin repo delete alice/spam --yes"}, | |
| 93 | Run: runAdminRepoDelete}) | |
| 68 | 94 | register(Command{Path: []string{"admin", "mr", "prune"}, |
| 69 | 95 | Summary: "drop merged or closed MRs' head refs and the objects only they kept, e.g. after a history rewrite (instance admins; audited)", |
| 70 | 96 | Usage: "admin mr prune <owner/name> <n> [<n>...] --yes", |
| 71 | Run: runAdminMRPrune}) | |
| 97 | Flags: []Flag{ | |
| 98 | {"--yes", "", "confirm the permanent prune", ""}, | |
| 99 | }, | |
| 100 | Examples: []string{"admin mr prune krz/gitbay 12 13 --yes"}, | |
| 101 | Run: runAdminMRPrune}) | |
| 72 | 102 | } |
| 73 | 103 | |
| 74 | 104 | // requireInstanceAdmin gates the admin noun. -1 means proceed. |
internal/control/adminhost.go +32 −13
| @@ -22,32 +22,51 @@ import ( | ||
| 22 | 22 | |
| 23 | 23 | func init() { |
| 24 | 24 | 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"}, | |
| 27 | 34 | ReadsStdin: true, Run: runAdminUserCreate}) |
| 28 | 35 | 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}) | |
| 32 | 40 | 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}) | |
| 36 | 45 | register(Command{Path: []string{"admin", "user", "delete"}, |
| 37 | 46 | Summary: "delete an account that anchors nothing (keys, emails and sessions go with it)", |
| 38 | 47 | Usage: "admin user delete <username> --yes", |
| 39 | Run: runAdminUserDelete}) | |
| 48 | Flags: []Flag{ | |
| 49 | {"--yes", "", "confirm the permanent delete", ""}, | |
| 50 | }, | |
| 51 | Examples: []string{"admin user delete alice --yes"}, | |
| 52 | Run: runAdminUserDelete}) | |
| 40 | 53 | 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}) | |
| 44 | 58 | register(Command{Path: []string{"admin", "invite"}, |
| 45 | 59 | Summary: "issue a registration invite and mail its code", |
| 46 | 60 | Usage: "admin invite --email <address>", |
| 47 | Run: runAdminInvite}) | |
| 61 | Flags: []Flag{ | |
| 62 | {"--email", "<address>", "who the invite is for", ""}, | |
| 63 | }, | |
| 64 | Examples: []string{"admin invite --email alice@example.org"}, | |
| 65 | Run: runAdminInvite}) | |
| 48 | 66 | register(Command{Path: []string{"admin", "stats"}, |
| 49 | 67 | Summary: "instance statistics: counts and per-repository disk usage", |
| 50 | 68 | Usage: "admin stats", |
| 69 | Examples: []string{"admin stats"}, | |
| 51 | 70 | ReadOnly: true, Run: runAdminStats}) |
| 52 | 71 | } |
| 53 | 72 | |
internal/control/audit.go +9 −2
| @@ -12,8 +12,15 @@ import ( | ||
| 12 | 12 | |
| 13 | 13 | func init() { |
| 14 | 14 | 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"}, | |
| 17 | 24 | ReadOnly: true, Run: runAudit}) |
| 18 | 25 | } |
| 19 | 26 | |
internal/control/build.go +55 −19
| @@ -22,37 +22,64 @@ import ( | ||
| 22 | 22 | func init() { |
| 23 | 23 | register(Command{Path: []string{"build", "list"}, |
| 24 | 24 | Summary: "list recent builds", |
| 25 | Usage: "build list <owner/name> [--ref <branch>] [--status <state>] [--job <name>] [--limit <n>] [--cursor <c>]", ReadOnly: true, Run: runBuildList}) | |
| 25 | Usage: "build list <owner/name> [--ref <branch>] [--status <state>] [--job <name>] [--limit <n>] [--cursor <c>]", | |
| 26 | Flags: []Flag{ | |
| 27 | {"--ref", "<branch>", "only builds on this branch", ""}, | |
| 28 | {"--status", "<state>", "only builds in this state", ""}, | |
| 29 | {"--job", "<name>", "only this job", ""}, | |
| 30 | {"--limit", "<n>", "rows per page", "50"}, | |
| 31 | {"--cursor", "<c>", "continue from the previous page", ""}, | |
| 32 | }, | |
| 33 | Examples: []string{"build list krz/gitbay --status failure"}, | |
| 34 | ReadOnly: true, Run: runBuildList}) | |
| 26 | 35 | 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}) | |
| 29 | 40 | register(Command{Path: []string{"build", "log"}, |
| 30 | 41 | Summary: "print a build's log, or follow it until the build ends", |
| 31 | Usage: "build log <owner/name> <n> [--follow]", ReadOnly: true, Run: runBuildLog}) | |
| 42 | Usage: "build log <owner/name> <n> [--follow]", | |
| 43 | Flags: []Flag{ | |
| 44 | {"--follow", "", "stream the log until the build ends", ""}, | |
| 45 | }, | |
| 46 | Examples: []string{"build log krz/gitbay 431 --follow"}, | |
| 47 | ReadOnly: true, Run: runBuildLog}) | |
| 32 | 48 | |
| 33 | 49 | 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}) | |
| 36 | 54 | |
| 37 | 55 | 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}) | |
| 40 | 60 | 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}) | |
| 43 | 65 | // Secrets: set over stdin, listed by name only, injected into the |
| 44 | 66 | // repo's builds as environment variables. Same discipline as mirror |
| 45 | 67 | // tokens — the value never appears in argv, logs, or output. |
| 46 | 68 | register(Command{Path: []string{"repo", "secret", "set"}, |
| 47 | 69 | Summary: "set a build secret", |
| 48 | 70 | Usage: "repo secret set <owner/name> <NAME> (value on stdin)", |
| 71 | Examples: []string{"repo secret set krz/gitbay DEPLOY_TOKEN"}, | |
| 49 | 72 | ReadsStdin: true, Run: runSecretSet}) |
| 50 | 73 | 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}) | |
| 53 | 78 | 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}) | |
| 56 | 83 | |
| 57 | 84 | // Runner commands: the claim/report loop for gitbay-runner. A runner |
| 58 | 85 | // executes arbitrary repo code, so handing out jobs is the instance |
| @@ -61,13 +88,22 @@ func init() { | ||
| 61 | 88 | // an admin key, which a runner host should not hold (#92). |
| 62 | 89 | register(Command{Path: []string{"runner", "next"}, |
| 63 | 90 | Summary: "claim the oldest pending build this key may run (runner protocol)", |
| 64 | Usage: "runner next [--untrusted] [<owner/name>...]", Run: runRunnerNext}) | |
| 91 | Usage: "runner next [--untrusted] [<owner/name>...]", | |
| 92 | Flags: []Flag{ | |
| 93 | {"--untrusted", "", "this runner may build a fork's merge request head", ""}, | |
| 94 | }, | |
| 95 | Examples: []string{"runner next krz/gitbay"}, | |
| 96 | Run: runRunnerNext}) | |
| 65 | 97 | 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}) | |
| 68 | 102 | 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}) | |
| 71 | 107 | } |
| 72 | 108 | |
| 73 | 109 | type BuildOut struct { |
internal/control/commitfile.go +8
| @@ -17,6 +17,14 @@ func init() { | ||
| 17 | 17 | Summary: "write a file and commit it", |
| 18 | 18 | Usage: "repo commit-file <owner/name> <path> " + |
| 19 | 19 | "--ref <branch> [--message <m>] [--file -]", |
| 20 | Flags: []Flag{ | |
| 21 | {"--ref", "<branch>", "branch to commit to", ""}, | |
| 22 | {"--message", "<m>", "the commit message", ""}, | |
| 23 | {"--file", "-", "read the new content from stdin", ""}, | |
| 24 | }, | |
| 25 | Examples: []string{ | |
| 26 | `repo commit-file krz/gitbay CHANGELOG.org --ref main --message "note the release" --file - < CHANGELOG.org`, | |
| 27 | }, | |
| 20 | 28 | ReadsStdin: true, |
| 21 | 29 | Run: runCommitFile, |
| 22 | 30 | }) |
internal/control/control.go +10 −51
| @@ -68,6 +68,14 @@ func (c *Ctx) usageWith(msg string) int { | ||
| 68 | 68 | return c.fail(protocol.ExitUsage, "%s\nusage: %s", msg, c.Cmd.Usage) |
| 69 | 69 | } |
| 70 | 70 | |
| 71 | // Flag is one flag in a command's help. | |
| 72 | type 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 | ||
| 71 | 79 | type Command struct { |
| 72 | 80 | Path []string // e.g. ["keys", "add"] |
| 73 | 81 | // Summary is one line of prose: what the command does, no argument |
| @@ -75,6 +83,8 @@ type Command struct { | ||
| 75 | 83 | // help renders them separately, so neither may carry the other's job. |
| 76 | 84 | Summary string |
| 77 | 85 | Usage string |
| 86 | Flags []Flag | |
| 87 | Examples []string // full argv after the program, repository named | |
| 78 | 88 | ReadsStdin bool |
| 79 | 89 | ReadOnly bool // safe for read-scoped API tokens |
| 80 | 90 | Run func(c *Ctx, args []string) int |
| @@ -315,57 +325,6 @@ func (c *Ctx) fail(code int, format string, args ...any) int { | ||
| 315 | 325 | return code |
| 316 | 326 | } |
| 317 | 327 | |
| 318 | func 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. | |
| 329 | type 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. | |
| 339 | func 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 | ||
| 369 | 328 | func joinPath(p []string) string { |
| 370 | 329 | out := "" |
| 371 | 330 | for i, s := range p { |
internal/control/control_test.go +8 −12
| @@ -125,25 +125,21 @@ func TestEveryCommandDocumentsItsUsage(t *testing.T) { | ||
| 125 | 125 | } |
| 126 | 126 | } |
| 127 | 127 | |
| 128 | // TestHelpPrefixNarrowsAndShowsFlags covers the reason the command exists: | |
| 129 | // before this, reading one command's flags meant reading all of them. | |
| 130 | func 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). | |
| 132 | func TestHelpPrefixNarrowsToTheNoun(t *testing.T) { | |
| 131 | 133 | var buf bytes.Buffer |
| 132 | 134 | c := &Ctx{Stdout: &buf, Stderr: io.Discard} |
| 133 | 135 | if code := runHelp(c, []string{"issue"}); code != protocol.ExitOK { |
| 134 | 136 | t.Fatalf("help issue exited %d", code) |
| 135 | 137 | } |
| 136 | 138 | 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) | |
| 140 | 142 | } |
| 141 | if !strings.HasPrefix(line, "issue ") { | |
| 142 | t.Errorf("help issue listed an unrelated command: %q", line) | |
| 143 | } | |
| 144 | } | |
| 145 | if !strings.Contains(out, "--state open|closed|all") { | |
| 146 | t.Error("help issue did not print issue list's flags") | |
| 147 | 143 | } |
| 148 | 144 | } |
| 149 | 145 | |
internal/control/dashboard.go +8 −2
| @@ -18,10 +18,16 @@ func init() { | ||
| 18 | 18 | register(Command{Path: []string{"dashboard"}, |
| 19 | 19 | Summary: "one read for the account dashboard: review queue, assigned and open work, pins, activity, builds", |
| 20 | 20 | Usage: "dashboard", |
| 21 | Examples: []string{"dashboard"}, | |
| 21 | 22 | ReadOnly: true, Run: runDashboard}) |
| 22 | 23 | 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"}, | |
| 25 | 31 | ReadOnly: true, Run: runFeed}) |
| 26 | 32 | } |
| 27 | 33 | |
internal/control/deploykey.go +14 −6
| @@ -14,15 +14,23 @@ import ( | ||
| 14 | 14 | |
| 15 | 15 | func init() { |
| 16 | 16 | 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"}, | |
| 19 | 23 | ReadsStdin: true, Run: runDeployKeyAdd}) |
| 20 | 24 | 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}) | |
| 23 | 29 | 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}) | |
| 26 | 34 | } |
| 27 | 35 | |
| 28 | 36 | func runDeployKeyAdd(c *Ctx, args []string) int { |
internal/control/deps.go +12 −6
| @@ -15,14 +15,20 @@ func init() { | ||
| 15 | 15 | // public registry what the repository depends on, which is the owner's |
| 16 | 16 | // disclosure to make, not the instance's. |
| 17 | 17 | 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}) | |
| 20 | 22 | 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}) | |
| 23 | 27 | 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}) | |
| 26 | 32 | } |
| 27 | 33 | |
| 28 | 34 | // DepsOut is what `repo deps status` emits: the check's state and what |
internal/control/diffcomment.go +27 −8
| @@ -16,18 +16,37 @@ import ( | ||
| 16 | 16 | |
| 17 | 17 | func init() { |
| 18 | 18 | 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 | }, | |
| 21 | 34 | ReadsStdin: true, Run: runDiffComment}) |
| 22 | 35 | 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}) | |
| 25 | 40 | 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}) | |
| 28 | 45 | 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}) | |
| 31 | 50 | } |
| 32 | 51 | |
| 33 | 52 | func runDiffComment(c *Ctx, args []string) int { |
internal/control/explore.go +14 −5
| @@ -11,9 +11,14 @@ import ( | ||
| 11 | 11 | |
| 12 | 12 | func init() { |
| 13 | 13 | 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"}, | |
| 17 | 22 | ReadOnly: true, |
| 18 | 23 | Run: runExplore, |
| 19 | 24 | }) |
| @@ -21,8 +26,12 @@ func init() { | ||
| 21 | 26 | Path: []string{"repo", "download"}, |
| 22 | 27 | // Not "repo archive": that name is taken by the read-only flag, |
| 23 | 28 | // 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"}, | |
| 26 | 35 | ReadOnly: true, |
| 27 | 36 | Run: runRepoDownload, |
| 28 | 37 | }) |
internal/control/ghimport.go +8 −2
| @@ -23,8 +23,14 @@ import ( | ||
| 23 | 23 | |
| 24 | 24 | func init() { |
| 25 | 25 | 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"}, | |
| 28 | 34 | ReadsStdin: true, Run: runImportIssues}) |
| 29 | 35 | } |
| 30 | 36 | |
internal/control/help.go added +227
| @@ -0,0 +1,227 @@ | ||
| 1 | package control | |
| 2 | ||
| 3 | import ( | |
| 4 | "fmt" | |
| 5 | "io" | |
| 6 | "slices" | |
| 7 | "strings" | |
| 8 | ||
| 9 | "gitbay.org/gitbay/internal/protocol" | |
| 10 | "gitbay.org/gitbay/internal/termtext" | |
| 11 | ) | |
| 12 | ||
| 13 | func 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. | |
| 27 | var 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). | |
| 62 | func NounSummaries() map[string]string { return nounSummaries } | |
| 63 | ||
| 64 | // helpEntry is one row of the registry as help reports it. | |
| 65 | type 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. | |
| 77 | func 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. | |
| 116 | func (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 | ||
| 123 | func (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. | |
| 129 | func (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 | ||
| 145 | func (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 | ||
| 197 | func (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 @@ | ||
| 1 | package control | |
| 2 | ||
| 3 | import ( | |
| 4 | "bytes" | |
| 5 | "regexp" | |
| 6 | "slices" | |
| 7 | "strings" | |
| 8 | "testing" | |
| 9 | ||
| 10 | "gitbay.org/gitbay/internal/protocol" | |
| 11 | ) | |
| 12 | ||
| 13 | func 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 | ||
| 24 | func 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. | |
| 51 | func 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. | |
| 63 | func 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 | ||
| 70 | func 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 | ||
| 82 | func 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 | ||
| 90 | var 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. | |
| 96 | var 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. | |
| 101 | func TestHelpIsComplete(t *testing.T) { | |
| 102 | for _, cmd := range Commands() { | |
| 103 | path := strings.Join(cmd.Path, " ") | |
| 104 | inUsage := map[string]bool{} | |
| 105 | for _, f := range usageFlag.FindAllString(cmd.Usage, -1) { | |
| 106 | if f != "--json" { | |
| 107 | inUsage[f] = true | |
| 108 | } | |
| 109 | } | |
| 110 | described := map[string]bool{} | |
| 111 | for _, f := range cmd.Flags { | |
| 112 | described[f.Name] = true | |
| 113 | if f.Desc == "" { | |
| 114 | t.Errorf("%s: %s has no description", path, f.Name) | |
| 115 | } | |
| 116 | if !inUsage[f.Name] { | |
| 117 | t.Errorf("%s: %s is described but not in the usage", path, f.Name) | |
| 118 | } | |
| 119 | } | |
| 120 | for f := range inUsage { | |
| 121 | if !described[f] { | |
| 122 | t.Errorf("%s: %s is in the usage with no description", path, f) | |
| 123 | } | |
| 124 | } | |
| 125 | if len(cmd.Examples) == 0 { | |
| 126 | t.Errorf("%s: no example", path) | |
| 127 | } | |
| 128 | for _, ex := range cmd.Examples { | |
| 129 | toParse := ex | |
| 130 | if m := trailingRedirect.FindStringSubmatch(ex); m != nil { | |
| 131 | toParse = m[1] | |
| 132 | } | |
| 133 | argv, err := protocol.Tokenize(toParse) | |
| 134 | if err != nil { | |
| 135 | t.Errorf("%s: example %q: %v", path, ex, err) | |
| 136 | continue | |
| 137 | } | |
| 138 | got, _, ok := Lookup(argv) | |
| 139 | if !ok || !slices.Equal(got.Path, cmd.Path) { | |
| 140 | t.Errorf("%s: example %q runs %v", path, ex, got.Path) | |
| 141 | } | |
| 142 | } | |
| 143 | } | |
| 144 | } | |
internal/control/identity.go +20 −11
| @@ -18,6 +18,7 @@ func init() { | ||
| 18 | 18 | Path: []string{"whoami"}, |
| 19 | 19 | Summary: "show the authenticated account", |
| 20 | 20 | Usage: "whoami", |
| 21 | Examples: []string{"whoami"}, | |
| 21 | 22 | ReadOnly: true, |
| 22 | 23 | Run: runWhoami, |
| 23 | 24 | }) |
| @@ -25,27 +26,35 @@ func init() { | ||
| 25 | 26 | Path: []string{"keys", "list"}, |
| 26 | 27 | Summary: "list registered SSH keys", |
| 27 | 28 | Usage: "keys list", |
| 29 | Examples: []string{"keys list"}, | |
| 28 | 30 | ReadOnly: true, |
| 29 | 31 | Run: runKeysList, |
| 30 | 32 | }) |
| 31 | 33 | 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"}, | |
| 35 | 42 | ReadsStdin: true, |
| 36 | 43 | Run: runKeysAdd, |
| 37 | 44 | }) |
| 38 | 45 | 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, | |
| 43 | 51 | }) |
| 44 | 52 | 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, | |
| 49 | 58 | }) |
| 50 | 59 | } |
| 51 | 60 | |
internal/control/import.go +8 −2
| @@ -17,8 +17,14 @@ import ( | ||
| 17 | 17 | |
| 18 | 18 | func init() { |
| 19 | 19 | 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"}, | |
| 22 | 28 | ReadsStdin: true, Run: runRepoImport}) |
| 23 | 29 | } |
| 24 | 30 | |
internal/control/issue.go +77 −15
| @@ -16,35 +16,97 @@ const maxBodyBytes = 64 << 10 | ||
| 16 | 16 | |
| 17 | 17 | func init() { |
| 18 | 18 | 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 | }, | |
| 21 | 31 | ReadsStdin: true, Run: runIssueCreate}) |
| 22 | 32 | register(Command{Path: []string{"issue", "list"}, |
| 23 | 33 | Summary: "list issues", |
| 24 | Usage: "issue list <owner/name> [--state open|closed|all] [--label <l>] [--assignee <user>] [--author <user>] [--milestone <title>|none] [--search <text>] [--limit <n>] [--cursor <c>]", ReadOnly: true, Run: runIssueList}) | |
| 34 | Usage: "issue list <owner/name> [--state open|closed|all] [--label <l>] [--assignee <user>] [--author <user>] [--milestone <title>|none] [--search <text>] [--limit <n>] [--cursor <c>]", | |
| 35 | Flags: []Flag{ | |
| 36 | {"--state", "open|closed|all", "which issues", "open"}, | |
| 37 | {"--label", "<l>", "only issues carrying this label", ""}, | |
| 38 | {"--assignee", "<user>", "only issues assigned to this user", ""}, | |
| 39 | {"--author", "<user>", "only issues opened by this user", ""}, | |
| 40 | {"--milestone", "<title>|none", "only issues in this milestone, or in none", ""}, | |
| 41 | {"--search", "<text>", "match title and body", ""}, | |
| 42 | {"--limit", "<n>", "rows per page", ""}, | |
| 43 | {"--cursor", "<c>", "continue from the previous page", ""}, | |
| 44 | }, | |
| 45 | Examples: []string{ | |
| 46 | "issue list krz/gitbay --label bug --state all", | |
| 47 | "issue list krz/gitbay --assignee cmc", | |
| 48 | }, | |
| 49 | ReadOnly: true, Run: runIssueList}) | |
| 25 | 50 | 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}) | |
| 28 | 55 | 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 | }, | |
| 31 | 68 | ReadsStdin: true, Run: runIssueEdit}) |
| 32 | 69 | 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 | }, | |
| 35 | 81 | ReadsStdin: true, Run: runIssueComment}) |
| 36 | 82 | 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}) | |
| 39 | 87 | 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}) | |
| 42 | 92 | register(Command{Path: []string{"issue", "label"}, |
| 43 | 93 | Summary: "labels", |
| 44 | Usage: "issue label <owner/name> <n> [--add <l>]... [--remove <l>]...", Run: runIssueLabel}) | |
| 94 | Usage: "issue label <owner/name> <n> [--add <l>]... [--remove <l>]...", | |
| 95 | Flags: []Flag{ | |
| 96 | {"--add", "<l>", "label to add, may repeat", ""}, | |
| 97 | {"--remove", "<l>", "label to remove, may repeat", ""}, | |
| 98 | }, | |
| 99 | Examples: []string{"issue label krz/gitbay 42 --add bug --remove needs-triage"}, | |
| 100 | Run: runIssueLabel}) | |
| 45 | 101 | register(Command{Path: []string{"issue", "assign"}, |
| 46 | 102 | Summary: "assignees", |
| 47 | Usage: "issue assign <owner/name> <n> [--add <user>]... [--remove <user>]...", Run: runIssueAssign}) | |
| 103 | Usage: "issue assign <owner/name> <n> [--add <user>]... [--remove <user>]...", | |
| 104 | Flags: []Flag{ | |
| 105 | {"--add", "<user>", "user to assign, may repeat", ""}, | |
| 106 | {"--remove", "<user>", "user to unassign, may repeat", ""}, | |
| 107 | }, | |
| 108 | Examples: []string{"issue assign krz/gitbay 42 --add cmc"}, | |
| 109 | Run: runIssueAssign}) | |
| 48 | 110 | } |
| 49 | 111 | |
| 50 | 112 | // issueArgs parses "<owner/name> <n>" plus flags handled by the caller. |
internal/control/label.go +14 −5
| @@ -14,14 +14,23 @@ import ( | ||
| 14 | 14 | |
| 15 | 15 | func init() { |
| 16 | 16 | 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}) | |
| 19 | 21 | register(Command{Path: []string{"label", "set"}, |
| 20 | 22 | Summary: "create a label or set its colour", |
| 21 | Usage: "label set <owner/name> <label> [--color rrggbb|'']", Run: runLabelSet}) | |
| 23 | Usage: "label set <owner/name> <label> [--color rrggbb|'']", | |
| 24 | Flags: []Flag{ | |
| 25 | {"--color", "rrggbb|''", "the label's colour, or '' to clear it", ""}, | |
| 26 | }, | |
| 27 | Examples: []string{"label set krz/gitbay bug --color d73a4a"}, | |
| 28 | Run: runLabelSet}) | |
| 22 | 29 | 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}) | |
| 25 | 34 | } |
| 26 | 35 | |
| 27 | 36 | // A colour is six hex digits, with or without the hash: over bare ssh a |
internal/control/migrate.go +7 −2
| @@ -16,10 +16,15 @@ func init() { | ||
| 16 | 16 | register(Command{Path: []string{"account", "export"}, |
| 17 | 17 | Summary: "write your account bundle (profile, repos, issues, MRs) as JSON", |
| 18 | 18 | Usage: "account export > bundle.json", |
| 19 | Examples: []string{"account export > bundle.json"}, | |
| 19 | 20 | ReadOnly: true, Run: runAccountExport}) |
| 20 | 21 | 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"}, | |
| 23 | 28 | ReadsStdin: true, Run: runAccountImportBundle}) |
| 24 | 29 | } |
| 25 | 30 | |
internal/control/milestone.go +33 −12
| @@ -17,25 +17,46 @@ import ( | ||
| 17 | 17 | func init() { |
| 18 | 18 | register(Command{Path: []string{"milestone", "create"}, |
| 19 | 19 | Summary: "create a milestone", |
| 20 | Usage: "milestone create <owner/name> <title> [--description <d>] [--due YYYY-MM-DD]", Run: runMilestoneCreate}) | |
| 20 | Usage: "milestone create <owner/name> <title> [--description <d>] [--due YYYY-MM-DD]", | |
| 21 | Flags: []Flag{ | |
| 22 | {"--description", "<d>", "what the milestone covers", ""}, | |
| 23 | {"--due", "YYYY-MM-DD", "target date", ""}, | |
| 24 | }, | |
| 25 | Examples: []string{`milestone create krz/gitbay v1.31.0 --due 2026-10-01`}, | |
| 26 | Run: runMilestoneCreate}) | |
| 21 | 27 | register(Command{Path: []string{"milestone", "list"}, |
| 22 | 28 | Summary: "list milestones with progress", |
| 23 | Usage: "milestone list <owner/name> [--state open|closed|all]", ReadOnly: true, Run: runMilestoneList}) | |
| 29 | Usage: "milestone list <owner/name> [--state open|closed|all]", | |
| 30 | Flags: []Flag{ | |
| 31 | {"--state", "open|closed|all", "which milestones", "open"}, | |
| 32 | }, | |
| 33 | Examples: []string{"milestone list krz/gitbay --state all"}, | |
| 34 | ReadOnly: true, Run: runMilestoneList}) | |
| 24 | 35 | 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}) | |
| 27 | 40 | 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}) | |
| 30 | 45 | 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}) | |
| 33 | 50 | 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}) | |
| 36 | 55 | 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}) | |
| 39 | 60 | } |
| 40 | 61 | |
| 41 | 62 | var duePat = regexp.MustCompile(`^\d{4}-\d{2}-\d{2}$`) |
internal/control/mirrorcmd.go +20 −8
| @@ -16,18 +16,30 @@ import ( | ||
| 16 | 16 | |
| 17 | 17 | func init() { |
| 18 | 18 | 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"}, | |
| 21 | 27 | ReadsStdin: true, Run: runMirrorAdd}) |
| 22 | 28 | 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}) | |
| 25 | 33 | 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}) | |
| 28 | 38 | 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}) | |
| 31 | 43 | } |
| 32 | 44 | |
| 33 | 45 | func runMirrorAdd(c *Ctx, args []string) int { |
internal/control/mr.go +142 −37
| @@ -20,78 +20,183 @@ import ( | ||
| 20 | 20 | func init() { |
| 21 | 21 | register(Command{Path: []string{"repo", "fork"}, |
| 22 | 22 | Summary: "fork a repository under your account", |
| 23 | Usage: "repo fork <owner/name> [--owner <o>] [--name <n>]", Run: runRepoFork}) | |
| 23 | Usage: "repo fork <owner/name> [--owner <o>] [--name <n>]", | |
| 24 | Flags: []Flag{ | |
| 25 | {"--owner", "<o>", "fork under this user or org, default your account", ""}, | |
| 26 | {"--name", "<n>", "name the fork", "the source's name"}, | |
| 27 | }, | |
| 28 | Examples: []string{"repo fork krz/gitbay"}, | |
| 29 | Run: runRepoFork}) | |
| 24 | 30 | 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}) | |
| 27 | 35 | 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}) | |
| 30 | 40 | 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}) | |
| 33 | 45 | 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}) | |
| 36 | 50 | 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}) | |
| 39 | 55 | 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}) | |
| 42 | 60 | 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 | }, | |
| 45 | 76 | ReadsStdin: true, Run: runMRCreate}) |
| 46 | 77 | 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"}, | |
| 49 | 85 | ReadOnly: true, Run: runMRRangeDiff}) |
| 50 | 86 | register(Command{Path: []string{"mr", "revisions"}, |
| 51 | 87 | Summary: "the heads a merge request has had", |
| 52 | 88 | Usage: "mr revisions <owner/name> <n>", |
| 89 | Examples: []string{"mr revisions krz/gitbay 431"}, | |
| 53 | 90 | ReadOnly: true, Run: runMRRevisions}) |
| 54 | 91 | 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}) | |
| 57 | 96 | 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}) | |
| 60 | 101 | register(Command{Path: []string{"mr", "list"}, |
| 61 | 102 | Summary: "list merge requests", |
| 62 | Usage: "mr list <owner/name> [--state open|merged|closed|source_gone|all] [--label <l>] [--author <user>] [--milestone <title>|none] [--search <text>] [--limit <n>] [--cursor <c>]", ReadOnly: true, Run: runMRList}) | |
| 103 | Usage: "mr list <owner/name> [--state open|merged|closed|source_gone|all] [--label <l>] [--author <user>] [--milestone <title>|none] [--search <text>] [--limit <n>] [--cursor <c>]", | |
| 104 | Flags: []Flag{ | |
| 105 | {"--state", "open|merged|closed|source_gone|all", "which merge requests", "open"}, | |
| 106 | {"--label", "<l>", "only MRs carrying this label", ""}, | |
| 107 | {"--author", "<user>", "only MRs opened by this user", ""}, | |
| 108 | {"--milestone", "<title>|none", "only MRs in this milestone, or in none", ""}, | |
| 109 | {"--search", "<text>", "match title and body", ""}, | |
| 110 | {"--limit", "<n>", "rows per page", ""}, | |
| 111 | {"--cursor", "<c>", "continue from the previous page", ""}, | |
| 112 | }, | |
| 113 | Examples: []string{ | |
| 114 | "mr list krz/gitbay --state open", | |
| 115 | "mr list krz/gitbay --author cmc --state all", | |
| 116 | }, | |
| 117 | ReadOnly: true, Run: runMRList}) | |
| 63 | 118 | 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}) | |
| 66 | 123 | 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}) | |
| 69 | 128 | 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"`}, | |
| 72 | 139 | ReadsStdin: true, Run: runMREdit}) |
| 73 | 140 | 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}) | |
| 76 | 145 | 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"`}, | |
| 79 | 154 | ReadsStdin: true, Run: runMRComment}) |
| 80 | 155 | register(Command{Path: []string{"mr", "review"}, |
| 81 | 156 | Summary: "review", |
| 82 | Usage: "mr review <owner/name> <n> --approve|--request-changes|--comment|--discard", Run: runMRReview}) | |
| 157 | Usage: "mr review <owner/name> <n> --approve|--request-changes|--comment|--discard", | |
| 158 | Flags: []Flag{ | |
| 159 | {"--approve", "", "approve the merge request", ""}, | |
| 160 | {"--request-changes", "", "ask for changes", ""}, | |
| 161 | {"--comment", "", "submit pending diff comments without a verdict", ""}, | |
| 162 | {"--discard", "", "throw away pending diff comments", ""}, | |
| 163 | }, | |
| 164 | Examples: []string{"mr review krz/gitbay 431 --approve"}, | |
| 165 | Run: runMRReview}) | |
| 83 | 166 | register(Command{Path: []string{"mr", "review", "request"}, |
| 84 | 167 | Summary: "ask specific people for a review", |
| 85 | Usage: "mr review request <owner/name> <n> [--add <user>]... [--remove <user>]...", Run: runMRReviewRequest}) | |
| 168 | Usage: "mr review request <owner/name> <n> [--add <user>]... [--remove <user>]...", | |
| 169 | Flags: []Flag{ | |
| 170 | {"--add", "<user>", "reviewer to add, may repeat", ""}, | |
| 171 | {"--remove", "<user>", "reviewer to remove, may repeat", ""}, | |
| 172 | }, | |
| 173 | Examples: []string{"mr review request krz/gitbay 431 --add cmc"}, | |
| 174 | Run: runMRReviewRequest}) | |
| 86 | 175 | register(Command{Path: []string{"mr", "label"}, |
| 87 | 176 | Summary: "labels", |
| 88 | Usage: "mr label <owner/name> <n> [--add <l>]... [--remove <l>]...", Run: runMRLabel}) | |
| 177 | Usage: "mr label <owner/name> <n> [--add <l>]... [--remove <l>]...", | |
| 178 | Flags: []Flag{ | |
| 179 | {"--add", "<l>", "label to add, may repeat", ""}, | |
| 180 | {"--remove", "<l>", "label to remove, may repeat", ""}, | |
| 181 | }, | |
| 182 | Examples: []string{"mr label krz/gitbay 431 --add needs-review"}, | |
| 183 | Run: runMRLabel}) | |
| 89 | 184 | register(Command{Path: []string{"mr", "merge"}, |
| 90 | 185 | Summary: "merge", |
| 91 | Usage: "mr merge <owner/name> <n> [--strategy ff|merge|squash|rebase]", Run: runMRMerge}) | |
| 186 | Usage: "mr merge <owner/name> <n> [--strategy ff|merge|squash|rebase]", | |
| 187 | Flags: []Flag{ | |
| 188 | {"--strategy", "ff|merge|squash|rebase", "how to merge", ""}, | |
| 189 | }, | |
| 190 | Examples: []string{"mr merge krz/gitbay 431 --strategy ff"}, | |
| 191 | Run: runMRMerge}) | |
| 92 | 192 | register(Command{Path: []string{"mr", "close"}, |
| 93 | 193 | Summary: "close without merging", |
| 94 | Usage: "mr close <owner/name> <n> [--by <m>]", Run: runMRClose}) | |
| 194 | Usage: "mr close <owner/name> <n> [--by <m>]", | |
| 195 | Flags: []Flag{ | |
| 196 | {"--by", "<m>", "the MR that supersedes this one", ""}, | |
| 197 | }, | |
| 198 | Examples: []string{"mr close krz/gitbay 431"}, | |
| 199 | Run: runMRClose}) | |
| 95 | 200 | } |
| 96 | 201 | |
| 97 | 202 | // ForkOut is what `repo fork` emits: where the fork landed, and what it |
internal/control/notifications.go +48 −17
| @@ -15,47 +15,78 @@ import ( | ||
| 15 | 15 | |
| 16 | 16 | func init() { |
| 17 | 17 | 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"}, | |
| 20 | 26 | ReadOnly: true, Run: runNotificationsList}) |
| 21 | 27 | register(Command{Path: []string{"notifications", "read"}, |
| 22 | 28 | Summary: "mark notifications read", |
| 23 | Usage: "notifications read <id>... | --all", Run: runNotificationsRead}) | |
| 29 | Usage: "notifications read <id>... | --all", | |
| 30 | Flags: []Flag{ | |
| 31 | {"--all", "", "mark every unread notification read", ""}, | |
| 32 | }, | |
| 33 | Examples: []string{"notifications read 12 13", "notifications read --all"}, | |
| 34 | Run: runNotificationsRead}) | |
| 24 | 35 | register(Command{Path: []string{"notifications", "settings", "show"}, |
| 25 | 36 | Summary: "your notification preferences", |
| 26 | 37 | Usage: "notifications settings show", |
| 38 | Examples: []string{"notifications settings show"}, | |
| 27 | 39 | ReadOnly: true, Run: runNotificationsSettingsShow}) |
| 28 | 40 | 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}) | |
| 31 | 45 | 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}) | |
| 34 | 50 | register(Command{Path: []string{"notifications", "device", "add"}, |
| 35 | 51 | Summary: "register an Apple device for push, token on stdin", |
| 36 | 52 | Usage: "notifications device add [--label <name>] < token", |
| 53 | Flags: []Flag{ | |
| 54 | {"--label", "<name>", "a name for the device", ""}, | |
| 55 | }, | |
| 56 | Examples: []string{"notifications device add --label iphone < token"}, | |
| 37 | 57 | // Mandatory: without it control.go swaps in an empty reader and |
| 38 | 58 | // this command stores an empty token without erroring. |
| 39 | 59 | ReadsStdin: true, Run: runNotificationsDeviceAdd}) |
| 40 | 60 | register(Command{Path: []string{"notifications", "device", "list"}, |
| 41 | 61 | Summary: "your registered devices", |
| 42 | 62 | Usage: "notifications device list", |
| 63 | Examples: []string{"notifications device list"}, | |
| 43 | 64 | ReadOnly: true, Run: runNotificationsDeviceList}) |
| 44 | 65 | 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}) | |
| 47 | 70 | 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}) | |
| 50 | 75 | 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}) | |
| 53 | 80 | 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}) | |
| 56 | 85 | 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}) | |
| 59 | 90 | } |
| 60 | 91 | |
| 61 | 92 | // notice is one thing that happened, in the shape both delivery routes |
internal/control/org.go +28 −14
| @@ -14,29 +14,43 @@ import ( | ||
| 14 | 14 | |
| 15 | 15 | func init() { |
| 16 | 16 | 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}) | |
| 19 | 20 | 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}) | |
| 22 | 24 | 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}) | |
| 25 | 28 | 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}) | |
| 28 | 32 | register(Command{Path: []string{"org", "delete"}, |
| 29 | 33 | Summary: "delete an empty organization", |
| 30 | Usage: "org delete <name> --yes", Run: runOrgDelete}) | |
| 34 | Usage: "org delete <name> --yes", | |
| 35 | Flags: []Flag{ | |
| 36 | {"--yes", "", "confirm the permanent delete", ""}, | |
| 37 | }, | |
| 38 | Examples: []string{"org delete krz --yes"}, Run: runOrgDelete}) | |
| 31 | 39 | register(Command{Path: []string{"org", "members", "add"}, |
| 32 | 40 | Summary: "add or update a member", |
| 33 | Usage: "org members add <org> <user> [--role member|admin]", Run: runOrgMembersAdd}) | |
| 41 | Usage: "org members add <org> <user> [--role member|admin]", | |
| 42 | Flags: []Flag{ | |
| 43 | {"--role", "member|admin", "the member's role", "member"}, | |
| 44 | }, | |
| 45 | Examples: []string{"org members add krz cmc --role admin"}, Run: runOrgMembersAdd}) | |
| 34 | 46 | 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}) | |
| 37 | 50 | 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}) | |
| 40 | 54 | } |
| 41 | 55 | |
| 42 | 56 | // orgAdmin loads an org and requires the caller to be one of its admins. |
internal/control/orglabel.go +35 −11
| @@ -13,25 +13,49 @@ import ( | ||
| 13 | 13 | func init() { |
| 14 | 14 | register(Command{Path: []string{"org", "label", "set"}, |
| 15 | 15 | Summary: "create an org label every org repository sees, or set its colour; folds in same-named repo labels", |
| 16 | Usage: "org label set <org> <label> [--color rrggbb|'']", Run: runOrgLabelSet}) | |
| 16 | Usage: "org label set <org> <label> [--color rrggbb|'']", | |
| 17 | Flags: []Flag{ | |
| 18 | {"--color", "rrggbb|''", "the label's colour, or '' to clear it", ""}, | |
| 19 | }, | |
| 20 | Examples: []string{"org label set krz bug --color d73a4a"}, | |
| 21 | Run: runOrgLabelSet}) | |
| 17 | 22 | 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}) | |
| 20 | 27 | 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}) | |
| 23 | 32 | register(Command{Path: []string{"org", "milestone", "create"}, |
| 24 | 33 | Summary: "create an org milestone spanning every org repository; folds in same-titled repo milestones", |
| 25 | Usage: "org milestone create <org> <title> [--description <d>] [--due YYYY-MM-DD]", Run: runOrgMilestoneCreate}) | |
| 34 | Usage: "org milestone create <org> <title> [--description <d>] [--due YYYY-MM-DD]", | |
| 35 | Flags: []Flag{ | |
| 36 | {"--description", "<d>", "what the milestone covers", ""}, | |
| 37 | {"--due", "YYYY-MM-DD", "target date", ""}, | |
| 38 | }, | |
| 39 | Examples: []string{"org milestone create krz mobile --due 2026-12-01"}, | |
| 40 | Run: runOrgMilestoneCreate}) | |
| 26 | 41 | register(Command{Path: []string{"org", "milestone", "list"}, |
| 27 | 42 | Summary: "list an org's milestones with progress across the repositories you can read", |
| 28 | Usage: "org milestone list <org> [--state open|closed|all]", ReadOnly: true, Run: runOrgMilestoneList}) | |
| 43 | Usage: "org milestone list <org> [--state open|closed|all]", | |
| 44 | Flags: []Flag{ | |
| 45 | {"--state", "open|closed|all", "which milestones", "open"}, | |
| 46 | }, | |
| 47 | Examples: []string{"org milestone list krz --state all"}, | |
| 48 | ReadOnly: true, Run: runOrgMilestoneList}) | |
| 29 | 49 | 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}) | |
| 32 | 54 | 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}) | |
| 35 | 59 | } |
| 36 | 60 | |
| 37 | 61 | // orgReader resolves an org for a read of its labels or milestones. |
internal/control/pagescmd.go +16 −8
| @@ -20,17 +20,25 @@ import ( | ||
| 20 | 20 | |
| 21 | 21 | func init() { |
| 22 | 22 | 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}) | |
| 25 | 27 | 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}) | |
| 28 | 32 | 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}) | |
| 31 | 37 | 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}) | |
| 34 | 42 | } |
| 35 | 43 | |
| 36 | 44 | // challengeLabel prefixes the domain for the ownership TXT record. |
internal/control/profile.go +17 −4
| @@ -16,14 +16,27 @@ import ( | ||
| 16 | 16 | |
| 17 | 17 | func init() { |
| 18 | 18 | 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}) | |
| 21 | 22 | register(Command{Path: []string{"profile", "set"}, |
| 22 | 23 | Summary: "set your profile", |
| 23 | Usage: "profile set [--description <d>] [--website <url>] [--link <label|url>]... ('' clears)", Run: runProfileSet}) | |
| 24 | Usage: "profile set [--description <d>] [--website <url>] [--link <label|url>]... ('' clears)", | |
| 25 | Flags: []Flag{ | |
| 26 | {"--description", "<d>", "one line about you", ""}, | |
| 27 | {"--website", "<url>", "your website", ""}, | |
| 28 | {"--link", "<label|url>", "a profile link, may repeat", ""}, | |
| 29 | }, | |
| 30 | Examples: []string{`profile set --description "gitbay's author" --website https://cleberg.net`}, Run: runProfileSet}) | |
| 24 | 31 | register(Command{Path: []string{"org", "profile"}, |
| 25 | 32 | Summary: "show or set an org's profile", |
| 26 | Usage: "org profile <org> [--description <d>] [--website <url>] [--link <label|url>]...", Run: runOrgProfile}) | |
| 33 | Usage: "org profile <org> [--description <d>] [--website <url>] [--link <label|url>]...", | |
| 34 | Flags: []Flag{ | |
| 35 | {"--description", "<d>", "one line about the org", ""}, | |
| 36 | {"--website", "<url>", "the org's website", ""}, | |
| 37 | {"--link", "<label|url>", "a profile link, may repeat", ""}, | |
| 38 | }, | |
| 39 | Examples: []string{"org profile krz", `org profile krz --description "a self-hosted forge"`}, Run: runOrgProfile}) | |
| 27 | 40 | } |
| 28 | 41 | |
| 29 | 42 | // maxProfileLinks caps the free-form link list. A profile is a header, |
internal/control/quota.go +6 −1
| @@ -85,7 +85,12 @@ func init() { | ||
| 85 | 85 | register(Command{Path: []string{"admin", "user", "limits"}, |
| 86 | 86 | Summary: "show or set an account's repository and storage caps (instance admins)", |
| 87 | 87 | Usage: "admin user limits <username> [--repos <n>|default] [--bytes <n>|default]", |
| 88 | Run: runAdminUserLimits}) | |
| 88 | Flags: []Flag{ | |
| 89 | {"--repos", "<n>|default", "the account's repository cap", ""}, | |
| 90 | {"--bytes", "<n>|default", "the account's storage cap", ""}, | |
| 91 | }, | |
| 92 | Examples: []string{"admin user limits alice", "admin user limits alice --repos 50"}, | |
| 93 | Run: runAdminUserLimits}) | |
| 89 | 94 | } |
| 90 | 95 | |
| 91 | 96 | func runAdminUserLimits(c *Ctx, args []string) int { |
internal/control/read.go +24 −9
| @@ -17,23 +17,37 @@ import ( | ||
| 17 | 17 | |
| 18 | 18 | func init() { |
| 19 | 19 | 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"}, | |
| 23 | 27 | ReadOnly: true, |
| 24 | 28 | Run: runRepoTree, |
| 25 | 29 | }) |
| 26 | 30 | 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"}, | |
| 30 | 38 | ReadOnly: true, |
| 31 | 39 | Run: runRepoCat, |
| 32 | 40 | }) |
| 33 | 41 | 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"}, | |
| 37 | 51 | ReadOnly: true, |
| 38 | 52 | Run: runRepoBlame, |
| 39 | 53 | }) |
| @@ -41,6 +55,7 @@ func init() { | ||
| 41 | 55 | Path: []string{"repo", "refs"}, |
| 42 | 56 | Summary: "list branches and tags", |
| 43 | 57 | Usage: "repo refs <owner/name>", |
| 58 | Examples: []string{"repo refs krz/gitbay"}, | |
| 44 | 59 | ReadOnly: true, |
| 45 | 60 | Run: runRepoRefs, |
| 46 | 61 | }) |
internal/control/register.go +19 −8
| @@ -20,27 +20,38 @@ func init() { | ||
| 20 | 20 | register(Command{Path: []string{"register"}, |
| 21 | 21 | Summary: "create an account (only meaningful for unregistered keys)", |
| 22 | 22 | Usage: "register --username <name> [--email <address> | --invite <code>]", |
| 23 | Flags: []Flag{ | |
| 24 | {"--username", "<name>", "the account's username", ""}, | |
| 25 | {"--email", "<address>", "for open registration", ""}, | |
| 26 | {"--invite", "<code>", "for invite-only registration", ""}, | |
| 27 | }, | |
| 28 | Examples: []string{"register --username cmc --email cmc@example.org"}, | |
| 23 | 29 | Run: func(c *Ctx, args []string) int { |
| 24 | 30 | return c.fail(protocol.ExitUsage, |
| 25 | 31 | "this SSH key already belongs to %s. To register a new account, connect with the key it should use:\n ssh -F /dev/null -i <newkey> git@<host> register ...", |
| 26 | 32 | c.User.Username) |
| 27 | 33 | }}) |
| 28 | 34 | 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}) | |
| 31 | 38 | 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}) | |
| 34 | 42 | register(Command{Path: []string{"email", "list"}, |
| 35 | 43 | Summary: "list the addresses on your account", |
| 36 | 44 | Usage: "email list", |
| 45 | Examples: []string{"email list"}, | |
| 37 | 46 | ReadOnly: true, Run: runEmailList}) |
| 38 | 47 | 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}) | |
| 41 | 51 | 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}) | |
| 44 | 55 | } |
| 45 | 56 | |
| 46 | 57 | func runEmailList(c *Ctx, args []string) int { |
internal/control/release.go +44 −10
| @@ -20,33 +20,67 @@ import ( | ||
| 20 | 20 | |
| 21 | 21 | func init() { |
| 22 | 22 | 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 | }, | |
| 25 | 35 | ReadsStdin: true, Run: runReleaseCreate}) |
| 26 | 36 | 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"`}, | |
| 29 | 46 | ReadsStdin: true, Run: runReleaseEdit}) |
| 30 | 47 | register(Command{Path: []string{"release", "list"}, |
| 31 | 48 | Summary: "list releases", |
| 32 | Usage: "release list <owner/name> [--limit <n>] [--cursor <c>]", ReadOnly: true, Run: runReleaseList}) | |
| 49 | Usage: "release list <owner/name> [--limit <n>] [--cursor <c>]", | |
| 50 | Flags: []Flag{ | |
| 51 | {"--limit", "<n>", "rows per page", ""}, | |
| 52 | {"--cursor", "<c>", "continue from the previous page", ""}, | |
| 53 | }, | |
| 54 | Examples: []string{"release list krz/gitbay --limit 10"}, | |
| 55 | ReadOnly: true, Run: runReleaseList}) | |
| 33 | 56 | 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}) | |
| 36 | 61 | register(Command{Path: []string{"release", "delete"}, |
| 37 | 62 | Summary: "delete a release and its assets", |
| 38 | Usage: "release delete <owner/name> <tag> --yes", Run: runReleaseDelete}) | |
| 63 | Usage: "release delete <owner/name> <tag> --yes", | |
| 64 | Flags: []Flag{ | |
| 65 | {"--yes", "", "confirm the permanent delete", ""}, | |
| 66 | }, | |
| 67 | Examples: []string{"release delete krz/gitbay v1.30.0 --yes"}, | |
| 68 | Run: runReleaseDelete}) | |
| 39 | 69 | register(Command{Path: []string{"release", "asset", "add"}, |
| 40 | 70 | Summary: "upload an asset from stdin", |
| 41 | 71 | Usage: "release asset add <owner/name> <tag> <filename> < file", |
| 72 | Examples: []string{"release asset add krz/gitbay v1.30.0 gitbay-darwin-arm64 < gitbay-darwin-arm64"}, | |
| 42 | 73 | ReadsStdin: true, Run: runAssetAdd}) |
| 43 | 74 | register(Command{Path: []string{"release", "asset", "get"}, |
| 44 | 75 | Summary: "write an asset to stdout", |
| 45 | 76 | Usage: "release asset get <owner/name> <tag> <filename> > file", |
| 77 | Examples: []string{"release asset get krz/gitbay v1.30.0 gitbay-darwin-arm64 > gitbay-darwin-arm64"}, | |
| 46 | 78 | ReadOnly: true, Run: runAssetGet}) |
| 47 | 79 | 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}) | |
| 50 | 84 | } |
| 51 | 85 | |
| 52 | 86 | var 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") } | ||
| 28 | 28 | func init() { |
| 29 | 29 | register(Command{Path: []string{"repo", "create"}, |
| 30 | 30 | Summary: "create a repository", |
| 31 | Usage: "repo create <owner/name> [--private]", Run: runRepoCreate}) | |
| 31 | Usage: "repo create <owner/name> [--private]", | |
| 32 | Flags: []Flag{ | |
| 33 | {"--private", "", "create it private", ""}, | |
| 34 | }, | |
| 35 | Examples: []string{"repo create krz/newthing --private"}, | |
| 36 | Run: runRepoCreate}) | |
| 32 | 37 | register(Command{Path: []string{"repo", "list"}, |
| 33 | 38 | Summary: "list repositories you own or can access", |
| 34 | Usage: "repo list [--limit <n>] [--cursor <c>]", ReadOnly: true, Run: runRepoList}) | |
| 39 | Usage: "repo list [--limit <n>] [--cursor <c>]", | |
| 40 | Flags: []Flag{ | |
| 41 | {"--limit", "<n>", "rows per page", ""}, | |
| 42 | {"--cursor", "<c>", "continue from the previous page", ""}, | |
| 43 | }, | |
| 44 | Examples: []string{"repo list --limit 20"}, | |
| 45 | ReadOnly: true, Run: runRepoList}) | |
| 35 | 46 | 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}) | |
| 38 | 51 | 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}) | |
| 41 | 56 | 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}) | |
| 44 | 61 | register(Command{Path: []string{"repo", "delete"}, |
| 45 | 62 | Summary: "delete a repository", |
| 46 | Usage: "repo delete <owner/name> --yes", Run: runRepoDelete}) | |
| 63 | Usage: "repo delete <owner/name> --yes", | |
| 64 | Flags: []Flag{ | |
| 65 | {"--yes", "", "confirm the permanent delete", ""}, | |
| 66 | }, | |
| 67 | Examples: []string{"repo delete cmc/scratch --yes"}, | |
| 68 | Run: runRepoDelete}) | |
| 47 | 69 | 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}) | |
| 50 | 74 | 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}) | |
| 53 | 79 | 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}) | |
| 56 | 84 | 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}) | |
| 59 | 89 | 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}) | |
| 62 | 94 | 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}) | |
| 65 | 99 | 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}) | |
| 68 | 104 | 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}) | |
| 71 | 109 | 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}) | |
| 74 | 114 | 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}) | |
| 77 | 119 | 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}) | |
| 80 | 124 | 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}) | |
| 83 | 129 | 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}) | |
| 86 | 134 | 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}) | |
| 89 | 139 | 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}) | |
| 92 | 144 | 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}) | |
| 95 | 149 | 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}) | |
| 98 | 154 | 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}) | |
| 101 | 159 | 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}) | |
| 104 | 164 | register(Command{Path: []string{"repo", "grep"}, |
| 105 | 165 | Summary: "search file contents", |
| 106 | Usage: "repo grep <owner/name> <query> [--ref <ref>]", ReadOnly: true, Run: runRepoGrep}) | |
| 166 | Usage: "repo grep <owner/name> <query> [--ref <ref>]", | |
| 167 | Flags: []Flag{ | |
| 168 | {"--ref", "<ref>", "branch, tag or commit to search", "the default branch"}, | |
| 169 | }, | |
| 170 | Examples: []string{"repo grep krz/gitbay TODO"}, | |
| 171 | ReadOnly: true, Run: runRepoGrep}) | |
| 107 | 172 | 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}) | |
| 110 | 177 | 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}) | |
| 113 | 182 | 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}) | |
| 116 | 187 | 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}) | |
| 119 | 192 | 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}) | |
| 122 | 197 | 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}) | |
| 125 | 202 | } |
| 126 | 203 | |
| 127 | 204 | const ( |
internal/control/runnerrepo.go +9 −4
| @@ -21,13 +21,18 @@ func init() { | ||
| 21 | 21 | register(Command{Path: []string{"repo", "runner", "add"}, |
| 22 | 22 | Summary: "attach a runner's public key to a repository", |
| 23 | 23 | Usage: "repo runner add <owner/name> < key.pub", |
| 24 | Examples: []string{"repo runner add krz/gitbay < key.pub"}, | |
| 24 | 25 | ReadsStdin: true, Run: runRepoRunnerAdd}) |
| 25 | 26 | 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}) | |
| 28 | 31 | 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}) | |
| 31 | 36 | } |
| 32 | 37 | |
| 33 | 38 | func runRepoRunnerAdd(c *Ctx, args []string) int { |
internal/control/search.go +9 −2
| @@ -12,8 +12,15 @@ import ( | ||
| 12 | 12 | |
| 13 | 13 | func init() { |
| 14 | 14 | 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 | }, | |
| 17 | 24 | ReadOnly: true, Run: runSearch}) |
| 18 | 25 | } |
| 19 | 26 | |
internal/control/sig.go +19 −7
| @@ -18,21 +18,33 @@ import ( | ||
| 18 | 18 | |
| 19 | 19 | func init() { |
| 20 | 20 | 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}) | |
| 23 | 25 | 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}) | |
| 26 | 29 | 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}) | |
| 29 | 33 | register(Command{Path: []string{"repo", "commit"}, |
| 30 | 34 | Summary: "show one commit with its patch", |
| 31 | 35 | Usage: "repo commit <owner/name> <sha>", |
| 36 | Examples: []string{"repo commit krz/gitbay a1b2c3d"}, | |
| 32 | 37 | ReadOnly: true, Run: runRepoCommit}) |
| 33 | 38 | register(Command{Path: []string{"repo", "log"}, |
| 34 | 39 | Summary: "commit log with signature states", |
| 35 | Usage: "repo log <owner/name> [--ref <r>] [--limit n] [--path <file>]", ReadOnly: true, Run: runRepoLog}) | |
| 40 | Usage: "repo log <owner/name> [--ref <r>] [--limit n] [--path <file>]", | |
| 41 | Flags: []Flag{ | |
| 42 | {"--ref", "<r>", "branch, tag or commit to start from", "the default branch"}, | |
| 43 | {"--limit", "n", "rows to show", "30"}, | |
| 44 | {"--path", "<file>", "only commits touching this path", ""}, | |
| 45 | }, | |
| 46 | Examples: []string{"repo log krz/gitbay --limit 10"}, | |
| 47 | ReadOnly: true, Run: runRepoLog}) | |
| 36 | 48 | } |
| 37 | 49 | |
| 38 | 50 | func runPGPAdd(c *Ctx, args []string) int { |
internal/control/snippet.go +38 −12
| @@ -19,31 +19,57 @@ const maxSnippetFiles = 64 | ||
| 19 | 19 | |
| 20 | 20 | func init() { |
| 21 | 21 | 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"}, | |
| 24 | 29 | ReadsStdin: true, Run: runSnippetCreate}) |
| 25 | 30 | 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}) | |
| 28 | 35 | register(Command{Path: []string{"snippet", "list"}, |
| 29 | 36 | Summary: "list your snippets, or an owner's public ones", |
| 30 | Usage: "snippet list [<owner>] [--limit n] [--cursor c]", ReadOnly: true, Run: runSnippetList}) | |
| 37 | Usage: "snippet list [<owner>] [--limit n] [--cursor c]", | |
| 38 | Flags: []Flag{ | |
| 39 | {"--limit", "n", "rows per page", ""}, | |
| 40 | {"--cursor", "c", "continue from the previous page", ""}, | |
| 41 | }, | |
| 42 | Examples: []string{"snippet list cmc"}, | |
| 43 | ReadOnly: true, Run: runSnippetList}) | |
| 31 | 44 | register(Command{Path: []string{"snippet", "edit"}, |
| 32 | 45 | Summary: "change a snippet's description or visibility", |
| 33 | Usage: "snippet edit <id> [--description <d>] [--visibility public|unlisted|private]", Run: runSnippetEdit}) | |
| 46 | Usage: "snippet edit <id> [--description <d>] [--visibility public|unlisted|private]", | |
| 47 | Flags: []Flag{ | |
| 48 | {"--description", "<d>", "one line about the snippet", ""}, | |
| 49 | {"--visibility", "public|unlisted|private", "who can find it", ""}, | |
| 50 | }, | |
| 51 | Examples: []string{"snippet edit a1b2c3 --visibility public"}, | |
| 52 | Run: runSnippetEdit}) | |
| 34 | 53 | 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}) | |
| 37 | 58 | register(Command{Path: []string{"snippet", "file", "set"}, |
| 38 | 59 | Summary: "add a file to a snippet, or replace one, from stdin", |
| 39 | 60 | Usage: "snippet file set <id> <filename> < file", |
| 61 | Examples: []string{"snippet file set a1b2c3 notes.md < notes.md"}, | |
| 40 | 62 | ReadsStdin: true, Run: runSnippetFileSet}) |
| 41 | 63 | 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}) | |
| 44 | 68 | 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}) | |
| 47 | 73 | } |
| 48 | 74 | |
| 49 | 75 | type SnippetFileOut struct { |
internal/control/status.go +14 −3
| @@ -15,10 +15,21 @@ func init() { | ||
| 15 | 15 | register(Command{Path: []string{"status", "set"}, |
| 16 | 16 | Summary: "report a commit status (CI)", |
| 17 | 17 | Usage: "status set <owner/name> <sha> --context <c> --state pending|success|failure|error [--description <d>] [--url <u>]", |
| 18 | Run: runStatusSet}) | |
| 18 | Flags: []Flag{ | |
| 19 | {"--context", "<c>", "the check this status reports for", ""}, | |
| 20 | {"--state", "pending|success|failure|error", "the check's outcome", ""}, | |
| 21 | {"--description", "<d>", "short text shown beside the state", ""}, | |
| 22 | {"--url", "<u>", "link to the check's own output", ""}, | |
| 23 | }, | |
| 24 | Examples: []string{ | |
| 25 | "status set krz/gitbay a1b2c3d --context ci/build --state success", | |
| 26 | }, | |
| 27 | Run: runStatusSet}) | |
| 19 | 28 | 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}) | |
| 22 | 33 | } |
| 23 | 34 | |
| 24 | 35 | var validStatusState = map[string]bool{"pending": true, "success": true, "failure": true, "error": true} |
internal/control/teams.go +27 −18
| @@ -14,32 +14,41 @@ import ( | ||
| 14 | 14 | |
| 15 | 15 | func init() { |
| 16 | 16 | 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}) | |
| 19 | 20 | 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}) | |
| 22 | 24 | 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}) | |
| 25 | 28 | 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}) | |
| 28 | 32 | 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}) | |
| 31 | 36 | 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}) | |
| 34 | 40 | 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}) | |
| 37 | 44 | 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}) | |
| 40 | 48 | 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}) | |
| 43 | 52 | } |
| 44 | 53 | |
| 45 | 54 | // orgAdminRef resolves an org and requires the caller to admin it. |
internal/control/theme.go +4 −2
| @@ -11,10 +11,12 @@ func init() { | ||
| 11 | 11 | register(Command{Path: []string{"web", "theme", "show"}, |
| 12 | 12 | Summary: "the colour scheme the web UI uses for you", |
| 13 | 13 | Usage: "web theme show", |
| 14 | Examples: []string{"web theme show"}, | |
| 14 | 15 | ReadOnly: true, Run: runWebThemeShow}) |
| 15 | 16 | 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}) | |
| 18 | 20 | } |
| 19 | 21 | |
| 20 | 22 | // themes are the values the layout knows how to stamp. system is the |
internal/control/token.go +14 −6
| @@ -16,14 +16,22 @@ func init() { | ||
| 16 | 16 | register(Command{Path: []string{"token", "create"}, |
| 17 | 17 | Summary: "mint an API token (shown once)", |
| 18 | 18 | Usage: "token create --name <n> [--scope full|read] [--ttl 30d|720h]", |
| 19 | Run: runTokenCreate}) | |
| 19 | Flags: []Flag{ | |
| 20 | {"--name", "<n>", "the token's name", ""}, | |
| 21 | {"--scope", "full|read", "what the token may do", "full"}, | |
| 22 | {"--ttl", "30d|720h", "how long the token is valid", "never expires"}, | |
| 23 | }, | |
| 24 | Examples: []string{"token create --name laptop --scope read --ttl 30d"}, | |
| 25 | Run: runTokenCreate}) | |
| 20 | 26 | 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}) | |
| 23 | 30 | 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}) | |
| 27 | 35 | } |
| 28 | 36 | |
| 29 | 37 | // parseTTL accepts Go durations plus a day suffix ("30d"). |
internal/control/web.go +11 −5
| @@ -14,14 +14,20 @@ func newStoredToken() (token, hash string, err error) { return store.NewToken() | ||
| 14 | 14 | |
| 15 | 15 | func init() { |
| 16 | 16 | 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}) | |
| 19 | 20 | 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}) | |
| 22 | 24 | register(Command{Path: []string{"web", "sessions", "revoke"}, |
| 23 | 25 | Summary: "end a browser session, or all of them", |
| 24 | Usage: "web sessions revoke <id>|--all", Run: runWebSessionsRevoke}) | |
| 26 | Usage: "web sessions revoke <id>|--all", | |
| 27 | Flags: []Flag{ | |
| 28 | {"--all", "", "revoke every browser session", ""}, | |
| 29 | }, | |
| 30 | Examples: []string{"web sessions revoke --all"}, Run: runWebSessionsRevoke}) | |
| 25 | 31 | } |
| 26 | 32 | |
| 27 | 33 | func runWebSessionsList(c *Ctx, args []string) int { |
internal/control/webhook.go +22 −8
| @@ -15,19 +15,33 @@ import ( | ||
| 15 | 15 | func init() { |
| 16 | 16 | register(Command{Path: []string{"webhook", "add"}, |
| 17 | 17 | Summary: "add a webhook", |
| 18 | Usage: "webhook add <owner/name> <url> [--secret <s>] [--events push,issue.created|*]", Run: runWebhookAdd}) | |
| 18 | Usage: "webhook add <owner/name> <url> [--secret <s>] [--events push,issue.created|*]", | |
| 19 | Flags: []Flag{ | |
| 20 | {"--secret", "<s>", "signs deliveries so the receiver can verify them", ""}, | |
| 21 | {"--events", "push,issue.created|*", "which events to send", "*"}, | |
| 22 | }, | |
| 23 | Examples: []string{"webhook add krz/gitbay https://ci.example.org/hook --events push"}, | |
| 24 | Run: runWebhookAdd}) | |
| 19 | 25 | 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}) | |
| 22 | 29 | 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}) | |
| 25 | 33 | register(Command{Path: []string{"webhook", "deliveries"}, |
| 26 | 34 | Summary: "recent deliveries", |
| 27 | Usage: "webhook deliveries <owner/name> [--limit n]", ReadOnly: true, Run: runWebhookDeliveries}) | |
| 35 | Usage: "webhook deliveries <owner/name> [--limit n]", | |
| 36 | Flags: []Flag{ | |
| 37 | {"--limit", "n", "rows to show", "20"}, | |
| 38 | }, | |
| 39 | Examples: []string{"webhook deliveries krz/gitbay --limit 50"}, | |
| 40 | ReadOnly: true, Run: runWebhookDeliveries}) | |
| 28 | 41 | 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}) | |
| 31 | 45 | } |
| 32 | 46 | |
| 33 | 47 | func runWebhookAdd(c *Ctx, args []string) int { |
internal/control/wiki.go +2
| @@ -18,6 +18,7 @@ func init() { | ||
| 18 | 18 | Path: []string{"wiki", "list"}, |
| 19 | 19 | Summary: "list a repository's wiki pages", |
| 20 | 20 | Usage: "wiki list <owner/name>", |
| 21 | Examples: []string{"wiki list krz/gitbay"}, | |
| 21 | 22 | ReadOnly: true, |
| 22 | 23 | Run: runWikiList, |
| 23 | 24 | }) |
| @@ -25,6 +26,7 @@ func init() { | ||
| 25 | 26 | Path: []string{"wiki", "show"}, |
| 26 | 27 | Summary: "print a wiki page", |
| 27 | 28 | Usage: "wiki show <owner/name> [<page>]", |
| 29 | Examples: []string{"wiki show krz/gitbay Quickstart"}, | |
| 28 | 30 | ReadOnly: true, |
| 29 | 31 | Run: runWikiShow, |
| 30 | 32 | }) |