Files
mesh-catalog/modules/mesh-delivery-gate/cmd/mesh-delivery-gate/group_merge.go
T

185 lines
7.0 KiB
Go

package main
import (
"fmt"
"strings"
"time"
)
// A group is merged whole or not at all, in its order (issue 1275, novox/hq ADR 0418).
//
// - Before the operator is asked, and again before any member is merged, every member's required statuses are read at
// its head on the forge; one that is not a success holds the group, and nothing is merged.
// - The members are merged one after the other in the group's order, and the pass stops at the first refusal: no later
// member is merged.
// - A group held or half merged is said as a stalled line, urgent, until every member is merged or a person decides.
// memberRead is what a status read of one member needs, taken under the lock.
type memberRead struct {
id, owner, repo, base, head string
number int
}
// readOf is a delivery as a status read needs it, at the head the Approve or the ask covers.
func readOf(d *Delivery, head string) memberRead {
if head == "" {
head = d.Commit
}
return memberRead{id: d.ID, owner: d.Owner(), repo: d.Repo(), base: d.Base, head: head, number: d.Number}
}
// notAsSuccess reads a member's required statuses at its head and says, in the forge's words, which are not a success;
// empty when all are. ownOnly is whether only the member's own repository check is missing or pending. An error is a
// read that could not be made: the member then counts as not mergeable.
func (h *Holder) notASuccess(m memberRead) (says string, ownOnly bool, err error) {
fc, err := h.Forge.Statuses(m.owner, m.repo, m.number, m.head, m.base)
if err != nil {
return "", false, err
}
if fc.Merge.Mergeable == nil {
return "", false, fmt.Errorf("the forge could not say whether %s may merge: %s", m.id, fc.Merge.Says)
}
if *fc.Merge.Mergeable {
return "", false, nil
}
var parts []string
ownOnly = len(fc.Merge.Blocking) > 0
for _, b := range fc.Merge.Blocking {
parts = append(parts, fmt.Sprintf("%s (%s)", b.Context, b.State))
if b.Context != ctxRepo || (b.State != "pending" && b.State != "missing") {
ownOnly = false
}
}
return fmt.Sprintf("%s requires %s; not a success: %s", fc.Merge.Branch, strings.Join(fc.Merge.Required, ", "),
strings.Join(parts, ", ")), ownOnly, nil
}
// unmergedLive are a group's members not yet merged and not over, in the group's order. Under the lock.
func (h *Holder) unmergedLive(g *Group) []*Delivery {
var out []*Delivery
for _, d := range h.membersOf(g) {
if d.MergedAs == "" && !d.State.Final() && (d.Ready == nil || d.Ready.Merged == nil) {
out = append(out, d)
}
}
return out
}
// groupMergeable reads every member's required statuses at the head the Approve covers, and says whether all are a success.
// When one is not, or cannot be read, nothing is handed to the bridge: the group is held, said urgent (ADR 0441 §2, §4), and
// the target's turn is given back so the next event that decides whether it may merge asks it again. Outside the lock.
func (h *Holder) groupMergeable(gid, target string, reads []memberRead) bool {
release := func() {
if d := h.deliveries[target]; d != nil && d.Ready != nil {
d.Ready.merging = false
d.Ready.LastTry = time.Time{}
}
}
for _, m := range reads {
says, ownOnly, err := h.notASuccess(m)
if err == nil && says == "" {
continue
}
h.mu.Lock()
if err != nil {
h.Logf("[mesh-delivery] group %s: the required statuses of %s cannot be read, so nothing is merged yet: %v", gid, m.id, err)
release()
h.again("a group's merge whose required statuses could not be read")
h.mu.Unlock()
return false
}
var own func()
if g := h.groups[gid]; g != nil {
if g.Hold == nil || g.Hold.Kind != "blocked" || g.Hold.Says != says || g.Hold.Member != m.id {
h.Logf("[mesh-delivery] group %s is approved and NOT merged: %s at %s: %s", gid, m.id, shortOf(m.head, 12), says)
}
since := time.Time{}
if g.Hold != nil && g.Hold.Kind == "blocked" {
since = g.Hold.Since
}
if since.IsZero() {
since = h.Now()
}
g.Hold = &GroupHold{Kind: "blocked", Member: m.id, Says: says, Since: since}
h.keepGroup(g)
}
if d := h.deliveries[m.id]; ownOnly && d != nil && d.Ready != nil {
own = h.askOwnAfterRefusal(d) // its own repository check is what is missing: asked, once for the head
}
release()
h.again("a group's merge held for a required status")
h.mu.Unlock()
if own != nil {
own()
}
return false
}
h.mu.Lock()
if g := h.groups[gid]; g != nil && g.Hold != nil {
g.Hold = nil
h.keepGroup(g)
}
h.mu.Unlock()
return true
}
// groupLines are the groups that are held or half merged, as urgent stalled lines the controller raises as conditions:
// one for each, on its first member not merged, until every member is merged or a person decides (issue 1275). A member
// the merge tool refused is said by readyLines, with its group; this adds a group held for a required status, and a
// group half merged with no refusal. Under the lock.
func (h *Holder) groupLines(now time.Time) []StalledLine {
var out []StalledLine
for _, g := range h.groups {
live := h.unmergedLive(g)
if len(live) == 0 {
continue
}
first, refused, running, merged := live[0], false, false, 0
for _, d := range live {
if d.Ready != nil && (d.Ready.RefusedSince != nil || d.Ready.Failed != "" || d.Ready.Refused != "") {
refused = true
}
if d.Ready != nil && d.Ready.merging {
running = true
}
}
for _, d := range h.membersOf(g) {
if d.MergedAs != "" || (d.Ready != nil && d.Ready.Merged != nil) {
merged++
}
}
heldOn := h.deliveries[holdMember(g)]
switch {
// A held member the merge tool refused is said by readyLines already: one line for an id, not two.
case g.Hold != nil && g.Hold.Kind == "blocked" && !running && !(heldOn != nil && heldOn.Ready != nil && heldOn.Ready.RefusedSince != nil):
out = append(out, StalledLine{ID: g.Hold.Member, Number: numberOf(h.deliveries[g.Hold.Member]), State: first.State,
For: now.Sub(g.Hold.Since).Round(time.Second).String(), Bound: "0s", Severity: "urgent",
H2: "none: the required status turns success, which asks the merge again — or the operator's decision",
Says: fmt.Sprintf("group %s is approved and nothing is merged (merged %d of %d): %s waits on a required status "+
"at its head: %s. Needed: that status turns success (its check asked again), or the operator decides.",
g.ID, merged, len(h.membersOf(g)), g.Hold.Member, g.Hold.Says)})
case merged > 0 && !refused && !running:
out = append(out, StalledLine{ID: first.ID, Number: first.Number, State: first.State,
For: now.Sub(g.Updated).Round(time.Second).String(), Bound: "0s", Severity: "urgent",
H2: "none: every member merged, or the operator decides",
Says: fmt.Sprintf("group %s is half merged (%d of %d members merged): %s is not merged. Needed: it merges "+
"(its Approve and checks hold at its head), or the operator decides.", g.ID, merged, len(h.membersOf(g)), first.ID)})
}
}
return out
}
func numberOf(d *Delivery) int {
if d == nil {
return 0
}
return d.Number
}
func holdMember(g *Group) string {
if g.Hold == nil {
return ""
}
return g.Hold.Member
}