Commit bc49092e23
Verified · cmc ci/build: success ci/test: success ci/vuln: success
Layout: unified · split
internal/control/admin.go +1 −1
| @@ -310,7 +310,7 @@ func setAdmin(c *Ctx, args []string, admin bool) int { | |||
| 310 | } | 310 | } |
| 311 | if err := c.Store.SetUserAdmin(u.ID, admin); err != nil { | 311 | if err := c.Store.SetUserAdmin(u.ID, admin); err != nil { |
| 312 | if errors.Is(err, store.ErrLastAdmin) { | 312 | if errors.Is(err, store.ErrLastAdmin) { |
| 313 | return c.fail(protocol.ExitUsage, "%v", err) | 313 | return c.failErr(err) |
| 314 | } | 314 | } |
| 315 | return c.fail(protocol.ExitFailure, "%v", err) | 315 | return c.fail(protocol.ExitFailure, "%v", err) |
| 316 | } | 316 | } |
internal/control/adminhost.go +5 −5
| @@ -87,7 +87,7 @@ func runAdminUserCreate(c *Ctx, args []string) int { | |||
| 87 | return c.fail(protocol.ExitUsage, usage) | 87 | return c.fail(protocol.ExitUsage, usage) |
| 88 | } | 88 | } |
| 89 | if err := policy.ValidateOwnerName(username); err != nil { | 89 | if err := policy.ValidateOwnerName(username); err != nil { |
| 90 | return c.fail(protocol.ExitUsage, "%v", err) | 90 | return c.failErr(err) |
| 91 | } | 91 | } |
| 92 | // Parse the key before creating anything, so a bad key leaves no | 92 | // Parse the key before creating anything, so a bad key leaves no |
| 93 | // half-made account behind. | 93 | // half-made account behind. |
| @@ -103,7 +103,7 @@ func runAdminUserCreate(c *Ctx, args []string) int { | |||
| 103 | } | 103 | } |
| 104 | uid, err := c.Store.CreateUser(username, isAdmin) | 104 | uid, err := c.Store.CreateUser(username, isAdmin) |
| 105 | if err != nil { | 105 | if err != nil { |
| 106 | return c.fail(protocol.ExitUsage, "%v", err) | 106 | return c.failErr(err) |
| 107 | } | 107 | } |
| 108 | if email != "" { | 108 | if email != "" { |
| 109 | by := "" | 109 | by := "" |
| @@ -111,14 +111,14 @@ func runAdminUserCreate(c *Ctx, args []string) int { | |||
| 111 | by = "admin" | 111 | by = "admin" |
| 112 | } | 112 | } |
| 113 | if err := c.Store.AddEmail(uid, email, by, true); err != nil { | 113 | if err := c.Store.AddEmail(uid, email, by, true); err != nil { |
| 114 | return c.fail(protocol.ExitUsage, "%v", err) | 114 | return c.failErr(err) |
| 115 | } | 115 | } |
| 116 | } | 116 | } |
| 117 | fp := "" | 117 | fp := "" |
| 118 | if pub != nil { | 118 | if pub != nil { |
| 119 | fp = ssh.FingerprintSHA256(pub) | 119 | fp = ssh.FingerprintSHA256(pub) |
| 120 | if err := c.Store.AddSSHKey(uid, fp, pub.Type(), pub.Marshal(), "full"); err != nil { | 120 | if err := c.Store.AddSSHKey(uid, fp, pub.Type(), pub.Marshal(), "full"); err != nil { |
| 121 | return c.fail(protocol.ExitUsage, "%v", err) | 121 | return c.failErr(err) |
| 122 | } | 122 | } |
| 123 | } | 123 | } |
| 124 | c.Store.Audit(c.User.ID, "admin user.created", map[string]any{"user": username}) | 124 | c.Store.Audit(c.User.ID, "admin user.created", map[string]any{"user": username}) |
| @@ -201,7 +201,7 @@ func runAdminUserDelete(c *Ctx, args []string) int { | |||
| 201 | return c.fail(protocol.ExitUsage, "that is your own account") | 201 | return c.fail(protocol.ExitUsage, "that is your own account") |
| 202 | } | 202 | } |
| 203 | if err := c.Store.DeleteUser(u.ID); err != nil { | 203 | if err := c.Store.DeleteUser(u.ID); err != nil { |
| 204 | return c.fail(protocol.ExitUsage, "%v", err) | 204 | return c.failErr(err) |
| 205 | } | 205 | } |
| 206 | c.Store.Audit(c.User.ID, "admin user.deleted", map[string]any{"user": u.Username}) | 206 | c.Store.Audit(c.User.ID, "admin user.deleted", map[string]any{"user": u.Username}) |
| 207 | return c.emit(map[string]string{"deleted": u.Username}, func(w io.Writer) { | 207 | return c.emit(map[string]string{"deleted": u.Username}, func(w io.Writer) { |
internal/control/build.go +1 −1
| @@ -173,7 +173,7 @@ func repoJobs(c *Ctx, repo store.Repo) ([]ci.Job, string, int) { | |||
| 173 | } | 173 | } |
| 174 | jobs, err := ci.Parse(raw) | 174 | jobs, err := ci.Parse(raw) |
| 175 | if err != nil { | 175 | if err != nil { |
| 176 | return nil, "", c.fail(protocol.ExitUsage, "%v", err) | 176 | return nil, "", c.failErr(err) |
| 177 | } | 177 | } |
| 178 | return jobs, sha, -1 | 178 | return jobs, sha, -1 |
| 179 | } | 179 | } |
internal/control/control.go +17
| @@ -5,6 +5,7 @@ package control | |||
| 5 | 5 | ||
| 6 | import ( | 6 | import ( |
| 7 | "encoding/json" | 7 | "encoding/json" |
| 8 | "errors" | ||
| 8 | "fmt" | 9 | "fmt" |
| 9 | "io" | 10 | "io" |
| 10 | "reflect" | 11 | "reflect" |
| @@ -164,6 +165,22 @@ func (c *Ctx) emit(data any, plain func(w io.Writer)) int { | |||
| 164 | return protocol.ExitOK | 165 | return protocol.ExitOK |
| 165 | } | 166 | } |
| 166 | 167 | ||
| 168 | // failErr reports an error from a store or helper call with the exit code | ||
| 169 | // its kind deserves: not-found is not-found, the database or I/O failing | ||
| 170 | // is a failure, and anything else is the caller's mistake, which is what | ||
| 171 | // most such errors are (a name that does not validate, a state that does | ||
| 172 | // not allow the change). A SQLite I/O error used to be a usage error and | ||
| 173 | // an HTTP 400 (#107). | ||
| 174 | func (c *Ctx) failErr(err error) int { | ||
| 175 | switch { | ||
| 176 | case errors.Is(err, store.ErrNotFound): | ||
| 177 | return c.fail(protocol.ExitNotFound, "%v", err) | ||
| 178 | case store.IsInternal(err): | ||
| 179 | return c.fail(protocol.ExitFailure, "%v", err) | ||
| 180 | } | ||
| 181 | return c.fail(protocol.ExitUsage, "%v", err) | ||
| 182 | } | ||
| 183 | |||
| 167 | func (c *Ctx) fail(code int, format string, args ...any) int { | 184 | func (c *Ctx) fail(code int, format string, args ...any) int { |
| 168 | msg := fmt.Sprintf(format, args...) | 185 | msg := fmt.Sprintf(format, args...) |
| 169 | if c.JSON { | 186 | if c.JSON { |
internal/control/control_test.go +24
| @@ -3,7 +3,10 @@ package control | |||
| 3 | import ( | 3 | import ( |
| 4 | "bytes" | 4 | "bytes" |
| 5 | "encoding/json" | 5 | "encoding/json" |
| 6 | "errors" | ||
| 7 | "fmt" | ||
| 6 | "io" | 8 | "io" |
| 9 | "io/fs" | ||
| 7 | "slices" | 10 | "slices" |
| 8 | "strings" | 11 | "strings" |
| 9 | "testing" | 12 | "testing" |
| @@ -223,3 +226,24 @@ func TestRefusalsHonourJSON(t *testing.T) { | |||
| 223 | } | 226 | } |
| 224 | } | 227 | } |
| 225 | } | 228 | } |
| 229 | |||
| 230 | // TestFailErrExitCodes: not-found, an internal failure, and the caller's | ||
| 231 | // mistake each get their own exit code (#107). | ||
| 232 | func 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) | ||
| 248 | } | ||
| 249 | } | ||
internal/control/deploykey.go +1 −1
| @@ -58,7 +58,7 @@ func runDeployKeyAdd(c *Ctx, args []string) int { | |||
| 58 | scope := fmt.Sprintf("deploy:%d:%s", repo.ID, mode) | 58 | scope := fmt.Sprintf("deploy:%d:%s", repo.ID, mode) |
| 59 | if err := c.Store.AddSSHKey(c.User.ID, fp, pub.Type(), pub.Marshal(), scope); err != nil { | 59 | if err := c.Store.AddSSHKey(c.User.ID, fp, pub.Type(), pub.Marshal(), scope); err != nil { |
| 60 | if errors.Is(err, store.ErrDuplicateKey) { | 60 | if errors.Is(err, store.ErrDuplicateKey) { |
| 61 | return c.fail(protocol.ExitUsage, "%v", err) | 61 | return c.failErr(err) |
| 62 | } | 62 | } |
| 63 | return c.fail(protocol.ExitFailure, "%v", err) | 63 | return c.fail(protocol.ExitFailure, "%v", err) |
| 64 | } | 64 | } |
internal/control/diffcomment.go +2 −2
| @@ -81,7 +81,7 @@ func runDiffComment(c *Ctx, args []string) int { | |||
| 81 | } | 81 | } |
| 82 | body, err := bodyFrom(c, message, file) | 82 | body, err := bodyFrom(c, message, file) |
| 83 | if err != nil { | 83 | if err != nil { |
| 84 | return c.fail(protocol.ExitUsage, "%v", err) | 84 | return c.failErr(err) |
| 85 | } | 85 | } |
| 86 | if strings.TrimSpace(body) == "" { | 86 | if strings.TrimSpace(body) == "" { |
| 87 | return c.fail(protocol.ExitUsage, "empty comment; use --message or --file -") | 87 | return c.fail(protocol.ExitUsage, "empty comment; use --message or --file -") |
| @@ -116,7 +116,7 @@ func runDiffComment(c *Ctx, args []string) int { | |||
| 116 | if errors.Is(err, store.ErrNotFound) { | 116 | if errors.Is(err, store.ErrNotFound) { |
| 117 | return c.fail(protocol.ExitNotFound, "%v", err) | 117 | return c.fail(protocol.ExitNotFound, "%v", err) |
| 118 | } | 118 | } |
| 119 | return c.fail(protocol.ExitUsage, "%v", err) | 119 | return c.failErr(err) |
| 120 | } | 120 | } |
| 121 | if parts, err := c.Store.MRParticipants(mr.ID); err == nil { | 121 | if parts, err := c.Store.MRParticipants(mr.ID); err == nil { |
| 122 | notifyUsers(c, parts, mrSubject(repo, mr.Number, mr.Title), | 122 | notifyUsers(c, parts, mrSubject(repo, mr.Number, mr.Title), |
internal/control/identity.go +1 −1
| @@ -109,7 +109,7 @@ func runKeysAdd(c *Ctx, args []string) int { | |||
| 109 | fp := ssh.FingerprintSHA256(pub) | 109 | fp := ssh.FingerprintSHA256(pub) |
| 110 | if err := c.Store.AddSSHKey(c.User.ID, fp, pub.Type(), pub.Marshal(), scope); err != nil { | 110 | if err := c.Store.AddSSHKey(c.User.ID, fp, pub.Type(), pub.Marshal(), scope); err != nil { |
| 111 | if errors.Is(err, store.ErrDuplicateKey) { | 111 | if errors.Is(err, store.ErrDuplicateKey) { |
| 112 | return c.fail(protocol.ExitUsage, "%v", err) | 112 | return c.failErr(err) |
| 113 | } | 113 | } |
| 114 | return c.fail(protocol.ExitFailure, "adding key: %v", err) | 114 | return c.fail(protocol.ExitFailure, "adding key: %v", err) |
| 115 | } | 115 | } |
internal/control/import.go +1 −1
| @@ -63,7 +63,7 @@ func runRepoImport(c *Ctx, args []string) int { | |||
| 63 | return c.fail(protocol.ExitUsage, "usage: repo import <owner/name> --from <url>") | 63 | return c.fail(protocol.ExitUsage, "usage: repo import <owner/name> --from <url>") |
| 64 | } | 64 | } |
| 65 | if err := policy.ValidateName(name); err != nil { | 65 | if err := policy.ValidateName(name); err != nil { |
| 66 | return c.fail(protocol.ExitUsage, "%v", err) | 66 | return c.failErr(err) |
| 67 | } | 67 | } |
| 68 | // Same ownership rule as repo create: yourself, or an org you admin. | 68 | // Same ownership rule as repo create: yourself, or an org you admin. |
| 69 | ownerKind, ownerID := "user", c.User.ID | 69 | ownerKind, ownerID := "user", c.User.ID |
internal/control/issue.go +8 −8
| @@ -162,7 +162,7 @@ func runIssueCreate(c *Ctx, args []string) int { | |||
| 162 | } | 162 | } |
| 163 | fmtName, err := markupFormat(format) | 163 | fmtName, err := markupFormat(format) |
| 164 | if err != nil { | 164 | if err != nil { |
| 165 | return c.fail(protocol.ExitUsage, "%v", err) | 165 | return c.failErr(err) |
| 166 | } | 166 | } |
| 167 | if fmtName == "" { | 167 | if fmtName == "" { |
| 168 | fmtName = "md" | 168 | fmtName = "md" |
| @@ -177,7 +177,7 @@ func runIssueCreate(c *Ctx, args []string) int { | |||
| 177 | } | 177 | } |
| 178 | b, err := bodyFrom(c, body, file) | 178 | b, err := bodyFrom(c, body, file) |
| 179 | if err != nil { | 179 | if err != nil { |
| 180 | return c.fail(protocol.ExitUsage, "%v", err) | 180 | return c.failErr(err) |
| 181 | } | 181 | } |
| 182 | n, err := c.Store.CreateIssue(repo.ID, c.User.ID, title, b, fmtName) | 182 | n, err := c.Store.CreateIssue(repo.ID, c.User.ID, title, b, fmtName) |
| 183 | if err != nil { | 183 | if err != nil { |
| @@ -326,7 +326,7 @@ func runIssueComment(c *Ctx, args []string) int { | |||
| 326 | } | 326 | } |
| 327 | fmtName, err := markupFormat(format) | 327 | fmtName, err := markupFormat(format) |
| 328 | if err != nil { | 328 | if err != nil { |
| 329 | return c.fail(protocol.ExitUsage, "%v", err) | 329 | return c.failErr(err) |
| 330 | } | 330 | } |
| 331 | if fmtName == "" { | 331 | if fmtName == "" { |
| 332 | fmtName = "md" | 332 | fmtName = "md" |
| @@ -340,7 +340,7 @@ func runIssueComment(c *Ctx, args []string) int { | |||
| 340 | } | 340 | } |
| 341 | body, err := bodyFrom(c, message, file) | 341 | body, err := bodyFrom(c, message, file) |
| 342 | if err != nil { | 342 | if err != nil { |
| 343 | return c.fail(protocol.ExitUsage, "%v", err) | 343 | return c.failErr(err) |
| 344 | } | 344 | } |
| 345 | if strings.TrimSpace(body) == "" { | 345 | if strings.TrimSpace(body) == "" { |
| 346 | return c.fail(protocol.ExitUsage, "empty comment; use --message or --file -") | 346 | return c.fail(protocol.ExitUsage, "empty comment; use --message or --file -") |
| @@ -424,13 +424,13 @@ func editText(c *Ctx, args []string, kind string) (rest []string, title, body, f | |||
| 424 | if file != "" { | 424 | if file != "" { |
| 425 | b, err := bodyFrom(c, "", file) | 425 | b, err := bodyFrom(c, "", file) |
| 426 | if err != nil { | 426 | if err != nil { |
| 427 | return nil, nil, nil, nil, c.fail(protocol.ExitUsage, "%v", err) | 427 | return nil, nil, nil, nil, c.failErr(err) |
| 428 | } | 428 | } |
| 429 | bodyV, haveBody = b, true | 429 | bodyV, haveBody = b, true |
| 430 | } | 430 | } |
| 431 | fmtName, err := markupFormat(formatV) | 431 | fmtName, err := markupFormat(formatV) |
| 432 | if err != nil { | 432 | if err != nil { |
| 433 | return nil, nil, nil, nil, c.fail(protocol.ExitUsage, "%v", err) | 433 | return nil, nil, nil, nil, c.failErr(err) |
| 434 | } | 434 | } |
| 435 | if !haveTitle && !haveBody && fmtName == "" { | 435 | if !haveTitle && !haveBody && fmtName == "" { |
| 436 | return nil, nil, nil, nil, c.fail(protocol.ExitUsage, "usage: %s edit <owner/name> <n> [--title <t>] [--body <b> | --file -] [--format md|org]", kind) | 436 | return nil, nil, nil, nil, c.fail(protocol.ExitUsage, "usage: %s edit <owner/name> <n> [--title <t>] [--body <b> | --file -] [--format md|org]", kind) |
| @@ -506,7 +506,7 @@ func addRemoveFlags(args []string) (rest, adds, removes []string, err error) { | |||
| 506 | func runIssueLabel(c *Ctx, args []string) int { | 506 | func runIssueLabel(c *Ctx, args []string) int { |
| 507 | rest, adds, removes, err := addRemoveFlags(args) | 507 | rest, adds, removes, err := addRemoveFlags(args) |
| 508 | if err != nil { | 508 | if err != nil { |
| 509 | return c.fail(protocol.ExitUsage, "%v", err) | 509 | return c.failErr(err) |
| 510 | } | 510 | } |
| 511 | if len(adds)+len(removes) == 0 { | 511 | if len(adds)+len(removes) == 0 { |
| 512 | return c.fail(protocol.ExitUsage, "usage: issue label <owner/name> <n> [--add <l>]... [--remove <l>]...") | 512 | return c.fail(protocol.ExitUsage, "usage: issue label <owner/name> <n> [--add <l>]... [--remove <l>]...") |
| @@ -543,7 +543,7 @@ func runIssueLabel(c *Ctx, args []string) int { | |||
| 543 | func runIssueAssign(c *Ctx, args []string) int { | 543 | func runIssueAssign(c *Ctx, args []string) int { |
| 544 | rest, adds, removes, err := addRemoveFlags(args) | 544 | rest, adds, removes, err := addRemoveFlags(args) |
| 545 | if err != nil { | 545 | if err != nil { |
| 546 | return c.fail(protocol.ExitUsage, "%v", err) | 546 | return c.failErr(err) |
| 547 | } | 547 | } |
| 548 | if len(adds)+len(removes) == 0 { | 548 | if len(adds)+len(removes) == 0 { |
| 549 | return c.fail(protocol.ExitUsage, "usage: issue assign <owner/name> <n> [--add <user>]... [--remove <user>]...") | 549 | return c.fail(protocol.ExitUsage, "usage: issue assign <owner/name> <n> [--add <user>]... [--remove <user>]...") |
internal/control/milestone.go +1 −1
| @@ -78,7 +78,7 @@ func runMilestoneCreate(c *Ctx, args []string) int { | |||
| 78 | return code | 78 | return code |
| 79 | } | 79 | } |
| 80 | if _, err := c.Store.CreateMilestone(repo.ID, title, description, due); err != nil { | 80 | if _, err := c.Store.CreateMilestone(repo.ID, title, description, due); err != nil { |
| 81 | return c.fail(protocol.ExitUsage, "%v", err) | 81 | return c.failErr(err) |
| 82 | } | 82 | } |
| 83 | return c.emit(map[string]string{"milestone": title}, func(w io.Writer) { | 83 | return c.emit(map[string]string{"milestone": title}, func(w io.Writer) { |
| 84 | fmt.Fprintf(w, "created milestone %q on %s\n", title, repo.Path()) | 84 | fmt.Fprintf(w, "created milestone %q on %s\n", title, repo.Path()) |
internal/control/mirrorcmd.go +1 −1
| @@ -63,7 +63,7 @@ func runMirrorAdd(c *Ctx, args []string) int { | |||
| 63 | // The worker's git process dials this URL from the server: same SSRF | 63 | // The worker's git process dials this URL from the server: same SSRF |
| 64 | // surface as a webhook target, same rules. | 64 | // surface as a webhook target, same rules. |
| 65 | if err := webhook.ValidateURL(urlArg, c.Cfg.Webhooks.AllowLocal); err != nil { | 65 | if err := webhook.ValidateURL(urlArg, c.Cfg.Webhooks.AllowLocal); err != nil { |
| 66 | return c.fail(protocol.ExitUsage, "%v", err) | 66 | return c.failErr(err) |
| 67 | } | 67 | } |
| 68 | repo, code := resolveRepo(c, path, policy.CanAdmin) | 68 | repo, code := resolveRepo(c, path, policy.CanAdmin) |
| 69 | if code >= 0 { | 69 | if code >= 0 { |
internal/control/mr.go +5 −5
| @@ -95,7 +95,7 @@ func runRepoFork(c *Ctx, args []string) int { | |||
| 95 | name = src.Name | 95 | name = src.Name |
| 96 | } | 96 | } |
| 97 | if err := policy.ValidateName(name); err != nil { | 97 | if err := policy.ValidateName(name); err != nil { |
| 98 | return c.fail(protocol.ExitUsage, "%v", err) | 98 | return c.failErr(err) |
| 99 | } | 99 | } |
| 100 | if code := checkRepoQuota(c); code >= 0 { | 100 | if code := checkRepoQuota(c); code >= 0 { |
| 101 | return code | 101 | return code |
| @@ -266,7 +266,7 @@ func runMRCreate(c *Ctx, args []string) int { | |||
| 266 | } | 266 | } |
| 267 | fmtName, err := markupFormat(format) | 267 | fmtName, err := markupFormat(format) |
| 268 | if err != nil { | 268 | if err != nil { |
| 269 | return c.fail(protocol.ExitUsage, "%v", err) | 269 | return c.failErr(err) |
| 270 | } | 270 | } |
| 271 | if fmtName == "" { | 271 | if fmtName == "" { |
| 272 | fmtName = "md" | 272 | fmtName = "md" |
| @@ -303,7 +303,7 @@ func runMRCreate(c *Ctx, args []string) int { | |||
| 303 | } | 303 | } |
| 304 | b, err := bodyFrom(c, body, file) | 304 | b, err := bodyFrom(c, body, file) |
| 305 | if err != nil { | 305 | if err != nil { |
| 306 | return c.fail(protocol.ExitUsage, "%v", err) | 306 | return c.failErr(err) |
| 307 | } | 307 | } |
| 308 | n, err := c.Store.CreateMR(repo.ID, c.User.ID, srcRepo.ID, srcBranch, target, title, b, headSHA, fmtName) | 308 | n, err := c.Store.CreateMR(repo.ID, c.User.ID, srcRepo.ID, srcBranch, target, title, b, headSHA, fmtName) |
| 309 | if err != nil { | 309 | if err != nil { |
| @@ -751,7 +751,7 @@ func runMRComment(c *Ctx, args []string) int { | |||
| 751 | } | 751 | } |
| 752 | fmtName, err := markupFormat(format) | 752 | fmtName, err := markupFormat(format) |
| 753 | if err != nil { | 753 | if err != nil { |
| 754 | return c.fail(protocol.ExitUsage, "%v", err) | 754 | return c.failErr(err) |
| 755 | } | 755 | } |
| 756 | if fmtName == "" { | 756 | if fmtName == "" { |
| 757 | fmtName = "md" | 757 | fmtName = "md" |
| @@ -765,7 +765,7 @@ func runMRComment(c *Ctx, args []string) int { | |||
| 765 | } | 765 | } |
| 766 | body, err := bodyFrom(c, message, file) | 766 | body, err := bodyFrom(c, message, file) |
| 767 | if err != nil { | 767 | if err != nil { |
| 768 | return c.fail(protocol.ExitUsage, "%v", err) | 768 | return c.failErr(err) |
| 769 | } | 769 | } |
| 770 | if strings.TrimSpace(body) == "" { | 770 | if strings.TrimSpace(body) == "" { |
| 771 | return c.fail(protocol.ExitUsage, "empty comment; use --message or --file -") | 771 | return c.fail(protocol.ExitUsage, "empty comment; use --message or --file -") |
internal/control/org.go +5 −5
| @@ -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.fail(protocol.ExitUsage, "%v", err) | 66 | return c.failErr(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.fail(protocol.ExitUsage, "%v", err) | 141 | return c.failErr(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) |
| @@ -146,7 +146,7 @@ func runOrgRename(c *Ctx, args []string) int { | |||
| 146 | return c.fail(protocol.ExitFailure, "repository directory %s already exists", newName) | 146 | return c.fail(protocol.ExitFailure, "repository directory %s already exists", newName) |
| 147 | } | 147 | } |
| 148 | if err := c.Store.RenameOrg(org.ID, newName); err != nil { | 148 | if err := c.Store.RenameOrg(org.ID, newName); err != nil { |
| 149 | return c.fail(protocol.ExitUsage, "%v", err) | 149 | return c.failErr(err) |
| 150 | } | 150 | } |
| 151 | // Repo paths on disk derive from the owner name; move the tree. If the | 151 | // Repo paths on disk derive from the owner name; move the tree. If the |
| 152 | // move fails, revert the database so name and disk stay consistent. | 152 | // move fails, revert the database so name and disk stay consistent. |
| @@ -220,7 +220,7 @@ func runOrgMembersAdd(c *Ctx, args []string) int { | |||
| 220 | return c.fail(protocol.ExitFailure, "%v", err) | 220 | return c.fail(protocol.ExitFailure, "%v", err) |
| 221 | } | 221 | } |
| 222 | if err := c.Store.SetOrgMember(org.ID, target.ID, role); err != nil { | 222 | if err := c.Store.SetOrgMember(org.ID, target.ID, role); err != nil { |
| 223 | return c.fail(protocol.ExitUsage, "%v", err) | 223 | return c.failErr(err) |
| 224 | } | 224 | } |
| 225 | return c.emit(map[string]string{"org": org.Name, "user": target.Username, "role": role}, func(w io.Writer) { | 225 | return c.emit(map[string]string{"org": org.Name, "user": target.Username, "role": role}, func(w io.Writer) { |
| 226 | fmt.Fprintf(w, "%s is now a %s of %s\n", target.Username, role, org.Name) | 226 | fmt.Fprintf(w, "%s is now a %s of %s\n", target.Username, role, org.Name) |
| @@ -246,7 +246,7 @@ func runOrgMembersRemove(c *Ctx, args []string) int { | |||
| 246 | if errors.Is(err, store.ErrNotFound) { | 246 | if errors.Is(err, store.ErrNotFound) { |
| 247 | return c.fail(protocol.ExitNotFound, "%s is not a member of %s", target.Username, org.Name) | 247 | return c.fail(protocol.ExitNotFound, "%s is not a member of %s", target.Username, org.Name) |
| 248 | } | 248 | } |
| 249 | return c.fail(protocol.ExitUsage, "%v", err) | 249 | return c.failErr(err) |
| 250 | } | 250 | } |
| 251 | return c.emit(map[string]string{"org": org.Name, "removed": target.Username}, func(w io.Writer) { | 251 | return c.emit(map[string]string{"org": org.Name, "removed": target.Username}, func(w io.Writer) { |
| 252 | fmt.Fprintf(w, "removed %s from %s\n", target.Username, org.Name) | 252 | fmt.Fprintf(w, "removed %s from %s\n", target.Username, org.Name) |
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.fail(protocol.ExitUsage, "%v", err) | 77 | return c.failErr(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
| @@ -345,7 +345,7 @@ func runProfileShow(c *Ctx, args []string) int { | |||
| 345 | func runProfileSet(c *Ctx, args []string) int { | 345 | func runProfileSet(c *Ctx, args []string) int { |
| 346 | rest, e, err := parseProfileFlags(c, args) | 346 | rest, e, err := parseProfileFlags(c, args) |
| 347 | if err != nil { | 347 | if err != nil { |
| 348 | return c.fail(protocol.ExitUsage, "%v", err) | 348 | return c.failErr(err) |
| 349 | } | 349 | } |
| 350 | if len(rest) != 0 { | 350 | if len(rest) != 0 { |
| 351 | return c.fail(protocol.ExitUsage, | 351 | return c.fail(protocol.ExitUsage, |
| @@ -360,7 +360,7 @@ func runProfileSet(c *Ctx, args []string) int { | |||
| 360 | } | 360 | } |
| 361 | p, err = applyProfile(p, e) | 361 | p, err = applyProfile(p, e) |
| 362 | if err != nil { | 362 | if err != nil { |
| 363 | return c.fail(protocol.ExitUsage, "%v", err) | 363 | return c.failErr(err) |
| 364 | } | 364 | } |
| 365 | if err := c.Store.SetOwnerProfile("user", c.User.ID, p); err != nil { | 365 | if err := c.Store.SetOwnerProfile("user", c.User.ID, p); err != nil { |
| 366 | return c.fail(protocol.ExitFailure, "%v", err) | 366 | return c.fail(protocol.ExitFailure, "%v", err) |
| @@ -373,7 +373,7 @@ func runProfileSet(c *Ctx, args []string) int { | |||
| 373 | func runOrgProfile(c *Ctx, args []string) int { | 373 | func runOrgProfile(c *Ctx, args []string) int { |
| 374 | rest, e, err := parseProfileFlags(c, args) | 374 | rest, e, err := parseProfileFlags(c, args) |
| 375 | if err != nil { | 375 | if err != nil { |
| 376 | return c.fail(protocol.ExitUsage, "%v", err) | 376 | return c.failErr(err) |
| 377 | } | 377 | } |
| 378 | if len(rest) != 1 { | 378 | if len(rest) != 1 { |
| 379 | return c.fail(protocol.ExitUsage, | 379 | return c.fail(protocol.ExitUsage, |
| @@ -393,7 +393,7 @@ func runOrgProfile(c *Ctx, args []string) int { | |||
| 393 | } | 393 | } |
| 394 | p, err = applyProfile(p, e) | 394 | p, err = applyProfile(p, e) |
| 395 | if err != nil { | 395 | if err != nil { |
| 396 | return c.fail(protocol.ExitUsage, "%v", err) | 396 | return c.failErr(err) |
| 397 | } | 397 | } |
| 398 | if err := c.Store.SetOwnerProfile("org", org.ID, p); err != nil { | 398 | if err := c.Store.SetOwnerProfile("org", org.ID, p); err != nil { |
| 399 | return c.fail(protocol.ExitFailure, "%v", err) | 399 | return c.fail(protocol.ExitFailure, "%v", err) |
internal/control/release.go +6 −6
| @@ -110,7 +110,7 @@ func runReleaseCreate(c *Ctx, args []string) int { | |||
| 110 | } | 110 | } |
| 111 | fmtName, err := markupFormat(format) | 111 | fmtName, err := markupFormat(format) |
| 112 | if err != nil { | 112 | if err != nil { |
| 113 | return c.fail(protocol.ExitUsage, "%v", err) | 113 | return c.failErr(err) |
| 114 | } | 114 | } |
| 115 | if fmtName == "" { | 115 | if fmtName == "" { |
| 116 | fmtName = "md" | 116 | fmtName = "md" |
| @@ -128,13 +128,13 @@ func runReleaseCreate(c *Ctx, args []string) int { | |||
| 128 | } | 128 | } |
| 129 | body, err := bodyFrom(c, notes, file) | 129 | body, err := bodyFrom(c, notes, file) |
| 130 | if err != nil { | 130 | if err != nil { |
| 131 | return c.fail(protocol.ExitUsage, "%v", err) | 131 | return c.failErr(err) |
| 132 | } | 132 | } |
| 133 | if title == "" { | 133 | if title == "" { |
| 134 | title = tag | 134 | title = tag |
| 135 | } | 135 | } |
| 136 | if _, err := c.Store.CreateRelease(repo.ID, tag, title, body, c.User.ID, fmtName); err != nil { | 136 | if _, err := c.Store.CreateRelease(repo.ID, tag, title, body, c.User.ID, fmtName); err != nil { |
| 137 | return c.fail(protocol.ExitUsage, "%v", err) | 137 | return c.failErr(err) |
| 138 | } | 138 | } |
| 139 | c.Store.RecordEvent(repo.ID, c.User.ID, "release.created", fmt.Sprintf(`{"tag":%q}`, tag)) | 139 | c.Store.RecordEvent(repo.ID, c.User.ID, "release.created", fmt.Sprintf(`{"tag":%q}`, tag)) |
| 140 | return c.emit(map[string]string{"tag": tag, "title": title}, func(w io.Writer) { | 140 | return c.emit(map[string]string{"tag": tag, "title": title}, func(w io.Writer) { |
| @@ -203,7 +203,7 @@ func runReleaseEdit(c *Ctx, args []string) int { | |||
| 203 | } | 203 | } |
| 204 | fmtName, err := markupFormat(format) | 204 | fmtName, err := markupFormat(format) |
| 205 | if err != nil { | 205 | if err != nil { |
| 206 | return c.fail(protocol.ExitUsage, "%v", err) | 206 | return c.failErr(err) |
| 207 | } | 207 | } |
| 208 | if path == "" || tag == "" || (!setTitle && !setNotes && fmtName == "") { | 208 | if path == "" || tag == "" || (!setTitle && !setNotes && fmtName == "") { |
| 209 | return c.fail(protocol.ExitUsage, usage) | 209 | return c.fail(protocol.ExitUsage, usage) |
| @@ -228,7 +228,7 @@ func runReleaseEdit(c *Ctx, args []string) int { | |||
| 228 | body := rel.Notes | 228 | body := rel.Notes |
| 229 | if setNotes { | 229 | if setNotes { |
| 230 | if body, err = bodyFrom(c, notes, file); err != nil { | 230 | if body, err = bodyFrom(c, notes, file); err != nil { |
| 231 | return c.fail(protocol.ExitUsage, "%v", err) | 231 | return c.failErr(err) |
| 232 | } | 232 | } |
| 233 | } | 233 | } |
| 234 | if fmtName == "" { | 234 | if fmtName == "" { |
| @@ -349,7 +349,7 @@ func runAssetAdd(c *Ctx, args []string) int { | |||
| 349 | } | 349 | } |
| 350 | sum := hex.EncodeToString(h.Sum(nil)) | 350 | sum := hex.EncodeToString(h.Sum(nil)) |
| 351 | if err := c.Store.AddReleaseAsset(rel.ID, name, n, sum); err != nil { | 351 | if err := c.Store.AddReleaseAsset(rel.ID, name, n, sum); err != nil { |
| 352 | return c.fail(protocol.ExitUsage, "%v", err) | 352 | return c.failErr(err) |
| 353 | } | 353 | } |
| 354 | if err := os.Rename(tmp.Name(), filepath.Join(dir, name)); err != nil { | 354 | if err := os.Rename(tmp.Name(), filepath.Join(dir, name)); err != nil { |
| 355 | c.Store.RemoveReleaseAsset(rel.ID, name) | 355 | c.Store.RemoveReleaseAsset(rel.ID, name) |
internal/control/repo.go +6 −6
| @@ -170,7 +170,7 @@ func runRepoCreate(c *Ctx, args []string) int { | |||
| 170 | return c.fail(protocol.ExitUsage, "usage: repo create <owner/name> [--private]") | 170 | return c.fail(protocol.ExitUsage, "usage: repo create <owner/name> [--private]") |
| 171 | } | 171 | } |
| 172 | if err := policyValidateRepoName(name); err != nil { | 172 | if err := policyValidateRepoName(name); err != nil { |
| 173 | return c.fail(protocol.ExitUsage, "%v", err) | 173 | return c.failErr(err) |
| 174 | } | 174 | } |
| 175 | ownerKind, ownerID := "user", c.User.ID | 175 | ownerKind, ownerID := "user", c.User.ID |
| 176 | if owner != c.User.Username { | 176 | if owner != c.User.Username { |
| @@ -382,7 +382,7 @@ func runRepoTransfer(c *Ctx, args []string) int { | |||
| 382 | return c.fail(protocol.ExitFailure, "repository directory already exists at %s/%s", newOwner, repo.Name) | 382 | return c.fail(protocol.ExitFailure, "repository directory already exists at %s/%s", newOwner, repo.Name) |
| 383 | } | 383 | } |
| 384 | if err := c.Store.TransferRepo(repo.ID, newKind, newID); err != nil { | 384 | if err := c.Store.TransferRepo(repo.ID, newKind, newID); err != nil { |
| 385 | return c.fail(protocol.ExitUsage, "%v", err) | 385 | return c.failErr(err) |
| 386 | } | 386 | } |
| 387 | if err := os.MkdirAll(filepath.Dir(newDir), 0o750); err != nil { | 387 | if err := os.MkdirAll(filepath.Dir(newDir), 0o750); err != nil { |
| 388 | c.Store.TransferRepo(repo.ID, repo.OwnerKind, repo.OwnerID) | 388 | c.Store.TransferRepo(repo.ID, repo.OwnerKind, repo.OwnerID) |
| @@ -554,7 +554,7 @@ func runSetWebsite(c *Ctx, args []string) int { | |||
| 554 | } | 554 | } |
| 555 | site := strings.TrimSpace(args[1]) | 555 | site := strings.TrimSpace(args[1]) |
| 556 | if err := validateWebsite(site); err != nil { | 556 | if err := validateWebsite(site); err != nil { |
| 557 | return c.fail(protocol.ExitUsage, "%v", err) | 557 | return c.failErr(err) |
| 558 | } | 558 | } |
| 559 | if len(site) > 256 { | 559 | if len(site) > 256 { |
| 560 | return c.fail(protocol.ExitUsage, "website URL too long (max 256)") | 560 | return c.fail(protocol.ExitUsage, "website URL too long (max 256)") |
| @@ -710,7 +710,7 @@ func editTopics(c *Ctx, args []string, add bool) int { | |||
| 710 | if add { | 710 | if add { |
| 711 | for _, t := range topics { | 711 | for _, t := range topics { |
| 712 | if err := policy.ValidateTopic(t); err != nil { | 712 | if err := policy.ValidateTopic(t); err != nil { |
| 713 | return c.fail(protocol.ExitUsage, "%v", err) | 713 | return c.failErr(err) |
| 714 | } | 714 | } |
| 715 | } | 715 | } |
| 716 | have, err := c.Store.ListTopics(repo.ID) | 716 | have, err := c.Store.ListTopics(repo.ID) |
| @@ -757,7 +757,7 @@ func runRepoSearch(c *Ctx, args []string) int { | |||
| 757 | return c.fail(protocol.ExitUsage, "usage: repo search <query>") | 757 | return c.fail(protocol.ExitUsage, "usage: repo search <query>") |
| 758 | } | 758 | } |
| 759 | if err := validQuery(args[0]); err != nil { | 759 | if err := validQuery(args[0]); err != nil { |
| 760 | return c.fail(protocol.ExitUsage, "%v", err) | 760 | return c.failErr(err) |
| 761 | } | 761 | } |
| 762 | q := strings.ToLower(args[0]) | 762 | q := strings.ToLower(args[0]) |
| 763 | 763 | ||
| @@ -833,7 +833,7 @@ func runRepoGrep(c *Ctx, args []string) int { | |||
| 833 | return c.fail(protocol.ExitUsage, "usage: repo grep <owner/name> <query> [--ref <ref>]") | 833 | return c.fail(protocol.ExitUsage, "usage: repo grep <owner/name> <query> [--ref <ref>]") |
| 834 | } | 834 | } |
| 835 | if err := validQuery(query); err != nil { | 835 | if err := validQuery(query); err != nil { |
| 836 | return c.fail(protocol.ExitUsage, "%v", err) | 836 | return c.failErr(err) |
| 837 | } | 837 | } |
| 838 | repo, code := resolveRepo(c, path, policy.CanRead) | 838 | repo, code := resolveRepo(c, path, policy.CanRead) |
| 839 | if code >= 0 { | 839 | if code >= 0 { |
internal/control/sig.go +2 −2
| @@ -45,12 +45,12 @@ 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.fail(protocol.ExitUsage, "%v", err) | 48 | return c.failErr(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 { |
| 52 | if errors.Is(err, store.ErrDuplicateKey) { | 52 | if errors.Is(err, store.ErrDuplicateKey) { |
| 53 | return c.fail(protocol.ExitUsage, "%v", err) | 53 | return c.failErr(err) |
| 54 | } | 54 | } |
| 55 | return c.fail(protocol.ExitFailure, "adding key: %v", err) | 55 | return c.fail(protocol.ExitFailure, "adding key: %v", err) |
| 56 | } | 56 | } |
internal/control/teams.go +2 −2
| @@ -94,10 +94,10 @@ func runTeamCreate(c *Ctx, args []string) int { | |||
| 94 | return code | 94 | return code |
| 95 | } | 95 | } |
| 96 | if err := policy.ValidateName(args[1]); err != nil { | 96 | if err := policy.ValidateName(args[1]); err != nil { |
| 97 | return c.fail(protocol.ExitUsage, "%v", err) | 97 | return c.failErr(err) |
| 98 | } | 98 | } |
| 99 | if _, err := c.Store.CreateTeam(org.ID, args[1]); err != nil { | 99 | if _, err := c.Store.CreateTeam(org.ID, args[1]); err != nil { |
| 100 | return c.fail(protocol.ExitUsage, "%v", err) | 100 | return c.failErr(err) |
| 101 | } | 101 | } |
| 102 | return c.emit(map[string]string{"team": args[1]}, func(w io.Writer) { | 102 | return c.emit(map[string]string{"team": args[1]}, func(w io.Writer) { |
| 103 | fmt.Fprintf(w, "created team %s/%s\n", org.Name, args[1]) | 103 | fmt.Fprintf(w, "created team %s/%s\n", org.Name, args[1]) |
internal/control/token.go +2 −2
| @@ -66,7 +66,7 @@ func runTokenCreate(c *Ctx, args []string) int { | |||
| 66 | if ttl != "" { | 66 | if ttl != "" { |
| 67 | d, err := parseTTL(ttl) | 67 | d, err := parseTTL(ttl) |
| 68 | if err != nil { | 68 | if err != nil { |
| 69 | return c.fail(protocol.ExitUsage, "%v", err) | 69 | return c.failErr(err) |
| 70 | } | 70 | } |
| 71 | t := time.Now().Add(d) | 71 | t := time.Now().Add(d) |
| 72 | expires = &t | 72 | expires = &t |
| @@ -78,7 +78,7 @@ func runTokenCreate(c *Ctx, args []string) int { | |||
| 78 | // The gb_ prefix makes leaked tokens findable by secret scanners. | 78 | // The gb_ prefix makes leaked tokens findable by secret scanners. |
| 79 | token := "gb_" + raw | 79 | token := "gb_" + raw |
| 80 | if err := c.Store.CreateAPIToken(c.User.ID, name, store.HashToken(token), scope, expires); err != nil { | 80 | if err := c.Store.CreateAPIToken(c.User.ID, name, store.HashToken(token), scope, expires); err != nil { |
| 81 | return c.fail(protocol.ExitUsage, "%v", err) | 81 | return c.failErr(err) |
| 82 | } | 82 | } |
| 83 | type out struct { | 83 | type out struct { |
| 84 | Name string `json:"name"` | 84 | Name string `json:"name"` |
internal/control/webhook.go +1 −1
| @@ -63,7 +63,7 @@ func runWebhookAdd(c *Ctx, args []string) int { | |||
| 63 | return code | 63 | return code |
| 64 | } | 64 | } |
| 65 | if err := webhook.ValidateURL(url, c.Cfg.Webhooks.AllowLocal); err != nil { | 65 | if err := webhook.ValidateURL(url, c.Cfg.Webhooks.AllowLocal); err != nil { |
| 66 | return c.fail(protocol.ExitUsage, "%v", err) | 66 | return c.failErr(err) |
| 67 | } | 67 | } |
| 68 | id, err := c.Store.AddWebhook(repo.ID, url, secret, events) | 68 | id, err := c.Store.AddWebhook(repo.ID, url, secret, events) |
| 69 | if err != nil { | 69 | if err != nil { |
internal/store/isinternal_test.go added +22
| @@ -0,0 +1,22 @@ | |||
| 1 | package store | ||
| 2 | |||
| 3 | import ( | ||
| 4 | "errors" | ||
| 5 | "testing" | ||
| 6 | ) | ||
| 7 | |||
| 8 | func TestIsInternal(t *testing.T) { | ||
| 9 | s := open(t) | ||
| 10 | if err := s.MigrateUp(); err != nil { | ||
| 11 | t.Fatal(err) | ||
| 12 | } | ||
| 13 | _, err := s.DB.Exec("INSERT INTO no_such_table (x) VALUES (1)") | ||
| 14 | if err == nil || !IsInternal(err) { | ||
| 15 | t.Errorf("a SQLite error is internal: %v", err) | ||
| 16 | } | ||
| 17 | for _, e := range []error{ErrNotFound, ErrExists, ErrDuplicateKey, errors.New("name must be lowercase")} { | ||
| 18 | if IsInternal(e) { | ||
| 19 | t.Errorf("%v is not internal", e) | ||
| 20 | } | ||
| 21 | } | ||
| 22 | } | ||
internal/store/store.go +13 −1
| @@ -2,6 +2,7 @@ | |||
| 2 | package store | 2 | package store |
| 3 | 3 | ||
| 4 | import ( | 4 | import ( |
| 5 | "context" | ||
| 5 | "database/sql" | 6 | "database/sql" |
| 6 | "embed" | 7 | "embed" |
| 7 | "errors" | 8 | "errors" |
| @@ -12,7 +13,7 @@ import ( | |||
| 12 | "strconv" | 13 | "strconv" |
| 13 | "strings" | 14 | "strings" |
| 14 | 15 | ||
| 15 | _ "modernc.org/sqlite" | 16 | "modernc.org/sqlite" |
| 16 | ) | 17 | ) |
| 17 | 18 | ||
| 18 | //go:embed migrations/*.sql | 19 | //go:embed migrations/*.sql |
| @@ -177,3 +178,14 @@ func (s *Store) migrateTo(target int) error { | |||
| 177 | } | 178 | } |
| 178 | return nil | 179 | return nil |
| 179 | } | 180 | } |
| 181 | |||
| 182 | // IsInternal reports whether err is the database or the I/O beneath it | ||
| 183 | // failing, as opposed to a sentinel or a message about the caller's | ||
| 184 | // input. Callers map it to a failure exit rather than a usage error. | ||
| 185 | func IsInternal(err error) bool { | ||
| 186 | var sqlErr *sqlite.Error | ||
| 187 | var pathErr *fs.PathError | ||
| 188 | return errors.As(err, &sqlErr) || errors.As(err, &pathErr) || | ||
| 189 | errors.Is(err, sql.ErrTxDone) || errors.Is(err, sql.ErrConnDone) || | ||
| 190 | errors.Is(err, context.DeadlineExceeded) || errors.Is(err, context.Canceled) | ||
| 191 | } | ||