failErr is a failure by default, failInput is usage !379

merged merged by cmc on 2026-09-11 23:42 UTC · krz/gitbay:fix-211-failerr into main

25 files changed, +93 −77

Layout: unified · split

.gitbay/wiki/Users.org +3 −2
@@ -632,8 +632,9 @@ addresses receive anything. Delivery retries on relay failure.
632Every read command takes =--json= and emits one envelope: 632Every read command takes =--json= and emits one envelope:
633={"protocol_version": 1, "data": ...}=. stdout is data, stderr is 633={"protocol_version": 1, "data": ...}=. stdout is data, stderr is
634messages. Exit codes are stable: 0 ok, 1 failure, 2 usage, 3 not found, 634messages. Exit codes are stable: 0 ok, 1 failure, 2 usage, 3 not found,
6354 denied, 5 server/protocol error. Nothing ever prompts; destructive 6354 denied, 5 server/protocol error. Usage means the arguments were wrong;
636commands take =--yes=. 636a refusal — a name already taken, a state that does not allow the change
637— is a failure. Nothing ever prompts; destructive commands take =--yes=.
637 638
638For HTTP automation see [[API]]. 639For HTTP automation see [[API]].
639 640
e2e/adminusers_test.go +3 −3
@@ -190,7 +190,7 @@ func TestAdminPromoteDemote(t *testing.T) {
190 t.Fatalf("disabled account promoted: exit %d %s", code, errOut) 190 t.Fatalf("disabled account promoted: exit %d %s", code, errOut)
191 } 191 }
192 // The only admin cannot step down. 192 // The only admin cannot step down.
193 if _, errOut, code := inst.ssh(t, rootKey, "", "admin", "user", "demote", "root"); code != 2 || !strings.Contains(errOut, "only instance admin") { 193 if _, errOut, code := inst.ssh(t, rootKey, "", "admin", "user", "demote", "root"); code != 1 || !strings.Contains(errOut, "only instance admin") {
194 t.Fatalf("last admin demoted: exit %d %s", code, errOut) 194 t.Fatalf("last admin demoted: exit %d %s", code, errOut)
195 } 195 }
196 if _, _, code := inst.ssh(t, rootKey, "", "admin", "user", "promote", "alice"); code != 0 { 196 if _, _, code := inst.ssh(t, rootKey, "", "admin", "user", "promote", "alice"); code != 0 {
@@ -209,7 +209,7 @@ func TestAdminPromoteDemote(t *testing.T) {
209 if _, _, code := inst.ssh(t, rootKey, "", "audit"); code != 4 { 209 if _, _, code := inst.ssh(t, rootKey, "", "audit"); code != 4 {
210 t.Fatal("demoted account still admin") 210 t.Fatal("demoted account still admin")
211 } 211 }
212 if _, _, code := inst.ssh(t, aliceKey, "", "admin", "user", "demote", "alice"); code != 2 { 212 if _, _, code := inst.ssh(t, aliceKey, "", "admin", "user", "demote", "alice"); code != 1 {
213 t.Fatal("last admin demoted") 213 t.Fatal("last admin demoted")
214 } 214 }
215 // Host-local recovery: the operator restores root without an admin key. 215 // Host-local recovery: the operator restores root without an admin key.
@@ -356,7 +356,7 @@ func TestAdminHostAndSSHAreOneSurface(t *testing.T) {
356 if _, _, code := inst.ssh(t, carolKey, "", "whoami"); code != 0 { 356 if _, _, code := inst.ssh(t, carolKey, "", "whoami"); code != 0 {
357 t.Fatal("created account cannot authenticate") 357 t.Fatal("created account cannot authenticate")
358 } 358 }
359 if _, errOut, code := inst.ssh(t, rootKey, "", "admin", "user", "create", "alice"); code != 2 || !strings.Contains(errOut, "taken") { 359 if _, errOut, code := inst.ssh(t, rootKey, "", "admin", "user", "create", "alice"); code != 1 || !strings.Contains(errOut, "taken") {
360 t.Fatalf("duplicate create: exit %d %s", code, errOut) 360 t.Fatalf("duplicate create: exit %d %s", code, errOut)
361 } 361 }
362 for _, args := range [][]string{{"admin", "stats"}, {"admin", "user", "disable", "carol"}, {"admin", "invite", "--email", "x@example.test"}} { 362 for _, args := range [][]string{{"admin", "stats"}, {"admin", "user", "disable", "carol"}, {"admin", "invite", "--email", "x@example.test"}} {
e2e/diffcomment_test.go +1 −1
@@ -76,7 +76,7 @@ func TestDiffComments(t *testing.T) {
76 } 76 }
77 json.Unmarshal([]byte(out), &env3) 77 json.Unmarshal([]byte(out), &env3)
78 if _, errOut, code = inst.ssh(t, bobKey, "", "mr", "diff-comment", "alice/lib", "1", 78 if _, errOut, code = inst.ssh(t, bobKey, "", "mr", "diff-comment", "alice/lib", "1",
79 "--reply", fmt.Sprint(env3.Data.ID), "--message", "'nested'"); code != 2 || !strings.Contains(errOut, "thread root") { 79 "--reply", fmt.Sprint(env3.Data.ID), "--message", "'nested'"); code != 1 || !strings.Contains(errOut, "thread root") {
80 t.Fatalf("nested reply: exit %d, %s", code, errOut) 80 t.Fatalf("nested reply: exit %d, %s", code, errOut)
81 } 81 }
82 82
e2e/org_test.go +2 −2
@@ -97,7 +97,7 @@ func TestOrganizations(t *testing.T) {
97 t.Fatalf("bob (now admin) removing alice: %s", errOut) 97 t.Fatalf("bob (now admin) removing alice: %s", errOut)
98 } 98 }
99 _, errOut, code := inst.ssh(t, bobKey, "", "org", "members", "remove", "krz", "bob") 99 _, errOut, code := inst.ssh(t, bobKey, "", "org", "members", "remove", "krz", "bob")
100 if code != 2 || !strings.Contains(errOut, "at least one admin") { 100 if code != 1 || !strings.Contains(errOut, "at least one admin") {
101 t.Fatalf("last admin removal: %d %s", code, errOut) 101 t.Fatalf("last admin removal: %d %s", code, errOut)
102 } 102 }
103 103
@@ -150,7 +150,7 @@ func TestOrganizations(t *testing.T) {
150 } 150 }
151 rnWork := t.TempDir() 151 rnWork := t.TempDir()
152 mustGit(t, rnWork, inst.gitEnv(aliceKey), "clone", inst.sshURL("oldname/thing"), "w1") 152 mustGit(t, rnWork, inst.gitEnv(aliceKey), "clone", inst.sshURL("oldname/thing"), "w1")
153 if _, errOut, code = inst.ssh(t, aliceKey, "", "org", "rename", "oldname", "bob"); code != 2 || !strings.Contains(errOut, "taken") { 153 if _, errOut, code = inst.ssh(t, aliceKey, "", "org", "rename", "oldname", "bob"); code != 1 || !strings.Contains(errOut, "taken") {
154 t.Fatalf("rename onto user name: %d %s", code, errOut) 154 t.Fatalf("rename onto user name: %d %s", code, errOut)
155 } 155 }
156 if _, errOut, code = inst.ssh(t, aliceKey, "", "org", "rename", "oldname", "newname"); code != 0 { 156 if _, errOut, code = inst.ssh(t, aliceKey, "", "org", "rename", "oldname", "newname"); code != 0 {
e2e/release_test.go +2 −2
@@ -36,7 +36,7 @@ func TestReleases(t *testing.T) {
36 "--title", "'First light'", "--notes", "'the **first** release'"); code != 0 { 36 "--title", "'First light'", "--notes", "'the **first** release'"); code != 0 {
37 t.Fatalf("release create: %s", errOut) 37 t.Fatalf("release create: %s", errOut)
38 } 38 }
39 if _, _, code := inst.ssh(t, aliceKey, "", "release", "create", "alice/app", "v1.0"); code != 2 { 39 if _, _, code := inst.ssh(t, aliceKey, "", "release", "create", "alice/app", "v1.0"); code != 1 {
40 t.Fatal("duplicate release accepted") 40 t.Fatal("duplicate release accepted")
41 } 41 }
42 42
@@ -67,7 +67,7 @@ func TestReleases(t *testing.T) {
67 if _, errOut, code := inst.ssh(t, aliceKey, payload, "release", "asset", "add", "alice/app", "v1.0", "tool-linux-amd64"); code != 0 { 67 if _, errOut, code := inst.ssh(t, aliceKey, payload, "release", "asset", "add", "alice/app", "v1.0", "tool-linux-amd64"); code != 0 {
68 t.Fatalf("asset add: %s", errOut) 68 t.Fatalf("asset add: %s", errOut)
69 } 69 }
70 if _, _, code := inst.ssh(t, aliceKey, payload, "release", "asset", "add", "alice/app", "v1.0", "tool-linux-amd64"); code != 2 { 70 if _, _, code := inst.ssh(t, aliceKey, payload, "release", "asset", "add", "alice/app", "v1.0", "tool-linux-amd64"); code != 1 {
71 t.Fatal("duplicate asset accepted") 71 t.Fatal("duplicate asset accepted")
72 } 72 }
73 if _, errOut, code := inst.ssh(t, aliceKey, "", "release", "asset", "add", "alice/app", "v1.0", "empty-file"); code != 2 || !strings.Contains(errOut, "empty asset") { 73 if _, errOut, code := inst.ssh(t, aliceKey, "", "release", "asset", "add", "alice/app", "v1.0", "empty-file"); code != 2 || !strings.Contains(errOut, "empty asset") {
e2e/ssh_test.go +2 −2
@@ -255,8 +255,8 @@ func TestControlPlaneOverBareSSH(t *testing.T) {
255 inst.admin(t, "admin", "user", "create", "bob", "--key", bobKey+".pub") 255 inst.admin(t, "admin", "user", "create", "bob", "--key", bobKey+".pub")
256 alicePub, _ := os.ReadFile(aliceKey + ".pub") 256 alicePub, _ := os.ReadFile(aliceKey + ".pub")
257 _, errOut, code = inst.ssh(t, bobKey, string(alicePub), "keys", "add") 257 _, errOut, code = inst.ssh(t, bobKey, string(alicePub), "keys", "add")
258 if code != 2 { 258 if code != 1 {
259 t.Fatalf("duplicate key add: exit %d, want 2", code) 259 t.Fatalf("duplicate key add: exit %d, want 1", code)
260 } 260 }
261 want := "that key is already registered to another account; remove it there first or use a different key" 261 want := "that key is already registered to another account; remove it there first or use a different key"
262 if !strings.Contains(errOut, want) { 262 if !strings.Contains(errOut, want) {
internal/control/adminhost.go +1 −1
@@ -72,7 +72,7 @@ func runAdminUserCreate(c *Ctx, args []string) int {
72 return c.fail(protocol.ExitUsage, usage) 72 return c.fail(protocol.ExitUsage, usage)
73 } 73 }
74 if err := policy.ValidateOwnerName(username); err != nil { 74 if err := policy.ValidateOwnerName(username); err != nil {
75 return c.failErr(err) 75 return c.failInput(err)
76 } 76 }
77 // Parse the key before creating anything, so a bad key leaves no 77 // Parse the key before creating anything, so a bad key leaves no
78 // half-made account behind. 78 // half-made account behind.
internal/control/control.go +16 −6
@@ -223,13 +223,23 @@ func (c *Ctx) emit(data any, plain func(w io.Writer)) int {
223 return protocol.ExitOK 223 return protocol.ExitOK
224} 224}
225 225
226// failErr reports an error from a store or helper call with the exit code 226// failErr reports an error from a store call: not-found is not-found,
227// its kind deserves: not-found is not-found, the database or I/O failing 227// and anything else — the database failing, a duplicate, a state that
228// is a failure, and anything else is the caller's mistake, which is what 228// does not allow the change — is a failure. An error about the caller's
229// most such errors are (a name that does not validate, a state that does 229// own arguments goes through failInput instead; this used to default to
230// not allow the change). A SQLite I/O error used to be a usage error and 230// usage, which turned every refusal into exit 2 (#211).
231// an HTTP 400 (#107).
232func (c *Ctx) failErr(err error) int { 231func (c *Ctx) failErr(err error) int {
232 if errors.Is(err, store.ErrNotFound) {
233 return c.fail(protocol.ExitNotFound, "%v", err)
234 }
235 return c.fail(protocol.ExitFailure, "%v", err)
236}
237
238// failInput reports an error about the caller's input — a name that does
239// not validate, a flag value out of range, a body that could not be read
240// — as a usage error, unless the database or I/O failed underneath it.
241// A SQLite I/O error used to be a usage error and an HTTP 400 (#107).
242func (c *Ctx) failInput(err error) int {
233 switch { 243 switch {
234 case errors.Is(err, store.ErrNotFound): 244 case errors.Is(err, store.ErrNotFound):
235 return c.fail(protocol.ExitNotFound, "%v", err) 245 return c.fail(protocol.ExitNotFound, "%v", err)
internal/control/control_test.go +24 −17
@@ -227,23 +227,30 @@ func TestRefusalsHonourJSON(t *testing.T) {
227 } 227 }
228} 228}
229 229
230// TestFailErrExitCodes: not-found, an internal failure, and the caller's 230// TestFailErrExitCodes: a store error is not-found or a failure, never
231// mistake each get their own exit code (#107). 231// usage (#211); an input error is usage unless the I/O beneath it failed
232// (#107).
232func TestFailErrExitCodes(t *testing.T) { 233func TestFailErrExitCodes(t *testing.T) {
233 code := func(err error) int { 234 ctx := func() *Ctx { return &Ctx{Stdout: &bytes.Buffer{}, Stderr: &bytes.Buffer{}} }
234 c := &Ctx{Stdout: &bytes.Buffer{}, Stderr: &bytes.Buffer{}} 235 notFound := fmt.Errorf("looking up: %w", store.ErrNotFound)
235 return c.failErr(err) 236 refused := errors.New("the name is taken")
236 } 237 ioErr := &fs.PathError{Op: "open", Path: "/x", Err: fs.ErrPermission}
237 if got := code(store.ErrNotFound); got != protocol.ExitNotFound { 238 for _, tc := range []struct {
238 t.Errorf("not found: %d", got) 239 name string
239 } 240 fn func(*Ctx, error) int
240 if got := code(fmt.Errorf("looking up: %w", store.ErrNotFound)); got != protocol.ExitNotFound { 241 err error
241 t.Errorf("wrapped not found: %d", got) 242 want int
242 } 243 }{
243 if got := code(errors.New("name must be lowercase")); got != protocol.ExitUsage { 244 {"failErr not found", (*Ctx).failErr, store.ErrNotFound, protocol.ExitNotFound},
244 t.Errorf("caller's mistake: %d", got) 245 {"failErr wrapped not found", (*Ctx).failErr, notFound, protocol.ExitNotFound},
245 } 246 {"failErr refusal", (*Ctx).failErr, refused, protocol.ExitFailure},
246 if got := code(&fs.PathError{Op: "open", Path: "/x", Err: fs.ErrPermission}); got != protocol.ExitFailure { 247 {"failErr i/o", (*Ctx).failErr, ioErr, protocol.ExitFailure},
247 t.Errorf("i/o failure: %d", got) 248 {"failInput not found", (*Ctx).failInput, notFound, protocol.ExitNotFound},
249 {"failInput caller's mistake", (*Ctx).failInput, errors.New("name must be lowercase"), protocol.ExitUsage},
250 {"failInput i/o", (*Ctx).failInput, ioErr, protocol.ExitFailure},
251 } {
252 if got := tc.fn(ctx(), tc.err); got != tc.want {
253 t.Errorf("%s: exit %d, want %d", tc.name, got, tc.want)
254 }
248 } 255 }
249} 256}
internal/control/diffcomment.go +1 −1
@@ -66,7 +66,7 @@ func runDiffComment(c *Ctx, args []string) int {
66 } 66 }
67 body, err := bodyFrom(c, message, file) 67 body, err := bodyFrom(c, message, file)
68 if err != nil { 68 if err != nil {
69 return c.failErr(err) 69 return c.failInput(err)
70 } 70 }
71 if strings.TrimSpace(body) == "" { 71 if strings.TrimSpace(body) == "" {
72 return c.fail(protocol.ExitUsage, "empty comment; use --message or --file -") 72 return c.fail(protocol.ExitUsage, "empty comment; use --message or --file -")
internal/control/import.go +1 −1
@@ -47,7 +47,7 @@ func runRepoImport(c *Ctx, args []string) int {
47 return c.fail(protocol.ExitUsage, "usage: repo import <owner/name> --from <url>") 47 return c.fail(protocol.ExitUsage, "usage: repo import <owner/name> --from <url>")
48 } 48 }
49 if err := policy.ValidateName(name); err != nil { 49 if err := policy.ValidateName(name); err != nil {
50 return c.failErr(err) 50 return c.failInput(err)
51 } 51 }
52 // Same ownership rule as repo create: yourself, or an org you admin. 52 // Same ownership rule as repo create: yourself, or an org you admin.
53 ownerKind, ownerID := "user", c.User.ID 53 ownerKind, ownerID := "user", c.User.ID
internal/control/issue.go +7 −7
@@ -127,7 +127,7 @@ func runIssueCreate(c *Ctx, args []string) int {
127 } 127 }
128 fmtName, err := markupFormat(format) 128 fmtName, err := markupFormat(format)
129 if err != nil { 129 if err != nil {
130 return c.failErr(err) 130 return c.failInput(err)
131 } 131 }
132 if fmtName == "" { 132 if fmtName == "" {
133 fmtName = "md" 133 fmtName = "md"
@@ -142,7 +142,7 @@ func runIssueCreate(c *Ctx, args []string) int {
142 } 142 }
143 b, err := bodyFrom(c, body, file) 143 b, err := bodyFrom(c, body, file)
144 if err != nil { 144 if err != nil {
145 return c.failErr(err) 145 return c.failInput(err)
146 } 146 }
147 n, err := c.Store.CreateIssue(repo.ID, c.User.ID, title, b, fmtName) 147 n, err := c.Store.CreateIssue(repo.ID, c.User.ID, title, b, fmtName)
148 if err != nil { 148 if err != nil {
@@ -182,7 +182,7 @@ func runIssueList(c *Ctx, args []string) int {
182 f.Search = fl.Value("--search") 182 f.Search = fl.Value("--search")
183 if fl.Has("--search") { 183 if fl.Has("--search") {
184 if err := validQuery(f.Search); err != nil { 184 if err := validQuery(f.Search); err != nil {
185 return c.failErr(err) 185 return c.failInput(err)
186 } 186 }
187 } 187 }
188 if path == "" || (f.State != "open" && f.State != "closed" && f.State != "all") { 188 if path == "" || (f.State != "open" && f.State != "closed" && f.State != "all") {
@@ -303,13 +303,13 @@ func editText(c *Ctx, args []string, kind string) (rest []string, title, body, f
303 if file != "" { 303 if file != "" {
304 b, err := bodyFrom(c, "", file) 304 b, err := bodyFrom(c, "", file)
305 if err != nil { 305 if err != nil {
306 return nil, nil, nil, nil, c.failErr(err) 306 return nil, nil, nil, nil, c.failInput(err)
307 } 307 }
308 bodyV, haveBody = b, true 308 bodyV, haveBody = b, true
309 } 309 }
310 fmtName, err := markupFormat(formatV) 310 fmtName, err := markupFormat(formatV)
311 if err != nil { 311 if err != nil {
312 return nil, nil, nil, nil, c.failErr(err) 312 return nil, nil, nil, nil, c.failInput(err)
313 } 313 }
314 if !haveTitle && !haveBody && fmtName == "" { 314 if !haveTitle && !haveBody && fmtName == "" {
315 return nil, nil, nil, nil, c.fail(protocol.ExitUsage, "usage: %s edit <owner/name> <n> [--title <t>] [--body <b> | --file -] [--format md|org]", kind) 315 return nil, nil, nil, nil, c.fail(protocol.ExitUsage, "usage: %s edit <owner/name> <n> [--title <t>] [--body <b> | --file -] [--format md|org]", kind)
@@ -375,7 +375,7 @@ func addRemoveFlags(args []string) (rest, adds, removes []string, err error) {
375func runIssueLabel(c *Ctx, args []string) int { 375func runIssueLabel(c *Ctx, args []string) int {
376 rest, adds, removes, err := addRemoveFlags(args) 376 rest, adds, removes, err := addRemoveFlags(args)
377 if err != nil { 377 if err != nil {
378 return c.failErr(err) 378 return c.failInput(err)
379 } 379 }
380 if len(adds)+len(removes) == 0 { 380 if len(adds)+len(removes) == 0 {
381 return c.fail(protocol.ExitUsage, "usage: issue label <owner/name> <n> [--add <l>]... [--remove <l>]...") 381 return c.fail(protocol.ExitUsage, "usage: issue label <owner/name> <n> [--add <l>]... [--remove <l>]...")
@@ -414,7 +414,7 @@ func runIssueLabel(c *Ctx, args []string) int {
414func runIssueAssign(c *Ctx, args []string) int { 414func runIssueAssign(c *Ctx, args []string) int {
415 rest, adds, removes, err := addRemoveFlags(args) 415 rest, adds, removes, err := addRemoveFlags(args)
416 if err != nil { 416 if err != nil {
417 return c.failErr(err) 417 return c.failInput(err)
418 } 418 }
419 if len(adds)+len(removes) == 0 { 419 if len(adds)+len(removes) == 0 {
420 return c.fail(protocol.ExitUsage, "usage: issue assign <owner/name> <n> [--add <user>]... [--remove <user>]...") 420 return c.fail(protocol.ExitUsage, "usage: issue assign <owner/name> <n> [--add <user>]... [--remove <user>]...")
internal/control/milestone.go +1 −3
@@ -64,9 +64,7 @@ func runMilestoneCreate(c *Ctx, args []string) int {
64 if errors.Is(err, store.ErrOrgScoped) { 64 if errors.Is(err, store.ErrOrgScoped) {
65 return c.fail(protocol.ExitFailure, "%s", orgScopedMsg(repo, "milestone", title, "create")) 65 return c.fail(protocol.ExitFailure, "%s", orgScopedMsg(repo, "milestone", title, "create"))
66 } 66 }
67 // A duplicate title is a failure, not a usage error, which is what 67 return c.failErr(err)
68 // failErr would make of it; org milestone create answers the same.
69 return c.fail(protocol.ExitFailure, "%v", err)
70 } 68 }
71 return c.emit(map[string]string{"milestone": title}, func(w io.Writer) { 69 return c.emit(map[string]string{"milestone": title}, func(w io.Writer) {
72 fmt.Fprintf(w, "created milestone %q on %s\n", title, repo.Path()) 70 fmt.Fprintf(w, "created milestone %q on %s\n", title, repo.Path())
internal/control/mirrorcmd.go +1 −1
@@ -44,7 +44,7 @@ func runMirrorAdd(c *Ctx, args []string) int {
44 // The worker's git process dials this URL from the server: same SSRF 44 // The worker's git process dials this URL from the server: same SSRF
45 // surface as a webhook target, same rules. 45 // surface as a webhook target, same rules.
46 if err := webhook.ValidateURL(urlArg, c.Cfg.Webhooks.AllowLocal); err != nil { 46 if err := webhook.ValidateURL(urlArg, c.Cfg.Webhooks.AllowLocal); err != nil {
47 return c.failErr(err) 47 return c.failInput(err)
48 } 48 }
49 repo, code := resolveRepo(c, path, policy.CanAdmin) 49 repo, code := resolveRepo(c, path, policy.CanAdmin)
50 if code >= 0 { 50 if code >= 0 {
internal/control/mr.go +5 −5
@@ -114,7 +114,7 @@ func runRepoFork(c *Ctx, args []string) int {
114 name = src.Name 114 name = src.Name
115 } 115 }
116 if err := policy.ValidateName(name); err != nil { 116 if err := policy.ValidateName(name); err != nil {
117 return c.failErr(err) 117 return c.failInput(err)
118 } 118 }
119 repoCreateMu.Lock() 119 repoCreateMu.Lock()
120 if code := checkRepoQuota(c); code >= 0 { 120 if code := checkRepoQuota(c); code >= 0 {
@@ -286,7 +286,7 @@ func runMRCreate(c *Ctx, args []string) int {
286 } 286 }
287 fmtName, err := markupFormat(format) 287 fmtName, err := markupFormat(format)
288 if err != nil { 288 if err != nil {
289 return c.failErr(err) 289 return c.failInput(err)
290 } 290 }
291 if fmtName == "" { 291 if fmtName == "" {
292 fmtName = "md" 292 fmtName = "md"
@@ -323,7 +323,7 @@ func runMRCreate(c *Ctx, args []string) int {
323 } 323 }
324 b, err := bodyFrom(c, body, file) 324 b, err := bodyFrom(c, body, file)
325 if err != nil { 325 if err != nil {
326 return c.failErr(err) 326 return c.failInput(err)
327 } 327 }
328 n, err := c.Store.CreateMR(repo.ID, c.User.ID, srcRepo.ID, srcBranch, target, title, b, headSHA, fmtName, f.Has("--draft")) 328 n, err := c.Store.CreateMR(repo.ID, c.User.ID, srcRepo.ID, srcBranch, target, title, b, headSHA, fmtName, f.Has("--draft"))
329 if err != nil { 329 if err != nil {
@@ -455,7 +455,7 @@ func runMRList(c *Ctx, args []string) int {
455 f.Search = fl.Value("--search") 455 f.Search = fl.Value("--search")
456 if fl.Has("--search") { 456 if fl.Has("--search") {
457 if err := validQuery(f.Search); err != nil { 457 if err := validQuery(f.Search); err != nil {
458 return c.failErr(err) 458 return c.failInput(err)
459 } 459 }
460 } 460 }
461 valid := map[string]bool{"open": true, "merged": true, "closed": true, "source_gone": true, "all": true} 461 valid := map[string]bool{"open": true, "merged": true, "closed": true, "source_gone": true, "all": true}
@@ -845,7 +845,7 @@ func runMRReview(c *Ctx, args []string) int {
845func runMRReviewRequest(c *Ctx, args []string) int { 845func runMRReviewRequest(c *Ctx, args []string) int {
846 rest, adds, removes, err := addRemoveFlags(args) 846 rest, adds, removes, err := addRemoveFlags(args)
847 if err != nil { 847 if err != nil {
848 return c.failErr(err) 848 return c.failInput(err)
849 } 849 }
850 if len(adds)+len(removes) == 0 { 850 if len(adds)+len(removes) == 0 {
851 return c.fail(protocol.ExitUsage, "usage: mr review request <owner/name> <n> [--add <user>]... [--remove <user>]...") 851 return c.fail(protocol.ExitUsage, "usage: mr review request <owner/name> <n> [--add <user>]... [--remove <user>]...")
internal/control/org.go +2 −2
@@ -63,7 +63,7 @@ func runOrgCreate(c *Ctx, args []string) int {
63 return c.fail(protocol.ExitUsage, "usage: org create <name>") 63 return c.fail(protocol.ExitUsage, "usage: org create <name>")
64 } 64 }
65 if err := policy.ValidateOwnerName(args[0]); err != nil { 65 if err := policy.ValidateOwnerName(args[0]); err != nil {
66 return c.failErr(err) 66 return c.failInput(err)
67 } 67 }
68 if _, err := c.Store.CreateOrg(args[0], c.User.ID); err != nil { 68 if _, err := c.Store.CreateOrg(args[0], c.User.ID); err != nil {
69 return c.fail(protocol.ExitFailure, "%v", err) 69 return c.fail(protocol.ExitFailure, "%v", err)
@@ -138,7 +138,7 @@ func runOrgRename(c *Ctx, args []string) int {
138 } 138 }
139 newName := args[1] 139 newName := args[1]
140 if err := policy.ValidateOwnerName(newName); err != nil { 140 if err := policy.ValidateOwnerName(newName); err != nil {
141 return c.failErr(err) 141 return c.failInput(err)
142 } 142 }
143 oldDir := filepath.Join(c.Cfg.Server.Root, "repos", org.Name) 143 oldDir := filepath.Join(c.Cfg.Server.Root, "repos", org.Name)
144 newDir := filepath.Join(c.Cfg.Server.Root, "repos", newName) 144 newDir := filepath.Join(c.Cfg.Server.Root, "repos", newName)
internal/control/pagescmd.go +1 −1
@@ -74,7 +74,7 @@ func runDomainAdd(c *Ctx, args []string) int {
74 } 74 }
75 domain := strings.ToLower(args[1]) 75 domain := strings.ToLower(args[1])
76 if err := validatePageDomain(c, domain); err != nil { 76 if err := validatePageDomain(c, domain); err != nil {
77 return c.failErr(err) 77 return c.failInput(err)
78 } 78 }
79 repo, code := resolveRepo(c, args[0], policy.CanAdmin) 79 repo, code := resolveRepo(c, args[0], policy.CanAdmin)
80 if code >= 0 { 80 if code >= 0 {
internal/control/profile.go +4 −4
@@ -347,7 +347,7 @@ func runProfileShow(c *Ctx, args []string) int {
347func runProfileSet(c *Ctx, args []string) int { 347func runProfileSet(c *Ctx, args []string) int {
348 rest, e, err := parseProfileFlags(c, args) 348 rest, e, err := parseProfileFlags(c, args)
349 if err != nil { 349 if err != nil {
350 return c.failErr(err) 350 return c.failInput(err)
351 } 351 }
352 if len(rest) != 0 { 352 if len(rest) != 0 {
353 return c.fail(protocol.ExitUsage, 353 return c.fail(protocol.ExitUsage,
@@ -362,7 +362,7 @@ func runProfileSet(c *Ctx, args []string) int {
362 } 362 }
363 p, err = applyProfile(p, e) 363 p, err = applyProfile(p, e)
364 if err != nil { 364 if err != nil {
365 return c.failErr(err) 365 return c.failInput(err)
366 } 366 }
367 if err := c.Store.SetOwnerProfile("user", c.User.ID, p); err != nil { 367 if err := c.Store.SetOwnerProfile("user", c.User.ID, p); err != nil {
368 return c.fail(protocol.ExitFailure, "%v", err) 368 return c.fail(protocol.ExitFailure, "%v", err)
@@ -375,7 +375,7 @@ func runProfileSet(c *Ctx, args []string) int {
375func runOrgProfile(c *Ctx, args []string) int { 375func runOrgProfile(c *Ctx, args []string) int {
376 rest, e, err := parseProfileFlags(c, args) 376 rest, e, err := parseProfileFlags(c, args)
377 if err != nil { 377 if err != nil {
378 return c.failErr(err) 378 return c.failInput(err)
379 } 379 }
380 if len(rest) != 1 { 380 if len(rest) != 1 {
381 return c.fail(protocol.ExitUsage, 381 return c.fail(protocol.ExitUsage,
@@ -395,7 +395,7 @@ func runOrgProfile(c *Ctx, args []string) int {
395 } 395 }
396 p, err = applyProfile(p, e) 396 p, err = applyProfile(p, e)
397 if err != nil { 397 if err != nil {
398 return c.failErr(err) 398 return c.failInput(err)
399 } 399 }
400 if err := c.Store.SetOwnerProfile("org", org.ID, p); err != nil { 400 if err := c.Store.SetOwnerProfile("org", org.ID, p); err != nil {
401 return c.fail(protocol.ExitFailure, "%v", err) 401 return c.fail(protocol.ExitFailure, "%v", err)
internal/control/release.go +4 −4
@@ -88,7 +88,7 @@ func runReleaseCreate(c *Ctx, args []string) int {
88 } 88 }
89 fmtName, err := markupFormat(format) 89 fmtName, err := markupFormat(format)
90 if err != nil { 90 if err != nil {
91 return c.failErr(err) 91 return c.failInput(err)
92 } 92 }
93 if fmtName == "" { 93 if fmtName == "" {
94 fmtName = "md" 94 fmtName = "md"
@@ -106,7 +106,7 @@ func runReleaseCreate(c *Ctx, args []string) int {
106 } 106 }
107 body, err := bodyFrom(c, notes, file) 107 body, err := bodyFrom(c, notes, file)
108 if err != nil { 108 if err != nil {
109 return c.failErr(err) 109 return c.failInput(err)
110 } 110 }
111 if title == "" { 111 if title == "" {
112 title = tag 112 title = tag
@@ -159,7 +159,7 @@ func runReleaseEdit(c *Ctx, args []string) int {
159 setTitle, setNotes := f.Has("--title"), f.Has("--notes") || f.Has("--file") 159 setTitle, setNotes := f.Has("--title"), f.Has("--notes") || f.Has("--file")
160 fmtName, err := markupFormat(format) 160 fmtName, err := markupFormat(format)
161 if err != nil { 161 if err != nil {
162 return c.failErr(err) 162 return c.failInput(err)
163 } 163 }
164 if path == "" || tag == "" || (!setTitle && !setNotes && fmtName == "") { 164 if path == "" || tag == "" || (!setTitle && !setNotes && fmtName == "") {
165 return c.fail(protocol.ExitUsage, usage) 165 return c.fail(protocol.ExitUsage, usage)
@@ -184,7 +184,7 @@ func runReleaseEdit(c *Ctx, args []string) int {
184 body := rel.Notes 184 body := rel.Notes
185 if setNotes { 185 if setNotes {
186 if body, err = bodyFrom(c, notes, file); err != nil { 186 if body, err = bodyFrom(c, notes, file); err != nil {
187 return c.failErr(err) 187 return c.failInput(err)
188 } 188 }
189 } 189 }
190 if fmtName == "" { 190 if fmtName == "" {
internal/control/repo.go +6 −6
@@ -184,7 +184,7 @@ func runRepoCreate(c *Ctx, args []string) int {
184 return c.fail(protocol.ExitUsage, "usage: repo create <owner/name> [--private]") 184 return c.fail(protocol.ExitUsage, "usage: repo create <owner/name> [--private]")
185 } 185 }
186 if err := policyValidateRepoName(name); err != nil { 186 if err := policyValidateRepoName(name); err != nil {
187 return c.failErr(err) 187 return c.failInput(err)
188 } 188 }
189 ownerKind, ownerID := "user", c.User.ID 189 ownerKind, ownerID := "user", c.User.ID
190 if owner != c.User.Username { 190 if owner != c.User.Username {
@@ -461,7 +461,7 @@ func runRepoRename(c *Ctx, args []string) int {
461 return c.fail(protocol.ExitUsage, "%s is already named %s", repo.Path(), newName) 461 return c.fail(protocol.ExitUsage, "%s is already named %s", repo.Path(), newName)
462 } 462 }
463 if err := policyValidateRepoName(newName); err != nil { 463 if err := policyValidateRepoName(newName); err != nil {
464 return c.failErr(err) 464 return c.failInput(err)
465 } 465 }
466 oldDir := RepoDir(c.Cfg.Server.Root, repo.OwnerName, repo.Name) 466 oldDir := RepoDir(c.Cfg.Server.Root, repo.OwnerName, repo.Name)
467 newDir := RepoDir(c.Cfg.Server.Root, repo.OwnerName, newName) 467 newDir := RepoDir(c.Cfg.Server.Root, repo.OwnerName, newName)
@@ -666,7 +666,7 @@ func runSetWebsite(c *Ctx, args []string) int {
666 } 666 }
667 site := strings.TrimSpace(args[1]) 667 site := strings.TrimSpace(args[1])
668 if err := validateWebsite(site); err != nil { 668 if err := validateWebsite(site); err != nil {
669 return c.failErr(err) 669 return c.failInput(err)
670 } 670 }
671 if len(site) > 256 { 671 if len(site) > 256 {
672 return c.fail(protocol.ExitUsage, "website URL too long (max 256)") 672 return c.fail(protocol.ExitUsage, "website URL too long (max 256)")
@@ -816,7 +816,7 @@ func editTopics(c *Ctx, args []string, add bool) int {
816 if add { 816 if add {
817 for _, t := range topics { 817 for _, t := range topics {
818 if err := policy.ValidateTopic(t); err != nil { 818 if err := policy.ValidateTopic(t); err != nil {
819 return c.failErr(err) 819 return c.failInput(err)
820 } 820 }
821 } 821 }
822 have, err := c.Store.ListTopics(repo.ID) 822 have, err := c.Store.ListTopics(repo.ID)
@@ -863,7 +863,7 @@ func runRepoSearch(c *Ctx, args []string) int {
863 return c.fail(protocol.ExitUsage, "usage: repo search <query>") 863 return c.fail(protocol.ExitUsage, "usage: repo search <query>")
864 } 864 }
865 if err := validQuery(args[0]); err != nil { 865 if err := validQuery(args[0]); err != nil {
866 return c.failErr(err) 866 return c.failInput(err)
867 } 867 }
868 q := strings.ToLower(args[0]) 868 q := strings.ToLower(args[0])
869 869
@@ -930,7 +930,7 @@ func runRepoGrep(c *Ctx, args []string) int {
930 return c.fail(protocol.ExitUsage, "usage: repo grep <owner/name> <query> [--ref <ref>]") 930 return c.fail(protocol.ExitUsage, "usage: repo grep <owner/name> <query> [--ref <ref>]")
931 } 931 }
932 if err := validQuery(query); err != nil { 932 if err := validQuery(query); err != nil {
933 return c.failErr(err) 933 return c.failInput(err)
934 } 934 }
935 repo, code := resolveRepo(c, path, policy.CanRead) 935 repo, code := resolveRepo(c, path, policy.CanRead)
936 if code >= 0 { 936 if code >= 0 {
internal/control/search.go +1 −1
@@ -86,7 +86,7 @@ func runSearch(c *Ctx, args []string) int {
86 return c.fail(protocol.ExitUsage, "usage: %s", usage) 86 return c.fail(protocol.ExitUsage, "usage: %s", usage)
87 } 87 }
88 if err := validQuery(f.Pos[0]); err != nil { 88 if err := validQuery(f.Pos[0]); err != nil {
89 return c.failErr(err) 89 return c.failInput(err)
90 } 90 }
91 kinds := f.List("--kind") 91 kinds := f.List("--kind")
92 for _, k := range kinds { 92 for _, k := range kinds {
internal/control/sig.go +1 −1
@@ -45,7 +45,7 @@ func runPGPAdd(c *Ctx, args []string) int {
45 } 45 }
46 meta, err := sig.ParsePGPKey(raw) 46 meta, err := sig.ParsePGPKey(raw)
47 if err != nil { 47 if err != nil {
48 return c.failErr(err) 48 return c.failInput(err)
49 } 49 }
50 uids, _ := json.Marshal(meta.Emails) 50 uids, _ := json.Marshal(meta.Emails)
51 if err := c.Store.AddPGPKey(c.User.ID, meta.Fingerprint, string(raw), string(uids), meta.ExpiresAt, meta.RevokedAt); err != nil { 51 if err := c.Store.AddPGPKey(c.User.ID, meta.Fingerprint, string(raw), string(uids), meta.ExpiresAt, meta.RevokedAt); err != nil {
internal/control/teams.go +1 −1
@@ -96,7 +96,7 @@ func runTeamCreate(c *Ctx, args []string) int {
96 return code 96 return code
97 } 97 }
98 if err := policy.ValidateName(args[1]); err != nil { 98 if err := policy.ValidateName(args[1]); err != nil {
99 return c.failErr(err) 99 return c.failInput(err)
100 } 100 }
101 if _, err := c.Store.CreateTeam(org.ID, args[1]); err != nil { 101 if _, err := c.Store.CreateTeam(org.ID, args[1]); err != nil {
102 return c.failErr(err) 102 return c.failErr(err)
internal/control/thread.go +2 −2
@@ -83,7 +83,7 @@ func runComment(c *Ctx, args []string, t thread, noun string,
83 } 83 }
84 fmtName, err := markupFormat(f.Value("--format")) 84 fmtName, err := markupFormat(f.Value("--format"))
85 if err != nil { 85 if err != nil {
86 return c.failErr(err) 86 return c.failInput(err)
87 } 87 }
88 if fmtName == "" { 88 if fmtName == "" {
89 fmtName = "md" 89 fmtName = "md"
@@ -97,7 +97,7 @@ func runComment(c *Ctx, args []string, t thread, noun string,
97 } 97 }
98 body, err := bodyFrom(c, f.Value("--message"), f.Value("--file")) 98 body, err := bodyFrom(c, f.Value("--message"), f.Value("--file"))
99 if err != nil { 99 if err != nil {
100 return c.failErr(err) 100 return c.failInput(err)
101 } 101 }
102 if strings.TrimSpace(body) == "" { 102 if strings.TrimSpace(body) == "" {
103 return c.fail(protocol.ExitUsage, "empty comment; use --message or --file -") 103 return c.fail(protocol.ExitUsage, "empty comment; use --message or --file -")
internal/control/token.go +1 −1
@@ -54,7 +54,7 @@ func runTokenCreate(c *Ctx, args []string) int {
54 if ttl != "" { 54 if ttl != "" {
55 d, err := parseTTL(ttl) 55 d, err := parseTTL(ttl)
56 if err != nil { 56 if err != nil {
57 return c.failErr(err) 57 return c.failInput(err)
58 } 58 }
59 t := time.Now().Add(d) 59 t := time.Now().Add(d)
60 expires = &t 60 expires = &t