mesh/merge-gate head 6f7f07635e02 merged into trunk 0f76b6e232a5 — pass: builds mesh-delivery → novox; no bus step; every machine composes with the change …
mesh/repo-check head 6f7f07635e02 merged into trunk 0f76b6e232a5 — pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-…
mesh/delivery delivered
A group member whose walk failed in an outage stayed failed after a later walk of its repository, containing its merge, was done; its group then stopped every later walk of the other repository, and the catalogue could deliver nothing. A failed or stopped delivery now reads covered when a done walk of its repository, made after its merge, whose commit contains it (the forge's word, as issue 362's containment), built every module it moves. A group counts it as delivered, and a member the group stopped for it no longer holds a walk back: the group stops a walk only for a member whose change is still undelivered. A stop covered under issue 480 now likewise makes its group read reached, not stopped. While the forge is asked, the walk is held back rather than stopped, for at most ten minutes from the first ask: a forge that keeps erroring or cannot say then lets the walk be asked to leave the stopped merge out, as before. The hold is said by waits, stalled and stalled walks. The pass reads kept walks each tick, so today's stuck deliveries clear after a restart.
951 lines
41 KiB
Go
951 lines
41 KiB
Go
package main
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"maps"
|
|
"regexp"
|
|
"sort"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// What mesh-delivery owns (novox/hq ADR 0239): the delivery — one commit in one repository — and the
|
|
// delivery group above it. Each is kept whole, one key of the module's own state per delivery and per
|
|
// group, written only by this holder.
|
|
|
|
// Delivery is one commit in one repository, from its pull request's head to every machine.
|
|
type Delivery struct {
|
|
// ID is owner/repository@<twelve characters of its commit>.
|
|
ID string `json:"id"`
|
|
Repository string `json:"repository"`
|
|
Commit string `json:"commit"`
|
|
// The pull request, when there is one: its number, title, base, head branch and page.
|
|
Number int `json:"number,omitempty"`
|
|
Title string `json:"title,omitempty"`
|
|
Base string `json:"base,omitempty"`
|
|
Branch string `json:"branch,omitempty"`
|
|
HTMLURL string `json:"html_url,omitempty"`
|
|
CloneURL string `json:"clone_url,omitempty"`
|
|
// The diffset, as the forge said it: what the planner maps onto modules.
|
|
Paths []string `json:"paths,omitempty"`
|
|
PathsTruncated bool `json:"paths_truncated,omitempty"`
|
|
Removed []string `json:"removed,omitempty"`
|
|
ModuleDirs []string `json:"module_dirs,omitempty"`
|
|
ModuleDirsSaid bool `json:"module_dirs_said,omitempty"`
|
|
// After are the repositories its pull request says it goes after (`after: <repository>` lines).
|
|
After []string `json:"after,omitempty"`
|
|
// Group is the delivery group it is in: its head branch name. Empty for a lone delivery.
|
|
Group string `json:"group,omitempty"`
|
|
|
|
State State `json:"state"`
|
|
Since time.Time `json:"since"`
|
|
Created time.Time `json:"created"`
|
|
|
|
// Plan is its delivery plan, as the controller's planner computed it (the build plan, the deploy plan,
|
|
// the steps that are not an ordinary send).
|
|
Plan *DeliveryPlan `json:"plan,omitempty"`
|
|
// Check is its own check's verdict.
|
|
Check *Verdict `json:"check,omitempty"`
|
|
// CheckAsk is this owner's own ask of its check, when the forge's announcement did not bring one: a head
|
|
// adopted while it already waited, or one whose announcement the controller missed.
|
|
CheckAsk *CheckAsk `json:"check_ask,omitempty"`
|
|
// TrunkNow is its trunk's head as this owner last heard it move while it was open: the commit the last merge
|
|
// into its trunk landed as (novox/hq ADR 0323 §7). A verdict that names another trunk commit is out of date.
|
|
// Empty until a merge into its trunk is heard after it was made.
|
|
TrunkNow string `json:"trunk_now,omitempty"`
|
|
// BaseSHA is the base branch's head the forge last announced its head with (ADR 0323): sent with a check this
|
|
// owner asks, for a verdict no build judges to name its trunk commit.
|
|
BaseSHA string `json:"base_sha,omitempty"`
|
|
// AskedAgain is when its head was announced again while its verdict was out of date — the merge tool's ask of
|
|
// its check against the trunk's head (ADR 0323 §5) — and no verdict has come since: its one-hour bound runs from
|
|
// then. Nil while nothing is asked, and cleared by a verdict or another move of its trunk.
|
|
AskedAgain *time.Time `json:"asked_again,omitempty"`
|
|
|
|
// The facts the table's guards read. NewerHead is the head that superseded it; ClosedUnmerged the pull
|
|
// request closed; MergedAs the commit it landed on the trunk as; HeldWhy what holds it for a person.
|
|
NewerHead string `json:"newer_head,omitempty"`
|
|
ClosedUnmerged bool `json:"closed_unmerged,omitempty"`
|
|
MergedAs string `json:"merged_as,omitempty"`
|
|
MergedAt time.Time `json:"merged_at"`
|
|
HeldWhy string `json:"held_why,omitempty"`
|
|
// FromForge is a delivery made from the forge's word at its merge: its head was never heard by this owner.
|
|
// History is why it is history, once it is (novox/hq issue 313).
|
|
FromForge bool `json:"from_forge,omitempty"`
|
|
History string `json:"history,omitempty"`
|
|
// CoveredBy says which later merges into the same trunk delivered every module it moves, once they did
|
|
// (novox/hq issue 319); Lifted why what held it for a person no longer holds (issue 316).
|
|
CoveredBy string `json:"covered_by,omitempty"`
|
|
Lifted string `json:"lifted,omitempty"`
|
|
// Stranded says why a merge whose walk ended done before its word, without moving every module it moves, is
|
|
// stopped: nothing later delivered them either (novox/hq issue 474). Set once the cover test says no.
|
|
Stranded string `json:"stranded,omitempty"`
|
|
// CoverTold is whether the forge was told that such a stop was covered since: its status, its view and its note
|
|
// (novox/hq issue 480). Kept, so a restart tells it once, and a stop covered before this was built is told too.
|
|
CoverTold bool `json:"cover_told,omitempty"`
|
|
// DeliveredBy says, for a delivery that failed or was stopped, the later finished walk of its repository whose
|
|
// commit contains its merge and which built every module it moves — each it did not build, its own walk said
|
|
// unchanged: its change reached the machines after all (novox/hq issue 747). Its state stays final, as a covered
|
|
// stop's does (issue 480), and it reads covered: its group counts it as delivered.
|
|
DeliveredBy string `json:"delivered_by,omitempty"`
|
|
// StoppedFor is, for a member its group stopped, the member whose end stopped it (novox/hq issue 747): once that
|
|
// member's change reached the machines, the stop has lapsed, and holds no walk of its repository back.
|
|
StoppedFor string `json:"stopped_for,omitempty"`
|
|
// TurnAwaits says, for a group member whose turn waits for a later merge to carry a superseded member before it,
|
|
// which merges and until when; TurnAwaitsUntil is that moment, which bounds the wait (ADR 0276 decision 9,
|
|
// novox/hq issue 445). `show` shows both, and `stalled` says them past that moment.
|
|
TurnAwaits string `json:"turn_awaits,omitempty"`
|
|
// HeldBack is why the walk it waits on is not let go: it carries a merge whose delivery was stopped, and is
|
|
// asked to leave it out (novox/hq ADR 0299). Said by `waits`, and by `stalled` past its bound.
|
|
HeldBack string `json:"held_back,omitempty"`
|
|
TurnAwaitsUntil *time.Time `json:"turn_awaits_until,omitempty"`
|
|
// ContainedIn is the forge's answer, by a later merge commit, to whether that commit contains this one's
|
|
// merge (gitea_contains): asked once per pair, and kept (ADR 0270).
|
|
ContainedIn map[string]bool `json:"contained_in,omitempty"`
|
|
// AnsweredBy is the commit of its repository the walk it follows carries for it: its own merge, or a later merge
|
|
// of its branch that contains it (novox/hq ADR 0276). Unwalked says it is held because no walk answered it.
|
|
AnsweredBy string `json:"answered_by,omitempty"`
|
|
// Carrier says, for a merge a later one carries, which: its delivery and walk, in words.
|
|
Carrier string `json:"carrier,omitempty"`
|
|
Unwalked bool `json:"unwalked,omitempty"`
|
|
// Walk is the walk that delivers it, as last read: its own trunk commit's, or one that carries a later merge
|
|
// of its branch; Steps the machines it reached.
|
|
Walk *WalkSeen `json:"walk,omitempty"`
|
|
Steps []Step `json:"steps,omitempty"`
|
|
// Time is its delivery time, class and phases (novox/hq ADR 0282), once its walk's end is known.
|
|
Time *DeliveryTime `json:"delivery_time,omitempty"`
|
|
// LetGo is when this owner asked the controller to start its walk, and why.
|
|
LetGo time.Time `json:"let_go"`
|
|
LetGoWhy string `json:"let_go_why,omitempty"`
|
|
|
|
// Ready is what this owner did with its pull request's ready mark (novox/hq issue 722, ready.go).
|
|
Ready *ReadyRun `json:"ready,omitempty"`
|
|
|
|
Transitions []Transition `json:"transitions,omitempty"`
|
|
// Owed are the forge's and the bus's effects of its transitions not yet done: said only after the
|
|
// delivery is kept, retried until done.
|
|
Owed []Effect `json:"owed,omitempty"`
|
|
}
|
|
|
|
// Transition is one move of the table, kept.
|
|
type Transition struct {
|
|
At time.Time `json:"at"`
|
|
From State `json:"from"`
|
|
To State `json:"to"`
|
|
Event Event `json:"event"`
|
|
Why string `json:"why"`
|
|
By string `json:"by,omitempty"`
|
|
}
|
|
|
|
// Verdict is a check's verdict: the gate's and the repository's own. ID is the build seat's ask that ran
|
|
// it, On the machine it ran on, At when this owner heard it; Report is the check's own account, as bounded
|
|
// as the controller says it. All of it is what `checks` answers, whole, beside the forge's short statuses.
|
|
type Verdict struct {
|
|
ID string `json:"id,omitempty"`
|
|
On string `json:"on,omitempty"`
|
|
At time.Time `json:"at"`
|
|
Gate string `json:"gate"`
|
|
Summary string `json:"summary"`
|
|
Modules []string `json:"modules,omitempty"`
|
|
Dependents []string `json:"dependents,omitempty"`
|
|
Repo string `json:"repo,omitempty"`
|
|
RepoSaid string `json:"repo_summary,omitempty"`
|
|
Report string `json:"report,omitempty"`
|
|
// Head and Trunk are what it judged (novox/hq ADR 0323 §3): the head merged into the trunk commit, twelve
|
|
// characters each. Trunk is empty for a verdict of a build seat that judged the head alone. Beside are the
|
|
// commits of the other repositories the check read, by directory; Took how long it ran.
|
|
Head string `json:"head,omitempty"`
|
|
Trunk string `json:"trunk,omitempty"`
|
|
Beside map[string]string `json:"beside,omitempty"`
|
|
Took string `json:"took,omitempty"`
|
|
}
|
|
|
|
// Judged is what a verdict judged, in words: "head 1a2b3c4d5e6f merged into trunk 7a8b9c0d1e2f" (ADR 0323 §3);
|
|
// empty for a verdict that names no trunk commit.
|
|
func (v *Verdict) Judged() string {
|
|
if v == nil || v.Trunk == "" {
|
|
return ""
|
|
}
|
|
return fmt.Sprintf("head %s merged into trunk %s", shortOf(v.Head, 12), shortOf(v.Trunk, 12))
|
|
}
|
|
|
|
// outOfDate is why a delivery's verdict no longer counts: it names a trunk commit that is not its trunk's head
|
|
// as this owner heard it (novox/hq ADR 0323 §7). Empty when it counts — or names no trunk commit, as a verdict
|
|
// of a build seat from before the rule does, which counts as it did.
|
|
func (d *Delivery) outOfDate() string {
|
|
if d.Check == nil || d.Check.Trunk == "" || d.TrunkNow == "" || sameCommit(d.Check.Trunk, d.TrunkNow) {
|
|
return ""
|
|
}
|
|
return outOfDateWords(d.Check.Trunk, d.TrunkNow)
|
|
}
|
|
|
|
// outOfDateWords is a verdict out of date, as the forge's holder says it on the pull request (ADR 0323 §4).
|
|
func outOfDateWords(judged, now string) string {
|
|
return fmt.Sprintf("out of date: judged against trunk %s, the trunk is now %s; judged again on a recheck or when "+
|
|
"it is merged", shortOf(judged, 12), shortOf(now, 12))
|
|
}
|
|
|
|
// CheckAsk is a head's own check, asked of the controller by this owner (or re-asked by healer H2).
|
|
type CheckAsk struct {
|
|
// ID is the ask's id as the controller answered it.
|
|
ID string `json:"id"`
|
|
At time.Time `json:"at"`
|
|
By string `json:"by"`
|
|
Why string `json:"why"`
|
|
// Reasked is how many times H2 asked it again: once at most, then the operator's.
|
|
Reasked int `json:"reasked,omitempty"`
|
|
}
|
|
|
|
// Passes is whether the verdict lets the delivery be ready: the gate passed or warned, and the
|
|
// repository's own check did not fail or error.
|
|
func (v *Verdict) Passes() bool {
|
|
if v == nil {
|
|
return false
|
|
}
|
|
gate := v.Gate == "pass" || v.Gate == "warning"
|
|
repo := v.Repo == "" || v.Repo == "pass" || v.Repo == "warning"
|
|
return gate && repo
|
|
}
|
|
|
|
// DeliveryPlan is a delivery plan as the controller's planner says it (mesh-controller link.ChangePlan).
|
|
type DeliveryPlan struct {
|
|
Repository string `json:"repository"`
|
|
Base string `json:"base"`
|
|
Head string `json:"head"`
|
|
Moved []string `json:"moved,omitempty"`
|
|
Dependents []string `json:"dependents,omitempty"`
|
|
New []string `json:"new,omitempty"`
|
|
Unread []string `json:"unread,omitempty"`
|
|
Tiers [][]string `json:"tiers,omitempty"`
|
|
Machines []MachinePlan `json:"machines,omitempty"`
|
|
Steps []string `json:"steps,omitempty"`
|
|
Summary string `json:"summary"`
|
|
// Class is the class of the slowest-budgeted module it moves, as the planner says it: core, leaf, or none when
|
|
// it moves no module (novox/hq ADR 0313 §2). Empty in a plan from a controller that did not say it.
|
|
Class string `json:"class,omitempty"`
|
|
// Trunk is the trunk commit the head was merged into when its verdict judged it (novox/hq ADR 0323 §3).
|
|
Trunk string `json:"trunk,omitempty"`
|
|
}
|
|
|
|
// receiving are the machines the plan sends a module to, in the plan's order; none when the plan names no
|
|
// machines, as a plan kept before they were said may not.
|
|
func (p *DeliveryPlan) receiving(module string) []string {
|
|
if p == nil {
|
|
return nil
|
|
}
|
|
var out []string
|
|
for _, m := range p.Machines {
|
|
if containsString(m.Receives, module) {
|
|
out = append(out, m.Machine)
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
// MachinePlan is one machine's part of a plan.
|
|
type MachinePlan struct {
|
|
Machine string `json:"machine"`
|
|
Receives []string `json:"receives,omitempty"`
|
|
Waits []string `json:"waits,omitempty"`
|
|
}
|
|
|
|
// waitsForItsOwnWalk is whether it is published again after the walk that carried a later commit for it failed.
|
|
func (d *Delivery) waitsForItsOwnWalk() bool {
|
|
return d.State == Published && d.Walk != nil && d.Walk.State == walkFailed && d.carriedLater()
|
|
}
|
|
|
|
// carriedLater is whether the walk it follows carries a later merge of its branch for it, not its own.
|
|
func (d *Delivery) carriedLater() bool {
|
|
return d.AnsweredBy != "" && d.MergedAs != "" && !sameCommit(d.AnsweredBy, d.MergedAs)
|
|
}
|
|
|
|
// MovesNothing is whether the delivery's plan moves no module the mesh holds: nothing for a walk to do. A
|
|
// module it adds is not moved: the controller builds and registers it and sends it nowhere, and opens no walk
|
|
// for it (novox/hq issues 300 and 319). A delivery with no plan known is not said to move nothing.
|
|
func (d *Delivery) MovesNothing() bool {
|
|
return d.Plan != nil && len(d.Plan.Moved) == 0
|
|
}
|
|
|
|
// Step is one machine's part of a delivering walk.
|
|
type Step struct {
|
|
Machine string `json:"machine"`
|
|
Module string `json:"module"`
|
|
Build string `json:"build,omitempty"`
|
|
State StepState `json:"state"`
|
|
Since time.Time `json:"since"`
|
|
Why string `json:"why,omitempty"`
|
|
}
|
|
|
|
// Effect is one thing owed outside the state: an event said, a line appended to a commit's note, the view
|
|
// on the pull request refreshed, a status set.
|
|
type Effect struct {
|
|
Kind string `json:"kind"`
|
|
Commit string `json:"commit,omitempty"`
|
|
Line string `json:"line,omitempty"`
|
|
Context string `json:"context,omitempty"`
|
|
State string `json:"state,omitempty"`
|
|
Tries int `json:"tries,omitempty"`
|
|
Last string `json:"last,omitempty"`
|
|
Since time.Time `json:"since"`
|
|
// Next is when a failed effect is tried again: later after every failure, up to retryCap.
|
|
Next *time.Time `json:"next,omitempty"`
|
|
}
|
|
|
|
// The kinds of effect.
|
|
const (
|
|
EffectEmit = "emit"
|
|
EffectNote = "note"
|
|
EffectView = "view"
|
|
EffectStatus = "status"
|
|
)
|
|
|
|
// FromForgesWord is whether the delivery was made from the forge's word at its merge: said by FromForge, or —
|
|
// for one made before FromForge was kept — by its first transition.
|
|
func (d *Delivery) FromForgesWord() bool {
|
|
if d.FromForge {
|
|
return true
|
|
}
|
|
return len(d.Transitions) > 0 && d.Transitions[0].Event == EvAnnounced &&
|
|
strings.Contains(d.Transitions[0].Why, "so it is made from the forge's word")
|
|
}
|
|
|
|
// shownOnItsPullRequest is whether its view and status are kept on its pull request: not for history, whose
|
|
// pull request the forge may no longer hold — or holds under its number another one.
|
|
// coveredStop is whether a delivery that ended stopped or failed was covered since, and reads covered. A stop
|
|
// because its walk ended done before its word without building its modules (novox/hq issue 474) is covered when a
|
|
// later merge into its trunk that contains it was delivered, carrying every module it moves to every machine running
|
|
// it (ADR 0270's cover test, kept as CoveredBy; issue 480). Any stop or failure is covered when a later finished walk
|
|
// of its repository whose commit contains its merge built its modules (DeliveredBy, novox/hq issue 747). Its state
|
|
// stays final, as a covered superseded member stays superseded (issue 442), and it reads covered, by that merge or
|
|
// walk. A person's stop with a cover found only while it was held keeps its error.
|
|
func (d *Delivery) coveredStop() bool {
|
|
switch d.State {
|
|
case Stopped:
|
|
return d.Stranded != "" && d.CoveredBy != "" || d.DeliveredBy != ""
|
|
case Failed:
|
|
return d.DeliveredBy != ""
|
|
}
|
|
return false
|
|
}
|
|
|
|
// coveredStopSaid is how a covered stop or failure reads: on the forge's status, in its view and note, and in `show`.
|
|
func (d *Delivery) coveredStopSaid() string {
|
|
ended := "its stop"
|
|
if d.State == Failed {
|
|
ended = "its failure"
|
|
}
|
|
if d.Stranded != "" && d.CoveredBy != "" {
|
|
return "covered after " + ended + ": a later merge that contains it was delivered: " + d.CoveredBy
|
|
}
|
|
return "covered after " + ended + ": a later walk that contains it was delivered: " + d.DeliveredBy
|
|
}
|
|
|
|
// memberReached is whether a group counts a member as delivered: its change reached the machines, or had nothing to
|
|
// reach them with — delivered, history or covered, a superseded member a later delivered walk carried (issue 442),
|
|
// or a stop or failure covered since (issues 480 and 747).
|
|
func memberReached(d *Delivery) bool {
|
|
return d.State.Reached() || d.State == Superseded && d.CoveredBy != "" || d.coveredStop()
|
|
}
|
|
|
|
// stoppedFor is the member whose end stopped this one in its group: kept since novox/hq issue 747, and read from the
|
|
// stop's reason for a stop made before ("its group <id> stopped: <member> …").
|
|
func (d *Delivery) stoppedFor() string {
|
|
if d.StoppedFor != "" || d.State != Stopped || d.Group == "" {
|
|
return d.StoppedFor
|
|
}
|
|
prefix := "its group " + d.Group + " stopped: "
|
|
for i := len(d.Transitions) - 1; i >= 0; i-- {
|
|
t := d.Transitions[i]
|
|
if t.Event != EvStop {
|
|
continue
|
|
}
|
|
if t.By != byOwner || !strings.HasPrefix(t.Why, prefix) {
|
|
return ""
|
|
}
|
|
member, _, _ := strings.Cut(strings.TrimPrefix(t.Why, prefix), " ")
|
|
return strings.TrimRight(member, ",;:")
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// stopLapsed is whether a member its group stopped for another member is stopped for nothing any more: that member's
|
|
// change reached the machines since — delivered, or its failure covered by a later walk (novox/hq issue 747). Its own
|
|
// change is still to deliver, by a later walk of its repository that contains it; its stop holds none back. find
|
|
// gives a delivery by its id.
|
|
func stopLapsed(d *Delivery, find func(string) *Delivery) bool {
|
|
if d.State != Stopped || d.coveredStop() {
|
|
return false
|
|
}
|
|
by := d.stoppedFor()
|
|
if by == "" || by == d.ID {
|
|
return false
|
|
}
|
|
o := find(by)
|
|
return o != nil && memberReached(o)
|
|
}
|
|
|
|
func (d *Delivery) shownOnItsPullRequest() bool {
|
|
return d.Number > 0 && d.History == "" && d.State != Retired
|
|
}
|
|
|
|
// Owner and Repo split the repository.
|
|
func (d *Delivery) Owner() string { o, _, _ := strings.Cut(d.Repository, "/"); return o }
|
|
func (d *Delivery) Repo() string { _, r, _ := strings.Cut(d.Repository, "/"); return r }
|
|
|
|
// IDOf is a delivery's id: the repository and twelve characters of its commit.
|
|
func IDOf(repository, commit string) string {
|
|
return strings.ToLower(repository) + "@" + shortOf(commit, 12)
|
|
}
|
|
|
|
func shortOf(commit string, n int) string {
|
|
if len(commit) > n {
|
|
return commit[:n]
|
|
}
|
|
return commit
|
|
}
|
|
|
|
// sameCommit is whether two commits are one, however either is abbreviated.
|
|
func sameCommit(a, b string) bool {
|
|
if a == "" || b == "" {
|
|
return false
|
|
}
|
|
if len(a) > len(b) {
|
|
a, b = b, a
|
|
}
|
|
return len(a) >= 7 && strings.HasPrefix(strings.ToLower(b), strings.ToLower(a))
|
|
}
|
|
|
|
// afterLines are the `after: <repository>` lines of a pull request's description.
|
|
var afterLines = regexp.MustCompile(`(?im)^\s*after:\s*([A-Za-z0-9_.\-/]+)\s*$`)
|
|
|
|
// AfterIn reads them.
|
|
func AfterIn(body string) []string {
|
|
var out []string
|
|
for _, m := range afterLines.FindAllStringSubmatch(body, -1) {
|
|
out = append(out, strings.TrimSuffix(m[1], ".git"))
|
|
}
|
|
sort.Strings(out)
|
|
return out
|
|
}
|
|
|
|
// The walk: the controller's plan of one trunk commit, as its `plan-moved` and `delivery-walks` say it
|
|
// (mesh-controller inventory.Plan). Read, never written here.
|
|
|
|
// The states of a walk as the controller keeps them.
|
|
const (
|
|
walkBuilding = "building"
|
|
walkRolling = "rolling"
|
|
walkDone = "done"
|
|
walkFailed = "failed"
|
|
walkSuperseded = "superseded"
|
|
)
|
|
|
|
// Walk is a walk whole, as the controller says it.
|
|
type Walk struct {
|
|
ID string `json:"id"`
|
|
Repository string `json:"repository"`
|
|
Branch string `json:"branch,omitempty"`
|
|
Commit string `json:"commit"`
|
|
Created time.Time `json:"created"`
|
|
Updated time.Time `json:"updated"`
|
|
State string `json:"state"`
|
|
Tier int `json:"tier"`
|
|
Tiers [][]string `json:"tiers"`
|
|
Modules map[string]*WalkModule `json:"modules"`
|
|
Note string `json:"note,omitempty"`
|
|
Revision int64 `json:"revision"`
|
|
Release *WalkRelease `json:"release,omitempty"`
|
|
Delivery *WalkDelivery `json:"delivery,omitempty"`
|
|
// Phases is the walk's time from its merge, phase by phase, as the controller works it out (novox/hq ADR
|
|
// 0282): what a delivery's time is kept from. Absent from a controller that does not say it.
|
|
Phases *WalkPhases `json:"phases,omitempty"`
|
|
}
|
|
|
|
// WalkModule is one module of a walk.
|
|
type WalkModule struct {
|
|
State string `json:"state,omitempty"`
|
|
SentAt *time.Time `json:"sent_at,omitempty"`
|
|
First []string `json:"first,omitempty"`
|
|
FirstAt *time.Time `json:"first_at,omitempty"`
|
|
Commit string `json:"commit,omitempty"`
|
|
Why string `json:"why,omitempty"`
|
|
Build string `json:"build,omitempty"`
|
|
Gate *WalkGate `json:"gate,omitempty"`
|
|
GatedBy string `json:"gated_by,omitempty"`
|
|
// Made is what the walk's build of it did (novox/hq ADR 0350 §4): "reused from <check build>", naming the pull
|
|
// request's head, the trunk commit and the digests, or "built: <why>". Empty from a controller before it.
|
|
Made string `json:"made,omitempty"`
|
|
}
|
|
|
|
// WalkGate is a module's judging on its first machine.
|
|
type WalkGate struct {
|
|
Machines []string `json:"machines"`
|
|
From string `json:"from,omitempty"`
|
|
To string `json:"to,omitempty"`
|
|
Since *time.Time `json:"since,omitempty"`
|
|
Passes int `json:"passes,omitempty"`
|
|
Verdict string `json:"verdict,omitempty"`
|
|
Why string `json:"why,omitempty"`
|
|
JudgedAt *time.Time `json:"judged_at,omitempty"`
|
|
Rollback string `json:"rollback,omitempty"`
|
|
Carried []WalkCarry `json:"carried,omitempty"`
|
|
}
|
|
|
|
// WalkCarry is one build a gated send carried.
|
|
type WalkCarry struct {
|
|
Module string `json:"module"`
|
|
Node string `json:"node"`
|
|
From string `json:"from,omitempty"`
|
|
To string `json:"to"`
|
|
Build string `json:"build,omitempty"`
|
|
}
|
|
|
|
// WalkRelease is a backlog walk's way through the machines.
|
|
type WalkRelease struct {
|
|
Order []string `json:"order"`
|
|
Next int `json:"next"`
|
|
Gate *WalkGate `json:"gate,omitempty"`
|
|
Done []string `json:"done,omitempty"`
|
|
By string `json:"by,omitempty"`
|
|
}
|
|
|
|
// WalkDelivery is what a walk waits for, and which merges it answers (novox/hq ADR 0276).
|
|
type WalkDelivery struct {
|
|
Awaits string `json:"awaits"`
|
|
Go *time.Time `json:"go,omitempty"`
|
|
By string `json:"by,omitempty"`
|
|
Why string `json:"why,omitempty"`
|
|
Stopped string `json:"stopped,omitempty"`
|
|
StoppedWhy string `json:"stopped_why,omitempty"`
|
|
// Merges are every merge the walk answers: each merge commit, its repository, and the commit of that
|
|
// repository the walk carries — the merge itself, or a later merge of its branch that contains it. Absent
|
|
// from a controller that does not say it: the walk then answers the merge of its own commit, and a merge
|
|
// the forge says its commit contains.
|
|
Merges []WalkMerge `json:"merges,omitempty"`
|
|
// TakenOverBy is, on a walk folded before it started, the walk that took it over and answers every merge
|
|
// it answered.
|
|
TakenOverBy string `json:"taken_over_by,omitempty"`
|
|
// WalkedAgainBy is, on a walk stopped without some of its merges, the batch that walks its other merges again,
|
|
// and LeftOut the merges it left out (novox/hq ADR 0299): a delivery of a merge left out ends stopped with it,
|
|
// one of a merge walked again follows that batch's walk.
|
|
WalkedAgainBy string `json:"walked_again_by,omitempty"`
|
|
LeftOut []WalkLeftOut `json:"left_out,omitempty"`
|
|
// Batch is set while the record is a batch, not yet a walk: its state `assembling` or `queued`. A batch, once
|
|
// cut, is the walk under the same id: its next record is `building`. A merge this owner reads in a batch is
|
|
// not held as unwalked while the batch is heard of.
|
|
Batch *WalkBatch `json:"batch,omitempty"`
|
|
// Unchanged are the modules the walk did not build because their build source holds none of the files its
|
|
// merges changed, each with why (novox/hq issue 611, ADR 0267 §5): the build the mesh holds is theirs. A delivery
|
|
// plan that read them whole names them; this says they matched what runs, not that the walk left them unbuilt.
|
|
// Absent from a controller that does not say it.
|
|
Unchanged map[string]string `json:"unchanged,omitempty"`
|
|
}
|
|
|
|
// WalkMerge is one merge a walk answers.
|
|
type WalkMerge struct {
|
|
Repository string `json:"repository"`
|
|
Commit string `json:"commit"`
|
|
// Carried is the commit of the repository the walk carries for it: Commit itself, or a later merge of its
|
|
// branch that contains it. Empty means Commit.
|
|
Carried string `json:"carried,omitempty"`
|
|
}
|
|
|
|
// WalkLeftOut is a merge a stop left out (novox/hq ADR 0299): a merge stopped, or a later merge of its
|
|
// repository's branch that contains it, Contains naming the stopped merge.
|
|
type WalkLeftOut struct {
|
|
Repository string `json:"repository"`
|
|
Commit string `json:"commit"`
|
|
Contains string `json:"contains,omitempty"`
|
|
// Shares are the modules it moves that a stopped merge moves too, when that is why it was left out: Contains
|
|
// then names that stopped merge, which it does not contain.
|
|
Shares []string `json:"shares,omitempty"`
|
|
}
|
|
|
|
// leftOut is the merge of a repository a stop left out, when it did.
|
|
func (w Walk) leftOut(repository, merge string) (WalkLeftOut, bool) {
|
|
if w.Delivery == nil || merge == "" {
|
|
return WalkLeftOut{}, false
|
|
}
|
|
for _, o := range w.Delivery.LeftOut {
|
|
if strings.EqualFold(o.Repository, repository) && sameCommit(o.Commit, merge) {
|
|
return o, true
|
|
}
|
|
}
|
|
return WalkLeftOut{}, false
|
|
}
|
|
|
|
// WalkBatch is a batch's window: when it closes unless another merge arrives, when it closes at the latest,
|
|
// and, once closed, the walk it waits behind.
|
|
type WalkBatch struct {
|
|
ClosesAt *time.Time `json:"closes_at,omitempty"`
|
|
AtMost *time.Time `json:"at_most,omitempty"`
|
|
Behind string `json:"behind,omitempty"`
|
|
}
|
|
|
|
// The states a batch's record is in before it is a walk (novox/hq ADR 0276).
|
|
const (
|
|
walkAssembling = "assembling"
|
|
walkQueued = "queued"
|
|
)
|
|
|
|
// IsBatch is whether the record is a batch still to be cut into a walk.
|
|
func (w Walk) IsBatch() bool { return w.State == walkAssembling || w.State == walkQueued }
|
|
|
|
// supersededNote reads the walk that took a superseded walk over from its note, for a controller that does not
|
|
// say TakenOverBy: "superseded at tier N by plan-… (…)".
|
|
var supersededNote = regexp.MustCompile(`^superseded at tier \d+ by (plan-[0-9A-Za-z_-]+)`)
|
|
|
|
// takenOverBy is the walk that took this one over, said or read from its note; empty for none.
|
|
func (w Walk) takenOverBy() string {
|
|
if w.State != walkSuperseded {
|
|
return ""
|
|
}
|
|
if w.Delivery != nil && w.Delivery.TakenOverBy != "" {
|
|
return w.Delivery.TakenOverBy
|
|
}
|
|
if m := supersededNote.FindStringSubmatch(w.Note); m != nil {
|
|
return m[1]
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// carriedFor is the commit the walk carries for a merge of a repository, and whether it answers that merge by
|
|
// what it says: the merge named among its merges, or its own commit.
|
|
func (w Walk) carriedFor(repository, merge string) (string, bool) {
|
|
if w.Delivery != nil {
|
|
for _, m := range w.Delivery.Merges {
|
|
if strings.EqualFold(m.Repository, repository) && sameCommit(m.Commit, merge) {
|
|
if m.Carried != "" {
|
|
return m.Carried, true
|
|
}
|
|
return m.Commit, true
|
|
}
|
|
}
|
|
}
|
|
if strings.EqualFold(w.Repository, repository) && sameCommit(w.Commit, merge) {
|
|
return w.Commit, true
|
|
}
|
|
return "", false
|
|
}
|
|
|
|
// WalkSeen is what a delivery keeps of its walk.
|
|
type WalkSeen struct {
|
|
ID string `json:"id"`
|
|
Commit string `json:"commit"`
|
|
State string `json:"state"`
|
|
Note string `json:"note,omitempty"`
|
|
Revision int64 `json:"revision"`
|
|
Waits bool `json:"waits,omitempty"`
|
|
WaitedFor string `json:"waited_for,omitempty"`
|
|
GoBy string `json:"go_by,omitempty"`
|
|
StoppedBy string `json:"stopped_by,omitempty"`
|
|
// StoppedWhy is why it was stopped; WalkedAgainBy, on a walk stopped without some merges, the batch that walks
|
|
// this delivery's merge again; Contains, when this delivery's merge was left out for containing a stopped
|
|
// merge, that merge (novox/hq ADR 0299).
|
|
StoppedWhy string `json:"stopped_why,omitempty"`
|
|
WalkedAgainBy string `json:"walked_again_by,omitempty"`
|
|
Contains string `json:"contains,omitempty"`
|
|
// Shares are, when this delivery's merge was left out for moving modules a stopped merge moves, those modules:
|
|
// built at the branch, which holds the stopped merge, they would deliver it (ADR 0299).
|
|
Shares []string `json:"shares,omitempty"`
|
|
Tier int `json:"tier"`
|
|
Tiers int `json:"tiers"`
|
|
Updated time.Time `json:"updated"`
|
|
// Branch is the branch the walk's commit was merged into; empty in a record kept before it was read.
|
|
Branch string `json:"branch,omitempty"`
|
|
// Created is when the controller made the walk: a delivery follows the newest walk that answers it.
|
|
Created time.Time `json:"created"`
|
|
// TakenOverBy is the walk that took a superseded one over (novox/hq ADR 0276): its deliveries follow it.
|
|
TakenOverBy string `json:"taken_over_by,omitempty"`
|
|
// Moved are the modules the walk sent, as its record names them; MovedSaid says they were read, which a
|
|
// record kept before novox/hq issue 474 did not. What a walk that ended before its word delivered is read
|
|
// from them.
|
|
Moved []string `json:"moved,omitempty"`
|
|
MovedSaid bool `json:"moved_said,omitempty"`
|
|
// Unchanged are the modules the walk said matched what runs, with why (novox/hq issue 611): not built, and
|
|
// nothing for it to deliver.
|
|
Unchanged map[string]string `json:"unchanged,omitempty"`
|
|
// Built is, per module, what the walk's build of it did (novox/hq ADR 0350 §4): "reused from <check build>" or
|
|
// "built: <why>" — what `show` says beside the build phase.
|
|
Built map[string]string `json:"built,omitempty"`
|
|
}
|
|
|
|
// Seen is a walk as a delivery keeps it.
|
|
func (w Walk) Seen() *WalkSeen {
|
|
s := &WalkSeen{ID: w.ID, Commit: w.Commit, State: w.State, Note: w.Note, Revision: w.Revision, Tier: w.Tier,
|
|
Tiers: len(w.Tiers), Updated: w.Updated}
|
|
if d := w.Delivery; d != nil {
|
|
s.WaitedFor = d.Awaits
|
|
s.Waits = d.Awaits != "" && d.Go == nil
|
|
s.GoBy = d.By
|
|
s.StoppedBy, s.StoppedWhy = d.Stopped, d.StoppedWhy
|
|
if len(d.Unchanged) > 0 {
|
|
s.Unchanged = maps.Clone(d.Unchanged)
|
|
}
|
|
}
|
|
s.Branch = w.Branch
|
|
s.Created = w.Created
|
|
s.TakenOverBy = w.takenOverBy()
|
|
// Only a module sent, or passed on its first machine, was built for the machines: one the record names with
|
|
// why it was not sent, or a walk that never started, built nothing (review of novox/hq issue 474).
|
|
for name, m := range w.Modules {
|
|
if m != nil && (m.SentAt != nil || m.Gate != nil && m.Gate.Verdict == "passed") {
|
|
s.Moved = append(s.Moved, name)
|
|
}
|
|
if m != nil && m.Made != "" {
|
|
if s.Built == nil {
|
|
s.Built = map[string]string{}
|
|
}
|
|
s.Built[name] = m.Made
|
|
}
|
|
}
|
|
sort.Strings(s.Moved)
|
|
s.MovedSaid = true
|
|
return s
|
|
}
|
|
|
|
// endedUnstarted is whether a published delivery's walk ended done without ever being let go: a batch's walk the
|
|
// controller judged to move nothing it holds is done when it is cut (novox/hq issue 474). No start can come for it,
|
|
// so its record settles the delivery.
|
|
func (d *Delivery) endedUnstarted() bool {
|
|
return d.State == Published && d.Walk != nil && d.Walk.State == walkDone && !d.Walk.Started()
|
|
}
|
|
|
|
// walkStopped is whether the walk it follows was stopped through this owner, and walkedAgain whether its merge is
|
|
// walked again by another walk since (novox/hq ADR 0299).
|
|
func (d *Delivery) walkStopped() bool {
|
|
return d.Walk != nil && d.Walk.State == walkFailed && d.Walk.StoppedBy != ""
|
|
}
|
|
|
|
func (d *Delivery) walkedAgain() bool { return d.walkStopped() && d.Walk.WalkedAgainBy != "" }
|
|
|
|
// stoppedWords is why a delivery ends with the walk it follows stopped: the stop, and the stopped merge its own
|
|
// contains when it was left out for that.
|
|
func (d *Delivery) stoppedWords() string {
|
|
s := fmt.Sprintf("its walk %s was stopped by %s", d.Walk.ID, d.Walk.StoppedBy)
|
|
if d.Walk.StoppedWhy != "" {
|
|
s += ": " + d.Walk.StoppedWhy
|
|
}
|
|
switch {
|
|
case len(d.Walk.Shares) > 0:
|
|
s += fmt.Sprintf("; its merge moves %s, as the stopped merge %s does: built at the branch, which holds that "+
|
|
"merge, it would deliver it, so it was left out with it (novox/hq ADR 0299)", strings.Join(d.Walk.Shares, ", "),
|
|
shortOf(d.Walk.Contains, 12))
|
|
case d.Walk.Contains != "":
|
|
s += fmt.Sprintf("; its merge contains the stopped merge %s, so it cannot be walked without delivering it, "+
|
|
"and was left out with it (novox/hq ADR 0299)", shortOf(d.Walk.Contains, 12))
|
|
}
|
|
return s
|
|
}
|
|
|
|
// walkMovedItsModules is whether the walk it follows answered every module its plan moves, and whether that is
|
|
// known: each built, or said unchanged — its build source holds none of the files the walk's merges changed, so the
|
|
// build the mesh holds is its build (novox/hq issue 611) — from the modules the walk's record names, or, for a
|
|
// record kept before they were, from a walk of no tier, which moved nothing. Unknown when its plan is not known.
|
|
func (d *Delivery) walkMovedItsModules() (moved, known bool) {
|
|
if d.Walk == nil || d.Plan == nil {
|
|
return false, false
|
|
}
|
|
if !d.Walk.MovedSaid && d.Walk.Tiers > 0 {
|
|
return false, false
|
|
}
|
|
return len(d.notMovedBy()) == 0, true
|
|
}
|
|
|
|
// notMovedBy is the modules its plan moves that the walk it follows neither built nor said unchanged: what a walk
|
|
// that ended without building them left undelivered.
|
|
func (d *Delivery) notMovedBy() []string {
|
|
var out []string
|
|
if d.Walk == nil || d.Plan == nil {
|
|
return nil
|
|
}
|
|
for _, m := range d.Plan.Moved {
|
|
if _, unchanged := d.Walk.Unchanged[m]; !containsString(d.Walk.Moved, m) && !unchanged {
|
|
out = append(out, m)
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
// unchangedOfIts is the modules its plan moves that the walk it follows said matched what runs, sorted (novox/hq
|
|
// issue 611).
|
|
func (d *Delivery) unchangedOfIts() []string {
|
|
var out []string
|
|
if d.Walk == nil || d.Plan == nil {
|
|
return nil
|
|
}
|
|
for _, m := range d.Plan.Moved {
|
|
if _, unchanged := d.Walk.Unchanged[m]; unchanged && !containsString(d.Walk.Moved, m) {
|
|
out = append(out, m)
|
|
}
|
|
}
|
|
sort.Strings(out)
|
|
return out
|
|
}
|
|
|
|
// builtNoneOfIts is whether the walk it follows built none of the modules its plan moves and said each unchanged:
|
|
// its merge moves nothing, whichever commit the walk carried (ADR 0270 §1).
|
|
func (d *Delivery) builtNoneOfIts() bool {
|
|
return d.Plan != nil && len(d.Plan.Moved) > 0 && len(d.unchangedOfIts()) == len(d.Plan.Moved)
|
|
}
|
|
|
|
// Started is whether the walk is past its wait: it waited for nobody, or was let go — whatever came of it
|
|
// since. A walk superseded or stopped while it waited never started.
|
|
func (s *WalkSeen) Started() bool { return s != nil && !s.Waits && (s.WaitedFor == "" || s.GoBy != "") }
|
|
|
|
// Waited is whether it waited for a word at all.
|
|
func (s *WalkSeen) Waited() bool { return s != nil && s.WaitedFor != "" }
|
|
|
|
// LetGoByAPerson is whether a person's `plans go` started it.
|
|
func (s *WalkSeen) LetGoByAPerson() bool { return s != nil && strings.HasPrefix(s.GoBy, "a person") }
|
|
|
|
// Open is whether the controller still works it.
|
|
func (w Walk) Open() bool { return w.State == walkBuilding || w.State == walkRolling }
|
|
|
|
// The steps that say a module reached every machine running it: sent to all at once, or to the rest after its
|
|
// first machines passed.
|
|
const (
|
|
stepEveryMachine = "every machine running it"
|
|
stepTheRest = "the rest running it"
|
|
)
|
|
|
|
// StepsOf are the machine steps a walk's record says, for one delivery: every module it moved, on its first
|
|
// machines from its gate, and the rest once sent.
|
|
func StepsOf(w Walk) []Step {
|
|
var out []Step
|
|
names := make([]string, 0, len(w.Modules))
|
|
for n := range w.Modules {
|
|
names = append(names, n)
|
|
}
|
|
sort.Strings(names)
|
|
for _, name := range names {
|
|
m := w.Modules[name]
|
|
if m == nil || m.FirstAt == nil {
|
|
if m != nil && m.SentAt != nil && m.FirstAt == nil && m.Why == "" {
|
|
out = append(out, Step{Machine: stepEveryMachine, Module: name, Build: m.Build,
|
|
State: StepSent, Since: *m.SentAt})
|
|
}
|
|
continue
|
|
}
|
|
gate := m.Gate
|
|
state, why, since := StepSent, "", *m.FirstAt
|
|
if gate != nil {
|
|
switch {
|
|
case gate.Rollback == "rolled-back":
|
|
state, why = StepRolledBack, gate.Why
|
|
case gate.Verdict == "failed":
|
|
state, why = StepFailed, gate.Why
|
|
case gate.Verdict == "passed":
|
|
state, why = StepPassed, gate.Why
|
|
case gate.Since != nil:
|
|
state = StepJudging
|
|
why = fmt.Sprintf("%d healthy judging(s) so far", gate.Passes)
|
|
}
|
|
if gate.JudgedAt != nil {
|
|
since = *gate.JudgedAt
|
|
}
|
|
}
|
|
for _, machine := range m.First {
|
|
out = append(out, Step{Machine: machine, Module: name, Build: m.Build, State: state, Since: since, Why: why})
|
|
}
|
|
if m.SentAt != nil && state == StepPassed {
|
|
out = append(out, Step{Machine: stepTheRest, Module: name, Build: m.Build, State: StepSent,
|
|
Since: *m.SentAt})
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
// Group is a delivery group: two or more deliveries sharing a head branch name, one level only.
|
|
type Group struct {
|
|
ID string `json:"id"`
|
|
Members []string `json:"members"`
|
|
Created time.Time `json:"created"`
|
|
Updated time.Time `json:"updated"`
|
|
// Order is the members in the order they are delivered, Pairs why, Cycle the members whose order
|
|
// contradicts itself; OrderOf the member commits the order was worked out for.
|
|
Order []string `json:"order,omitempty"`
|
|
Pairs []Pair `json:"pairs,omitempty"`
|
|
Cycle []string `json:"cycle,omitempty"`
|
|
// Contradictions is each two members whose rules order them both ways at one rank, said with how a
|
|
// person declares the order (novox/hq ADR 0249).
|
|
Contradictions []Contradiction `json:"contradictions,omitempty"`
|
|
OrderOf map[string]string `json:"order_of,omitempty"`
|
|
// Check is its composed check: asked of the controller for these member commits, and its verdict.
|
|
Check *GroupCheck `json:"check,omitempty"`
|
|
// Closed is set once a member reached the trunk: a group delivering takes no new member.
|
|
Closed bool `json:"closed,omitempty"`
|
|
// Said is the derived state last said, so a change of it is said once.
|
|
Said string `json:"said,omitempty"`
|
|
Owed []Effect `json:"owed,omitempty"`
|
|
}
|
|
|
|
// Pair is one "before" among a group's members: the rule that won, and the rules that ordered it the other
|
|
// way and lost by precedence (novox/hq ADR 0249).
|
|
type Pair struct {
|
|
Before string `json:"before"`
|
|
After string `json:"after"`
|
|
Why string `json:"why"`
|
|
Over []string `json:"over,omitempty"`
|
|
}
|
|
|
|
// Contradiction is two members that rules of one rank order both ways, which no precedence resolves: the
|
|
// rules, and Said, which names them and says how to declare the order.
|
|
type Contradiction struct {
|
|
Members []string `json:"members"`
|
|
Rules []Pair `json:"rules"`
|
|
Said string `json:"said"`
|
|
}
|
|
|
|
// orderSaid is a pair as a person reads it: what won, and over what.
|
|
func (p Pair) orderSaid() string {
|
|
s := p.Before + " before " + p.After + ": " + p.Why
|
|
if len(p.Over) > 0 {
|
|
s += ", over " + strings.Join(p.Over, ", ")
|
|
}
|
|
return s
|
|
}
|
|
|
|
// GroupCheck is a group's composed check.
|
|
type GroupCheck struct {
|
|
Asked string `json:"asked"`
|
|
At time.Time `json:"at"`
|
|
Commits map[string]string `json:"commits"`
|
|
Verdict string `json:"verdict,omitempty"`
|
|
Summary string `json:"summary,omitempty"`
|
|
// Trunks are the trunk commit each member's head was merged into, by member id (novox/hq ADR 0323 §6).
|
|
Trunks map[string]string `json:"trunks,omitempty"`
|
|
// OutOfDate is why its verdict no longer counts: a merge that is not one of the group's members moved a
|
|
// member's trunk, at OutOfDateAt. It counts as outstanding (ADR 0239 §18), and runs again once a member is
|
|
// judged again after it.
|
|
OutOfDate string `json:"out_of_date,omitempty"`
|
|
OutOfDateAt time.Time `json:"out_of_date_at"`
|
|
}
|
|
|
|
// Commits is each member's commit, by id.
|
|
func commitsOf(ds []*Delivery) map[string]string {
|
|
out := map[string]string{}
|
|
for _, d := range ds {
|
|
out[d.ID] = d.Commit
|
|
}
|
|
return out
|
|
}
|
|
|
|
// sameMembers is whether two member→commit maps are one.
|
|
func sameMembers(a, b map[string]string) bool {
|
|
if len(a) != len(b) {
|
|
return false
|
|
}
|
|
for k, v := range a {
|
|
if b[k] != v {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
func mustJSON(v any) json.RawMessage {
|
|
raw, _ := json.Marshal(v)
|
|
return raw
|
|
}
|