Commit dca7370cd1
Verified · cmc ci/build: success
cmd/gitbay/coverage_test.go +1 −1
| @@ -13,7 +13,7 @@ import ( | ||
| 13 | 13 | // registry must be reachable by typing it, or the CLI is not the |
| 14 | 14 | // complete interface the design claims. |
| 15 | 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 | 17 | "runner next": "the CI runner's wire protocol, not for humans", |
| 18 | 18 | "runner done": "the CI runner's wire protocol, not for humans", |
| 19 | 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 | 120 | DisableFlagParsing: true, |
| 121 | 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 | 125 | for _, a := range args { |
| 124 | 126 | if a == "--help" || a == "-h" { |
| 125 | return cmd.Help() | |
| 127 | os.Exit(runServerHelp(o)) | |
| 126 | 128 | } |
| 127 | 129 | } |
| 128 | 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 | 146 | func runPass(o passOpts, args []string) int { |
| 135 | 147 | t, err := resolveTarget() |
| 136 | 148 | if err != nil { |
e2e/api_test.go +35 −3
| @@ -7,6 +7,8 @@ import ( | ||
| 7 | 7 | "io" |
| 8 | 8 | "net/http" |
| 9 | 9 | "net/url" |
| 10 | "os" | |
| 11 | "path/filepath" | |
| 10 | 12 | "strings" |
| 11 | 13 | "testing" |
| 12 | 14 | "time" |
| @@ -96,11 +98,41 @@ func TestJSONAPI(t *testing.T) { | ||
| 96 | 98 | t.Fatalf("unknown command: %d", status) |
| 97 | 99 | } |
| 98 | 100 | |
| 99 | // Raw-output commands (no envelope) are wrapped. | |
| 100 | status, body = inst.apiCall(t, token, []string{"help"}, "") | |
| 101 | if status != 200 || !strings.Contains(body["output"].(string), "repo create") { | |
| 101 | // Raw-output commands (no envelope) are wrapped. Reading a file is the | |
| 102 | // cheapest raw output, so give the repo one commit to read from. | |
| 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 | 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 | 137 | // Git transport is refused by name. |
| 106 | 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 | 11 | func init() { |
| 12 | 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 | 17 | func runAudit(c *Ctx, args []string) int { |
internal/control/build.go +22 −11
| @@ -18,37 +18,48 @@ import ( | ||
| 18 | 18 | |
| 19 | 19 | func init() { |
| 20 | 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 | 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 | 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 | 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 | 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 | 37 | // Secrets: set over stdin, listed by name only, injected into the |
| 33 | 38 | // repo's builds as environment variables. Same discipline as mirror |
| 34 | 39 | // tokens — the value never appears in argv, logs, or output. |
| 35 | 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 | 43 | ReadsStdin: true, SSHOnly: true, Run: runSecretSet}) |
| 38 | 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 | 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 | 51 | // Runner commands: the claim/report loop for gitbay-runner. Admin-only — |
| 44 | 52 | // a runner executes arbitrary repo code, so handing out jobs is the |
| 45 | 53 | // instance operator's call. |
| 46 | 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 | 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 | 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 | 65 | type buildOut struct { |
internal/control/commitfile.go +3 −2
| @@ -12,8 +12,9 @@ import ( | ||
| 12 | 12 | |
| 13 | 13 | func init() { |
| 14 | 14 | register(Command{ |
| 15 | Path: []string{"repo", "commit-file"}, | |
| 16 | Summary: "write a file and commit it: repo commit-file <owner/name> <path> " + | |
| 15 | Path: []string{"repo", "commit-file"}, | |
| 16 | Summary: "write a file and commit it", | |
| 17 | Usage: "repo commit-file <owner/name> <path> " + | |
| 17 | 18 | "--ref <branch> [--message <m>] [--file -]", |
| 18 | 19 | ReadsStdin: true, |
| 19 | 20 | Run: runCommitFile, |
internal/control/control.go +42 −6
| @@ -9,6 +9,7 @@ import ( | ||
| 9 | 9 | "io" |
| 10 | 10 | "reflect" |
| 11 | 11 | "slices" |
| 12 | "strings" | |
| 12 | 13 | |
| 13 | 14 | "gitbay.org/gitbay/internal/config" |
| 14 | 15 | "gitbay.org/gitbay/internal/protocol" |
| @@ -36,8 +37,12 @@ type Ctx struct { | ||
| 36 | 37 | } |
| 37 | 38 | |
| 38 | 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 | 44 | Summary string |
| 45 | Usage string | |
| 41 | 46 | ReadsStdin bool |
| 42 | 47 | ReadOnly bool // safe for read-scoped API tokens |
| 43 | 48 | SSHOnly bool // refused over the HTTP API (credential minting) |
| @@ -161,13 +166,44 @@ func init() { | ||
| 161 | 166 | register(Command{ |
| 162 | 167 | Path: []string{"help"}, |
| 163 | 168 | Summary: "list available commands", |
| 169 | Usage: "help [<prefix>...]", | |
| 164 | 170 | ReadOnly: true, |
| 165 | Run: func(c *Ctx, args []string) int { | |
| 166 | for _, cmd := range registry { | |
| 167 | fmt.Fprintf(c.Stdout, "%-24s %s\n", joinPath(cmd.Path), cmd.Summary) | |
| 171 | Run: runHelp, | |
| 172 | }) | |
| 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 | |
| 170 | }, | |
| 206 | } | |
| 171 | 207 | }) |
| 172 | 208 | } |
| 173 | 209 | |
internal/control/control_test.go +77
| @@ -1,6 +1,10 @@ | ||
| 1 | 1 | package control |
| 2 | 2 | |
| 3 | 3 | import ( |
| 4 | "bytes" | |
| 5 | "encoding/json" | |
| 6 | "io" | |
| 7 | "slices" | |
| 4 | 8 | "strings" |
| 5 | 9 | "testing" |
| 6 | 10 | |
| @@ -86,3 +90,76 @@ func TestMRDedupKeyCannotCollideWithASHA(t *testing.T) { | ||
| 86 | 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 | 16 | func init() { |
| 17 | 17 | register(Command{Path: []string{"dashboard"}, |
| 18 | 18 | Summary: "one read for the account dashboard: review queue, assigned and open work, pins, activity, builds", |
| 19 | Usage: "dashboard", | |
| 19 | 20 | ReadOnly: true, Run: runDashboard}) |
| 20 | 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 | 24 | ReadOnly: true, Run: runFeed}) |
| 23 | 25 | } |
| 24 | 26 | |
internal/control/deploykey.go +6 −3
| @@ -14,12 +14,15 @@ import ( | ||
| 14 | 14 | |
| 15 | 15 | func init() { |
| 16 | 16 | register(Command{Path: []string{"repo", "deploy-key", "add"}, |
| 17 | Summary: "bind a read-only (or --rw) key to one repository: 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 | 19 | ReadsStdin: true, Run: runDeployKeyAdd}) |
| 19 | 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 | 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 | 28 | func runDeployKeyAdd(c *Ctx, args []string) int { |
internal/control/deps.go +6 −3
| @@ -15,11 +15,14 @@ func init() { | ||
| 15 | 15 | // public registry what the repository depends on, which is the owner's |
| 16 | 16 | // disclosure to make, not the instance's. |
| 17 | 17 | register(Command{Path: []string{"repo", "deps", "enable"}, |
| 18 | Summary: "check dependencies for updates: repo deps enable <owner/name>", Run: runDepsEnable}) | |
| 18 | Summary: "check dependencies for updates", | |
| 19 | Usage: "repo deps enable <owner/name>", Run: runDepsEnable}) | |
| 19 | 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 | 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 | 28 | func runDepsEnable(c *Ctx, args []string) int { |
internal/control/diffcomment.go +8 −4
| @@ -16,14 +16,18 @@ import ( | ||
| 16 | 16 | |
| 17 | 17 | func init() { |
| 18 | 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 | 21 | ReadsStdin: true, Run: runDiffComment}) |
| 21 | 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 | 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 | 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 | 33 | func runDiffComment(c *Ctx, args []string) int { |
internal/control/explore.go +4 −2
| @@ -12,7 +12,8 @@ import ( | ||
| 12 | 12 | func init() { |
| 13 | 13 | register(Command{ |
| 14 | 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 | 17 | ReadOnly: true, |
| 17 | 18 | Run: runExplore, |
| 18 | 19 | }) |
| @@ -20,7 +21,8 @@ func init() { | ||
| 20 | 21 | Path: []string{"repo", "download"}, |
| 21 | 22 | // Not "repo archive": that name is taken by the read-only flag, |
| 22 | 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 | 26 | ReadOnly: true, |
| 25 | 27 | Run: runRepoDownload, |
| 26 | 28 | }) |
internal/control/ghimport.go +2 −1
| @@ -20,7 +20,8 @@ import ( | ||
| 20 | 20 | |
| 21 | 21 | func init() { |
| 22 | 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 | 25 | ReadsStdin: true, Run: runImportIssues}) |
| 25 | 26 | } |
| 26 | 27 | |
internal/control/identity.go +5 −1
| @@ -15,24 +15,28 @@ func init() { | ||
| 15 | 15 | register(Command{ |
| 16 | 16 | Path: []string{"whoami"}, |
| 17 | 17 | Summary: "show the authenticated account", |
| 18 | Usage: "whoami", | |
| 18 | 19 | ReadOnly: true, |
| 19 | 20 | Run: runWhoami, |
| 20 | 21 | }) |
| 21 | 22 | register(Command{ |
| 22 | 23 | Path: []string{"keys", "list"}, |
| 23 | 24 | Summary: "list registered SSH keys", |
| 25 | Usage: "keys list", | |
| 24 | 26 | ReadOnly: true, |
| 25 | 27 | Run: runKeysList, |
| 26 | 28 | }) |
| 27 | 29 | register(Command{ |
| 28 | 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 | 33 | ReadsStdin: true, |
| 31 | 34 | Run: runKeysAdd, |
| 32 | 35 | }) |
| 33 | 36 | register(Command{ |
| 34 | 37 | Path: []string{"keys", "remove"}, |
| 35 | 38 | Summary: "remove an SSH key by fingerprint", |
| 39 | Usage: "keys remove <fingerprint>", | |
| 36 | 40 | Run: runKeysRemove, |
| 37 | 41 | }) |
| 38 | 42 | } |
internal/control/import.go +2 −1
| @@ -17,7 +17,8 @@ import ( | ||
| 17 | 17 | |
| 18 | 18 | func init() { |
| 19 | 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 | 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 | 17 | func init() { |
| 18 | 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 | 21 | ReadsStdin: true, Run: runIssueCreate}) |
| 21 | 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 | 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 | 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 | 31 | ReadsStdin: true, Run: runIssueEdit}) |
| 28 | 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 | 35 | ReadsStdin: true, Run: runIssueComment}) |
| 31 | 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 | 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 | 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 | 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 | 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 | 14 | func init() { |
| 15 | 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 | 18 | ReadOnly: true, Run: runAccountExport}) |
| 18 | 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 | 22 | ReadsStdin: true, Run: runAccountImportBundle}) |
| 21 | 23 | } |
| 22 | 24 | |
internal/control/milestone.go +14 −7
| @@ -16,19 +16,26 @@ import ( | ||
| 16 | 16 | |
| 17 | 17 | func init() { |
| 18 | 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 | 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 | 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 | 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 | 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 | 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 | 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 | 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 | 17 | func init() { |
| 18 | 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 | 21 | ReadsStdin: true, SSHOnly: true, Run: runMirrorAdd}) |
| 21 | 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 | 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 | 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 | 33 | func runMirrorAdd(c *Ctx, args []string) int { |
internal/control/mr.go +30 −15
| @@ -17,38 +17,53 @@ import ( | ||
| 17 | 17 | |
| 18 | 18 | func init() { |
| 19 | 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 | 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 | 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 | 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 | 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 | 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 | 37 | ReadsStdin: true, Run: runMRCreate}) |
| 32 | 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 | 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 | 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 | 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 | 50 | ReadsStdin: true, Run: runMREdit}) |
| 41 | 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 | 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 | 57 | ReadsStdin: true, Run: runMRComment}) |
| 46 | 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 | 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 | 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 | 69 | func runRepoFork(c *Ctx, args []string) int { |
internal/control/org.go +16 −8
| @@ -14,21 +14,29 @@ import ( | ||
| 14 | 14 | |
| 15 | 15 | func init() { |
| 16 | 16 | register(Command{Path: []string{"org", "create"}, |
| 17 | Summary: "create an organization (you become its first admin): org create <name>", Run: runOrgCreate}) | |
| 17 | Summary: "create an organization (you become its first admin)", | |
| 18 | Usage: "org create <name>", Run: runOrgCreate}) | |
| 18 | 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 | 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 | 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 | 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 | 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 | 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 | 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 | 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 | 21 | func init() { |
| 22 | 22 | register(Command{Path: []string{"repo", "domain", "add"}, |
| 23 | Summary: "claim a custom pages domain (verify with a DNS TXT record): 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 | 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 | 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 | 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 | 36 | // challengeLabel prefixes the domain for the ownership TXT record. |
internal/control/profile.go +6 −3
| @@ -16,11 +16,14 @@ import ( | ||
| 16 | 16 | |
| 17 | 17 | func init() { |
| 18 | 18 | register(Command{Path: []string{"profile", "show"}, |
| 19 | Summary: "show a user's or org's profile: 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 | 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 | 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 | 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 | 18 | func init() { |
| 19 | 19 | register(Command{ |
| 20 | 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 | 23 | ReadOnly: true, |
| 23 | 24 | Run: runRepoTree, |
| 24 | 25 | }) |
| 25 | 26 | register(Command{ |
| 26 | 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 | 30 | ReadOnly: true, |
| 29 | 31 | Run: runRepoCat, |
| 30 | 32 | }) |
| 31 | 33 | register(Command{ |
| 32 | 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 | 37 | ReadOnly: true, |
| 35 | 38 | Run: runRepoBlame, |
| 36 | 39 | }) |
| 37 | 40 | register(Command{ |
| 38 | 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 | 44 | ReadOnly: true, |
| 41 | 45 | Run: runRepoRefs, |
| 42 | 46 | }) |
internal/control/register.go +5 −2
| @@ -19,15 +19,18 @@ import ( | ||
| 19 | 19 | func init() { |
| 20 | 20 | register(Command{Path: []string{"register"}, |
| 21 | 21 | Summary: "create an account (only meaningful for unregistered keys)", |
| 22 | Usage: "register --username <name> [--email <address> | --invite <code>]", | |
| 22 | 23 | Run: func(c *Ctx, args []string) int { |
| 23 | 24 | return c.fail(protocol.ExitUsage, |
| 24 | 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 | 26 | c.User.Username) |
| 26 | 27 | }}) |
| 27 | 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 | 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 | 36 | func siteHost(cfg config.Config) string { |
internal/control/release.go +16 −8
| @@ -19,25 +19,33 @@ import ( | ||
| 19 | 19 | |
| 20 | 20 | func init() { |
| 21 | 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 | 24 | ReadsStdin: true, Run: runReleaseCreate}) |
| 24 | 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 | 28 | ReadsStdin: true, Run: runReleaseEdit}) |
| 27 | 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 | 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 | 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 | 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 | 41 | ReadsStdin: true, Run: runAssetAdd}) |
| 36 | 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 | 45 | ReadOnly: true, Run: runAssetGet}) |
| 39 | 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 | 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 | 26 | func init() { |
| 27 | 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 | 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 | 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 | 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 | 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 | 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 | 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 | 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 | 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 | 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 | 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 | 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 | 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 | 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 | 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 | 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 | 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 | 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 | 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 | 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 | 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 | 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 | 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 | 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 | 101 | const ( |
internal/control/sig.go +10 −5
| @@ -18,16 +18,21 @@ import ( | ||
| 18 | 18 | |
| 19 | 19 | func init() { |
| 20 | 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 | 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 | 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 | 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 | 32 | ReadOnly: true, Run: runRepoCommit}) |
| 29 | 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 | 38 | func runPGPAdd(c *Ctx, args []string) int { |
internal/control/status.go +4 −2
| @@ -13,10 +13,12 @@ import ( | ||
| 13 | 13 | |
| 14 | 14 | func init() { |
| 15 | 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 | 18 | Run: runStatusSet}) |
| 18 | 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 | 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 | 15 | func init() { |
| 16 | 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 | 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 | 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 | 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 | 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 | 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 | 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 | 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 | 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 | 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 | 15 | func init() { |
| 16 | 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 | 19 | SSHOnly: true, Run: runTokenCreate}) |
| 19 | 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 | 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 | 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 | 14 | func init() { |
| 15 | 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 | 20 | func runWebLogin(c *Ctx, args []string) int { |
internal/control/webhook.go +10 −5
| @@ -14,15 +14,20 @@ import ( | ||
| 14 | 14 | |
| 15 | 15 | func init() { |
| 16 | 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 | 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 | 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 | 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 | 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 | 33 | func runWebhookAdd(c *Ctx, args []string) int { |
internal/control/wiki.go +4 −2
| @@ -17,13 +17,15 @@ import ( | ||
| 17 | 17 | func init() { |
| 18 | 18 | register(Command{ |
| 19 | 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 | 22 | ReadOnly: true, |
| 22 | 23 | Run: runWikiList, |
| 23 | 24 | }) |
| 24 | 25 | register(Command{ |
| 25 | 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 | 29 | ReadOnly: true, |
| 28 | 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 | 79 | status := statusForExit(code) |
| 80 | 80 | |
| 81 | 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 | 83 | var body map[string]any |
| 84 | 84 | if err := json.Unmarshal(stdout.Bytes(), &body); err != nil || body == nil { |
| 85 | 85 | body = map[string]any{ |