package check import ( "context" "fmt" "maps" "slices" "strings" "git.novox.be/novox/mesh-sdk/go/catalogue" "git.novox.be/novox/mesh-sdk/go/composer" "git.novox.be/novox/mesh-sdk/go/delivery" ) // The check refuses a change whose module graph has a cycle and names it (novox/hq ADR 0603 §2): modules that would each // wait for the other for good, through their bases or the provisions they await. The graph is worked out from the // catalogue as the change leaves it (every trunk manifest, each one a pull request touches replaced by its candidate's), // with the contract's ManifestGraph, the way the indexer works out its graph's edges. A cycle the trunk already has among // the same modules is the trunk's: said in the verdict's notes, not the change's failure. // ManifestReader reads a kept manifest by its digest (the indexer's manifests state). type ManifestReader interface { Manifest(ctx context.Context, digest string) (catalogue.Manifest, error) } // refuseCycle fails the check for a cycle of the change's module graph, naming it; nil when the case touches no manifest, // when nothing reads manifests, or when there is none. A manifest that cannot be read fails the check, said, never passes it. func (st *state) refuseCycle(ctx context.Context) *delivery.Verdict { if st.j.Manifests == nil || st.j.Read == nil || st.c.Try != nil { return nil } trunk := st.j.Manifests.ManifestDigests() req, touched := judgeRequest(trunk, st.c.Pulls) if len(touched) == 0 { return nil } read := func(digests map[string]composer.Manifest) (map[string]catalogue.Manifest, error) { out := map[string]catalogue.Manifest{} for _, module := range slices.Sorted(maps.Keys(digests)) { m, err := st.j.Read.Manifest(ctx, digests[module].Digest) if err != nil { return nil, fmt.Errorf("the manifest %s of %s: %w", digests[module].Digest, module, err) } out[module] = m } return out, nil } change, err := read(req.Cases[0].Manifests) if err != nil { return fail(goModule, "the change's module graph cannot be worked out, so no cycle can be ruled out: "+err.Error()) } cycle := delivery.ManifestGraph(change).Cycle() if cycle == nil { st.c.note("the change's module graph has no cycle") return nil } words := strings.Join(cycle, " -> ") if before, err := read(req.Cases[1].Manifests); err == nil { among := delivery.Graph{} for name, n := range delivery.ManifestGraph(before) { if slices.Contains(cycle, name) { among[name] = n } } if among.Cycle() != nil { st.c.note("the module graph has the cycle " + words + ", which the trunk already has among the same modules (ADR 0603 §2)") return nil } } return fail(goModule, fmt.Sprintf("the change's module graph has the cycle %s: these modules would each wait for the other for "+ "good, through their bases or the provisions they await (ADR 0603 §2)", words)) }