package check import ( "fmt" "maps" "slices" "sort" "strings" "sync" "time" "git.novox.be/novox/mesh-sdk/go/delivery" ) // Pull is one pull request the checker judges: its head, the trunk head of its repository it is merged onto and the // modules the indexer says it touches, with the fingerprints the indexer wrote for them. type Pull struct { Repository string Number int Head string Trunk string Modules []string Fps map[string]delivery.CandidateModule } // Case is one check: a candidate's pull requests that are judged (those of a repository whose trunk is not known are left // out; hq is judged as any repository since R4g, its checks Go tests: ADR 0527), the trunk head of each repository and the key of the verdict. type Case struct { // Key is the verdict key, `.`. Key string Group string HeadsHash string // Heads is the heads hash the gate matches an Approve's members by (GateHeads): a verdict carries it. Heads string Pulls []Pull // Trunk is the trunk head of each repository of Pulls. Trunk map[string]string // Left says why pull requests of the candidate are not judged (a repository with no known trunk); empty when all are. Left []string // Queued is when the checker took the case up, for the time of its queue. Queued time.Time // Try narrows a try's check (its level, its tests); nil for a pull request's. Try *TryOptions notesMu sync.Mutex notes []string // Progress says what the check is doing, so a check that ran past its bound names the step it was stuck at. Progress *Progress // Kind is what asked the check (KindNew, KindNodes, KindTrunk): a re-judge after a trunk move is asked on the build // seats' recheck stream, every other check on check (ADR 0536 ยง4). Kind string } // Progress is the step a check is at, written by the check and read when its bound passes. type Progress struct { mu sync.Mutex stage, node string } // Set records the step now running and the node it runs on, empty for none. func (p *Progress) Set(stage, node string) { p.mu.Lock() p.stage, p.node = stage, node p.mu.Unlock() } // Now is the step recorded last and its node. func (p *Progress) Now() (stage, node string) { p.mu.Lock() defer p.mu.Unlock() return p.stage, p.node } // CaseOf is the check of a candidate against the trunk heads (the gate's source.main): the pull requests that are judged. // The checker judges pull requests into main only (ADR 0484 clause 11): the pull message carries no base, a merge into // another branch is a closed message, so every candidate is into the trunk; a repository whose trunk head is not known has // none to merge onto. It answers nil when no pull request is left to judge. func CaseOf(c delivery.Candidate, trunk map[string]string) *Case { key, err := delivery.CheckerVerdictKey(c.Group, c.HeadsHash) if err != nil { return nil } cs := &Case{Key: key, Group: c.Group, HeadsHash: c.HeadsHash, Heads: GateHeads(c), Trunk: map[string]string{}, Progress: &Progress{}} for _, p := range c.Pulls { switch head, known := trunk[p.Repository]; { case !known || head == "": cs.Left = append(cs.Left, fmt.Sprintf("%s#%d: the trunk head of %s is not known", p.Repository, p.Number, p.Repository)) default: cs.Pulls = append(cs.Pulls, Pull{Repository: p.Repository, Number: p.Number, Head: p.Head.Head, Trunk: head, Modules: p.Modules, Fps: p.Fps}) cs.Trunk[p.Repository] = head } } if len(cs.Pulls) == 0 { return nil } sort.SliceStable(cs.Pulls, func(i, j int) bool { if cs.Pulls[i].Repository != cs.Pulls[j].Repository { return cs.Pulls[i].Repository < cs.Pulls[j].Repository } return cs.Pulls[i].Number < cs.Pulls[j].Number }) return cs } // GateHeads is the hash the gate matches the members an Approve covers by (mesh-catalog mesh-delivery-gate onestatus.go // headsOf): delivery.HeadsHash over every pull request of the candidate, hq's included, as // `#=`. A lone pull request is a group of one. Empty for a candidate // with no pull request. func GateHeads(c delivery.Candidate) string { heads := make([]delivery.Head, 0, len(c.Pulls)) for _, p := range c.Pulls { heads = append(heads, delivery.Head{Repository: strings.ToLower(p.Repository), Number: p.Number, Head: strings.ToLower(p.Head.Head)}) } hash, err := delivery.HeadsHash(heads) if err != nil { return "" } return hash } // sameTrunk says whether two verdicts' trunk heads are the same. func sameTrunk(a, b map[string]string) bool { return maps.Equal(a, b) } // Trunks holds the trunk head of each repository, as the gate's source.main says (a watch puts it in). type Trunks struct { mu sync.Mutex heads map[string]string } // Put takes a source.main value in and answers whether the heads changed. func (t *Trunks) Put(value []byte, deleted bool) bool { var heads map[string]string if !deleted { s, err := delivery.DecodeSource(value) if err != nil { return false } heads = s.Heads } t.mu.Lock() defer t.mu.Unlock() if maps.Equal(t.heads, heads) { return false } t.heads = maps.Clone(heads) return true } // Heads are the trunk heads now (a copy). func (t *Trunks) Heads() map[string]string { t.mu.Lock() defer t.mu.Unlock() return maps.Clone(t.heads) } // note puts a word of the check's way on the case, for a try's line (issue 1921). func (c *Case) note(s string) { c.notesMu.Lock() defer c.notesMu.Unlock() if !slices.Contains(c.notes, s) { c.notes = append(c.notes, s) } } // Notes are the words noted on the case, in the order they were noted. func (c *Case) Notes() []string { c.notesMu.Lock() defer c.notesMu.Unlock() return slices.Clone(c.notes) }