package delivery import ( "encoding/json" "fmt" "reflect" "strings" "time" ) // **The merge turns** (novox/hq ADR 0548 clauses 3 and 4). An approved merge holds a turn on each repository it goes into: // before it reads the checker's verdicts for the merge, the gate writes one key per repository in its bucket turns, naming // the deliveries that wait in the order of their Approve, the verdict key each waits for and since when. The gate is the // key's one writer; the checker is granted to read the bucket, reads it absent as empty, and claims no check of a repository // a turn names, except the check of the waiting delivery's own heads (TurnHolds). The key goes when the last delivery in it // merged or left. const ( // TurnsBucket is the gate's bucket of merge turns. TurnsBucket = GateModule + "_turns" // TurnsHistory is the revisions the turns bucket keeps per key. TurnsHistory = 5 ) // TurnsKey is the key, in TurnsBucket, of a repository's turn: `.`, lowercase, each part escaped (EscapeName), // so the key names the repository the checker's verdicts name as a trunk whatever its case. func TurnsKey(repository string) (string, error) { repository = strings.ToLower(repository) if _, err := escapeRepository(repository); err != nil { return "", err } owner, name, _ := strings.Cut(repository, "/") return EscapeName(owner) + "." + EscapeName(name), nil } // ParseTurnsKey is the inverse of TurnsKey: the repository, lowercase. func ParseTurnsKey(key string) (string, error) { owner, name, ok := strings.Cut(key, ".") if !ok || owner == "" || name == "" || strings.Contains(name, ".") { return "", fmt.Errorf("delivery: %q is not a turns key, .", key) } o, err := UnescapeName(owner) if err != nil { return "", fmt.Errorf("delivery: %q is not a turns key: %w", key, err) } n, err := UnescapeName(name) if err != nil { return "", fmt.Errorf("delivery: %q is not a turns key: %w", key, err) } repository := o + "/" + n if again, err := TurnsKey(repository); err != nil || again != key { return "", fmt.Errorf("delivery: %q is not a turns key: it does not name a lowercase /", key) } return repository, nil } // Turn is the value of a turns key: the repository and the approved deliveries that wait to merge into it, in the order of // their Approve. A turn with no delivery is no value: its key is deleted. type Turn struct { V int `json:"v"` Repository string `json:"repository"` Waiting []TurnWait `json:"waiting"` } // TurnWait is one approved delivery in a turn: its group (a lone pull request's branch), the hash of the heads the Approve // covers (the checker's verdict key's hash, HeadsHash over its members), the Approve's ask key, when it was approved and since // when it waits; and, while a check of another delivery runs on the repository, that check's verdict key, when the gate saw // it start, and the step the checker says it is at. type TurnWait struct { Group string `json:"group"` Heads string `json:"heads"` Ask string `json:"ask,omitempty"` Approved time.Time `json:"approved"` Since time.Time `json:"since"` // WaitsFor is the verdict key (`.`) of the running check the merge waits for; empty when none runs. WaitsFor string `json:"waits-for,omitempty"` WaitsSince time.Time `json:"waits-since,omitzero"` Step string `json:"step,omitempty"` } func (t Turn) check() error { repository := strings.ToLower(t.Repository) if repository != t.Repository { return fmt.Errorf("delivery: the turn's repository %q is not lowercase", t.Repository) } if _, err := escapeRepository(repository); err != nil { return err } if len(t.Waiting) == 0 { return fmt.Errorf("delivery: the turn of %s names no delivery: its key is deleted instead", t.Repository) } seen := map[string]bool{} for i, w := range t.Waiting { if w.Group == "" { return fmt.Errorf("delivery: a delivery in the turn of %s names no group", t.Repository) } if !isHex12(w.Heads) { return fmt.Errorf("delivery: the heads hash %q in the turn of %s is not 12 lowercase hex digits", w.Heads, t.Repository) } if seen[w.Heads] { return fmt.Errorf("delivery: the turn of %s names the heads %s twice", t.Repository, w.Heads) } seen[w.Heads] = true if w.Approved.IsZero() || w.Since.IsZero() { return fmt.Errorf("delivery: the delivery %s in the turn of %s says no time of its Approve or of its wait", w.Group, t.Repository) } if i > 0 && w.Approved.Before(t.Waiting[i-1].Approved) { return fmt.Errorf("delivery: the turn of %s is not in the order of the Approve: %s was approved before %s", t.Repository, w.Group, t.Waiting[i-1].Group) } if w.WaitsFor != "" { if _, _, err := ParseCheckerVerdictKey(w.WaitsFor); err != nil { return fmt.Errorf("delivery: the delivery %s in the turn of %s waits for what is not a verdict key: %w", w.Group, t.Repository, err) } } else if !w.WaitsSince.IsZero() || w.Step != "" { return fmt.Errorf("delivery: the delivery %s in the turn of %s says when and where a check runs, and names no check", w.Group, t.Repository) } } return nil } // TurnHolds says whether a turn holds off the check of the heads: it names deliveries, and none of them of these heads. A // turn never holds the check of heads that wait in it (the carry a waiting merge needs): two approved merges in one turn whose // trunk moved would otherwise each hold the other's carry, and neither would ever merge (review of ADR 0548's build, round 1). func TurnHolds(t Turn, heads string) bool { if len(t.Waiting) == 0 { return false } for _, w := range t.Waiting { if w.Heads == heads { return false } } return true } // EncodeTurn is t as JSON at Version; it refuses a turn with no delivery, a repository that is not lowercase /, // a delivery without its group, heads hash or times, the same heads twice, an order other than the Approve's, and a check // waited for that is not a verdict key. func EncodeTurn(t Turn) ([]byte, error) { if err := t.check(); err != nil { return nil, err } t.V = Version return json.Marshal(t) } // DecodeTurn reads a turn; it refuses an unknown version (ErrUnknownVersion) and what EncodeTurn refuses. func DecodeTurn(data []byte) (Turn, error) { var t Turn if err := decode(data, &t); err != nil { return Turn{}, err } if err := t.check(); err != nil { return Turn{}, err } return t, nil } // SameTurn says whether two turns are the same value, so the gate writes only when they differ. func SameTurn(a, b Turn) bool { if len(a.Waiting) != len(b.Waiting) || a.Repository != b.Repository || a.V != b.V { return false } for i := range a.Waiting { x, y := a.Waiting[i], b.Waiting[i] if !x.Approved.Equal(y.Approved) || !x.Since.Equal(y.Since) || !x.WaitsSince.Equal(y.WaitsSince) { return false } x.Approved, y.Approved, x.Since, y.Since, x.WaitsSince, y.WaitsSince = time.Time{}, time.Time{}, time.Time{}, time.Time{}, time.Time{}, time.Time{} if !reflect.DeepEqual(x, y) { return false } } return true }