133 lines
5.1 KiB
Go
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}
|
|
}
|