185 lines
7.0 KiB
Go
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
|
|
}
|