Sign in

Search issue titles and bodies, change requests and comments. Private repositories you cannot read are not included.

vole
Clone

Read only, no credentials:

https://rodent.software/git/vole.git

Write over SSH with a registered key:

git clone ssh://git@0.0.0.0:2222/vole.git

A free and open-source forge that lives in your Git repo.

refactor

c5902451905acdaa478f8476844ccd4a510d9ca9 · Keenan Nemetz · 16d ago · parent ec2aba4b

internal/hook/hook.go → server/hook/hook.go +51−60

@@ -11,17 +11,15 @@ import (
11 11 "strings"
12 12 "time"
13 13
14 "github.com/rodent-software/vole/internal/access"
15 "github.com/rodent-software/vole/internal/acl"
16 "github.com/rodent-software/vole/internal/gitx"
17 "github.com/rodent-software/vole/internal/identity"
18 "github.com/rodent-software/vole/internal/issues"
19 "github.com/rodent-software/vole/internal/lfs"
20 "github.com/rodent-software/vole/internal/locks"
21 "github.com/rodent-software/vole/internal/repo"
22 "github.com/rodent-software/vole/internal/requests"
23 "github.com/rodent-software/vole/internal/runs"
24 "github.com/rodent-software/vole/internal/workflow"
14 "github.com/rodent-software/vole/ci"
15 "github.com/rodent-software/vole/ci/runs"
16 "github.com/rodent-software/vole/core/git"
17 "github.com/rodent-software/vole/core/lfs"
18 "github.com/rodent-software/vole/forge/access"
19 "github.com/rodent-software/vole/forge/identity"
20 "github.com/rodent-software/vole/forge/repo"
21 "github.com/rodent-software/vole/internal/format"
22 "github.com/rodent-software/vole/metadata"
25 23 )
26 24
27 25 type Config struct {
@@ -69,7 +67,7 @@ func PreReceive(cfg Config) int {
69 67 // assume a directory is a repository.
70 68 var (
71 69 acls *access.Store
72 lockTable *locks.Store
70 lockTable *lfs.LockStore
73 71 err error
74 72 )
75 73 if cfg.UsersDB != "" {
@@ -81,7 +79,7 @@ func PreReceive(cfg Config) int {
81 79 defer acls.Close()
82 80 // The lock table shares that database, so the push path learns what is held
83 81 // without a second endpoint and without the client's cooperation.
84 lockTable, err = locks.Open(cfg.UsersDB)
82 lockTable, err = lfs.OpenLocks(cfg.UsersDB)
85 83 if err != nil {
86 84 reject(cfg.Out, "cannot open the lock table: %v", err)
87 85 return 1
@@ -123,7 +121,7 @@ func PreReceive(cfg Config) int {
123 121 who := func(accountID string) string { return names(accountID) }
124 122
125 123 role := a.RoleFor(actorID)
126 if !role.AtLeast(acl.Write) {
124 if !role.AtLeast(access.Write) {
127 125 reject(cfg.Out, "%s does not have write access to %s (role: %s)", actor, m.Name, role)
128 126 return 1
129 127 }
@@ -137,7 +135,7 @@ func PreReceive(cfg Config) int {
137 135 return 0
138 136 }
139 137
140 g := gitx.Open(cfg.RepoDir)
138 g := git.Open(cfg.RepoDir)
141 139 // receive-pack reveals the pushed objects through a quarantine: it points
142 140 // GIT_OBJECT_DIRECTORY at the temporary object directory and lists the real
143 141 // one in the alternates. gitx scrubs those variables so commands against other
@@ -163,7 +161,7 @@ func PreReceive(cfg Config) int {
163 161 // The metadata branch is append-only in shape: linear history, no merge
164 162 // commits. A merge node in metadata says nothing and makes the log harder
165 163 // to read than the change it describes.
166 if c.ref == issues.Ref {
164 if c.ref == metadata.Ref {
167 165 if merges, err := g.CountMerges(c.old, c.new); err != nil {
168 166 problems = append(problems, fmt.Sprintf("%s: cannot check history shape: %v", c.ref, err))
169 167 } else if merges > 0 {
@@ -174,36 +172,36 @@ func PreReceive(cfg Config) int {
174 172
175 173 // A rewrite of the metadata branch is allowed, because discussion content
176 174 // sometimes has to be redacted, but only by an admin and never silently.
177 if c.old != "" && c.old != gitx.ZeroSHA && c.new != gitx.ZeroSHA {
175 if c.old != "" && c.old != git.ZeroSHA && c.new != git.ZeroSHA {
178 176 ff, err := g.IsAncestor(c.old, c.new)
179 177 if err != nil {
180 178 problems = append(problems, fmt.Sprintf("%s: cannot check fast-forward: %v", c.ref, err))
181 179 } else if !ff {
182 if !role.AtLeast(acl.Admin) {
180 if !role.AtLeast(access.Admin) {
183 181 problems = append(problems, fmt.Sprintf(
184 182 "%s: rewriting history requires admin access; %s is %s", c.ref, actor, role))
185 183 } else {
186 184 auditLine(cfg.AuditPath, fmt.Sprintf("force-push %s %s -> %s by %s in %s",
187 c.ref, short(c.old), short(c.new), actor, m.Name))
185 c.ref, format.SHA(c.old), format.SHA(c.new), actor, m.Name))
188 186 }
189 187 }
190 188 }
191 189
192 190 // Workflows are executable policy for the CI phase, so the version being
193 191 // pushed is validated here rather than when a runner picks it up.
194 if strings.HasPrefix(c.ref, "refs/heads/") && c.new != "" && c.new != gitx.ZeroSHA {
192 if strings.HasPrefix(c.ref, "refs/heads/") && c.new != "" && c.new != git.ZeroSHA {
195 193 touches, err := touchesWorkflows(g, c)
196 194 if err != nil {
197 problems = append(problems, fmt.Sprintf("%s: cannot check %s: %v", c.ref, workflow.Dir, err))
195 problems = append(problems, fmt.Sprintf("%s: cannot check %s: %v", c.ref, ci.Dir, err))
198 196 } else if touches {
199 problems = append(problems, workflow.ValidateTree(g, c.new)...)
197 problems = append(problems, ci.ValidateTree(g, c.new)...)
200 198 }
201 199 }
202 200
203 201 // Integrity checks run before the protected-branch gate, because an admin's
204 202 // approval bypass is about review, not about whether the repository works:
205 203 // a pointer with no object behind it is broken no matter who pushed it.
206 if cfg.MaxBlobBytes > 0 && c.new != gitx.ZeroSHA {
204 if cfg.MaxBlobBytes > 0 && c.new != git.ZeroSHA {
207 205 overs, err := oversizedBlobs(g, c, cfg.MaxBlobBytes)
208 206 if err != nil {
209 207 problems = append(problems, fmt.Sprintf("%s: cannot check object sizes: %v", c.ref, err))
@@ -213,7 +211,7 @@ func PreReceive(cfg Config) int {
213 211 }
214 212 // LFS objects live outside the pack, so receive-pack cannot notice that a
215 213 // pointer names nothing. This is the only place that can.
216 if cfg.LFS != nil && c.new != gitx.ZeroSHA {
214 if cfg.LFS != nil && c.new != git.ZeroSHA {
217 215 lfsProblems, warnings, err := lfsFindings(g, m.Name, c.new, cfg.LFS, cfg.LFSWarnBytes)
218 216 if err != nil {
219 217 problems = append(problems, fmt.Sprintf("%s: cannot check LFS pointers: %v", c.ref, err))
@@ -235,7 +233,7 @@ func PreReceive(cfg Config) int {
235 233 } else if lockProblems, err := lockedPathFindings(g, c, held, actorID, who); err != nil {
236 234 problems = append(problems, fmt.Sprintf("%s: cannot check locks: %v", c.ref, err))
237 235 } else if len(lockProblems) > 0 {
238 if role.AtLeast(acl.Admin) {
236 if role.AtLeast(access.Admin) {
239 237 for _, p := range lockProblems {
240 238 auditLine(cfg.AuditPath, fmt.Sprintf("lock override %s by %s in %s: %s",
241 239 c.ref, actor, m.Name, p))
@@ -254,7 +252,7 @@ func PreReceive(cfg Config) int {
254 252 // mistyped tag is a real problem and the alternative is a repository
255 253 // nobody can repair.
256 254 if relProblems := releaseFindings(g, c); len(relProblems) > 0 {
257 if role.AtLeast(acl.Admin) {
255 if role.AtLeast(access.Admin) {
258 256 for _, p := range relProblems {
259 257 auditLine(cfg.AuditPath, fmt.Sprintf("release rewrite %s by %s in %s: %s",
260 258 c.ref, actor, m.Name, p))
@@ -268,7 +266,7 @@ func PreReceive(cfg Config) int {
268 266 if a.IsProtected(c.ref) {
269 267 // Admins keep the bypass they always had. Everyone else needs an
270 268 // approved request that names the exact commit they are landing.
271 if role.AtLeast(acl.Admin) {
269 if role.AtLeast(access.Admin) {
272 270 continue
273 271 }
274 272 if a.RequiredApprovals() <= 0 {
@@ -277,14 +275,14 @@ func PreReceive(cfg Config) int {
277 275 c.ref, role))
278 276 continue
279 277 }
280 reqs, err := requests.Load(g, issues.Ref)
278 reqs, err := metadata.LoadRequests(g, metadata.Ref)
281 279 if err != nil {
282 280 problems = append(problems, fmt.Sprintf(
283 281 "%s: cannot read change requests, refusing the push: %v", c.ref, err))
284 282 continue
285 283 }
286 284 branch := strings.TrimPrefix(c.ref, "refs/heads/")
287 m, err := requests.FindApprovedMatch(g, reqs, requests.ApprovalRequirement{
285 m, err := metadata.FindApprovedMatch(g, reqs, metadata.ApprovalRequirement{
288 286 Branch: branch,
289 287 NewTip: c.new,
290 288 OldTip: c.old,
@@ -298,7 +296,7 @@ func PreReceive(cfg Config) int {
298 296 problems = append(problems, fmt.Sprintf(
299 297 "%s is protected: %s needs %d approval(s) on an open change request whose head is exactly %s. "+
300 298 "Add requests/<ulid>/request.md with base=%q and head=%s, then have an approver commit approvals/<their-account-id>.md naming that same head.",
301 c.ref, actor, a.RequiredApprovals(), short(c.new), branch, short(c.new)))
299 c.ref, actor, a.RequiredApprovals(), format.SHA(c.new), branch, format.SHA(c.new)))
302 300 continue
303 301 }
304 302 // Approvals say a person read the change; checks say it works. Both are
@@ -311,12 +309,12 @@ func PreReceive(cfg Config) int {
311 309 }
312 310 if len(gaps) > 0 {
313 311 problems = append(problems, fmt.Sprintf("%s: checks have not passed for %s:\n %s",
314 c.ref, short(m.Request.Head), strings.Join(gaps, "\n ")))
312 c.ref, format.SHA(m.Request.Head), strings.Join(gaps, "\n ")))
315 313 continue
316 314 }
317 315 }
318 316 auditLine(cfg.AuditPath, fmt.Sprintf("merge %s via request %s (head %s) approved by %s",
319 c.ref, m.Request.ID, short(m.Request.Head), strings.Join(m.Approvers, ",")))
317 c.ref, m.Request.ID, format.SHA(m.Request.Head), strings.Join(m.Approvers, ",")))
320 318 }
321 319 // The issues branch is user-authored content, so it is validated at the
322 320 // boundary: bad data never reaches the tree the web reads.
@@ -324,15 +322,15 @@ func PreReceive(cfg Config) int {
324 322 // Run records share the branch but not the authorship: they are written by
325 323 // the runner that ran the job, and the gate's question about them is who was
326 324 // allowed to. See records.go.
327 if c.ref == issues.Ref {
325 if c.ref == metadata.Ref {
328 326 problems = append(problems, recordFindings(g, c, a, actor, actorID,
329 327 m.Name, cfg.UsersDB, cfg.RunsDB, who)...)
330 328 }
331 if c.ref == issues.Ref && c.new != gitx.ZeroSHA {
332 for _, prob := range issues.Validate(g, c.new) {
329 if c.ref == metadata.Ref && c.new != git.ZeroSHA {
330 for _, prob := range metadata.ValidateIssues(g, c.new) {
333 331 problems = append(problems, "issues: "+prob)
334 332 }
335 for _, prob := range requests.Validate(g, c.new) {
333 for _, prob := range metadata.ValidateRequests(g, c.new) {
336 334 problems = append(problems, "requests: "+prob)
337 335 }
338 336 // An approval is a statement about the actor's own review, so the file
@@ -361,7 +359,7 @@ func PreReceive(cfg Config) int {
361 359 return 1
362 360 }
363 361
364 func oversizedBlobs(g *gitx.Repo, c change, max int64) ([]string, error) {
362 func oversizedBlobs(g *git.Repo, c change, max int64) ([]string, error) {
365 363 sizes, err := g.NewBlobSizes(c.old, c.new)
366 364 if err != nil {
367 365 return nil, err
@@ -407,18 +405,18 @@ func readChanges(r io.Reader) ([]change, error) {
407 405 // Every write counts: added, modified, renamed, retyped. Checking only additions
408 406 // is not enough, because a file that already exists can simply be edited to name
409 407 // a different commit.
410 func approvalOwnership(g *gitx.Repo, c change, actor, actorID string, role acl.Role, auditPath string, who func(string) string) []string {
408 func approvalOwnership(g *git.Repo, c change, actor, actorID string, role access.Role, auditPath string, who func(string) string) []string {
411 409 // On the first push the branch has no old commit, so every approval in the
412 410 // tree is new and must be checked. Skipping that would let whoever creates the
413 411 // branch seed approvals in other people's names.
414 if c.old == "" || c.old == gitx.ZeroSHA {
412 if c.old == "" || c.old == git.ZeroSHA {
415 413 blobs, err := g.LsTreeR(c.new)
416 414 if err != nil {
417 415 return []string{fmt.Sprintf("requests: cannot verify approvals in this push: %v", err)}
418 416 }
419 417 var problems []string
420 418 for _, b := range blobs {
421 if owner := requests.ApprovalPath(b.Name); owner != "" && owner != actorID {
419 if owner := metadata.ApprovalPath(b.Name); owner != "" && owner != actorID {
422 420 problems = append(problems, fmt.Sprintf(
423 421 "requests: %s: %s may not create an approval for %s", b.Name, actor, who(owner)))
424 422 }
@@ -432,7 +430,7 @@ func approvalOwnership(g *gitx.Repo, c change, actor, actorID string, role acl.R
432 430 }
433 431 var problems []string
434 432 for path, st := range status {
435 owner := requests.ApprovalPath(path)
433 owner := metadata.ApprovalPath(path)
436 434 if owner == "" || owner == actorID {
437 435 continue
438 436 }
@@ -441,7 +439,7 @@ func approvalOwnership(g *gitx.Repo, c change, actor, actorID string, role acl.R
441 439 // statement. The owner may withdraw; an admin may clean up, because the
442 440 // alternative (force-push) is heavier for a one-line fix. Neither is
443 441 // silent: the admin path is audited.
444 if !role.AtLeast(acl.Admin) {
442 if !role.AtLeast(access.Admin) {
445 443 problems = append(problems, fmt.Sprintf(
446 444 "requests: %s: %s may not delete %s's approval; only %s can withdraw it",
447 445 path, actor, who(owner), who(owner)))
@@ -476,8 +474,8 @@ func approvalOwnership(g *gitx.Repo, c change, actor, actorID string, role acl.R
476 474 // - A request's own file is not covered either, for the same reason: its state,
477 475 // head and base are shared, and updating them is the author's normal edit.
478 476 // - A first push has nothing to compare against, so it is skipped.
479 func commentOwnership(g *gitx.Repo, c change, actor, actorID string, role acl.Role, auditPath, usersDB string, out io.Writer) []string {
480 if c.old == "" || c.old == gitx.ZeroSHA {
477 func commentOwnership(g *git.Repo, c change, actor, actorID string, role access.Role, auditPath, usersDB string, out io.Writer) []string {
478 if c.old == "" || c.old == git.ZeroSHA {
481 479 return nil
482 480 }
483 481 status, err := g.DiffStatus(c.old, c.new)
@@ -507,7 +505,7 @@ func commentOwnership(g *gitx.Repo, c change, actor, actorID string, role acl.Ro
507 505 for path, st := range status {
508 506 // An issue comment and a review comment on a change request are the same
509 507 // file shape, so one rule covers both.
510 if !issues.IsCommentPath(path) && !requests.IsCommentPath(path) {
508 if !metadata.IsIssueCommentPath(path) && !metadata.IsRequestCommentPath(path) {
511 509 continue
512 510 }
513 511 // A rename has no blob at the destination, so its author cannot be
@@ -521,7 +519,7 @@ func commentOwnership(g *gitx.Repo, c change, actor, actorID string, role acl.Ro
521 519 "issues: %s: cannot establish whose comment this is, refusing the push: %v", path, err))
522 520 continue
523 521 }
524 author, err := issues.AuthorOf(raw)
522 author, err := metadata.AuthorOf(raw)
525 523 if err != nil {
526 524 problems = append(problems, fmt.Sprintf(
527 525 "issues: %s: cannot establish whose comment this is, refusing the push: %v", path, err))
@@ -537,7 +535,7 @@ func commentOwnership(g *gitx.Repo, c change, actor, actorID string, role acl.Ro
537 535 // An admin may repair a thread — a comment by somebody who has left
538 536 // cannot be fixed by them — but never silently: the override is audited,
539 537 // like a force-push and a release rewrite.
540 if role.AtLeast(acl.Admin) {
538 if role.AtLeast(access.Admin) {
541 539 auditLine(auditPath, fmt.Sprintf("comment-%s %s (author %s) by admin %s", st, path, author, actor))
542 540 reject(out, "admin override, audited: %s %s %s's comment", actor, verb, author)
543 541 continue
@@ -573,14 +571,14 @@ func ownsAuthor(roster *identity.Store, author, actor, actorID string) bool {
573 571
574 572 // touchesWorkflows reports whether this push changes anything under the workflow
575 573 // directory, so repositories without CI pay one diff and no more.
576 func touchesWorkflows(g *gitx.Repo, c change) (bool, error) {
577 if c.old == "" || c.old == gitx.ZeroSHA {
574 func touchesWorkflows(g *git.Repo, c change) (bool, error) {
575 if c.old == "" || c.old == git.ZeroSHA {
578 576 blobs, err := g.LsTreeR(c.new)
579 577 if err != nil {
580 578 return false, err
581 579 }
582 580 for _, b := range blobs {
583 if strings.HasPrefix(b.Name, workflow.Dir+"/") {
581 if strings.HasPrefix(b.Name, ci.Dir+"/") {
584 582 return true, nil
585 583 }
586 584 }
@@ -591,7 +589,7 @@ func touchesWorkflows(g *gitx.Repo, c change) (bool, error) {
591 589 return false, err
592 590 }
593 591 for p := range status {
594 if strings.HasPrefix(p, workflow.Dir+"/") {
592 if strings.HasPrefix(p, ci.Dir+"/") {
595 593 return true, nil
596 594 }
597 595 }
@@ -637,13 +635,6 @@ func auditLine(path, line string) {
637 635 f.Close()
638 636 }
639 637
640 func short(sha string) string {
641 if len(sha) > 8 {
642 return sha[:8]
643 }
644 return sha
645 }
646
647 638 func reject(w io.Writer, format string, args ...any) {
648 639 fmt.Fprintf(w, "vole: "+format+"\n", args...)
649 640 }
@@ -708,8 +699,8 @@ func PostReceive(cfg Config) int {
708 699 // shortOrNone renders a sha for the log, naming the two states that are not a commit:
709 700 // a ref being created and one being deleted.
710 701 func shortOrNone(sha string) string {
711 if sha == "" || sha == gitx.ZeroSHA {
702 if sha == "" || sha == git.ZeroSHA {
712 703 return "none"
713 704 }
714 return short(sha)
705 return format.SHA(sha)
715 706 }