diff --git a/modules/mesh-delivery-indexer/internal/index/catalog_branches_test.go b/modules/mesh-delivery-indexer/internal/index/catalog_branches_test.go index d7e625f9..7de43afc 100644 --- a/modules/mesh-delivery-indexer/internal/index/catalog_branches_test.go +++ b/modules/mesh-delivery-indexer/internal/index/catalog_branches_test.go @@ -63,4 +63,36 @@ func TestACatalogRevisionIsKeptPerWatchedBranch(t *testing.T) { if a := answer("beta", 0); !a.NoneYet { t.Errorf("a branch with no revision answered %+v, want none-yet", a) } + // A-205: `source` answers the branch's revision taken in from that source revision; a candidate's group alone answers its + // newest revision, whatever its heads. + bySource := func(branch string, source int64) (meshcatalog.RevisionAnswer, error) { + return Revision(ctx, ix.Revisions, "indexer", "reader", meshcatalog.RevisionInput{Branch: branch, Source: source}) + } + if a, err := bySource("main", 5); err != nil || a.Revision.Revision != 1 || a.Entries[0].Commit != "c1" { + t.Errorf("main's source 5 answered %+v (%v)", a.Revision, err) + } + if a, err := bySource("feature/x=2ey", 7); err != nil || a.Revision.Revision != 1 || a.Entries[0].Commit != "c2" { + t.Errorf("the branch's source 7 answered %+v (%v), want its revision 1 at c2", a.Revision, err) + } + if a, err := bySource("feature/x=2ey", 8); err != nil || a.Revision.Revision != 2 { + t.Errorf("the branch's source 8 answered %+v (%v)", a.Revision, err) + } + if _, err := bySource("feature/x=2ey", 9); err == nil { + t.Error("a source no revision was taken in from was answered") + } + for i, heads := range []string{"h1", "h2"} { + rev, objects, err := CatalogRevisionOf(meshcatalog.CatalogRevision{Revision: int64(3 + i), Kind: "candidate", Group: "run-ab", Heads: heads}, tip("c9")) + if err != nil { + t.Fatal(err) + } + if err := KeepRevision(ctx, ix.Revisions, rev, objects); err != nil { + t.Fatal(err) + } + } + if a, err := Revision(ctx, ix.Revisions, "indexer", "reader", meshcatalog.RevisionInput{Group: "run-ab"}); err != nil || a.Revision.Revision != 4 || a.Revision.Heads != "h2" || a.Entries[0].Commit != "c9" { + t.Errorf("a group alone answered %+v (%v), want its newest revision 4", a.Revision, err) + } + if _, err := Revision(ctx, ix.Revisions, "indexer", "reader", meshcatalog.RevisionInput{Group: "run-none"}); err == nil { + t.Error("a candidate never kept was answered") + } } diff --git a/modules/mesh-delivery-indexer/internal/index/revision_verb.go b/modules/mesh-delivery-indexer/internal/index/revision_verb.go index d279affc..3a0d48e0 100644 --- a/modules/mesh-delivery-indexer/internal/index/revision_verb.go +++ b/modules/mesh-delivery-indexer/internal/index/revision_verb.go @@ -71,11 +71,21 @@ func Revision(ctx context.Context, o Objects, holder, caller string, in meshcata } name := RevisionObject(newest) switch { + case in.Group != "" && in.Heads == "": + // A candidate's group alone answers its newest revision, whatever its heads (A-205). + if name, err = newestCandidate(ctx, o, in.Group); err != nil { + return out, err + } case in.Group != "" || in.Heads != "": if in.Group == "" || in.Heads == "" { return out, fmt.Errorf("a candidate's revision is named by its group and its heads, both") } name = RevisionObject(meshcatalog.CatalogRevision{Kind: "candidate", Group: in.Group, Heads: in.Heads}) + case in.Source != 0: + // The branch's revision taken in from that source revision (A-205). + if name, err = revisionOfSource(ctx, o, branch, in.Source); err != nil { + return out, err + } case in.Revision != 0 && in.Revision != newest.Revision: name = RevisionObject(meshcatalog.CatalogRevision{Branch: branch, Revision: in.Revision}) } @@ -121,3 +131,62 @@ func RevisionTool(o Objects, holder string) stdio.Tool { return Revision(context.Background(), o, holder, stdio.CallerOf(args), in) }} } + +// newestCandidate is the object of the newest revision a candidate group keeps, over whatever heads it was kept at (A-205). +func newestCandidate(ctx context.Context, o Objects, group string) (string, error) { + names, err := o.Names(ctx) + if err != nil { + return "", fmt.Errorf("the candidate revisions cannot be listed: %w", err) + } + best, newest := "", int64(-1) + for _, n := range names { + if !strings.HasPrefix(n, "candidate."+group+".") { + continue + } + rev, _, err := ReadRevision(ctx, o, n) + if err != nil { + return "", err + } + if rev.Group == group && rev.Revision > newest { + best, newest = n, rev.Revision + } + } + if best == "" { + return "", fmt.Errorf("the catalog keeps no candidate of the group %s", group) + } + return best, nil +} + +// revisionOfSource is the object of the newest revision of branch taken in from the source revision source (A-205); a source +// none was taken in from is an error. +func revisionOfSource(ctx context.Context, o Objects, branch string, source int64) (string, error) { + names, err := o.Names(ctx) + if err != nil { + return "", fmt.Errorf("the catalog graph revisions cannot be listed: %w", err) + } + number := TrunkRevisionOf + if !isTrunkBranch(branch) { + number = func(n string) (int64, bool) { return BranchRevisionOf(branch, n) } + } + var found []int64 + for _, n := range names { + if r, ok := number(n); ok { + found = append(found, r) + } + } + slices.SortFunc(found, func(a, b int64) int { return int(b - a) }) + for _, r := range found { + name := RevisionObject(meshcatalog.CatalogRevision{Branch: branch, Revision: r}) + rev, _, err := ReadRevision(ctx, o, name) + if err != nil { + return "", err + } + if rev.Source == source { + return name, nil + } + if rev.Source < source { + break + } + } + return "", fmt.Errorf("no revision of %q was taken in from the source revision %d", branch, source) +}