182 lines
6.4 KiB
Go
182 lines
6.4 KiB
Go
package index
|
|
|
|
import (
|
|
"fmt"
|
|
"maps"
|
|
"slices"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"git.novox.be/novox/mesh-sdk/go/delivery"
|
|
)
|
|
|
|
// Builds holds the builder's log (mesh-delivery-builder.build-log, novox/hq ADR 0571 clause 1) for what the indexer reads
|
|
// of past builds: the directory of a module whose modules key has none (PathOf; issue 1614), filled once and changing no
|
|
// fingerprint, and the places a module registered from its own repository was built from (RegistrationsOf). It is filled
|
|
// from the log's entries at the indexer's start (every one the log keeps, 30 days) and then each new one as the builder
|
|
// appends it; no entry is ever taken out, since only age takes one from the log.
|
|
//
|
|
// It lists no module and maps no directory to a module: the indexer's modules are those its own messages and bucket name
|
|
// (ADR 0501 clause 6, issue 1851). Safe for use from a watch and from the message handler at once.
|
|
type Builds struct {
|
|
mu sync.Mutex
|
|
records map[string]delivery.BuildRecord // by the entry's event, module, fingerprint and time
|
|
}
|
|
|
|
// NewBuilds is an empty Builds.
|
|
func NewBuilds() *Builds { return &Builds{records: map[string]delivery.BuildRecord{}} }
|
|
|
|
// Put takes one entry of the builder's log. A build's end is an outcome: done registers, failed or queued does not; its
|
|
// start is a request, which registers nothing either. An entry that does not read, or names no module or repository, is
|
|
// refused and said.
|
|
func (b *Builds) Put(value []byte) error {
|
|
e, err := delivery.DecodeBuildLogEntry(value)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if e.Module == "" || e.Repository == "" {
|
|
return fmt.Errorf("a build log entry naming no module or repository: %s", value)
|
|
}
|
|
r := delivery.BuildRecord{ID: e.At.UTC().Format(time.RFC3339Nano) + "." + e.FP, Repository: e.Repository, Ref: e.Ref, Module: e.Module,
|
|
Path: e.Directory, Commit: e.Commit, At: e.At}
|
|
switch {
|
|
case e.Event != delivery.BuildEndedEvent:
|
|
r.State = e.Event
|
|
case e.Outcome == delivery.BuildFailed:
|
|
r.Failed = "the build failed"
|
|
case e.Outcome != delivery.BuildDone:
|
|
r.State = e.Outcome
|
|
}
|
|
b.mu.Lock()
|
|
defer b.mu.Unlock()
|
|
b.records[e.Event+"."+e.Module+"."+r.ID] = r
|
|
return nil
|
|
}
|
|
|
|
func newer(a, b delivery.BuildRecord) bool {
|
|
return a.At.After(b.At) || (a.At.Equal(b.At) && a.ID > b.ID)
|
|
}
|
|
|
|
// PathOf is the directory of a module of the repository as its newest build record of that repository names it, its
|
|
// newest successful trunk build's first ("." for the repository's root), and false when no record of the repository
|
|
// names the module (issue 1614).
|
|
func (b *Builds) PathOf(module, repository string) (string, bool) {
|
|
var best *delivery.BuildRecord
|
|
trunk := false
|
|
b.mu.Lock()
|
|
defer b.mu.Unlock()
|
|
for _, r := range b.records {
|
|
if r.Module != module || !delivery.SameRepository(r.Repository, repository) {
|
|
continue
|
|
}
|
|
t := registered(r) && trunkLike[r.Ref]
|
|
if best == nil || (t && !trunk) || (t == trunk && newer(r, *best)) {
|
|
c := r
|
|
best, trunk = &c, t
|
|
}
|
|
}
|
|
if best == nil {
|
|
return "", false
|
|
}
|
|
return pathOf(strings.Trim(best.Path, "/")), true
|
|
}
|
|
|
|
// Registration is a module registered in the catalogue from its own repository, as the builder log.s newest end of a build
|
|
// of it names it: the repository, the directory of its manifest there ("" or "." for the root) and the commit it was
|
|
// built at.
|
|
type Registration struct {
|
|
Module, Repository, Path, Commit, Ref string
|
|
}
|
|
|
|
// registered is whether the record is an outcome that did not fail: what registers a module. It asks for no particular branch: a
|
|
// repository's trunk is named main, master, or what its owner chose (the registered photos, de-spiegel and link2pay were
|
|
// built from nox-mesh, and every record of them names that ref).
|
|
func registered(r delivery.BuildRecord) bool {
|
|
return r.State == "" && r.Failed == ""
|
|
}
|
|
|
|
// RegistrationsOf are the places a module was registered from, by its outcome records: the newest record of each
|
|
// repository and directory (a module's name was once another repository's, as photos was a directory of the catalogue's).
|
|
// Records of a trunk's ref (main, master, trunk, none) come before those of another branch, then the newer first, so the
|
|
// first that holds the module's manifest is the one indexed.
|
|
func (b *Builds) RegistrationsOf(module string) []Registration {
|
|
type place struct{ repository, path string }
|
|
byPlace := map[place][]delivery.BuildRecord{}
|
|
b.mu.Lock()
|
|
for _, r := range b.records {
|
|
if r.Module == module && registered(r) {
|
|
k := place{delivery.RepositoryName(r.Repository), pathOf(strings.Trim(r.Path, "/"))}
|
|
byPlace[k] = append(byPlace[k], r)
|
|
}
|
|
}
|
|
b.mu.Unlock()
|
|
// Within one place the record is that of the ref a registration comes from, never a newer pull request's or try's: a
|
|
// trunk's ref (main, master, trunk, none), else the ref most of the place's records name (the registered photos, de-spiegel
|
|
// and link2pay were built from nox-mesh), the newer of two equal; then its newest record.
|
|
best := map[place]delivery.BuildRecord{}
|
|
for k, recs := range byPlace {
|
|
count, latest := map[string]int{}, map[string]delivery.BuildRecord{}
|
|
for _, r := range recs {
|
|
count[r.Ref]++
|
|
if cur, ok := latest[r.Ref]; !ok || newer(r, cur) {
|
|
latest[r.Ref] = r
|
|
}
|
|
}
|
|
var pick string
|
|
first := true
|
|
for ref, rec := range latest {
|
|
switch {
|
|
case first:
|
|
case trunkLike[ref] != trunkLike[pick]:
|
|
if !trunkLike[ref] {
|
|
continue
|
|
}
|
|
case count[ref] != count[pick]:
|
|
if count[ref] < count[pick] {
|
|
continue
|
|
}
|
|
case !newer(rec, latest[pick]):
|
|
continue
|
|
}
|
|
pick, first = ref, false
|
|
}
|
|
best[k] = latest[pick]
|
|
}
|
|
recs := slices.Collect(maps.Values(best))
|
|
slices.SortFunc(recs, func(x, y delivery.BuildRecord) int {
|
|
switch {
|
|
case trunkLike[x.Ref] != trunkLike[y.Ref]:
|
|
if trunkLike[x.Ref] {
|
|
return -1
|
|
}
|
|
return 1
|
|
case newer(x, y):
|
|
return -1
|
|
case newer(y, x):
|
|
return 1
|
|
}
|
|
return strings.Compare(x.Repository+x.Path, y.Repository+y.Path)
|
|
})
|
|
out := make([]Registration, 0, len(recs))
|
|
for _, r := range recs {
|
|
out = append(out, Registration{Module: r.Module, Repository: r.Repository, Path: r.Path, Commit: r.Commit, Ref: r.Ref})
|
|
}
|
|
return out
|
|
}
|
|
|
|
// Registered is the registrations of every module a record names, by module name.
|
|
func (b *Builds) Registered() []Registration {
|
|
b.mu.Lock()
|
|
names := map[string]bool{}
|
|
for _, r := range b.records {
|
|
names[r.Module] = true
|
|
}
|
|
b.mu.Unlock()
|
|
var out []Registration
|
|
for _, name := range slices.Sorted(maps.Keys(names)) {
|
|
out = append(out, b.RegistrationsOf(name)...)
|
|
}
|
|
return out
|
|
}
|