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/vole.git

Write over SSH with a registered key:

git clone ssh://git@rodent.software:2222/vole.git

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

Test the merge result before landing it

e5478085e5b53a2892d0649d6f07c6f0a5653378 · Keenan Nemetz · 11d ago · parent db5e4382

Test the merge result before landing it

A protected branch could receive a non-fast-forward landing whose tree was
never built: the gate asked for checks on the request head, and the merge
commit was made at landing time. A pushed merge with the right parents but
extra content was accepted too.

Publish the merge commit under refs/vole/candidates/, test it as the base
branch, and make the gate require its results. Approvals name the head and
the base branch, so a moved base retests without a new approval. A merger
who clicks while that run is still going waits for it instead of clicking
again. A pushed merge commit now has to carry exactly the tree a clean merge
would produce.

Closes: 01M61QQ56TESQ1HTMB1Z5C3733

metadata/requests.go +59−1

@@ -31,6 +31,7 @@ import (
31 31
32 32 "github.com/rodent-software/vole/core/git"
33 33 voleid "github.com/rodent-software/vole/core/id"
34 "github.com/rodent-software/vole/internal/format"
34 35 )
35 36
36 37 const (
@@ -481,6 +482,33 @@ type Merge struct {
481 482 Approvers []string
482 483 }
483 484
485 // TreeMismatchError is a pushed merge commit whose parents name a request but whose
486 // tree carries something else. It is separate from "no match" because the refusal
487 // has to say what is wrong: the commit has the shape of a landing without being one.
488 type TreeMismatchError struct {
489 Request *Request
490 NewTip string
491 Expected string
492 Actual string
493 Conflicts []string
494 }
495
496 func (e *TreeMismatchError) Error() string {
497 if len(e.Conflicts) > 0 {
498 return fmt.Sprintf(
499 "the merge commit %s resolves conflicts between %s and change request %s (head %s) that "+
500 "nobody reviewed. Push the branch, resolve the conflicts there, and get the new head "+
501 "approved and tested",
502 format.SHA(e.NewTip), e.Request.Base, e.Request.ID, format.SHA(e.Request.Head))
503 }
504 return fmt.Sprintf(
505 "the merge commit %s does not carry the result of merging %s into this branch: "+
506 "its tree is %s, but the merge of change request %s (head %s) is %s. "+
507 "A merge may not add content nobody approved; push the branch and merge it with Vole instead",
508 format.SHA(e.NewTip), e.Request.Base, format.SHA(e.Actual), e.Request.ID,
509 format.SHA(e.Request.Head), format.SHA(e.Expected))
510 }
511
484 512 // FindApprovedMatch reports whether a push to a protected branch is authorised by
485 513 // an approved request.
486 514 //
@@ -506,7 +534,7 @@ func FindApprovedMatch(g *git.Repo, list []*Request, req ApprovalRequirement) (*
506 534 switch {
507 535 case r.Head == req.NewTip:
508 536 matches = true
509 case req.OldTip != "" && req.OldTip != git.ZeroSHA:
537 case req.OldTip != "" && req.OldTip != git.ZeroSHA && g != nil:
510 538 parents, err := g.CommitParents(req.NewTip)
511 539 if err != nil {
512 540 nearest = err
@@ -516,6 +544,36 @@ func FindApprovedMatch(g *git.Repo, list []*Request, req ApprovalRequirement) (*
516 544 matches = (parents[0] == req.OldTip && parents[1] == r.Head) ||
517 545 (parents[1] == req.OldTip && parents[0] == r.Head)
518 546 }
547 if matches {
548 // Parents alone are not enough: "merge, then push" could carry a tree
549 // nobody approved. A landing merge must be exactly the merge of the
550 // reviewed head and the branch tip.
551 expected, conflicts, err := g.MergeTree(req.OldTip, r.Head)
552 if err != nil {
553 nearest = err
554 continue
555 }
556 if len(conflicts) > 0 {
557 // A conflict has no clean merge to compare against. Refuse it and
558 // let the author push a new head rather than accepting a resolution
559 // the server did not review.
560 nearest = &TreeMismatchError{
561 Request: r, NewTip: req.NewTip, Conflicts: conflicts,
562 }
563 continue
564 }
565 actual, err := g.TreeOf(req.NewTip)
566 if err != nil {
567 nearest = err
568 continue
569 }
570 if actual != expected {
571 nearest = &TreeMismatchError{
572 Request: r, NewTip: req.NewTip, Expected: expected, Actual: actual,
573 }
574 continue
575 }
576 }
519 577 }
520 578 if !matches {
521 579 continue