help: filter by prefix, and document every command's arguments !129
36 files changed, +468 −160
Layout: unified · split
cmd/gitbay/coverage_test.go +1 −1
| @@ -13,7 +13,7 @@ import ( | |||
| 13 | // registry must be reachable by typing it, or the CLI is not the | 13 | // registry must be reachable by typing it, or the CLI is not the |
| 14 | // complete interface the design claims. | 14 | // complete interface the design claims. |
| 15 | var notInCLI = map[string]string{ | 15 | var notInCLI = map[string]string{ |
| 16 | "help": "cobra provides its own", | 16 | "help": "cobra owns `gitbay help`; the registry's is reached by `<cmd> --help`", |
| 17 | "runner next": "the CI runner's wire protocol, not for humans", | 17 | "runner next": "the CI runner's wire protocol, not for humans", |
| 18 | "runner done": "the CI runner's wire protocol, not for humans", | 18 | "runner done": "the CI runner's wire protocol, not for humans", |
| 19 | "runner log": "the CI runner's wire protocol, not for humans", | 19 | "runner log": "the CI runner's wire protocol, not for humans", |
cmd/gitbay/main.go +14 −2
| @@ -119,10 +119,12 @@ func pass(use, short string, o passOpts) *cobra.Command { | |||
| 119 | }, | 119 | }, |
| 120 | DisableFlagParsing: true, | 120 | DisableFlagParsing: true, |
| 121 | RunE: func(cmd *cobra.Command, args []string) error { | 121 | RunE: func(cmd *cobra.Command, args []string) error { |
| 122 | // cobra still owns `forge <cmd> --help`. | 122 | // The registry is the only place flags are written down, so |
| 123 | // --help asks the server rather than reprinting the one-line | ||
| 124 | // summary cobra holds. | ||
| 123 | for _, a := range args { | 125 | for _, a := range args { |
| 124 | if a == "--help" || a == "-h" { | 126 | if a == "--help" || a == "-h" { |
| 125 | return cmd.Help() | 127 | os.Exit(runServerHelp(o)) |
| 126 | } | 128 | } |
| 127 | } | 129 | } |
| 128 | os.Exit(runPass(o, args)) | 130 | os.Exit(runPass(o, args)) |
| @@ -131,6 +133,16 @@ func pass(use, short string, o passOpts) *cobra.Command { | |||
| 131 | } | 133 | } |
| 132 | } | 134 | } |
| 133 | 135 | ||
| 136 | // runServerHelp prints the registry's usage for one command. | ||
| 137 | func runServerHelp(o passOpts) int { | ||
| 138 | t, err := resolveTarget() | ||
| 139 | if err != nil { | ||
| 140 | fmt.Fprintln(os.Stderr, "gitbay:", err) | ||
| 141 | return protocol.ExitFailure | ||
| 142 | } | ||
| 143 | return runSSH(t, append([]string{"help"}, o.server...), strings.NewReader("")) | ||
| 144 | } | ||
| 145 | |||
| 134 | func runPass(o passOpts, args []string) int { | 146 | func runPass(o passOpts, args []string) int { |
| 135 | t, err := resolveTarget() | 147 | t, err := resolveTarget() |
| 136 | if err != nil { | 148 | if err != nil { |
e2e/api_test.go +35 −3
| @@ -7,6 +7,8 @@ import ( | |||
| 7 | "io" | 7 | "io" |
| 8 | "net/http" | 8 | "net/http" |
| 9 | "net/url" | 9 | "net/url" |
| 10 | "os" | ||
| 11 | "path/filepath" | ||
| 10 | "strings" | 12 | "strings" |
| 11 | "testing" | 13 | "testing" |
| 12 | "time" | 14 | "time" |
| @@ -96,11 +98,41 @@ func TestJSONAPI(t *testing.T) { | |||
| 96 | t.Fatalf("unknown command: %d", status) | 98 | t.Fatalf("unknown command: %d", status) |
| 97 | } | 99 | } |
| 98 | 100 | ||
| 99 | // Raw-output commands (no envelope) are wrapped. | 101 | // Raw-output commands (no envelope) are wrapped. Reading a file is the |
| 100 | status, body = inst.apiCall(t, token, []string{"help"}, "") | 102 | // cheapest raw output, so give the repo one commit to read from. |
| 101 | if status != 200 || !strings.Contains(body["output"].(string), "repo create") { | 103 | work := t.TempDir() |
| 104 | aliceEnv := inst.gitEnv(aliceKey) | ||
| 105 | mustGit(t, work, aliceEnv, "clone", inst.sshURL("alice/proj"), "proj") | ||
| 106 | dir := filepath.Join(work, "proj") | ||
| 107 | if err := os.WriteFile(filepath.Join(dir, "README"), []byte("plain text, not json\n"), 0o644); err != nil { | ||
| 108 | t.Fatal(err) | ||
| 109 | } | ||
| 110 | mustGit(t, dir, aliceEnv, "checkout", "-q", "-b", "main") | ||
| 111 | mustGit(t, dir, aliceEnv, "add", "README") | ||
| 112 | mustGit(t, dir, aliceEnv, "commit", "-q", "-m", "init") | ||
| 113 | mustGit(t, dir, aliceEnv, "push", "-q", "origin", "main") | ||
| 114 | |||
| 115 | // A tarball is not an envelope, so it comes back under "output". | ||
| 116 | status, body = inst.apiCall(t, token, []string{"repo", "download", "alice/proj"}, "") | ||
| 117 | raw, isRaw := body["output"].(string) | ||
| 118 | if status != 200 || !isRaw || raw == "" { | ||
| 119 | t.Fatalf("repo download via API: %d %v", status, body) | ||
| 120 | } | ||
| 121 | |||
| 122 | // help answers with the registry as data, so a consumer can read one | ||
| 123 | // command's arguments without scraping the whole listing. | ||
| 124 | status, body = inst.apiCall(t, token, []string{"help", "issue", "create"}, "") | ||
| 125 | if status != 200 { | ||
| 102 | t.Fatalf("help via API: %d %v", status, body) | 126 | t.Fatalf("help via API: %d %v", status, body) |
| 103 | } | 127 | } |
| 128 | rows, ok := body["data"].([]any) | ||
| 129 | if !ok || len(rows) != 1 { | ||
| 130 | t.Fatalf("help issue create: %v", body) | ||
| 131 | } | ||
| 132 | row := rows[0].(map[string]any) | ||
| 133 | if row["path"] != "issue create" || !strings.Contains(row["usage"].(string), "--title") { | ||
| 134 | t.Fatalf("help issue create row: %v", row) | ||
| 135 | } | ||
| 104 | 136 | ||
| 105 | // Git transport is refused by name. | 137 | // Git transport is refused by name. |
| 106 | if status, _ = inst.apiCall(t, token, []string{"git-upload-pack", "alice/proj"}, ""); status != 400 { | 138 | if status, _ = inst.apiCall(t, token, []string{"git-upload-pack", "alice/proj"}, ""); status != 400 { |
internal/control/audit.go +2 −1
| @@ -10,7 +10,8 @@ import ( | |||
| 10 | 10 | ||
| 11 | func init() { | 11 | func init() { |
| 12 | register(Command{Path: []string{"audit"}, | 12 | register(Command{Path: []string{"audit"}, |
| 13 | Summary: "instance audit log (admins): audit [--limit <n>]", ReadOnly: true, SSHOnly: true, Run: runAudit}) | 13 | Summary: "instance audit log (admins)", |
| 14 | Usage: "audit [--limit <n>]", ReadOnly: true, SSHOnly: true, Run: runAudit}) | ||
| 14 | } | 15 | } |
| 15 | 16 | ||
| 16 | func runAudit(c *Ctx, args []string) int { | 17 | func runAudit(c *Ctx, args []string) int { |
internal/control/build.go +22 −11
| @@ -18,37 +18,48 @@ import ( | |||
| 18 | 18 | ||
| 19 | func init() { | 19 | func init() { |
| 20 | register(Command{Path: []string{"build", "list"}, | 20 | register(Command{Path: []string{"build", "list"}, |
| 21 | Summary: "list recent builds: build list <owner/name>", ReadOnly: true, Run: runBuildList}) | 21 | Summary: "list recent builds", |
| 22 | Usage: "build list <owner/name>", ReadOnly: true, Run: runBuildList}) | ||
| 22 | register(Command{Path: []string{"build", "show"}, | 23 | register(Command{Path: []string{"build", "show"}, |
| 23 | Summary: "show one build: build show <owner/name> <n>", ReadOnly: true, Run: runBuildShow}) | 24 | Summary: "show one build", |
| 25 | Usage: "build show <owner/name> <n>", ReadOnly: true, Run: runBuildShow}) | ||
| 24 | register(Command{Path: []string{"build", "log"}, | 26 | register(Command{Path: []string{"build", "log"}, |
| 25 | Summary: "print a build's log: build log <owner/name> <n>", ReadOnly: true, Run: runBuildLog}) | 27 | Summary: "print a build's log", |
| 28 | Usage: "build log <owner/name> <n>", ReadOnly: true, Run: runBuildLog}) | ||
| 26 | 29 | ||
| 27 | register(Command{Path: []string{"build", "jobs"}, | 30 | register(Command{Path: []string{"build", "jobs"}, |
| 28 | Summary: "list the jobs a trigger can name: build jobs <owner/name>", ReadOnly: true, Run: runBuildJobs}) | 31 | Summary: "list the jobs a trigger can name", |
| 32 | Usage: "build jobs <owner/name>", ReadOnly: true, Run: runBuildJobs}) | ||
| 29 | 33 | ||
| 30 | register(Command{Path: []string{"build", "trigger"}, | 34 | register(Command{Path: []string{"build", "trigger"}, |
| 31 | Summary: "queue a job now (scheduled or not): build trigger <owner/name> <job>", Run: runBuildTrigger}) | 35 | Summary: "queue a job now (scheduled or not)", |
| 36 | Usage: "build trigger <owner/name> <job>", Run: runBuildTrigger}) | ||
| 32 | // Secrets: set over stdin, listed by name only, injected into the | 37 | // Secrets: set over stdin, listed by name only, injected into the |
| 33 | // repo's builds as environment variables. Same discipline as mirror | 38 | // repo's builds as environment variables. Same discipline as mirror |
| 34 | // tokens — the value never appears in argv, logs, or output. | 39 | // tokens — the value never appears in argv, logs, or output. |
| 35 | register(Command{Path: []string{"repo", "secret", "set"}, | 40 | register(Command{Path: []string{"repo", "secret", "set"}, |
| 36 | Summary: "set a build secret: repo secret set <owner/name> <NAME> (value on stdin)", | 41 | Summary: "set a build secret", |
| 42 | Usage: "repo secret set <owner/name> <NAME> (value on stdin)", | ||
| 37 | ReadsStdin: true, SSHOnly: true, Run: runSecretSet}) | 43 | ReadsStdin: true, SSHOnly: true, Run: runSecretSet}) |
| 38 | register(Command{Path: []string{"repo", "secret", "remove"}, | 44 | register(Command{Path: []string{"repo", "secret", "remove"}, |
| 39 | Summary: "remove a build secret: repo secret remove <owner/name> <NAME>", Run: runSecretRemove}) | 45 | Summary: "remove a build secret", |
| 46 | Usage: "repo secret remove <owner/name> <NAME>", Run: runSecretRemove}) | ||
| 40 | register(Command{Path: []string{"repo", "secret", "list"}, | 47 | register(Command{Path: []string{"repo", "secret", "list"}, |
| 41 | Summary: "list build secret names: repo secret list <owner/name>", ReadOnly: true, Run: runSecretList}) | 48 | Summary: "list build secret names", |
| 49 | Usage: "repo secret list <owner/name>", ReadOnly: true, Run: runSecretList}) | ||
| 42 | 50 | ||
| 43 | // Runner commands: the claim/report loop for gitbay-runner. Admin-only — | 51 | // Runner commands: the claim/report loop for gitbay-runner. Admin-only — |
| 44 | // a runner executes arbitrary repo code, so handing out jobs is the | 52 | // a runner executes arbitrary repo code, so handing out jobs is the |
| 45 | // instance operator's call. | 53 | // instance operator's call. |
| 46 | register(Command{Path: []string{"runner", "next"}, | 54 | register(Command{Path: []string{"runner", "next"}, |
| 47 | Summary: "claim the oldest pending build (runner protocol)", SSHOnly: true, Run: runRunnerNext}) | 55 | Summary: "claim the oldest pending build (runner protocol)", |
| 56 | Usage: "runner next", SSHOnly: true, Run: runRunnerNext}) | ||
| 48 | register(Command{Path: []string{"runner", "log"}, | 57 | register(Command{Path: []string{"runner", "log"}, |
| 49 | Summary: "append a build's log from stdin: runner log <build-id>", SSHOnly: true, ReadsStdin: true, Run: runRunnerLog}) | 58 | Summary: "append a build's log from stdin", |
| 59 | Usage: "runner log <build-id>", SSHOnly: true, ReadsStdin: true, Run: runRunnerLog}) | ||
| 50 | register(Command{Path: []string{"runner", "done"}, | 60 | register(Command{Path: []string{"runner", "done"}, |
| 51 | Summary: "finish a build: runner done <build-id> success|failure", SSHOnly: true, Run: runRunnerDone}) | 61 | Summary: "finish a build", |
| 62 | Usage: "runner done <build-id> success|failure", SSHOnly: true, Run: runRunnerDone}) | ||
| 52 | } | 63 | } |
| 53 | 64 | ||
| 54 | type buildOut struct { | 65 | type buildOut struct { |
internal/control/commitfile.go +3 −2
| @@ -12,8 +12,9 @@ import ( | |||
| 12 | 12 | ||
| 13 | func init() { | 13 | func init() { |
| 14 | register(Command{ | 14 | register(Command{ |
| 15 | Path: []string{"repo", "commit-file"}, | 15 | Path: []string{"repo", "commit-file"}, |
| 16 | Summary: "write a file and commit it: repo commit-file <owner/name> <path> " + | 16 | Summary: "write a file and commit it", |
| 17 | Usage: "repo commit-file <owner/name> <path> " + | ||
| 17 | "--ref <branch> [--message <m>] [--file -]", | 18 | "--ref <branch> [--message <m>] [--file -]", |
| 18 | ReadsStdin: true, | 19 | ReadsStdin: true, |
| 19 | Run: runCommitFile, | 20 | Run: runCommitFile, |
internal/control/control.go +42 −6
| @@ -9,6 +9,7 @@ import ( | |||
| 9 | "io" | 9 | "io" |
| 10 | "reflect" | 10 | "reflect" |
| 11 | "slices" | 11 | "slices" |
| 12 | "strings" | ||
| 12 | 13 | ||
| 13 | "gitbay.org/gitbay/internal/config" | 14 | "gitbay.org/gitbay/internal/config" |
| 14 | "gitbay.org/gitbay/internal/protocol" | 15 | "gitbay.org/gitbay/internal/protocol" |
| @@ -36,8 +37,12 @@ type Ctx struct { | |||
| 36 | } | 37 | } |
| 37 | 38 | ||
| 38 | type Command struct { | 39 | type Command struct { |
| 39 | Path []string // e.g. ["keys", "add"] | 40 | Path []string // e.g. ["keys", "add"] |
| 41 | // Summary is one line of prose: what the command does, no argument | ||
| 42 | // syntax. Usage is the argument syntax, opening with the command path. | ||
| 43 | // help renders them separately, so neither may carry the other's job. | ||
| 40 | Summary string | 44 | Summary string |
| 45 | Usage string | ||
| 41 | ReadsStdin bool | 46 | ReadsStdin bool |
| 42 | ReadOnly bool // safe for read-scoped API tokens | 47 | ReadOnly bool // safe for read-scoped API tokens |
| 43 | SSHOnly bool // refused over the HTTP API (credential minting) | 48 | SSHOnly bool // refused over the HTTP API (credential minting) |
| @@ -161,13 +166,44 @@ func init() { | |||
| 161 | register(Command{ | 166 | register(Command{ |
| 162 | Path: []string{"help"}, | 167 | Path: []string{"help"}, |
| 163 | Summary: "list available commands", | 168 | Summary: "list available commands", |
| 169 | Usage: "help [<prefix>...]", | ||
| 164 | ReadOnly: true, | 170 | ReadOnly: true, |
| 165 | Run: func(c *Ctx, args []string) int { | 171 | Run: runHelp, |
| 166 | for _, cmd := range registry { | 172 | }) |
| 167 | fmt.Fprintf(c.Stdout, "%-24s %s\n", joinPath(cmd.Path), cmd.Summary) | 173 | } |
| 174 | |||
| 175 | // helpEntry is one row of the registry as help reports it. | ||
| 176 | type helpEntry struct { | ||
| 177 | Path string `json:"path"` | ||
| 178 | Summary string `json:"summary"` | ||
| 179 | Usage string `json:"usage"` | ||
| 180 | } | ||
| 181 | |||
| 182 | // runHelp lists the registry, sorted, so a noun's commands sit together. | ||
| 183 | // A prefix narrows the listing and adds each command's argument syntax — | ||
| 184 | // the only place flags are written down. The unfiltered listing stays one | ||
| 185 | // line per command. | ||
| 186 | func runHelp(c *Ctx, args []string) int { | ||
| 187 | prefix := joinPath(args) | ||
| 188 | var matched []helpEntry | ||
| 189 | for _, cmd := range registry { | ||
| 190 | p := joinPath(cmd.Path) | ||
| 191 | if prefix != "" && p != prefix && !strings.HasPrefix(p, prefix+" ") { | ||
| 192 | continue | ||
| 193 | } | ||
| 194 | matched = append(matched, helpEntry{Path: p, Summary: cmd.Summary, Usage: cmd.Usage}) | ||
| 195 | } | ||
| 196 | if len(matched) == 0 { | ||
| 197 | return c.fail(protocol.ExitNotFound, "no command matches %q; try: help", prefix) | ||
| 198 | } | ||
| 199 | slices.SortFunc(matched, func(a, b helpEntry) int { return strings.Compare(a.Path, b.Path) }) | ||
| 200 | return c.emit(matched, func(w io.Writer) { | ||
| 201 | for _, e := range matched { | ||
| 202 | fmt.Fprintf(w, "%-24s %s\n", e.Path, e.Summary) | ||
| 203 | if prefix != "" { | ||
| 204 | fmt.Fprintf(w, " %s\n", e.Usage) | ||
| 168 | } | 205 | } |
| 169 | return protocol.ExitOK | 206 | } |
| 170 | }, | ||
| 171 | }) | 207 | }) |
| 172 | } | 208 | } |
| 173 | 209 | ||
internal/control/control_test.go +77
| @@ -1,6 +1,10 @@ | |||
| 1 | package control | 1 | package control |
| 2 | 2 | ||
| 3 | import ( | 3 | import ( |
| 4 | "bytes" | ||
| 5 | "encoding/json" | ||
| 6 | "io" | ||
| 7 | "slices" | ||
| 4 | "strings" | 8 | "strings" |
| 5 | "testing" | 9 | "testing" |
| 6 | 10 | ||
| @@ -86,3 +90,76 @@ func TestMRDedupKeyCannotCollideWithASHA(t *testing.T) { | |||
| 86 | t.Error("different merge requests share a dedup key") | 90 | t.Error("different merge requests share a dedup key") |
| 87 | } | 91 | } |
| 88 | } | 92 | } |
| 93 | |||
| 94 | // TestEveryCommandDocumentsItsUsage is what makes `help <prefix>` and | ||
| 95 | // `gitbay <cmd> --help` worth typing: both render Usage, so a command that | ||
| 96 | // omits it documents nothing. Usage opens with the command path so the | ||
| 97 | // printed line can be typed as-is, and the summary must not carry the | ||
| 98 | // argument syntax it used to. | ||
| 99 | func TestEveryCommandDocumentsItsUsage(t *testing.T) { | ||
| 100 | for _, cmd := range Commands() { | ||
| 101 | path := strings.Join(cmd.Path, " ") | ||
| 102 | if cmd.Usage == "" { | ||
| 103 | t.Errorf("command %q has no Usage", path) | ||
| 104 | continue | ||
| 105 | } | ||
| 106 | if cmd.Usage != path && !strings.HasPrefix(cmd.Usage, path+" ") { | ||
| 107 | t.Errorf("command %q has Usage %q, which does not open with the command path", path, cmd.Usage) | ||
| 108 | } | ||
| 109 | if strings.Contains(cmd.Summary, ": "+path) { | ||
| 110 | t.Errorf("command %q still carries its usage in the summary: %q", path, cmd.Summary) | ||
| 111 | } | ||
| 112 | } | ||
| 113 | } | ||
| 114 | |||
| 115 | // TestHelpPrefixNarrowsAndShowsFlags covers the reason the command exists: | ||
| 116 | // before this, reading one command's flags meant reading all of them. | ||
| 117 | func TestHelpPrefixNarrowsAndShowsFlags(t *testing.T) { | ||
| 118 | var buf bytes.Buffer | ||
| 119 | c := &Ctx{Stdout: &buf, Stderr: io.Discard} | ||
| 120 | if code := runHelp(c, []string{"issue"}); code != protocol.ExitOK { | ||
| 121 | t.Fatalf("help issue exited %d", code) | ||
| 122 | } | ||
| 123 | out := buf.String() | ||
| 124 | for _, line := range strings.Split(strings.TrimSpace(out), "\n") { | ||
| 125 | if strings.HasPrefix(line, " ") { | ||
| 126 | continue // the indented usage line | ||
| 127 | } | ||
| 128 | if !strings.HasPrefix(line, "issue ") { | ||
| 129 | t.Errorf("help issue listed an unrelated command: %q", line) | ||
| 130 | } | ||
| 131 | } | ||
| 132 | if !strings.Contains(out, "--state open|closed|all") { | ||
| 133 | t.Error("help issue did not print issue list's flags") | ||
| 134 | } | ||
| 135 | } | ||
| 136 | |||
| 137 | func TestHelpUnknownPrefixIsNotFound(t *testing.T) { | ||
| 138 | var buf bytes.Buffer | ||
| 139 | c := &Ctx{Stdout: &buf, Stderr: io.Discard} | ||
| 140 | if code := runHelp(c, []string{"nope"}); code != protocol.ExitNotFound { | ||
| 141 | t.Errorf("help nope exited %d, want %d", code, protocol.ExitNotFound) | ||
| 142 | } | ||
| 143 | } | ||
| 144 | |||
| 145 | // TestHelpListsEveryCommandSorted pins the unfiltered listing: one row per | ||
| 146 | // registered command, ordered so a noun's commands sit together. | ||
| 147 | func TestHelpListsEveryCommandSorted(t *testing.T) { | ||
| 148 | var buf bytes.Buffer | ||
| 149 | c := &Ctx{Stdout: &buf, Stderr: io.Discard, JSON: true} | ||
| 150 | if code := runHelp(c, nil); code != protocol.ExitOK { | ||
| 151 | t.Fatalf("help exited %d", code) | ||
| 152 | } | ||
| 153 | var env struct { | ||
| 154 | Data []helpEntry `json:"data"` | ||
| 155 | } | ||
| 156 | if err := json.Unmarshal(buf.Bytes(), &env); err != nil { | ||
| 157 | t.Fatalf("help --json: %v", err) | ||
| 158 | } | ||
| 159 | if len(env.Data) != len(Commands()) { | ||
| 160 | t.Errorf("help listed %d commands, registry has %d", len(env.Data), len(Commands())) | ||
| 161 | } | ||
| 162 | if !slices.IsSortedFunc(env.Data, func(a, b helpEntry) int { return strings.Compare(a.Path, b.Path) }) { | ||
| 163 | t.Error("help output is not sorted by path") | ||
| 164 | } | ||
| 165 | } | ||
internal/control/dashboard.go +3 −1
| @@ -16,9 +16,11 @@ import ( | |||
| 16 | func init() { | 16 | func init() { |
| 17 | register(Command{Path: []string{"dashboard"}, | 17 | register(Command{Path: []string{"dashboard"}, |
| 18 | Summary: "one read for the account dashboard: review queue, assigned and open work, pins, activity, builds", | 18 | Summary: "one read for the account dashboard: review queue, assigned and open work, pins, activity, builds", |
| 19 | Usage: "dashboard", | ||
| 19 | ReadOnly: true, Run: runDashboard}) | 20 | ReadOnly: true, Run: runDashboard}) |
| 20 | register(Command{Path: []string{"feed"}, | 21 | register(Command{Path: []string{"feed"}, |
| 21 | Summary: "activity on repositories you can reach: feed [--limit <n>] [--cursor <c>]", | 22 | Summary: "activity on repositories you can reach", |
| 23 | Usage: "feed [--limit <n>] [--cursor <c>]", | ||
| 22 | ReadOnly: true, Run: runFeed}) | 24 | ReadOnly: true, Run: runFeed}) |
| 23 | } | 25 | } |
| 24 | 26 | ||
internal/control/deploykey.go +6 −3
| @@ -14,12 +14,15 @@ 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: repo deploy-key add <owner/name> [--rw] < key.pub", | 17 | Summary: "bind a read-only (or --rw) key to one repository", |
| 18 | Usage: "repo deploy-key add <owner/name> [--rw] < key.pub", | ||
| 18 | ReadsStdin: true, Run: runDeployKeyAdd}) | 19 | ReadsStdin: true, Run: runDeployKeyAdd}) |
| 19 | register(Command{Path: []string{"repo", "deploy-key", "list"}, | 20 | register(Command{Path: []string{"repo", "deploy-key", "list"}, |
| 20 | Summary: "list deploy keys: repo deploy-key list <owner/name>", ReadOnly: true, Run: runDeployKeyList}) | 21 | Summary: "list deploy keys", |
| 22 | Usage: "repo deploy-key list <owner/name>", ReadOnly: true, Run: runDeployKeyList}) | ||
| 21 | register(Command{Path: []string{"repo", "deploy-key", "remove"}, | 23 | register(Command{Path: []string{"repo", "deploy-key", "remove"}, |
| 22 | Summary: "remove a deploy key: repo deploy-key remove <owner/name> <fingerprint>", Run: runDeployKeyRemove}) | 24 | Summary: "remove a deploy key", |
| 25 | Usage: "repo deploy-key remove <owner/name> <fingerprint>", Run: runDeployKeyRemove}) | ||
| 23 | } | 26 | } |
| 24 | 27 | ||
| 25 | func runDeployKeyAdd(c *Ctx, args []string) int { | 28 | func runDeployKeyAdd(c *Ctx, args []string) int { |
internal/control/deps.go +6 −3
| @@ -15,11 +15,14 @@ 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: repo deps enable <owner/name>", Run: runDepsEnable}) | 18 | Summary: "check dependencies for updates", |
| 19 | Usage: "repo deps enable <owner/name>", Run: runDepsEnable}) | ||
| 19 | register(Command{Path: []string{"repo", "deps", "disable"}, | 20 | register(Command{Path: []string{"repo", "deps", "disable"}, |
| 20 | Summary: "stop checking dependencies: repo deps disable <owner/name>", Run: runDepsDisable}) | 21 | Summary: "stop checking dependencies", |
| 22 | Usage: "repo deps disable <owner/name>", Run: runDepsDisable}) | ||
| 21 | register(Command{Path: []string{"repo", "deps", "status"}, | 23 | register(Command{Path: []string{"repo", "deps", "status"}, |
| 22 | Summary: "show dependency check state: repo deps status <owner/name>", ReadOnly: true, Run: runDepsStatus}) | 24 | Summary: "show dependency check state", |
| 25 | Usage: "repo deps status <owner/name>", ReadOnly: true, Run: runDepsStatus}) | ||
| 23 | } | 26 | } |
| 24 | 27 | ||
| 25 | func runDepsEnable(c *Ctx, args []string) int { | 28 | func runDepsEnable(c *Ctx, args []string) int { |
internal/control/diffcomment.go +8 −4
| @@ -16,14 +16,18 @@ 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: mr diff-comment <owner/name> <n> --path <file> --line <l> [--old] [--reply <id>] [--message <m> | --file -]", | 19 | Summary: "comment on a diff line", |
| 20 | Usage: "mr diff-comment <owner/name> <n> --path <file> --line <l> [--old] [--reply <id>] [--message <m> | --file -]", | ||
| 20 | ReadsStdin: true, Run: runDiffComment}) | 21 | ReadsStdin: true, Run: runDiffComment}) |
| 21 | register(Command{Path: []string{"mr", "threads"}, | 22 | register(Command{Path: []string{"mr", "threads"}, |
| 22 | Summary: "review threads on an MR: mr threads <owner/name> <n>", ReadOnly: true, Run: runMRThreads}) | 23 | Summary: "review threads on an MR", |
| 24 | Usage: "mr threads <owner/name> <n>", ReadOnly: true, Run: runMRThreads}) | ||
| 23 | register(Command{Path: []string{"mr", "resolve"}, | 25 | register(Command{Path: []string{"mr", "resolve"}, |
| 24 | Summary: "resolve a review thread: mr resolve <owner/name> <n> <thread-id>", Run: runMRResolve}) | 26 | Summary: "resolve a review thread", |
| 27 | Usage: "mr resolve <owner/name> <n> <thread-id>", Run: runMRResolve}) | ||
| 25 | register(Command{Path: []string{"mr", "unresolve"}, | 28 | register(Command{Path: []string{"mr", "unresolve"}, |
| 26 | Summary: "reopen a review thread: mr unresolve <owner/name> <n> <thread-id>", Run: runMRUnresolve}) | 29 | Summary: "reopen a review thread", |
| 30 | Usage: "mr unresolve <owner/name> <n> <thread-id>", Run: runMRUnresolve}) | ||
| 27 | } | 31 | } |
| 28 | 32 | ||
| 29 | func runDiffComment(c *Ctx, args []string) int { | 33 | func runDiffComment(c *Ctx, args []string) int { |
internal/control/explore.go +4 −2
| @@ -12,7 +12,8 @@ import ( | |||
| 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: explore [--limit <n>] [--cursor <c>]", | 15 | Summary: "list public repositories", |
| 16 | Usage: "explore [--limit <n>] [--cursor <c>]", | ||
| 16 | ReadOnly: true, | 17 | ReadOnly: true, |
| 17 | Run: runExplore, | 18 | Run: runExplore, |
| 18 | }) | 19 | }) |
| @@ -20,7 +21,8 @@ func init() { | |||
| 20 | Path: []string{"repo", "download"}, | 21 | Path: []string{"repo", "download"}, |
| 21 | // Not "repo archive": that name is taken by the read-only flag, | 22 | // Not "repo archive": that name is taken by the read-only flag, |
| 22 | // and renaming it would break every script that sets it. | 23 | // and renaming it would break every script that sets it. |
| 23 | Summary: "write a tar.gz of a ref to stdout: repo download <owner/name> [--ref <r>] > repo.tar.gz", | 24 | Summary: "write a tar.gz of a ref to stdout", |
| 25 | Usage: "repo download <owner/name> [--ref <r>] > repo.tar.gz", | ||
| 24 | ReadOnly: true, | 26 | ReadOnly: true, |
| 25 | Run: runRepoDownload, | 27 | Run: runRepoDownload, |
| 26 | }) | 28 | }) |
internal/control/ghimport.go +2 −1
| @@ -20,7 +20,8 @@ import ( | |||
| 20 | 20 | ||
| 21 | func init() { | 21 | func init() { |
| 22 | register(Command{Path: []string{"repo", "import-issues"}, | 22 | register(Command{Path: []string{"repo", "import-issues"}, |
| 23 | Summary: "import GitHub issue and PR history: repo import-issues <owner/name> --from <ghowner/ghrepo> [--token-stdin] [--api-base <url>]", | 23 | Summary: "import GitHub issue and PR history", |
| 24 | Usage: "repo import-issues <owner/name> --from <ghowner/ghrepo> [--token-stdin] [--api-base <url>]", | ||
| 24 | ReadsStdin: true, Run: runImportIssues}) | 25 | ReadsStdin: true, Run: runImportIssues}) |
| 25 | } | 26 | } |
| 26 | 27 | ||
internal/control/identity.go +5 −1
| @@ -15,24 +15,28 @@ func init() { | |||
| 15 | register(Command{ | 15 | register(Command{ |
| 16 | Path: []string{"whoami"}, | 16 | Path: []string{"whoami"}, |
| 17 | Summary: "show the authenticated account", | 17 | Summary: "show the authenticated account", |
| 18 | Usage: "whoami", | ||
| 18 | ReadOnly: true, | 19 | ReadOnly: true, |
| 19 | Run: runWhoami, | 20 | Run: runWhoami, |
| 20 | }) | 21 | }) |
| 21 | register(Command{ | 22 | register(Command{ |
| 22 | Path: []string{"keys", "list"}, | 23 | Path: []string{"keys", "list"}, |
| 23 | Summary: "list registered SSH keys", | 24 | Summary: "list registered SSH keys", |
| 25 | Usage: "keys list", | ||
| 24 | ReadOnly: true, | 26 | ReadOnly: true, |
| 25 | Run: runKeysList, | 27 | Run: runKeysList, |
| 26 | }) | 28 | }) |
| 27 | register(Command{ | 29 | register(Command{ |
| 28 | Path: []string{"keys", "add"}, | 30 | Path: []string{"keys", "add"}, |
| 29 | Summary: "register an SSH public key (authorized_keys format on stdin) [--scope full|git]", | 31 | Summary: "register an SSH public key (authorized_keys format)", |
| 32 | Usage: "keys add [--scope full|git] < key.pub", | ||
| 30 | ReadsStdin: true, | 33 | ReadsStdin: true, |
| 31 | Run: runKeysAdd, | 34 | Run: runKeysAdd, |
| 32 | }) | 35 | }) |
| 33 | register(Command{ | 36 | register(Command{ |
| 34 | Path: []string{"keys", "remove"}, | 37 | Path: []string{"keys", "remove"}, |
| 35 | Summary: "remove an SSH key by fingerprint", | 38 | Summary: "remove an SSH key by fingerprint", |
| 39 | Usage: "keys remove <fingerprint>", | ||
| 36 | Run: runKeysRemove, | 40 | Run: runKeysRemove, |
| 37 | }) | 41 | }) |
| 38 | } | 42 | } |
internal/control/import.go +2 −1
| @@ -17,7 +17,8 @@ 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: repo import <owner/name> --from <url> [--private] [--token-stdin]", | 20 | Summary: "server-side mirror of a foreign repository", |
| 21 | Usage: "repo import <owner/name> --from <url> [--private] [--token-stdin]", | ||
| 21 | ReadsStdin: true, Run: runRepoImport}) | 22 | ReadsStdin: true, Run: runRepoImport}) |
| 22 | } | 23 | } |
| 23 | 24 | ||
internal/control/issue.go +18 −9
| @@ -16,26 +16,35 @@ 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: issue create <owner/name> --title <t> [--body <b> | --file -] [--format md|org]", | 19 | Summary: "open an issue", |
| 20 | Usage: "issue create <owner/name> --title <t> [--body <b> | --file -] [--format md|org]", | ||
| 20 | ReadsStdin: true, Run: runIssueCreate}) | 21 | ReadsStdin: true, Run: runIssueCreate}) |
| 21 | register(Command{Path: []string{"issue", "list"}, | 22 | register(Command{Path: []string{"issue", "list"}, |
| 22 | Summary: "list issues: issue list <owner/name> [--state open|closed|all] [--limit <n>] [--cursor <c>]", ReadOnly: true, Run: runIssueList}) | 23 | Summary: "list issues", |
| 24 | Usage: "issue list <owner/name> [--state open|closed|all] [--limit <n>] [--cursor <c>]", ReadOnly: true, Run: runIssueList}) | ||
| 23 | register(Command{Path: []string{"issue", "show"}, | 25 | register(Command{Path: []string{"issue", "show"}, |
| 24 | Summary: "show an issue with comments: issue show <owner/name> <n>", ReadOnly: true, Run: runIssueShow}) | 26 | Summary: "show an issue with comments", |
| 27 | Usage: "issue show <owner/name> <n>", ReadOnly: true, Run: runIssueShow}) | ||
| 25 | register(Command{Path: []string{"issue", "edit"}, | 28 | register(Command{Path: []string{"issue", "edit"}, |
| 26 | Summary: "edit title or body: issue edit <owner/name> <n> [--title <t>] [--body <b> | --file -] [--format md|org]", | 29 | Summary: "edit title or body", |
| 30 | Usage: "issue edit <owner/name> <n> [--title <t>] [--body <b> | --file -] [--format md|org]", | ||
| 27 | ReadsStdin: true, Run: runIssueEdit}) | 31 | ReadsStdin: true, Run: runIssueEdit}) |
| 28 | register(Command{Path: []string{"issue", "comment"}, | 32 | register(Command{Path: []string{"issue", "comment"}, |
| 29 | Summary: "comment: issue comment <owner/name> <n> [--message <m> | --file -] [--format md|org]", | 33 | Summary: "comment", |
| 34 | Usage: "issue comment <owner/name> <n> [--message <m> | --file -] [--format md|org]", | ||
| 30 | ReadsStdin: true, Run: runIssueComment}) | 35 | ReadsStdin: true, Run: runIssueComment}) |
| 31 | register(Command{Path: []string{"issue", "close"}, | 36 | register(Command{Path: []string{"issue", "close"}, |
| 32 | Summary: "close an issue: issue close <owner/name> <n>", Run: runIssueClose}) | 37 | Summary: "close an issue", |
| 38 | Usage: "issue close <owner/name> <n>", Run: runIssueClose}) | ||
| 33 | register(Command{Path: []string{"issue", "reopen"}, | 39 | register(Command{Path: []string{"issue", "reopen"}, |
| 34 | Summary: "reopen an issue: issue reopen <owner/name> <n>", Run: runIssueReopen}) | 40 | Summary: "reopen an issue", |
| 41 | Usage: "issue reopen <owner/name> <n>", Run: runIssueReopen}) | ||
| 35 | register(Command{Path: []string{"issue", "label"}, | 42 | register(Command{Path: []string{"issue", "label"}, |
| 36 | Summary: "labels: issue label <owner/name> <n> [--add <l>]... [--remove <l>]...", Run: runIssueLabel}) | 43 | Summary: "labels", |
| 44 | Usage: "issue label <owner/name> <n> [--add <l>]... [--remove <l>]...", Run: runIssueLabel}) | ||
| 37 | register(Command{Path: []string{"issue", "assign"}, | 45 | register(Command{Path: []string{"issue", "assign"}, |
| 38 | Summary: "assignees: issue assign <owner/name> <n> [--add <user>]... [--remove <user>]...", Run: runIssueAssign}) | 46 | Summary: "assignees", |
| 47 | Usage: "issue assign <owner/name> <n> [--add <user>]... [--remove <user>]...", Run: runIssueAssign}) | ||
| 39 | } | 48 | } |
| 40 | 49 | ||
| 41 | // issueArgs parses "<owner/name> <n>" plus flags handled by the caller. | 50 | // issueArgs parses "<owner/name> <n>" plus flags handled by the caller. |
internal/control/migrate.go +4 −2
| @@ -13,10 +13,12 @@ import ( | |||
| 13 | 13 | ||
| 14 | func init() { | 14 | func init() { |
| 15 | register(Command{Path: []string{"account", "export"}, | 15 | register(Command{Path: []string{"account", "export"}, |
| 16 | Summary: "write your account bundle (profile, repos, issues, MRs) as JSON to stdout", | 16 | Summary: "write your account bundle (profile, repos, issues, MRs) as JSON", |
| 17 | Usage: "account export > bundle.json", | ||
| 17 | ReadOnly: true, Run: runAccountExport}) | 18 | ReadOnly: true, Run: runAccountExport}) |
| 18 | register(Command{Path: []string{"account", "import-bundle"}, | 19 | register(Command{Path: []string{"account", "import-bundle"}, |
| 19 | Summary: "replay an account bundle from stdin (see gitbay migrate)", | 20 | Summary: "replay an account bundle (see gitbay migrate)", |
| 21 | Usage: "account import-bundle [--source <host>] < bundle.json", | ||
| 20 | ReadsStdin: true, Run: runAccountImportBundle}) | 22 | ReadsStdin: true, Run: runAccountImportBundle}) |
| 21 | } | 23 | } |
| 22 | 24 | ||
internal/control/milestone.go +14 −7
| @@ -16,19 +16,26 @@ import ( | |||
| 16 | 16 | ||
| 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: milestone create <owner/name> <title> [--description <d>] [--due YYYY-MM-DD]", Run: runMilestoneCreate}) | 19 | Summary: "create a milestone", |
| 20 | Usage: "milestone create <owner/name> <title> [--description <d>] [--due YYYY-MM-DD]", Run: runMilestoneCreate}) | ||
| 20 | register(Command{Path: []string{"milestone", "list"}, | 21 | register(Command{Path: []string{"milestone", "list"}, |
| 21 | Summary: "list milestones with progress: milestone list <owner/name> [--state open|closed|all]", ReadOnly: true, Run: runMilestoneList}) | 22 | Summary: "list milestones with progress", |
| 23 | Usage: "milestone list <owner/name> [--state open|closed|all]", ReadOnly: true, Run: runMilestoneList}) | ||
| 22 | register(Command{Path: []string{"milestone", "close"}, | 24 | register(Command{Path: []string{"milestone", "close"}, |
| 23 | Summary: "close a milestone: milestone close <owner/name> <title>", Run: runMilestoneClose}) | 25 | Summary: "close a milestone", |
| 26 | Usage: "milestone close <owner/name> <title>", Run: runMilestoneClose}) | ||
| 24 | register(Command{Path: []string{"milestone", "reopen"}, | 27 | register(Command{Path: []string{"milestone", "reopen"}, |
| 25 | Summary: "reopen a milestone: milestone reopen <owner/name> <title>", Run: runMilestoneReopen}) | 28 | Summary: "reopen a milestone", |
| 29 | Usage: "milestone reopen <owner/name> <title>", Run: runMilestoneReopen}) | ||
| 26 | register(Command{Path: []string{"issue", "milestone"}, | 30 | register(Command{Path: []string{"issue", "milestone"}, |
| 27 | Summary: "set or clear an issue's milestone: issue milestone <owner/name> <n> <title|none>", Run: runIssueMilestone}) | 31 | Summary: "set or clear an issue's milestone", |
| 32 | Usage: "issue milestone <owner/name> <n> <title|none>", Run: runIssueMilestone}) | ||
| 28 | register(Command{Path: []string{"mr", "milestone"}, | 33 | register(Command{Path: []string{"mr", "milestone"}, |
| 29 | Summary: "set or clear an MR's milestone: mr milestone <owner/name> <n> <title|none>", Run: runMRMilestone}) | 34 | Summary: "set or clear an MR's milestone", |
| 35 | Usage: "mr milestone <owner/name> <n> <title|none>", Run: runMRMilestone}) | ||
| 30 | register(Command{Path: []string{"issue", "templates"}, | 36 | register(Command{Path: []string{"issue", "templates"}, |
| 31 | Summary: "list issue templates (.gitbay/issue-template*.md): issue templates <owner/name>", ReadOnly: true, Run: runIssueTemplates}) | 37 | Summary: "list issue templates (.gitbay/issue-template*.md)", |
| 38 | Usage: "issue templates <owner/name>", ReadOnly: true, Run: runIssueTemplates}) | ||
| 32 | } | 39 | } |
| 33 | 40 | ||
| 34 | var duePat = regexp.MustCompile(`^\d{4}-\d{2}-\d{2}$`) | 41 | var duePat = regexp.MustCompile(`^\d{4}-\d{2}-\d{2}$`) |
internal/control/mirrorcmd.go +8 −4
| @@ -16,14 +16,18 @@ 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: repo mirror add <owner/name> <https-url> --direction push|pull [--username <u>] [--token-stdin]", | 19 | Summary: "mirror to or from a remote", |
| 20 | Usage: "repo mirror add <owner/name> <https-url> --direction push|pull [--username <u>] [--token-stdin]", | ||
| 20 | ReadsStdin: true, SSHOnly: true, Run: runMirrorAdd}) | 21 | ReadsStdin: true, SSHOnly: true, Run: runMirrorAdd}) |
| 21 | register(Command{Path: []string{"repo", "mirror", "list"}, | 22 | register(Command{Path: []string{"repo", "mirror", "list"}, |
| 22 | Summary: "list mirrors with sync status: repo mirror list <owner/name>", ReadOnly: true, Run: runMirrorList}) | 23 | Summary: "list mirrors with sync status", |
| 24 | Usage: "repo mirror list <owner/name>", ReadOnly: true, Run: runMirrorList}) | ||
| 23 | register(Command{Path: []string{"repo", "mirror", "remove"}, | 25 | register(Command{Path: []string{"repo", "mirror", "remove"}, |
| 24 | Summary: "remove a mirror: repo mirror remove <owner/name> <id>", Run: runMirrorRemove}) | 26 | Summary: "remove a mirror", |
| 27 | Usage: "repo mirror remove <owner/name> <id>", Run: runMirrorRemove}) | ||
| 25 | register(Command{Path: []string{"repo", "mirror", "sync"}, | 28 | register(Command{Path: []string{"repo", "mirror", "sync"}, |
| 26 | Summary: "schedule an immediate sync: repo mirror sync <owner/name>", Run: runMirrorSync}) | 29 | Summary: "schedule an immediate sync", |
| 30 | Usage: "repo mirror sync <owner/name>", Run: runMirrorSync}) | ||
| 27 | } | 31 | } |
| 28 | 32 | ||
| 29 | func runMirrorAdd(c *Ctx, args []string) int { | 33 | func runMirrorAdd(c *Ctx, args []string) int { |
internal/control/mr.go +30 −15
| @@ -17,38 +17,53 @@ import ( | |||
| 17 | 17 | ||
| 18 | func init() { | 18 | func init() { |
| 19 | register(Command{Path: []string{"repo", "fork"}, | 19 | register(Command{Path: []string{"repo", "fork"}, |
| 20 | Summary: "fork a repository under your account: repo fork <owner/name> [--name <n>]", Run: runRepoFork}) | 20 | Summary: "fork a repository under your account", |
| 21 | Usage: "repo fork <owner/name> [--name <n>]", Run: runRepoFork}) | ||
| 21 | register(Command{Path: []string{"repo", "settings", "require-approvals"}, | 22 | register(Command{Path: []string{"repo", "settings", "require-approvals"}, |
| 22 | Summary: "require N fresh approvals to merge: repo settings require-approvals <owner/name> <n> (0 = off)", Run: runRequireApprovals}) | 23 | Summary: "require N fresh approvals to merge", |
| 24 | Usage: "repo settings require-approvals <owner/name> <n> (0 = off)", Run: runRequireApprovals}) | ||
| 23 | register(Command{Path: []string{"repo", "settings", "require-resolved"}, | 25 | register(Command{Path: []string{"repo", "settings", "require-resolved"}, |
| 24 | Summary: "require all review threads resolved to merge: repo settings require-resolved <owner/name> on|off", Run: runRequireResolved}) | 26 | Summary: "require all review threads resolved to merge", |
| 27 | Usage: "repo settings require-resolved <owner/name> on|off", Run: runRequireResolved}) | ||
| 25 | register(Command{Path: []string{"repo", "settings", "require-checks"}, | 28 | register(Command{Path: []string{"repo", "settings", "require-checks"}, |
| 26 | Summary: "gate merges on green statuses: repo settings require-checks <owner/name> on|off", Run: runRequireChecks}) | 29 | Summary: "gate merges on green statuses", |
| 30 | Usage: "repo settings require-checks <owner/name> on|off", Run: runRequireChecks}) | ||
| 27 | register(Command{Path: []string{"repo", "settings", "require-signed"}, | 31 | register(Command{Path: []string{"repo", "settings", "require-signed"}, |
| 28 | Summary: "require verified commit signatures: repo settings require-signed <owner/name> on|off", Run: runRequireSigned}) | 32 | Summary: "require verified commit signatures", |
| 33 | Usage: "repo settings require-signed <owner/name> on|off", Run: runRequireSigned}) | ||
| 29 | register(Command{Path: []string{"mr", "create"}, | 34 | register(Command{Path: []string{"mr", "create"}, |
| 30 | Summary: "open a merge request: mr create <target owner/name> --source [owner/name:]<branch> --target <branch> --title <t> [--body <b> | --file -] [--format md|org]", | 35 | Summary: "open a merge request", |
| 36 | Usage: "mr create <target owner/name> --source [owner/name:]<branch> --target <branch> --title <t> [--body <b> | --file -] [--format md|org]", | ||
| 31 | ReadsStdin: true, Run: runMRCreate}) | 37 | ReadsStdin: true, Run: runMRCreate}) |
| 32 | register(Command{Path: []string{"mr", "list"}, | 38 | register(Command{Path: []string{"mr", "list"}, |
| 33 | Summary: "list merge requests: mr list <owner/name> [--state open|merged|closed|source_gone|all] [--limit <n>] [--cursor <c>]", ReadOnly: true, Run: runMRList}) | 39 | Summary: "list merge requests", |
| 40 | Usage: "mr list <owner/name> [--state open|merged|closed|source_gone|all] [--limit <n>] [--cursor <c>]", ReadOnly: true, Run: runMRList}) | ||
| 34 | register(Command{Path: []string{"mr", "show"}, | 41 | register(Command{Path: []string{"mr", "show"}, |
| 35 | Summary: "show a merge request: mr show <owner/name> <n>", ReadOnly: true, Run: runMRShow}) | 42 | Summary: "show a merge request", |
| 43 | Usage: "mr show <owner/name> <n>", ReadOnly: true, Run: runMRShow}) | ||
| 36 | register(Command{Path: []string{"mr", "diff"}, | 44 | register(Command{Path: []string{"mr", "diff"}, |
| 37 | Summary: "show the diff: mr diff <owner/name> <n>", ReadOnly: true, Run: runMRDiff}) | 45 | Summary: "show the diff", |
| 46 | Usage: "mr diff <owner/name> <n>", ReadOnly: true, Run: runMRDiff}) | ||
| 38 | register(Command{Path: []string{"mr", "edit"}, | 47 | register(Command{Path: []string{"mr", "edit"}, |
| 39 | Summary: "edit title or body: mr edit <owner/name> <n> [--title <t>] [--body <b> | --file -] [--format md|org]", | 48 | Summary: "edit title or body", |
| 49 | Usage: "mr edit <owner/name> <n> [--title <t>] [--body <b> | --file -] [--format md|org]", | ||
| 40 | ReadsStdin: true, Run: runMREdit}) | 50 | ReadsStdin: true, Run: runMREdit}) |
| 41 | register(Command{Path: []string{"mr", "retarget"}, | 51 | register(Command{Path: []string{"mr", "retarget"}, |
| 42 | Summary: "retarget onto another branch: mr retarget <owner/name> <n> <branch>", Run: runMRRetarget}) | 52 | Summary: "retarget onto another branch", |
| 53 | Usage: "mr retarget <owner/name> <n> <branch>", Run: runMRRetarget}) | ||
| 43 | register(Command{Path: []string{"mr", "comment"}, | 54 | register(Command{Path: []string{"mr", "comment"}, |
| 44 | Summary: "comment: mr comment <owner/name> <n> [--message <m> | --file -] [--format md|org]", | 55 | Summary: "comment", |
| 56 | Usage: "mr comment <owner/name> <n> [--message <m> | --file -] [--format md|org]", | ||
| 45 | ReadsStdin: true, Run: runMRComment}) | 57 | ReadsStdin: true, Run: runMRComment}) |
| 46 | register(Command{Path: []string{"mr", "review"}, | 58 | register(Command{Path: []string{"mr", "review"}, |
| 47 | Summary: "review: mr review <owner/name> <n> --approve|--request-changes|--comment", Run: runMRReview}) | 59 | Summary: "review", |
| 60 | Usage: "mr review <owner/name> <n> --approve|--request-changes|--comment", Run: runMRReview}) | ||
| 48 | register(Command{Path: []string{"mr", "merge"}, | 61 | register(Command{Path: []string{"mr", "merge"}, |
| 49 | Summary: "merge: mr merge <owner/name> <n> [--strategy ff|merge|squash|rebase]", Run: runMRMerge}) | 62 | Summary: "merge", |
| 63 | Usage: "mr merge <owner/name> <n> [--strategy ff|merge|squash|rebase]", Run: runMRMerge}) | ||
| 50 | register(Command{Path: []string{"mr", "close"}, | 64 | register(Command{Path: []string{"mr", "close"}, |
| 51 | Summary: "close without merging: mr close <owner/name> <n>", Run: runMRClose}) | 65 | Summary: "close without merging", |
| 66 | Usage: "mr close <owner/name> <n>", Run: runMRClose}) | ||
| 52 | } | 67 | } |
| 53 | 68 | ||
| 54 | func runRepoFork(c *Ctx, args []string) int { | 69 | func runRepoFork(c *Ctx, args []string) int { |
internal/control/org.go +16 −8
| @@ -14,21 +14,29 @@ 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): org create <name>", Run: runOrgCreate}) | 17 | Summary: "create an organization (you become its first admin)", |
| 18 | Usage: "org create <name>", Run: runOrgCreate}) | ||
| 18 | register(Command{Path: []string{"org", "list"}, | 19 | register(Command{Path: []string{"org", "list"}, |
| 19 | Summary: "list organizations you belong to", ReadOnly: true, Run: runOrgList}) | 20 | Summary: "list organizations you belong to", |
| 21 | Usage: "org list", ReadOnly: true, Run: runOrgList}) | ||
| 20 | register(Command{Path: []string{"org", "show"}, | 22 | register(Command{Path: []string{"org", "show"}, |
| 21 | Summary: "show an organization and its members: org show <name>", ReadOnly: true, Run: runOrgShow}) | 23 | Summary: "show an organization and its members", |
| 24 | Usage: "org show <name>", ReadOnly: true, Run: runOrgShow}) | ||
| 22 | register(Command{Path: []string{"org", "rename"}, | 25 | register(Command{Path: []string{"org", "rename"}, |
| 23 | Summary: "rename an organization: org rename <old> <new> (clone URLs change)", Run: runOrgRename}) | 26 | Summary: "rename an organization", |
| 27 | Usage: "org rename <old> <new> (clone URLs change)", Run: runOrgRename}) | ||
| 24 | register(Command{Path: []string{"org", "delete"}, | 28 | register(Command{Path: []string{"org", "delete"}, |
| 25 | Summary: "delete an empty organization: org delete <name> --yes", Run: runOrgDelete}) | 29 | Summary: "delete an empty organization", |
| 30 | Usage: "org delete <name> --yes", Run: runOrgDelete}) | ||
| 26 | register(Command{Path: []string{"org", "members", "add"}, | 31 | register(Command{Path: []string{"org", "members", "add"}, |
| 27 | Summary: "add or update a member: org members add <org> <user> [--role member|admin]", Run: runOrgMembersAdd}) | 32 | Summary: "add or update a member", |
| 33 | Usage: "org members add <org> <user> [--role member|admin]", Run: runOrgMembersAdd}) | ||
| 28 | register(Command{Path: []string{"org", "members", "remove"}, | 34 | register(Command{Path: []string{"org", "members", "remove"}, |
| 29 | Summary: "remove a member: org members remove <org> <user>", Run: runOrgMembersRemove}) | 35 | Summary: "remove a member", |
| 36 | Usage: "org members remove <org> <user>", Run: runOrgMembersRemove}) | ||
| 30 | register(Command{Path: []string{"org", "members", "list"}, | 37 | register(Command{Path: []string{"org", "members", "list"}, |
| 31 | Summary: "list members: org members list <org>", ReadOnly: true, Run: runOrgMembersList}) | 38 | Summary: "list members", |
| 39 | Usage: "org members list <org>", ReadOnly: true, Run: runOrgMembersList}) | ||
| 32 | } | 40 | } |
| 33 | 41 | ||
| 34 | // orgAdmin loads an org and requires the caller to be one of its admins. | 42 | // orgAdmin loads an org and requires the caller to be one of its admins. |
internal/control/pagescmd.go +8 −4
| @@ -20,13 +20,17 @@ 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): repo domain add <owner/name> <domain>", Run: runDomainAdd}) | 23 | Summary: "claim a custom pages domain (verify with a DNS TXT record)", |
| 24 | Usage: "repo domain add <owner/name> <domain>", Run: runDomainAdd}) | ||
| 24 | register(Command{Path: []string{"repo", "domain", "verify"}, | 25 | register(Command{Path: []string{"repo", "domain", "verify"}, |
| 25 | Summary: "check the DNS challenge and activate a claim: repo domain verify <owner/name> <domain>", Run: runDomainVerify}) | 26 | Summary: "check the DNS challenge and activate a claim", |
| 27 | Usage: "repo domain verify <owner/name> <domain>", Run: runDomainVerify}) | ||
| 26 | register(Command{Path: []string{"repo", "domain", "remove"}, | 28 | register(Command{Path: []string{"repo", "domain", "remove"}, |
| 27 | Summary: "remove a custom pages domain: repo domain remove <owner/name> <domain>", Run: runDomainRemove}) | 29 | Summary: "remove a custom pages domain", |
| 30 | Usage: "repo domain remove <owner/name> <domain>", Run: runDomainRemove}) | ||
| 28 | register(Command{Path: []string{"repo", "domain", "list"}, | 31 | register(Command{Path: []string{"repo", "domain", "list"}, |
| 29 | Summary: "list custom pages domains: repo domain list <owner/name>", ReadOnly: true, Run: runDomainList}) | 32 | Summary: "list custom pages domains", |
| 33 | Usage: "repo domain list <owner/name>", ReadOnly: true, Run: runDomainList}) | ||
| 30 | } | 34 | } |
| 31 | 35 | ||
| 32 | // challengeLabel prefixes the domain for the ownership TXT record. | 36 | // challengeLabel prefixes the domain for the ownership TXT record. |
internal/control/profile.go +6 −3
| @@ -16,11 +16,14 @@ 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: profile show [name]", ReadOnly: true, Run: runProfileShow}) | 19 | Summary: "show a user's or org's profile", |
| 20 | Usage: "profile show [name]", ReadOnly: true, Run: runProfileShow}) | ||
| 20 | register(Command{Path: []string{"profile", "set"}, | 21 | register(Command{Path: []string{"profile", "set"}, |
| 21 | Summary: "set your profile: profile set [--description <d>] [--website <url>] [--about <text>|--file -] [--about-format md|org] [--link <label|url>]... ('' clears)", ReadsStdin: true, Run: runProfileSet}) | 22 | Summary: "set your profile", |
| 23 | Usage: "profile set [--description <d>] [--website <url>] [--about <text>|--file -] [--about-format md|org] [--link <label|url>]... ('' clears)", ReadsStdin: true, Run: runProfileSet}) | ||
| 22 | register(Command{Path: []string{"org", "profile"}, | 24 | register(Command{Path: []string{"org", "profile"}, |
| 23 | Summary: "show or set an org's profile: org profile <org> [--description <d>] [--website <url>] [--about <text>|--file -] [--about-format md|org] [--link <label|url>]...", ReadsStdin: true, Run: runOrgProfile}) | 25 | Summary: "show or set an org's profile", |
| 26 | Usage: "org profile <org> [--description <d>] [--website <url>] [--about <text>|--file -] [--about-format md|org] [--link <label|url>]...", ReadsStdin: true, Run: runOrgProfile}) | ||
| 24 | } | 27 | } |
| 25 | 28 | ||
| 26 | // maxProfileLinks caps the free-form link list. A profile is a header, | 29 | // maxProfileLinks caps the free-form link list. A profile is a header, |
internal/control/read.go +8 −4
| @@ -18,25 +18,29 @@ import ( | |||
| 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: repo tree <owner/name> [<path>] [--ref <ref>]", | 21 | Summary: "list a directory", |
| 22 | Usage: "repo tree <owner/name> [<path>] [--ref <ref>]", | ||
| 22 | ReadOnly: true, | 23 | ReadOnly: true, |
| 23 | Run: runRepoTree, | 24 | Run: runRepoTree, |
| 24 | }) | 25 | }) |
| 25 | register(Command{ | 26 | register(Command{ |
| 26 | Path: []string{"repo", "cat"}, | 27 | Path: []string{"repo", "cat"}, |
| 27 | Summary: "read a file: repo cat <owner/name> <path> [--ref <ref>]", | 28 | Summary: "read a file", |
| 29 | Usage: "repo cat <owner/name> <path> [--ref <ref>]", | ||
| 28 | ReadOnly: true, | 30 | ReadOnly: true, |
| 29 | Run: runRepoCat, | 31 | Run: runRepoCat, |
| 30 | }) | 32 | }) |
| 31 | register(Command{ | 33 | register(Command{ |
| 32 | Path: []string{"repo", "blame"}, | 34 | Path: []string{"repo", "blame"}, |
| 33 | Summary: "attribute lines to commits: repo blame <owner/name> <path> [--ref <ref>] [--from <n>] [--to <n>]", | 35 | Summary: "attribute lines to commits", |
| 36 | Usage: "repo blame <owner/name> <path> [--ref <ref>] [--from <n>] [--to <n>]", | ||
| 34 | ReadOnly: true, | 37 | ReadOnly: true, |
| 35 | Run: runRepoBlame, | 38 | Run: runRepoBlame, |
| 36 | }) | 39 | }) |
| 37 | register(Command{ | 40 | register(Command{ |
| 38 | Path: []string{"repo", "refs"}, | 41 | Path: []string{"repo", "refs"}, |
| 39 | Summary: "list branches and tags: repo refs <owner/name>", | 42 | Summary: "list branches and tags", |
| 43 | Usage: "repo refs <owner/name>", | ||
| 40 | ReadOnly: true, | 44 | ReadOnly: true, |
| 41 | Run: runRepoRefs, | 45 | Run: runRepoRefs, |
| 42 | }) | 46 | }) |
internal/control/register.go +5 −2
| @@ -19,15 +19,18 @@ import ( | |||
| 19 | func init() { | 19 | 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 | Run: func(c *Ctx, args []string) int { | 23 | Run: func(c *Ctx, args []string) int { |
| 23 | return c.fail(protocol.ExitUsage, | 24 | return c.fail(protocol.ExitUsage, |
| 24 | "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 ...", | 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 ...", |
| 25 | c.User.Username) | 26 | c.User.Username) |
| 26 | }}) | 27 | }}) |
| 27 | register(Command{Path: []string{"email", "add"}, | 28 | register(Command{Path: []string{"email", "add"}, |
| 28 | Summary: "add an address and mail a verification code: email add <address>", Run: runEmailAdd}) | 29 | Summary: "add an address and mail a verification code", |
| 30 | Usage: "email add <address>", Run: runEmailAdd}) | ||
| 29 | register(Command{Path: []string{"email", "verify"}, | 31 | register(Command{Path: []string{"email", "verify"}, |
| 30 | Summary: "confirm a verification code: email verify <code>", Run: runEmailVerify}) | 32 | Summary: "confirm a verification code", |
| 33 | Usage: "email verify <code>", Run: runEmailVerify}) | ||
| 31 | } | 34 | } |
| 32 | 35 | ||
| 33 | func siteHost(cfg config.Config) string { | 36 | func siteHost(cfg config.Config) string { |
internal/control/release.go +16 −8
| @@ -19,25 +19,33 @@ import ( | |||
| 19 | 19 | ||
| 20 | func init() { | 20 | func init() { |
| 21 | register(Command{Path: []string{"release", "create"}, | 21 | register(Command{Path: []string{"release", "create"}, |
| 22 | Summary: "create a release on a tag: release create <owner/name> <tag> [--title <t>] [--notes <n> | --file -] [--format md|org]", | 22 | Summary: "create a release on a tag", |
| 23 | Usage: "release create <owner/name> <tag> [--title <t>] [--notes <n> | --file -] [--format md|org]", | ||
| 23 | ReadsStdin: true, Run: runReleaseCreate}) | 24 | ReadsStdin: true, Run: runReleaseCreate}) |
| 24 | register(Command{Path: []string{"release", "edit"}, | 25 | register(Command{Path: []string{"release", "edit"}, |
| 25 | Summary: "update a release's title and notes: release edit <owner/name> <tag> [--title <t>] [--notes <n> | --file -] [--format md|org]", | 26 | Summary: "update a release's title and notes", |
| 27 | Usage: "release edit <owner/name> <tag> [--title <t>] [--notes <n> | --file -] [--format md|org]", | ||
| 26 | ReadsStdin: true, Run: runReleaseEdit}) | 28 | ReadsStdin: true, Run: runReleaseEdit}) |
| 27 | register(Command{Path: []string{"release", "list"}, | 29 | register(Command{Path: []string{"release", "list"}, |
| 28 | Summary: "list releases: release list <owner/name>", ReadOnly: true, Run: runReleaseList}) | 30 | Summary: "list releases", |
| 31 | Usage: "release list <owner/name>", ReadOnly: true, Run: runReleaseList}) | ||
| 29 | register(Command{Path: []string{"release", "show"}, | 32 | register(Command{Path: []string{"release", "show"}, |
| 30 | Summary: "show a release with assets: release show <owner/name> <tag>", ReadOnly: true, Run: runReleaseShow}) | 33 | Summary: "show a release with assets", |
| 34 | Usage: "release show <owner/name> <tag>", ReadOnly: true, Run: runReleaseShow}) | ||
| 31 | register(Command{Path: []string{"release", "delete"}, | 35 | register(Command{Path: []string{"release", "delete"}, |
| 32 | Summary: "delete a release and its assets: release delete <owner/name> <tag> --yes", Run: runReleaseDelete}) | 36 | Summary: "delete a release and its assets", |
| 37 | Usage: "release delete <owner/name> <tag> --yes", Run: runReleaseDelete}) | ||
| 33 | register(Command{Path: []string{"release", "asset", "add"}, | 38 | register(Command{Path: []string{"release", "asset", "add"}, |
| 34 | Summary: "upload an asset from stdin: release asset add <owner/name> <tag> <filename> < file", | 39 | Summary: "upload an asset from stdin", |
| 40 | Usage: "release asset add <owner/name> <tag> <filename> < file", | ||
| 35 | ReadsStdin: true, Run: runAssetAdd}) | 41 | ReadsStdin: true, Run: runAssetAdd}) |
| 36 | register(Command{Path: []string{"release", "asset", "get"}, | 42 | register(Command{Path: []string{"release", "asset", "get"}, |
| 37 | Summary: "write an asset to stdout: release asset get <owner/name> <tag> <filename> > file", | 43 | Summary: "write an asset to stdout", |
| 44 | Usage: "release asset get <owner/name> <tag> <filename> > file", | ||
| 38 | ReadOnly: true, Run: runAssetGet}) | 45 | ReadOnly: true, Run: runAssetGet}) |
| 39 | register(Command{Path: []string{"release", "asset", "remove"}, | 46 | register(Command{Path: []string{"release", "asset", "remove"}, |
| 40 | Summary: "remove an asset: release asset remove <owner/name> <tag> <filename>", Run: runAssetRemove}) | 47 | Summary: "remove an asset", |
| 48 | Usage: "release asset remove <owner/name> <tag> <filename>", Run: runAssetRemove}) | ||
| 41 | } | 49 | } |
| 42 | 50 | ||
| 43 | var assetNamePat = regexp.MustCompile(`^[A-Za-z0-9][A-Za-z0-9._+-]{0,199}$`) | 51 | var assetNamePat = regexp.MustCompile(`^[A-Za-z0-9][A-Za-z0-9._+-]{0,199}$`) |
internal/control/repo.go +48 −24
| @@ -25,53 +25,77 @@ func HooksDir(root string) string { return filepath.Join(root, "hooks") } | |||
| 25 | 25 | ||
| 26 | func init() { | 26 | func init() { |
| 27 | register(Command{Path: []string{"repo", "create"}, | 27 | register(Command{Path: []string{"repo", "create"}, |
| 28 | Summary: "create a repository: repo create <owner/name> [--private]", Run: runRepoCreate}) | 28 | Summary: "create a repository", |
| 29 | Usage: "repo create <owner/name> [--private]", Run: runRepoCreate}) | ||
| 29 | register(Command{Path: []string{"repo", "list"}, | 30 | register(Command{Path: []string{"repo", "list"}, |
| 30 | Summary: "list repositories you own or can access: repo list [--limit <n>] [--cursor <c>]", ReadOnly: true, Run: runRepoList}) | 31 | Summary: "list repositories you own or can access", |
| 32 | Usage: "repo list [--limit <n>] [--cursor <c>]", ReadOnly: true, Run: runRepoList}) | ||
| 31 | register(Command{Path: []string{"repo", "show"}, | 33 | register(Command{Path: []string{"repo", "show"}, |
| 32 | Summary: "show repository details: repo show <owner/name>", ReadOnly: true, Run: runRepoShow}) | 34 | Summary: "show repository details", |
| 35 | Usage: "repo show <owner/name>", ReadOnly: true, Run: runRepoShow}) | ||
| 33 | register(Command{Path: []string{"repo", "transfer"}, | 36 | register(Command{Path: []string{"repo", "transfer"}, |
| 34 | Summary: "move a repository to another owner: repo transfer <owner/name> <new-owner> (clone URLs change)", Run: runRepoTransfer}) | 37 | Summary: "move a repository to another owner", |
| 38 | Usage: "repo transfer <owner/name> <new-owner> (clone URLs change)", Run: runRepoTransfer}) | ||
| 35 | register(Command{Path: []string{"repo", "delete"}, | 39 | register(Command{Path: []string{"repo", "delete"}, |
| 36 | Summary: "delete a repository: repo delete <owner/name> --yes", Run: runRepoDelete}) | 40 | Summary: "delete a repository", |
| 41 | Usage: "repo delete <owner/name> --yes", Run: runRepoDelete}) | ||
| 37 | register(Command{Path: []string{"repo", "access", "grant"}, | 42 | register(Command{Path: []string{"repo", "access", "grant"}, |
| 38 | Summary: "grant access: repo access grant <owner/name> <user> read|write|admin", Run: runAccessGrant}) | 43 | Summary: "grant access", |
| 44 | Usage: "repo access grant <owner/name> <user> read|write|admin", Run: runAccessGrant}) | ||
| 39 | register(Command{Path: []string{"repo", "access", "revoke"}, | 45 | register(Command{Path: []string{"repo", "access", "revoke"}, |
| 40 | Summary: "revoke access: repo access revoke <owner/name> <user>", Run: runAccessRevoke}) | 46 | Summary: "revoke access", |
| 47 | Usage: "repo access revoke <owner/name> <user>", Run: runAccessRevoke}) | ||
| 41 | register(Command{Path: []string{"repo", "access", "list"}, | 48 | register(Command{Path: []string{"repo", "access", "list"}, |
| 42 | Summary: "list access grants: repo access list <owner/name>", ReadOnly: true, Run: runAccessList}) | 49 | Summary: "list access grants", |
| 50 | Usage: "repo access list <owner/name>", ReadOnly: true, Run: runAccessList}) | ||
| 43 | register(Command{Path: []string{"repo", "settings", "show"}, | 51 | register(Command{Path: []string{"repo", "settings", "show"}, |
| 44 | Summary: "show settings: repo settings show <owner/name>", ReadOnly: true, Run: runSettingsShow}) | 52 | Summary: "show settings", |
| 53 | Usage: "repo settings show <owner/name>", ReadOnly: true, Run: runSettingsShow}) | ||
| 45 | register(Command{Path: []string{"repo", "settings", "protect"}, | 54 | register(Command{Path: []string{"repo", "settings", "protect"}, |
| 46 | Summary: "protect a branch: repo settings protect <owner/name> <branch>", Run: runProtect}) | 55 | Summary: "protect a branch", |
| 56 | Usage: "repo settings protect <owner/name> <branch>", Run: runProtect}) | ||
| 47 | register(Command{Path: []string{"repo", "settings", "unprotect"}, | 57 | register(Command{Path: []string{"repo", "settings", "unprotect"}, |
| 48 | Summary: "unprotect a branch: repo settings unprotect <owner/name> <branch>", Run: runUnprotect}) | 58 | Summary: "unprotect a branch", |
| 59 | Usage: "repo settings unprotect <owner/name> <branch>", Run: runUnprotect}) | ||
| 49 | register(Command{Path: []string{"repo", "settings", "description"}, | 60 | register(Command{Path: []string{"repo", "settings", "description"}, |
| 50 | Summary: "set the repository description: repo settings description <owner/name> <text> ('' clears)", Run: runSetDescription}) | 61 | Summary: "set the repository description", |
| 62 | Usage: "repo settings description <owner/name> <text> ('' clears)", Run: runSetDescription}) | ||
| 51 | register(Command{Path: []string{"repo", "settings", "visibility"}, | 63 | register(Command{Path: []string{"repo", "settings", "visibility"}, |
| 52 | Summary: "set repository visibility: repo settings visibility <owner/name> public|private", Run: runSetVisibility}) | 64 | Summary: "set repository visibility", |
| 65 | Usage: "repo settings visibility <owner/name> public|private", Run: runSetVisibility}) | ||
| 53 | register(Command{Path: []string{"repo", "settings", "website"}, | 66 | register(Command{Path: []string{"repo", "settings", "website"}, |
| 54 | Summary: "set the repository website: repo settings website <owner/name> <url> ('' clears)", Run: runSetWebsite}) | 67 | Summary: "set the repository website", |
| 68 | Usage: "repo settings website <owner/name> <url> ('' clears)", Run: runSetWebsite}) | ||
| 55 | register(Command{Path: []string{"repo", "settings", "git-daemon"}, | 69 | register(Command{Path: []string{"repo", "settings", "git-daemon"}, |
| 56 | Summary: "expose over git://: repo settings git-daemon <owner/name> on|off", Run: runGitDaemon}) | 70 | Summary: "expose over git://", |
| 71 | Usage: "repo settings git-daemon <owner/name> on|off", Run: runGitDaemon}) | ||
| 57 | register(Command{Path: []string{"repo", "archive"}, | 72 | register(Command{Path: []string{"repo", "archive"}, |
| 58 | Summary: "archive a repository (read-only: pushes and issue/MR writes refused): repo archive <owner/name>", Run: runArchive}) | 73 | Summary: "archive a repository (read-only: pushes and issue/MR writes refused)", |
| 74 | Usage: "repo archive <owner/name>", Run: runArchive}) | ||
| 59 | register(Command{Path: []string{"repo", "unarchive"}, | 75 | register(Command{Path: []string{"repo", "unarchive"}, |
| 60 | Summary: "unarchive a repository: repo unarchive <owner/name>", Run: runUnarchive}) | 76 | Summary: "unarchive a repository", |
| 77 | Usage: "repo unarchive <owner/name>", Run: runUnarchive}) | ||
| 61 | register(Command{Path: []string{"repo", "topics"}, | 78 | register(Command{Path: []string{"repo", "topics"}, |
| 62 | Summary: "list topics: repo topics <owner/name>", ReadOnly: true, Run: runTopicsList}) | 79 | Summary: "list topics", |
| 80 | Usage: "repo topics <owner/name>", ReadOnly: true, Run: runTopicsList}) | ||
| 63 | register(Command{Path: []string{"repo", "topics", "add"}, | 81 | register(Command{Path: []string{"repo", "topics", "add"}, |
| 64 | Summary: "add topics: repo topics add <owner/name> <topic>...", Run: runTopicsAdd}) | 82 | Summary: "add topics", |
| 83 | Usage: "repo topics add <owner/name> <topic>...", Run: runTopicsAdd}) | ||
| 65 | register(Command{Path: []string{"repo", "topics", "remove"}, | 84 | register(Command{Path: []string{"repo", "topics", "remove"}, |
| 66 | Summary: "remove topics: repo topics remove <owner/name> <topic>...", Run: runTopicsRemove}) | 85 | Summary: "remove topics", |
| 86 | Usage: "repo topics remove <owner/name> <topic>...", Run: runTopicsRemove}) | ||
| 67 | register(Command{Path: []string{"repo", "search"}, | 87 | register(Command{Path: []string{"repo", "search"}, |
| 68 | Summary: "find repositories by name, description, or topic: repo search <query>", ReadOnly: true, Run: runRepoSearch}) | 88 | Summary: "find repositories by name, description, or topic", |
| 89 | Usage: "repo search <query>", ReadOnly: true, Run: runRepoSearch}) | ||
| 69 | register(Command{Path: []string{"repo", "grep"}, | 90 | register(Command{Path: []string{"repo", "grep"}, |
| 70 | Summary: "search file contents: repo grep <owner/name> <query> [--ref <ref>]", ReadOnly: true, Run: runRepoGrep}) | 91 | Summary: "search file contents", |
| 92 | Usage: "repo grep <owner/name> <query> [--ref <ref>]", ReadOnly: true, Run: runRepoGrep}) | ||
| 71 | register(Command{Path: []string{"repo", "pin"}, | 93 | register(Command{Path: []string{"repo", "pin"}, |
| 72 | Summary: "pin a repository to your dashboard: repo pin <owner/name>", Run: runRepoPin}) | 94 | Summary: "pin a repository to your dashboard", |
| 95 | Usage: "repo pin <owner/name>", Run: runRepoPin}) | ||
| 73 | register(Command{Path: []string{"repo", "unpin"}, | 96 | register(Command{Path: []string{"repo", "unpin"}, |
| 74 | Summary: "unpin a repository: repo unpin <owner/name>", Run: runRepoUnpin}) | 97 | Summary: "unpin a repository", |
| 98 | Usage: "repo unpin <owner/name>", Run: runRepoUnpin}) | ||
| 75 | } | 99 | } |
| 76 | 100 | ||
| 77 | const ( | 101 | const ( |
internal/control/sig.go +10 −5
| @@ -18,16 +18,21 @@ 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, on stdin)", ReadsStdin: true, Run: runPGPAdd}) | 21 | Summary: "register an OpenPGP public key (armored)", |
| 22 | Usage: "pgp add < key.asc", ReadsStdin: true, Run: runPGPAdd}) | ||
| 22 | register(Command{Path: []string{"pgp", "list"}, | 23 | register(Command{Path: []string{"pgp", "list"}, |
| 23 | Summary: "list registered OpenPGP keys", ReadOnly: true, Run: runPGPList}) | 24 | Summary: "list registered OpenPGP keys", |
| 25 | Usage: "pgp list", ReadOnly: true, Run: runPGPList}) | ||
| 24 | register(Command{Path: []string{"pgp", "remove"}, | 26 | register(Command{Path: []string{"pgp", "remove"}, |
| 25 | Summary: "remove an OpenPGP key by fingerprint", Run: runPGPRemove}) | 27 | Summary: "remove an OpenPGP key by fingerprint", |
| 28 | Usage: "pgp remove <fingerprint>", Run: runPGPRemove}) | ||
| 26 | register(Command{Path: []string{"repo", "commit"}, | 29 | register(Command{Path: []string{"repo", "commit"}, |
| 27 | Summary: "show one commit with its patch: repo commit <owner/name> <sha>", | 30 | Summary: "show one commit with its patch", |
| 31 | Usage: "repo commit <owner/name> <sha>", | ||
| 28 | ReadOnly: true, Run: runRepoCommit}) | 32 | ReadOnly: true, Run: runRepoCommit}) |
| 29 | register(Command{Path: []string{"repo", "log"}, | 33 | register(Command{Path: []string{"repo", "log"}, |
| 30 | Summary: "commit log with signature states: repo log <owner/name> [--ref <r>] [--limit n] [--path <file>]", ReadOnly: true, Run: runRepoLog}) | 34 | Summary: "commit log with signature states", |
| 35 | Usage: "repo log <owner/name> [--ref <r>] [--limit n] [--path <file>]", ReadOnly: true, Run: runRepoLog}) | ||
| 31 | } | 36 | } |
| 32 | 37 | ||
| 33 | func runPGPAdd(c *Ctx, args []string) int { | 38 | func runPGPAdd(c *Ctx, args []string) int { |
internal/control/status.go +4 −2
| @@ -13,10 +13,12 @@ import ( | |||
| 13 | 13 | ||
| 14 | func init() { | 14 | func init() { |
| 15 | register(Command{Path: []string{"status", "set"}, | 15 | register(Command{Path: []string{"status", "set"}, |
| 16 | Summary: "report a commit status (CI): status set <owner/name> <sha> --context <c> --state pending|success|failure|error [--description <d>] [--url <u>]", | 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 | Run: runStatusSet}) | 18 | Run: runStatusSet}) |
| 18 | register(Command{Path: []string{"status", "list"}, | 19 | register(Command{Path: []string{"status", "list"}, |
| 19 | Summary: "statuses on a commit: status list <owner/name> <sha>", ReadOnly: true, Run: runStatusList}) | 20 | Summary: "statuses on a commit", |
| 21 | Usage: "status list <owner/name> <sha>", ReadOnly: true, Run: runStatusList}) | ||
| 20 | } | 22 | } |
| 21 | 23 | ||
| 22 | var validStatusState = map[string]bool{"pending": true, "success": true, "failure": true, "error": true} | 24 | var validStatusState = map[string]bool{"pending": true, "success": true, "failure": true, "error": true} |
internal/control/teams.go +18 −9
| @@ -14,23 +14,32 @@ 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: org team create <org> <team>", Run: runTeamCreate}) | 17 | Summary: "create a team", |
| 18 | Usage: "org team create <org> <team>", Run: runTeamCreate}) | ||
| 18 | register(Command{Path: []string{"org", "team", "delete"}, | 19 | register(Command{Path: []string{"org", "team", "delete"}, |
| 19 | Summary: "delete a team (its grants with it): org team delete <org> <team>", Run: runTeamDelete}) | 20 | Summary: "delete a team (its grants with it)", |
| 21 | Usage: "org team delete <org> <team>", Run: runTeamDelete}) | ||
| 20 | register(Command{Path: []string{"org", "team", "list"}, | 22 | register(Command{Path: []string{"org", "team", "list"}, |
| 21 | Summary: "list an org's teams: org team list <org>", ReadOnly: true, Run: runTeamList}) | 23 | Summary: "list an org's teams", |
| 24 | Usage: "org team list <org>", ReadOnly: true, Run: runTeamList}) | ||
| 22 | register(Command{Path: []string{"org", "team", "show"}, | 25 | register(Command{Path: []string{"org", "team", "show"}, |
| 23 | Summary: "show a team's members and grants: org team show <org> <team>", ReadOnly: true, Run: runTeamShow}) | 26 | Summary: "show a team's members and grants", |
| 27 | Usage: "org team show <org> <team>", ReadOnly: true, Run: runTeamShow}) | ||
| 24 | register(Command{Path: []string{"org", "team", "add"}, | 28 | register(Command{Path: []string{"org", "team", "add"}, |
| 25 | Summary: "add org members to a team: org team add <org> <team> <user>...", Run: runTeamAdd}) | 29 | Summary: "add org members to a team", |
| 30 | Usage: "org team add <org> <team> <user>...", Run: runTeamAdd}) | ||
| 26 | register(Command{Path: []string{"org", "team", "remove"}, | 31 | register(Command{Path: []string{"org", "team", "remove"}, |
| 27 | Summary: "remove members from a team: org team remove <org> <team> <user>...", Run: runTeamRemove}) | 32 | Summary: "remove members from a team", |
| 33 | Usage: "org team remove <org> <team> <user>...", Run: runTeamRemove}) | ||
| 28 | register(Command{Path: []string{"org", "team", "grant"}, | 34 | register(Command{Path: []string{"org", "team", "grant"}, |
| 29 | Summary: "grant a team a role on an org repo: org team grant <org> <team> <owner/name> read|write|admin", Run: runTeamGrant}) | 35 | Summary: "grant a team a role on an org repo", |
| 36 | Usage: "org team grant <org> <team> <owner/name> read|write|admin", Run: runTeamGrant}) | ||
| 30 | register(Command{Path: []string{"org", "team", "revoke"}, | 37 | register(Command{Path: []string{"org", "team", "revoke"}, |
| 31 | Summary: "revoke a team's grant: org team revoke <org> <team> <owner/name>", Run: runTeamRevoke}) | 38 | Summary: "revoke a team's grant", |
| 39 | Usage: "org team revoke <org> <team> <owner/name>", Run: runTeamRevoke}) | ||
| 32 | register(Command{Path: []string{"org", "settings", "members-role"}, | 40 | register(Command{Path: []string{"org", "settings", "members-role"}, |
| 33 | Summary: "role plain membership implies on every org repo: org settings members-role <org> write|read|none (default write)", Run: runOrgMembersRole}) | 41 | Summary: "role plain membership implies on every org repo", |
| 42 | Usage: "org settings members-role <org> write|read|none (default write)", Run: runOrgMembersRole}) | ||
| 34 | } | 43 | } |
| 35 | 44 | ||
| 36 | // orgAdminRef resolves an org and requires the caller to admin it. | 45 | // orgAdminRef resolves an org and requires the caller to admin it. |
internal/control/token.go +6 −3
| @@ -14,12 +14,15 @@ import ( | |||
| 14 | 14 | ||
| 15 | func init() { | 15 | func init() { |
| 16 | register(Command{Path: []string{"token", "create"}, | 16 | register(Command{Path: []string{"token", "create"}, |
| 17 | Summary: "mint an API token (shown once): token create --name <n> [--scope full|read] [--ttl 30d|720h]", | 17 | Summary: "mint an API token (shown once)", |
| 18 | Usage: "token create --name <n> [--scope full|read] [--ttl 30d|720h]", | ||
| 18 | SSHOnly: true, Run: runTokenCreate}) | 19 | SSHOnly: true, Run: runTokenCreate}) |
| 19 | register(Command{Path: []string{"token", "list"}, | 20 | register(Command{Path: []string{"token", "list"}, |
| 20 | Summary: "list API tokens", ReadOnly: true, SSHOnly: true, Run: runTokenList}) | 21 | Summary: "list API tokens", |
| 22 | Usage: "token list", ReadOnly: true, SSHOnly: true, Run: runTokenList}) | ||
| 21 | register(Command{Path: []string{"token", "revoke"}, | 23 | register(Command{Path: []string{"token", "revoke"}, |
| 22 | Summary: "revoke an API token by name: token revoke <name>", | 24 | Summary: "revoke an API token by name", |
| 25 | Usage: "token revoke <name>", | ||
| 23 | SSHOnly: true, Run: runTokenRevoke}) | 26 | SSHOnly: true, Run: runTokenRevoke}) |
| 24 | } | 27 | } |
| 25 | 28 | ||
internal/control/web.go +2 −1
| @@ -13,7 +13,8 @@ func newStoredToken() (token, hash string, err error) { return store.NewToken() | |||
| 13 | 13 | ||
| 14 | func init() { | 14 | func init() { |
| 15 | register(Command{Path: []string{"web", "login"}, | 15 | register(Command{Path: []string{"web", "login"}, |
| 16 | Summary: "mint a one-time browser login URL", Run: runWebLogin}) | 16 | Summary: "mint a one-time browser login URL", |
| 17 | Usage: "web login", Run: runWebLogin}) | ||
| 17 | } | 18 | } |
| 18 | 19 | ||
| 19 | func runWebLogin(c *Ctx, args []string) int { | 20 | func runWebLogin(c *Ctx, args []string) int { |
internal/control/webhook.go +10 −5
| @@ -14,15 +14,20 @@ import ( | |||
| 14 | 14 | ||
| 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: webhook add <owner/name> <url> [--secret <s>] [--events push,issue.created|*]", Run: runWebhookAdd}) | 17 | Summary: "add a webhook", |
| 18 | Usage: "webhook add <owner/name> <url> [--secret <s>] [--events push,issue.created|*]", Run: runWebhookAdd}) | ||
| 18 | register(Command{Path: []string{"webhook", "list"}, | 19 | register(Command{Path: []string{"webhook", "list"}, |
| 19 | Summary: "list webhooks: webhook list <owner/name>", ReadOnly: true, Run: runWebhookList}) | 20 | Summary: "list webhooks", |
| 21 | Usage: "webhook list <owner/name>", ReadOnly: true, Run: runWebhookList}) | ||
| 20 | register(Command{Path: []string{"webhook", "remove"}, | 22 | register(Command{Path: []string{"webhook", "remove"}, |
| 21 | Summary: "remove a webhook: webhook remove <owner/name> <id>", Run: runWebhookRemove}) | 23 | Summary: "remove a webhook", |
| 24 | Usage: "webhook remove <owner/name> <id>", Run: runWebhookRemove}) | ||
| 22 | register(Command{Path: []string{"webhook", "deliveries"}, | 25 | register(Command{Path: []string{"webhook", "deliveries"}, |
| 23 | Summary: "recent deliveries: webhook deliveries <owner/name> [--limit n]", ReadOnly: true, Run: runWebhookDeliveries}) | 26 | Summary: "recent deliveries", |
| 27 | Usage: "webhook deliveries <owner/name> [--limit n]", ReadOnly: true, Run: runWebhookDeliveries}) | ||
| 24 | register(Command{Path: []string{"webhook", "redeliver"}, | 28 | register(Command{Path: []string{"webhook", "redeliver"}, |
| 25 | Summary: "queue a delivery again: webhook redeliver <owner/name> <delivery-id>", Run: runWebhookRedeliver}) | 29 | Summary: "queue a delivery again", |
| 30 | Usage: "webhook redeliver <owner/name> <delivery-id>", Run: runWebhookRedeliver}) | ||
| 26 | } | 31 | } |
| 27 | 32 | ||
| 28 | func runWebhookAdd(c *Ctx, args []string) int { | 33 | func runWebhookAdd(c *Ctx, args []string) int { |
internal/control/wiki.go +4 −2
| @@ -17,13 +17,15 @@ import ( | |||
| 17 | func init() { | 17 | func init() { |
| 18 | register(Command{ | 18 | register(Command{ |
| 19 | Path: []string{"wiki", "list"}, | 19 | Path: []string{"wiki", "list"}, |
| 20 | Summary: "list a repository's wiki pages: wiki list <owner/name>", | 20 | Summary: "list a repository's wiki pages", |
| 21 | Usage: "wiki list <owner/name>", | ||
| 21 | ReadOnly: true, | 22 | ReadOnly: true, |
| 22 | Run: runWikiList, | 23 | Run: runWikiList, |
| 23 | }) | 24 | }) |
| 24 | register(Command{ | 25 | register(Command{ |
| 25 | Path: []string{"wiki", "show"}, | 26 | Path: []string{"wiki", "show"}, |
| 26 | Summary: "print a wiki page: wiki show <owner/name> [<page>]", | 27 | Summary: "print a wiki page", |
| 28 | Usage: "wiki show <owner/name> [<page>]", | ||
| 27 | ReadOnly: true, | 29 | ReadOnly: true, |
| 28 | Run: runWikiShow, | 30 | Run: runWikiShow, |
| 29 | }) | 31 | }) |
internal/httpd/api.go +1 −1
| @@ -79,7 +79,7 @@ func (s *Server) apiCmd(w http.ResponseWriter, r *http.Request) { | |||
| 79 | status := statusForExit(code) | 79 | status := statusForExit(code) |
| 80 | 80 | ||
| 81 | // Commands normally emit exactly one JSON envelope; inject exit_code. | 81 | // Commands normally emit exactly one JSON envelope; inject exit_code. |
| 82 | // A few (mr diff, help) write raw text instead — wrap those. | 82 | // A few (mr diff, repo download) write raw bytes instead — wrap those. |
| 83 | var body map[string]any | 83 | var body map[string]any |
| 84 | if err := json.Unmarshal(stdout.Bytes(), &body); err != nil || body == nil { | 84 | if err := json.Unmarshal(stdout.Bytes(), &body); err != nil || body == nil { |
| 85 | body = map[string]any{ | 85 | body = map[string]any{ |