Commit b347d6c8c4

b347d6c8c464e3c965455795f4e22e0aaeed0652

parent: acb7f82838

Verified · cmc ci/build: success ci/test: success

cmc <hello@cleberg.net> · 2026-09-11 19:38 UTC

control, e2e, wiki: failErr is a failure by default, failInput is usage

failErr mapped any error that was neither not-found nor internal to
exit 2, so a duplicate key, a taken name, the last admin stepping
down and every other store refusal answered as a usage error. It now
answers not-found or failure. The former body is failInput, for the
call sites that validate the caller's own arguments; the callers that
pass a store error stay on failErr. The e2e assertions that pinned
those refusals to exit 2 now expect 1; milestone create's explicit
branch goes back to failErr.

Closes #211
.gitbay/wiki/Users.org +3 −2
@@ -632,8 +632,9 @@ addresses receive anything. Delivery retries on relay failure.
632632Every read command takes =--json= and emits one envelope:
633633={"protocol_version": 1, "data": ...}=. stdout is data, stderr is
634634messages. Exit codes are stable: 0 ok, 1 failure, 2 usage, 3 not found,
6354 denied, 5 server/protocol error. Nothing ever prompts; destructive
636commands take =--yes=.
6354 denied, 5 server/protocol error. Usage means the arguments were wrong;
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=.
637638
638639For HTTP automation see [[API]].
639640
e2e/adminusers_test.go +3 −3
@@ -190,7 +190,7 @@ func TestAdminPromoteDemote(t *testing.T) {
190190 t.Fatalf("disabled account promoted: exit %d %s", code, errOut)
191191 }
192192 // 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") {
194194 t.Fatalf("last admin demoted: exit %d %s", code, errOut)
195195 }
196196 if _, _, code := inst.ssh(t, rootKey, "", "admin", "user", "promote", "alice"); code != 0 {
@@ -209,7 +209,7 @@ func TestAdminPromoteDemote(t *testing.T) {
209209 if _, _, code := inst.ssh(t, rootKey, "", "audit"); code != 4 {
210210 t.Fatal("demoted account still admin")
211211 }
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 {
213213 t.Fatal("last admin demoted")
214214 }
215215 // Host-local recovery: the operator restores root without an admin key.
@@ -356,7 +356,7 @@ func TestAdminHostAndSSHAreOneSurface(t *testing.T) {
356356 if _, _, code := inst.ssh(t, carolKey, "", "whoami"); code != 0 {
357357 t.Fatal("created account cannot authenticate")
358358 }
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") {
360360 t.Fatalf("duplicate create: exit %d %s", code, errOut)
361361 }
362362 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) {
7676 }
7777 json.Unmarshal([]byte(out), &env3)
7878 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") {
8080 t.Fatalf("nested reply: exit %d, %s", code, errOut)
8181 }
8282
e2e/org_test.go +2 −2
@@ -97,7 +97,7 @@ func TestOrganizations(t *testing.T) {
9797 t.Fatalf("bob (now admin) removing alice: %s", errOut)
9898 }
9999 _, 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") {
101101 t.Fatalf("last admin removal: %d %s", code, errOut)
102102 }
103103
@@ -150,7 +150,7 @@ func TestOrganizations(t *testing.T) {
150150 }
151151 rnWork := t.TempDir()
152152 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") {
154154 t.Fatalf("rename onto user name: %d %s", code, errOut)
155155 }
156156 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) {
3636 "--title", "'First light'", "--notes", "'the **first** release'"); code != 0 {
3737 t.Fatalf("release create: %s", errOut)
3838 }
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 {
4040 t.Fatal("duplicate release accepted")
4141 }
4242
@@ -67,7 +67,7 @@ func TestReleases(t *testing.T) {
6767 if _, errOut, code := inst.ssh(t, aliceKey, payload, "release", "asset", "add", "alice/app", "v1.0", "tool-linux-amd64"); code != 0 {
6868 t.Fatalf("asset add: %s", errOut)
6969 }
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 {
7171 t.Fatal("duplicate asset accepted")
7272 }
7373 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) {
255255 inst.admin(t, "admin", "user", "create", "bob", "--key", bobKey+".pub")
256256 alicePub, _ := os.ReadFile(aliceKey + ".pub")
257257 _, errOut, code = inst.ssh(t, bobKey, string(alicePub), "keys", "add")
258 if code != 2 {
259 t.Fatalf("duplicate key add: exit %d, want 2", code)
258 if code != 1 {
259 t.Fatalf("duplicate key add: exit %d, want 1", code)
260260 }
261261 want := "that key is already registered to another account; remove it there first or use a different key"
262262 if !strings.Contains(errOut, want) {
internal/control/adminhost.go +1 −1
@@ -72,7 +72,7 @@ func runAdminUserCreate(c *Ctx, args []string) int {
7272 return c.fail(protocol.ExitUsage, usage)
7373 }
7474 if err := policy.ValidateOwnerName(username); err != nil {
75 return c.failErr(err)
75 return c.failInput(err)
7676 }
7777 // Parse the key before creating anything, so a bad key leaves no
7878 // 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 {
223223 return protocol.ExitOK
224224}
225225
226// failErr reports an error from a store or helper call with the exit code
227// its kind deserves: not-found is not-found, the database or I/O failing
228// is a failure, and anything else is the caller's mistake, which is what
229// most such errors are (a name that does not validate, a state that does
230// not allow the change). A SQLite I/O error used to be a usage error and
231// an HTTP 400 (#107).
226// failErr reports an error from a store call: not-found is not-found,
227// and anything else — the database failing, a duplicate, a state that
228// does not allow the change — is a failure. An error about the caller's
229// own arguments goes through failInput instead; this used to default to
230// usage, which turned every refusal into exit 2 (#211).
232231func (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 {
233243 switch {
234244 case errors.Is(err, store.ErrNotFound):
235245 return c.fail(protocol.ExitNotFound, "%v", err)
internal/control/control_test.go +24 −17
@@ -227,23 +227,30 @@ func TestRefusalsHonourJSON(t *testing.T) {
227227 }
228228}
229229
230// TestFailErrExitCodes: not-found, an internal failure, and the caller's
231// mistake each get their own exit code (#107).
230// TestFailErrExitCodes: a store error is not-found or a failure, never
231// usage (#211); an input error is usage unless the I/O beneath it failed
232// (#107).
232233func TestFailErrExitCodes(t *testing.T) {
233 code := func(err error) int {
234 c := &Ctx{Stdout: &bytes.Buffer{}, Stderr: &bytes.Buffer{}}
235 return c.failErr(err)
236 }
237 if got := code(store.ErrNotFound); got != protocol.ExitNotFound {
238 t.Errorf("not found: %d", got)
239 }
240 if got := code(fmt.Errorf("looking up: %w", store.ErrNotFound)); got != protocol.ExitNotFound {
241 t.Errorf("wrapped not found: %d", got)
242 }
243 if got := code(errors.New("name must be lowercase")); got != protocol.ExitUsage {
244 t.Errorf("caller's mistake: %d", got)
245 }
246 if got := code(&fs.PathError{Op: "open", Path: "/x", Err: fs.ErrPermission}); got != protocol.ExitFailure {
247 t.Errorf("i/o failure: %d", got)
234 ctx := func() *Ctx { return &Ctx{Stdout: &bytes.Buffer{}, Stderr: &bytes.Buffer{}} }
235 notFound := fmt.Errorf("looking up: %w", store.ErrNotFound)
236 refused := errors.New("the name is taken")
237 ioErr := &fs.PathError{Op: "open", Path: "/x", Err: fs.ErrPermission}
238 for _, tc := range []struct {
239 name string
240 fn func(*Ctx, error) int
241 err error
242 want int
243 }{
244 {"failErr not found", (*Ctx).failErr, store.ErrNotFound, protocol.ExitNotFound},
245 {"failErr wrapped not found", (*Ctx).failErr, notFound, protocol.ExitNotFound},
246 {"failErr refusal", (*Ctx).failErr, refused, protocol.ExitFailure},
247 {"failErr i/o", (*Ctx).failErr, ioErr, protocol.ExitFailure},
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 }
248255 }
249256}
internal/control/diffcomment.go +1 −1
@@ -66,7 +66,7 @@ func runDiffComment(c *Ctx, args []string) int {
6666 }
6767 body, err := bodyFrom(c, message, file)
6868 if err != nil {
69 return c.failErr(err)
69 return c.failInput(err)
7070 }
7171 if strings.TrimSpace(body) == "" {
7272 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 {
4747 return c.fail(protocol.ExitUsage, "usage: repo import <owner/name> --from <url>")
4848 }
4949 if err := policy.ValidateName(name); err != nil {
50 return c.failErr(err)
50 return c.failInput(err)
5151 }
5252 // Same ownership rule as repo create: yourself, or an org you admin.
5353 ownerKind, ownerID := "user", c.User.ID
internal/control/issue.go +7 −7
@@ -127,7 +127,7 @@ func runIssueCreate(c *Ctx, args []string) int {
127127 }
128128 fmtName, err := markupFormat(format)
129129 if err != nil {
130 return c.failErr(err)
130 return c.failInput(err)
131131 }
132132 if fmtName == "" {
133133 fmtName = "md"
@@ -142,7 +142,7 @@ func runIssueCreate(c *Ctx, args []string) int {
142142 }
143143 b, err := bodyFrom(c, body, file)
144144 if err != nil {
145 return c.failErr(err)
145 return c.failInput(err)
146146 }
147147 n, err := c.Store.CreateIssue(repo.ID, c.User.ID, title, b, fmtName)
148148 if err != nil {
@@ -182,7 +182,7 @@ func runIssueList(c *Ctx, args []string) int {
182182 f.Search = fl.Value("--search")
183183 if fl.Has("--search") {
184184 if err := validQuery(f.Search); err != nil {
185 return c.failErr(err)
185 return c.failInput(err)
186186 }
187187 }
188188 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
303303 if file != "" {
304304 b, err := bodyFrom(c, "", file)
305305 if err != nil {
306 return nil, nil, nil, nil, c.failErr(err)
306 return nil, nil, nil, nil, c.failInput(err)
307307 }
308308 bodyV, haveBody = b, true
309309 }
310310 fmtName, err := markupFormat(formatV)
311311 if err != nil {
312 return nil, nil, nil, nil, c.failErr(err)
312 return nil, nil, nil, nil, c.failInput(err)
313313 }
314314 if !haveTitle && !haveBody && fmtName == "" {
315315 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) {
375375func runIssueLabel(c *Ctx, args []string) int {
376376 rest, adds, removes, err := addRemoveFlags(args)
377377 if err != nil {
378 return c.failErr(err)
378 return c.failInput(err)
379379 }
380380 if len(adds)+len(removes) == 0 {
381381 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 {
414414func runIssueAssign(c *Ctx, args []string) int {
415415 rest, adds, removes, err := addRemoveFlags(args)
416416 if err != nil {
417 return c.failErr(err)
417 return c.failInput(err)
418418 }
419419 if len(adds)+len(removes) == 0 {
420420 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 {
6464 if errors.Is(err, store.ErrOrgScoped) {
6565 return c.fail(protocol.ExitFailure, "%s", orgScopedMsg(repo, "milestone", title, "create"))
6666 }
67 // A duplicate title is a failure, not a usage error, which is what
68 // failErr would make of it; org milestone create answers the same.
69 return c.fail(protocol.ExitFailure, "%v", err)
67 return c.failErr(err)
7068 }
7169 return c.emit(map[string]string{"milestone": title}, func(w io.Writer) {
7270 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 {
4444 // The worker's git process dials this URL from the server: same SSRF
4545 // surface as a webhook target, same rules.
4646 if err := webhook.ValidateURL(urlArg, c.Cfg.Webhooks.AllowLocal); err != nil {
47 return c.failErr(err)
47 return c.failInput(err)
4848 }
4949 repo, code := resolveRepo(c, path, policy.CanAdmin)
5050 if code >= 0 {
internal/control/mr.go +5 −5
@@ -114,7 +114,7 @@ func runRepoFork(c *Ctx, args []string) int {
114114 name = src.Name
115115 }
116116 if err := policy.ValidateName(name); err != nil {
117 return c.failErr(err)
117 return c.failInput(err)
118118 }
119119 repoCreateMu.Lock()
120120 if code := checkRepoQuota(c); code >= 0 {
@@ -286,7 +286,7 @@ func runMRCreate(c *Ctx, args []string) int {
286286 }
287287 fmtName, err := markupFormat(format)
288288 if err != nil {
289 return c.failErr(err)
289 return c.failInput(err)
290290 }
291291 if fmtName == "" {
292292 fmtName = "md"
@@ -323,7 +323,7 @@ func runMRCreate(c *Ctx, args []string) int {
323323 }
324324 b, err := bodyFrom(c, body, file)
325325 if err != nil {
326 return c.failErr(err)
326 return c.failInput(err)
327327 }
328328 n, err := c.Store.CreateMR(repo.ID, c.User.ID, srcRepo.ID, srcBranch, target, title, b, headSHA, fmtName, f.Has("--draft"))
329329 if err != nil {
@@ -455,7 +455,7 @@ func runMRList(c *Ctx, args []string) int {
455455 f.Search = fl.Value("--search")
456456 if fl.Has("--search") {
457457 if err := validQuery(f.Search); err != nil {
458 return c.failErr(err)
458 return c.failInput(err)
459459 }
460460 }
461461 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 {
845845func runMRReviewRequest(c *Ctx, args []string) int {
846846 rest, adds, removes, err := addRemoveFlags(args)
847847 if err != nil {
848 return c.failErr(err)
848 return c.failInput(err)
849849 }
850850 if len(adds)+len(removes) == 0 {
851851 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 {
6363 return c.fail(protocol.ExitUsage, "usage: org create <name>")
6464 }
6565 if err := policy.ValidateOwnerName(args[0]); err != nil {
66 return c.failErr(err)
66 return c.failInput(err)
6767 }
6868 if _, err := c.Store.CreateOrg(args[0], c.User.ID); err != nil {
6969 return c.fail(protocol.ExitFailure, "%v", err)
@@ -138,7 +138,7 @@ func runOrgRename(c *Ctx, args []string) int {
138138 }
139139 newName := args[1]
140140 if err := policy.ValidateOwnerName(newName); err != nil {
141 return c.failErr(err)
141 return c.failInput(err)
142142 }
143143 oldDir := filepath.Join(c.Cfg.Server.Root, "repos", org.Name)
144144 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 {
7474 }
7575 domain := strings.ToLower(args[1])
7676 if err := validatePageDomain(c, domain); err != nil {
77 return c.failErr(err)
77 return c.failInput(err)
7878 }
7979 repo, code := resolveRepo(c, args[0], policy.CanAdmin)
8080 if code >= 0 {
internal/control/profile.go +4 −4
@@ -347,7 +347,7 @@ func runProfileShow(c *Ctx, args []string) int {
347347func runProfileSet(c *Ctx, args []string) int {
348348 rest, e, err := parseProfileFlags(c, args)
349349 if err != nil {
350 return c.failErr(err)
350 return c.failInput(err)
351351 }
352352 if len(rest) != 0 {
353353 return c.fail(protocol.ExitUsage,
@@ -362,7 +362,7 @@ func runProfileSet(c *Ctx, args []string) int {
362362 }
363363 p, err = applyProfile(p, e)
364364 if err != nil {
365 return c.failErr(err)
365 return c.failInput(err)
366366 }
367367 if err := c.Store.SetOwnerProfile("user", c.User.ID, p); err != nil {
368368 return c.fail(protocol.ExitFailure, "%v", err)
@@ -375,7 +375,7 @@ func runProfileSet(c *Ctx, args []string) int {
375375func runOrgProfile(c *Ctx, args []string) int {
376376 rest, e, err := parseProfileFlags(c, args)
377377 if err != nil {
378 return c.failErr(err)
378 return c.failInput(err)
379379 }
380380 if len(rest) != 1 {
381381 return c.fail(protocol.ExitUsage,
@@ -395,7 +395,7 @@ func runOrgProfile(c *Ctx, args []string) int {
395395 }
396396 p, err = applyProfile(p, e)
397397 if err != nil {
398 return c.failErr(err)
398 return c.failInput(err)
399399 }
400400 if err := c.Store.SetOwnerProfile("org", org.ID, p); err != nil {
401401 return c.fail(protocol.ExitFailure, "%v", err)
internal/control/release.go +4 −4
@@ -88,7 +88,7 @@ func runReleaseCreate(c *Ctx, args []string) int {
8888 }
8989 fmtName, err := markupFormat(format)
9090 if err != nil {
91 return c.failErr(err)
91 return c.failInput(err)
9292 }
9393 if fmtName == "" {
9494 fmtName = "md"
@@ -106,7 +106,7 @@ func runReleaseCreate(c *Ctx, args []string) int {
106106 }
107107 body, err := bodyFrom(c, notes, file)
108108 if err != nil {
109 return c.failErr(err)
109 return c.failInput(err)
110110 }
111111 if title == "" {
112112 title = tag
@@ -159,7 +159,7 @@ func runReleaseEdit(c *Ctx, args []string) int {
159159 setTitle, setNotes := f.Has("--title"), f.Has("--notes") || f.Has("--file")
160160 fmtName, err := markupFormat(format)
161161 if err != nil {
162 return c.failErr(err)
162 return c.failInput(err)
163163 }
164164 if path == "" || tag == "" || (!setTitle && !setNotes && fmtName == "") {
165165 return c.fail(protocol.ExitUsage, usage)
@@ -184,7 +184,7 @@ func runReleaseEdit(c *Ctx, args []string) int {
184184 body := rel.Notes
185185 if setNotes {
186186 if body, err = bodyFrom(c, notes, file); err != nil {
187 return c.failErr(err)
187 return c.failInput(err)
188188 }
189189 }
190190 if fmtName == "" {
internal/control/repo.go +6 −6
@@ -184,7 +184,7 @@ func runRepoCreate(c *Ctx, args []string) int {
184184 return c.fail(protocol.ExitUsage, "usage: repo create <owner/name> [--private]")
185185 }
186186 if err := policyValidateRepoName(name); err != nil {
187 return c.failErr(err)
187 return c.failInput(err)
188188 }
189189 ownerKind, ownerID := "user", c.User.ID
190190 if owner != c.User.Username {
@@ -461,7 +461,7 @@ func runRepoRename(c *Ctx, args []string) int {
461461 return c.fail(protocol.ExitUsage, "%s is already named %s", repo.Path(), newName)
462462 }
463463 if err := policyValidateRepoName(newName); err != nil {
464 return c.failErr(err)
464 return c.failInput(err)
465465 }
466466 oldDir := RepoDir(c.Cfg.Server.Root, repo.OwnerName, repo.Name)
467467 newDir := RepoDir(c.Cfg.Server.Root, repo.OwnerName, newName)
@@ -666,7 +666,7 @@ func runSetWebsite(c *Ctx, args []string) int {
666666 }
667667 site := strings.TrimSpace(args[1])
668668 if err := validateWebsite(site); err != nil {
669 return c.failErr(err)
669 return c.failInput(err)
670670 }
671671 if len(site) > 256 {
672672 return c.fail(protocol.ExitUsage, "website URL too long (max 256)")
@@ -816,7 +816,7 @@ func editTopics(c *Ctx, args []string, add bool) int {
816816 if add {
817817 for _, t := range topics {
818818 if err := policy.ValidateTopic(t); err != nil {
819 return c.failErr(err)
819 return c.failInput(err)
820820 }
821821 }
822822 have, err := c.Store.ListTopics(repo.ID)
@@ -863,7 +863,7 @@ func runRepoSearch(c *Ctx, args []string) int {
863863 return c.fail(protocol.ExitUsage, "usage: repo search <query>")
864864 }
865865 if err := validQuery(args[0]); err != nil {
866 return c.failErr(err)
866 return c.failInput(err)
867867 }
868868 q := strings.ToLower(args[0])
869869
@@ -930,7 +930,7 @@ func runRepoGrep(c *Ctx, args []string) int {
930930 return c.fail(protocol.ExitUsage, "usage: repo grep <owner/name> <query> [--ref <ref>]")
931931 }
932932 if err := validQuery(query); err != nil {
933 return c.failErr(err)
933 return c.failInput(err)
934934 }
935935 repo, code := resolveRepo(c, path, policy.CanRead)
936936 if code >= 0 {
internal/control/search.go +1 −1
@@ -86,7 +86,7 @@ func runSearch(c *Ctx, args []string) int {
8686 return c.fail(protocol.ExitUsage, "usage: %s", usage)
8787 }
8888 if err := validQuery(f.Pos[0]); err != nil {
89 return c.failErr(err)
89 return c.failInput(err)
9090 }
9191 kinds := f.List("--kind")
9292 for _, k := range kinds {
internal/control/sig.go +1 −1
@@ -45,7 +45,7 @@ func runPGPAdd(c *Ctx, args []string) int {
4545 }
4646 meta, err := sig.ParsePGPKey(raw)
4747 if err != nil {
48 return c.failErr(err)
48 return c.failInput(err)
4949 }
5050 uids, _ := json.Marshal(meta.Emails)
5151 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 {
9696 return code
9797 }
9898 if err := policy.ValidateName(args[1]); err != nil {
99 return c.failErr(err)
99 return c.failInput(err)
100100 }
101101 if _, err := c.Store.CreateTeam(org.ID, args[1]); err != nil {
102102 return c.failErr(err)
internal/control/thread.go +2 −2
@@ -83,7 +83,7 @@ func runComment(c *Ctx, args []string, t thread, noun string,
8383 }
8484 fmtName, err := markupFormat(f.Value("--format"))
8585 if err != nil {
86 return c.failErr(err)
86 return c.failInput(err)
8787 }
8888 if fmtName == "" {
8989 fmtName = "md"
@@ -97,7 +97,7 @@ func runComment(c *Ctx, args []string, t thread, noun string,
9797 }
9898 body, err := bodyFrom(c, f.Value("--message"), f.Value("--file"))
9999 if err != nil {
100 return c.failErr(err)
100 return c.failInput(err)
101101 }
102102 if strings.TrimSpace(body) == "" {
103103 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 {
5454 if ttl != "" {
5555 d, err := parseTTL(ttl)
5656 if err != nil {
57 return c.failErr(err)
57 return c.failInput(err)
5858 }
5959 t := time.Now().Add(d)
6060 expires = &t