# Conflicts: # modules/mesh-delivery-checker/cmd/mesh-delivery-checker/judge.go # modules/mesh-delivery-checker/manifest_test.go # modules/mesh-delivery-checker/module.json
264 lines
9.0 KiB
Go
264 lines
9.0 KiB
Go
package index
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"maps"
|
|
"reflect"
|
|
"slices"
|
|
"strings"
|
|
|
|
"git.novox.be/novox/mesh-sdk/go/delivery"
|
|
)
|
|
|
|
// The merge previews the checker writes (mesh-delivery-checker_previews; novox/hq ADR 0484 clause 3, issue 1445): a pull
|
|
// request's head merged onto its trunk's head, with the tree, the manifest digest and the lock hashes of each module
|
|
// directory. From one the indexer gives the candidate's touched module its new-form fingerprint, by the rules a module of
|
|
// the trunk is given its own (Recompute), or says why it has none.
|
|
|
|
// The reasons a candidate's touched module has no fingerprint of the new form while a preview exists or is missing.
|
|
const (
|
|
// NonePreviewStale is for a module whose preview was made onto a trunk head that is not the trunk's head now.
|
|
NonePreviewStale = "the merge preview is out of date: the trunk moved since it was made"
|
|
// NonePreviewConflict opens the reason of a module whose pull request's head does not merge into the trunk; the files
|
|
// in conflict follow.
|
|
NonePreviewConflict = "does not merge: the head conflicts with the trunk in "
|
|
// NonePreviewNoModule is for a module whose directory the preview names no manifest for: mesh-sdk's words, which the
|
|
// assigner reads as the change removing the module (issue 1297).
|
|
NonePreviewNoModule = delivery.CandidateNoneNoModule
|
|
// NonePreviewNoManifest is for a module whose manifest at the merge the indexer does not hold.
|
|
NonePreviewNoManifest = "the manifest of the merge is not kept"
|
|
// NoneNoDirectory is for a module whose directory in its repository neither the modules bucket nor the preview tells.
|
|
NoneNoDirectory = "the module's directory in its repository is not known"
|
|
)
|
|
|
|
// PutPreview takes a change of the previews bucket in without writing any candidate: a put of a preview (key
|
|
// delivery.PreviewKey) or its delete. It answers whether the previews held changed. A key or a value that is not a
|
|
// preview is said in the log and left out.
|
|
func (ix *Indexer) PutPreview(key string, value []byte, deleted bool) bool {
|
|
if _, _, _, err := delivery.ParsePreviewKey(key); err != nil {
|
|
ix.logf("previews.%s is not a preview key, left out: %v", key, err)
|
|
return false
|
|
}
|
|
ix.mu.Lock()
|
|
defer ix.mu.Unlock()
|
|
if deleted {
|
|
if _, had := ix.previews[key]; !had {
|
|
return false
|
|
}
|
|
delete(ix.previews, key)
|
|
return true
|
|
}
|
|
pv, err := delivery.DecodePreview(value)
|
|
if err != nil {
|
|
ix.logf("previews.%s is not a preview, left out: %v", key, err)
|
|
return false
|
|
}
|
|
if ix.previews == nil {
|
|
ix.previews = map[string]delivery.Preview{}
|
|
}
|
|
if cur, had := ix.previews[key]; had && delivery.SamePreview(cur, pv) {
|
|
return false
|
|
}
|
|
ix.previews[key] = pv
|
|
return true
|
|
}
|
|
|
|
// ApplyPreview takes one change of the previews in (PutPreview) and, when the previews held changed, writes the
|
|
// candidates' fingerprints again (RecomputeCandidates).
|
|
func (ix *Indexer) ApplyPreview(ctx context.Context, key string, value []byte, deleted bool) error {
|
|
if !ix.PutPreview(key, value, deleted) {
|
|
return nil
|
|
}
|
|
return ix.RecomputeCandidates(ctx)
|
|
}
|
|
|
|
// RecomputeCandidates writes again every candidate whose pull requests' fingerprints, from the previews held, are no longer
|
|
// what it holds. A candidate is written only when it differs, by compare-and-set on the revision read.
|
|
func (ix *Indexer) RecomputeCandidates(ctx context.Context) error {
|
|
if ix.Store == nil {
|
|
return nil
|
|
}
|
|
ix.mu.Lock()
|
|
none := len(ix.previews) == 0
|
|
ix.mu.Unlock()
|
|
if none {
|
|
return nil // no preview held: every candidate has what the pull messages gave it
|
|
}
|
|
keys, err := ix.Store.Keys(ctx)
|
|
if err != nil {
|
|
return fmt.Errorf("listing the candidates: %w", err)
|
|
}
|
|
for _, key := range keys {
|
|
err := write(ctx, ix.Store, key, func(value []byte, found bool) ([]byte, bool, error) {
|
|
if !found {
|
|
return nil, false, nil
|
|
}
|
|
c, err := delivery.DecodeCandidate(value)
|
|
if err != nil {
|
|
ix.logf("candidates.%s holds what is not a candidate, left: %v", key, err)
|
|
return nil, false, nil
|
|
}
|
|
if !ix.withPreviews(ctx, &c) {
|
|
return nil, false, nil
|
|
}
|
|
data, err := delivery.EncodeCandidate(c)
|
|
return data, false, err
|
|
})
|
|
if err != nil {
|
|
return fmt.Errorf("candidates.%s: %w", key, err)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// previewOf is the preview of a head merged onto the trunk head the source names now: found, or stale when only a preview
|
|
// onto another trunk head exists, or neither (found false, stale false) when the source is not known or none exists.
|
|
func (ix *Indexer) previewOf(repository, head string) (pv delivery.Preview, found, stale bool) {
|
|
ix.mu.Lock()
|
|
defer ix.mu.Unlock()
|
|
trunk := ix.source.heads[repository]
|
|
if trunk == "" {
|
|
return delivery.Preview{}, false, false
|
|
}
|
|
if key, err := delivery.PreviewKey(repository, head, trunk); err == nil {
|
|
if pv, ok := ix.previews[key]; ok {
|
|
return pv, true, false
|
|
}
|
|
}
|
|
for _, pv := range ix.previews {
|
|
if pv.Repository == repository && pv.Head == head {
|
|
return delivery.Preview{}, false, true
|
|
}
|
|
}
|
|
return delivery.Preview{}, false, false
|
|
}
|
|
|
|
// withPreviews gives each touched module of the candidate's pull requests the fingerprint its preview gives it, or the
|
|
// reason it has none, and the candidate its fpset. It answers whether the candidate changed. A module of a pull request with
|
|
// no preview keeps what it had.
|
|
func (ix *Indexer) withPreviews(ctx context.Context, c *delivery.Candidate) bool {
|
|
changed := false
|
|
for i := range c.Pulls {
|
|
p := &c.Pulls[i]
|
|
pv, found, stale := ix.previewOf(p.Repository, p.Head.Head)
|
|
if !found && !stale {
|
|
continue
|
|
}
|
|
// A touched module's directory: the one the modules bucket holds of the repository, else the preview's directory
|
|
// whose manifest is the one the pull request gave the module (a module the pull request adds; ADR 0501 clause 6).
|
|
dirs := map[string]string{}
|
|
if held, err := ix.heldModules(ctx); err == nil {
|
|
for name, m := range held {
|
|
if delivery.SameRepository(m.Repository, p.Repository) && m.Path != "" {
|
|
dirs[name] = m.Path
|
|
}
|
|
}
|
|
} else {
|
|
ix.logf("the modules cannot be listed, so no touched module's directory is known: %v", err)
|
|
}
|
|
for _, m := range p.Modules {
|
|
if _, known := dirs[m]; known || p.Fps[m].Manifest == "" {
|
|
continue
|
|
}
|
|
for dir, pm := range pv.Modules {
|
|
if pm.Manifest == p.Fps[m].Manifest {
|
|
dirs[m] = dir
|
|
}
|
|
}
|
|
}
|
|
fps := maps.Clone(p.Fps)
|
|
for _, m := range p.Modules {
|
|
next := delivery.CandidateModule{Manifest: p.Fps[m].Manifest}
|
|
switch {
|
|
case stale:
|
|
next.None2 = NonePreviewStale
|
|
case len(pv.Conflicts) > 0:
|
|
next.None2 = NonePreviewConflict + strings.Join(pv.Conflicts, ", ")
|
|
default:
|
|
dir, known := dirs[m]
|
|
if !known {
|
|
next.None2 = NoneNoDirectory
|
|
break
|
|
}
|
|
next = ix.candidateModule(ctx, m, dir, pv)
|
|
}
|
|
if fps == nil {
|
|
fps = map[string]delivery.CandidateModule{}
|
|
}
|
|
if !reflect.DeepEqual(fps[m], next) {
|
|
fps[m] = next
|
|
changed = true
|
|
}
|
|
}
|
|
if changed || !maps.EqualFunc(fps, p.Fps, func(a, b delivery.CandidateModule) bool { return reflect.DeepEqual(a, b) }) {
|
|
p.Fps = fps
|
|
}
|
|
}
|
|
if changed {
|
|
c.FPSet = delivery.FingerprintSet(c.Pulls)
|
|
}
|
|
return changed
|
|
}
|
|
|
|
// candidateModule is the fingerprint of module m, whose directory is dir, at the preview: the module's inputs from the
|
|
// merge (tree, manifest, locks), its bases' and the builder's fingerprints as the trunk's modules hold them, and the toolchains
|
|
// the build modules agree on, by the rules Recompute applies to a module of the trunk.
|
|
func (ix *Indexer) candidateModule(ctx context.Context, m, dir string, pv delivery.Preview) delivery.CandidateModule {
|
|
pm, ok := pv.Modules[dir]
|
|
if !ok && dir == "" {
|
|
pm, ok = pv.Modules[delivery.RootPath]
|
|
}
|
|
if !ok && dir == delivery.RootPath {
|
|
pm, ok = pv.Modules[""]
|
|
}
|
|
if !ok {
|
|
return delivery.CandidateModule{None2: NonePreviewNoModule}
|
|
}
|
|
out := delivery.CandidateModule{Manifest: pm.Manifest}
|
|
if ix.Manifests == nil {
|
|
out.None2 = NonePreviewNoManifest
|
|
return out
|
|
}
|
|
value, _, found, err := ix.Manifests.Get(ctx, pm.Manifest)
|
|
if err != nil || !found {
|
|
out.None2 = NonePreviewNoManifest
|
|
return out
|
|
}
|
|
kept, err := delivery.DecodeManifest(value)
|
|
if err != nil {
|
|
out.None2 = NonePreviewNoManifest
|
|
return out
|
|
}
|
|
in := inputsOfNamed(m, kept.JSON, pm.Tree, pm.Locks, pm.Paths, ix.baseFP(ctx))
|
|
if in.Kept == nil {
|
|
out.None2 = in.None2
|
|
return out
|
|
}
|
|
if in.None2 != "" && in.None2 != delivery.ToolchainNoReport && in.None2 != None2Pending {
|
|
out.None2 = in.None2 // a context, or a base with no fingerprint
|
|
return out
|
|
}
|
|
tools := ix.agreed()
|
|
toolchains := map[string]delivery.Toolchain2{}
|
|
for _, artifact := range slices.Sorted(maps.Keys(in.Languages)) {
|
|
t := tools[in.Languages[artifact]]
|
|
if t.none != "" {
|
|
out.None2 = t.none
|
|
return out
|
|
}
|
|
toolchains[artifact] = t.tc
|
|
}
|
|
inputs := *in.Kept // no builder input (ADR 0530 §1)
|
|
fp, none := delivery.SourceFingerprint2(inputs.With(m, toolchains))
|
|
if fp == "" {
|
|
out.None2 = none
|
|
return out
|
|
}
|
|
out.FP2 = fp
|
|
if len(inputs.Paths) > 0 {
|
|
out.Paths = inputs.Paths // the builder's fetch for the candidate names them (ADR 0499 clause 4)
|
|
}
|
|
return out
|
|
}
|