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