Files
mesh-delivery/modules/mesh-delivery-indexer/internal/index/candidate_values.go
T

133 lines
5.1 KiB
Go

package index
import (
"encoding/json"
"fmt"
"path"
"reflect"
"strings"
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient"
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliveryclient"
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog"
"git.novox.be/novox/mesh-sdk/go/keys"
sdkstep "git.novox.be/novox/mesh-delivery/sdk/buildstep"
)
// The values of the state candidates (novox/hq ADR 0437 §5, ADR 0453 clauses 6 and 16): the contract's types, written by
// the indexer alone, so their key and writing rules are the indexer's (ADR 0634 §1).
// CandidateNoneNoModule is the None2 of a candidate's module whose directory the merge preview names no manifest for: the
// change removes the module. The indexer writes it and the assigner reads it as a removal (issue 1297).
const CandidateNoneNoModule = "the merge preview names no manifest for the module's directory"
// CandidatesKey is the key, in the candidates state, of a group: `<group>`, the branch name the group's pull requests share
// (a slash is allowed), escaped so a branch such as `fix/1.2` keys; a lone pull request is a group of one under its branch.
func CandidatesKey(group string) (string, error) {
if group == "" {
return "", fmt.Errorf("index: the group name is empty")
}
return meshcatalog.CandidatesByGroupKey(keys.EscapeName(group)), nil
}
func checkCandidateModule(m meshcatalog.CandidatesByGroupValuePullsItemFpsValue) error {
switch {
case m.Fp2 != "" && m.None2 != "":
return fmt.Errorf("index: a module has both fp2 and none2")
case m.Fp2 == "" && m.None2 == "":
return fmt.Errorf("index: a module has neither fp2 nor none2")
}
for p, object := range m.Paths {
if p == "" || p != path.Clean(p) || strings.HasPrefix(p, "/") || p == ".." || strings.HasPrefix(p, "../") {
return fmt.Errorf("index: the named path %q is not a clean path inside the module's directory", p)
}
if object == "" {
return fmt.Errorf("index: the named path %q names no git object", p)
}
}
if err := sdkstep.PackageVersions(m.Packages).Check(); err != nil {
return err
}
if m.Fp2 != "" {
if _, err := meshcatalogclient.KeyPart(m.Fp2); err != nil {
return err
}
}
if m.Manifest != "" {
if _, err := meshcatalogclient.ManifestsKey(m.Manifest); err != nil {
return err
}
}
return nil
}
func checkCandidate(c meshcatalog.CandidatesByGroupValue) error {
if len(c.Pulls) == 0 {
return fmt.Errorf("index: a candidate names no pull request")
}
heads := make([]meshdeliveryclient.Head, len(c.Pulls))
for i, p := range c.Pulls {
heads[i] = meshdeliveryclient.Head{Repository: p.Repository, Number: int(p.Number), Head: p.Head}
}
want, _ := meshdeliveryclient.HeadsHash(heads)
if c.Heads != want {
return fmt.Errorf("index: the candidate's heads hash %q is not the hash %q of its pull requests' heads", c.Heads, want)
}
seen := map[string]meshcatalog.CandidatesByGroupValuePullsItemFpsValue{}
for _, p := range c.Pulls {
touched := map[string]bool{}
for _, m := range p.Modules {
touched[m] = true
}
for module, m := range p.Fps {
if !touched[module] {
return fmt.Errorf("index: the candidate gives a fingerprint of the module %q, which pull request %s#%d does not touch", module, p.Repository, p.Number)
}
if err := checkCandidateModule(m); err != nil {
return fmt.Errorf("index: the candidate's module %q: %w", module, err)
}
if before, ok := seen[module]; ok && !reflect.DeepEqual(before, m) {
return fmt.Errorf("index: two pull requests of the candidate give the module %q different fingerprints", module)
}
seen[module] = m
}
}
if wantSet := meshcatalogclient.FingerprintSet(c.Pulls); c.Fpset != wantSet {
return fmt.Errorf("index: the candidate's fpset %q is not the hash %q of its fingerprints", c.Fpset, wantSet)
}
return nil
}
// EncodeCandidate is c as JSON at the kept values' version; it refuses what a reader could not take.
func EncodeCandidate(c meshcatalog.CandidatesByGroupValue) ([]byte, error) {
if err := checkCandidate(c); err != nil {
return nil, err
}
c.V = keys.Version
return json.Marshal(c)
}
// DecodeCandidate reads a candidate; it refuses an unknown version (keys.ErrUnknownVersion) and what EncodeCandidate refuses.
func DecodeCandidate(data []byte) (meshcatalog.CandidatesByGroupValue, error) {
if err := keys.CheckVersion(data); err != nil {
return meshcatalog.CandidatesByGroupValue{}, err
}
var c meshcatalog.CandidatesByGroupValue
if err := json.Unmarshal(data, &c); err != nil {
return meshcatalog.CandidatesByGroupValue{}, fmt.Errorf("index: a candidate does not fit its shape: %w", err)
}
if err := checkCandidate(c); err != nil {
return meshcatalog.CandidatesByGroupValue{}, err
}
return c, nil
}
// SameCandidate says whether two candidates are the same value.
func SameCandidate(a, b meshcatalog.CandidatesByGroupValue) bool { return reflect.DeepEqual(a, b) }
// headOf is a candidate's pull request as the head the heads hash is over.
func headOf(p meshcatalog.CandidatesByGroupValuePullsItem) meshdeliveryclient.Head {
return meshdeliveryclient.Head{Repository: p.Repository, Number: int(p.Number), Head: p.Head}
}