diff --git a/modules/mesh-delivery-gate/cmd/mesh-delivery-gate/record.go b/modules/mesh-delivery-gate/cmd/mesh-delivery-gate/record.go index c68d2b82c..502a564e1 100644 --- a/modules/mesh-delivery-gate/cmd/mesh-delivery-gate/record.go +++ b/modules/mesh-delivery-gate/cmd/mesh-delivery-gate/record.go @@ -194,10 +194,12 @@ type NodeAway struct { Since time.Time `json:"since"` } -// PinnedRevision is a graph revision the record points at: its half (catalog or assignment) and its revision. +// PinnedRevision is a graph revision the record points at: its half (catalog or assignment), its revision and, for a catalog +// revision, the watched branch it is numbered on. type PinnedRevision struct { Revision string `json:"revision"` Half string `json:"half"` + Branch string `json:"branch,omitempty"` } // RecordEnded is how a record ended: when, why, by whom, and the nodes away it ended without. diff --git a/modules/mesh-delivery-gate/cmd/mesh-delivery-gate/record_test.go b/modules/mesh-delivery-gate/cmd/mesh-delivery-gate/record_test.go index 283f035ca..59285a28e 100644 --- a/modules/mesh-delivery-gate/cmd/mesh-delivery-gate/record_test.go +++ b/modules/mesh-delivery-gate/cmd/mesh-delivery-gate/record_test.go @@ -258,7 +258,7 @@ func TestTheDeliveryRecordIsALedgerInstanceKeyedByMilestoneAndHeads(t *testing.T return nil }} old, _, _ := rs.Open("46/F", heads, false) - if err := rs.Pin(old, HalfCatalog, 1); err != nil { + if err := rs.Pin(old, HalfCatalog, "main", 1); err != nil { t.Fatal(err) } now = at.Add(89 * 24 * time.Hour) @@ -273,7 +273,7 @@ func TestTheDeliveryRecordIsALedgerInstanceKeyedByMilestoneAndHeads(t *testing.T if r, _ := rs.Read(old.Instance); r != nil { t.Fatal("a record past its retention is kept") } - if !slices.Equal(freed, []string{"46/F [1] the record is past its retention"}) { + if !slices.Equal(freed, []string{"46/F [{main 1}] the record is past its retention"}) { t.Fatalf("freed %v", freed) } list, _ := rs.List() @@ -299,11 +299,15 @@ func TestTheDeliveryRecordIsALedgerInstanceKeyedByMilestoneAndHeads(t *testing.T }} r, _, _ := rs.Open("47/G", heads, false) for range 2 { - if err := rs.Pin(r, HalfAssignment, 9); err != nil { + if err := rs.Pin(r, HalfAssignment, "", 9); err != nil { t.Fatal(err) } } - if err := rs.Pin(r, HalfCatalog, 9); err != nil { + if err := rs.Pin(r, HalfCatalog, "main", 9); err != nil { + t.Fatal(err) + } + // Revisions are numbered per watched branch: the same number on another branch is another pin. + if err := rs.Pin(r, HalfCatalog, "feat/x", 9); err != nil { t.Fatal(err) } pins := 0 @@ -313,28 +317,36 @@ func TestTheDeliveryRecordIsALedgerInstanceKeyedByMilestoneAndHeads(t *testing.T } } back, _ := rs.Read(r.Instance) - if pins != 2 || len(back.Pinned) != 2 { + if pins != 3 || len(back.Pinned) != 3 { t.Fatalf("%d pins written, the record holds %+v", pins, back.Pinned) } - if len(got) != 3 || !reflect.DeepEqual(got[0], got[1]) || got[0].subject != meshdelivery.SubjectPinned || - !slices.Equal(got[0].pins.Assignment, []int64{9}) || got[2].pins.Catalog[0] != 9 || got[0].pins.Record != r.Instance { + if len(got) != 4 || !reflect.DeepEqual(got[0], got[1]) || got[0].subject != meshdelivery.SubjectPinned || + !slices.Equal(got[0].pins.Assignment, []int64{9}) || !slices.Equal(got[2].pins.Catalog, []meshdelivery.PinsCatalogItem{{Branch: "main", Revision: 9}}) || + !slices.Equal(got[3].pins.Catalog, []meshdelivery.PinsCatalogItem{{Branch: "feat/x", Revision: 9}}) || got[0].pins.Record != r.Instance { t.Fatalf("said %+v", got) } got = nil - if err := rs.Free(back, HalfAssignment, 9, "removed by hand"); err != nil { + if err := rs.Free(back, HalfAssignment, "", 9, "removed by hand"); err != nil { t.Fatal(err) } - if err := rs.Free(back, HalfAssignment, 9, "again"); err != nil { + if err := rs.Free(back, HalfAssignment, "", 9, "again"); err != nil { t.Fatal(err) } if len(got) != 1 || got[0].subject != meshdelivery.SubjectFreed || got[0].pins.Why != "removed by hand" || !slices.Equal(got[0].pins.Assignment, []int64{9}) { t.Fatalf("said %+v", got) } + got = nil + if err := rs.Free(back, HalfCatalog, "feat/x", 9, "removed"); err != nil { + t.Fatal(err) + } + if len(got) != 1 || !slices.Equal(got[0].pins.Catalog, []meshdelivery.PinsCatalogItem{{Branch: "feat/x", Revision: 9}}) { + t.Fatalf("said %+v", got) + } if again, _ := rs.Read(r.Instance); len(again.Pinned) != 1 { t.Fatalf("pinned after the free %+v", again.Pinned) } - if err := rs.Pin(r, "trunk", 1); err == nil { + if err := rs.Pin(r, "trunk", "", 1); err == nil { t.Fatal("a pin of no half is taken") } }) diff --git a/modules/mesh-delivery-gate/cmd/mesh-delivery-gate/recordevents.go b/modules/mesh-delivery-gate/cmd/mesh-delivery-gate/recordevents.go index cb123df7c..d5a34a740 100644 --- a/modules/mesh-delivery-gate/cmd/mesh-delivery-gate/recordevents.go +++ b/modules/mesh-delivery-gate/cmd/mesh-delivery-gate/recordevents.go @@ -14,6 +14,7 @@ import ( "time" "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-contracts/go/seats/meshdelivery" + "git.novox.be/novox/mesh-sdk/go/keys" ) // The halves of the revision view a record pins (design 47 *Two graphs, one source of truth*). @@ -39,13 +40,24 @@ func (rs *Records) say(subject string, body any, id string) error { return rs.Say(subject, body, id) } -// pinKey is a pinned revision's key in the record: its half and its number, so the two halves' numbers never meet. -func pinKey(half string, revision int64) string { return half + "-" + strconv.FormatInt(revision, 10) } +// pinKey is a pinned revision's key in the record: its half, for a catalog revision its watched branch (revisions are numbered +// per branch, A-171) escaped as a name with its slashes escaped too, and its number, so no two revisions' keys meet. +func pinKey(half, branch string, revision int64) string { + key := half + "-" + if half == HalfCatalog { + key += strings.ReplaceAll(keys.EscapeName(branch), "/", "=2f") + "-" + } + return key + strconv.FormatInt(revision, 10) +} -// number is the revision's number on its half's seat. +// number is the revision's number on its half's seat: what follows the key's last dash. func (p PinnedRevision) number() (int64, bool) { - n, err := strconv.ParseInt(strings.TrimPrefix(p.Revision, p.Half+"-"), 10, 64) - return n, err == nil && strings.HasPrefix(p.Revision, p.Half+"-") + i := strings.LastIndex(p.Revision, "-") + if i < 0 || !strings.HasPrefix(p.Revision, p.Half+"-") { + return 0, false + } + n, err := strconv.ParseInt(p.Revision[i+1:], 10, 64) + return n, err == nil } // pinsOf is the seat's Pins of a record for these revisions. @@ -58,7 +70,7 @@ func pinsOf(r *Record, pins []PinnedRevision, why string) meshdelivery.Pins { } switch p.Half { case HalfCatalog: - out.Catalog = append(out.Catalog, n) + out.Catalog = append(out.Catalog, meshdelivery.PinsCatalogItem{Branch: p.Branch, Revision: n}) case HalfAssignment: out.Assignment = append(out.Assignment, n) } @@ -74,12 +86,18 @@ func checkHalf(half string) error { } // Pin writes that the record points at a graph revision, unless it does already, and says `pinned` (again, when written -// before: the id keeps it once). -func (rs *Records) Pin(r *Record, half string, revision int64) error { +// before: the id keeps it once). A catalog revision names its watched branch, which the record keeps and the event says. +func (rs *Records) Pin(r *Record, half, branch string, revision int64) error { if err := checkHalf(half); err != nil { return err } - p := PinnedRevision{Revision: pinKey(half, revision), Half: half} + if half == HalfCatalog && branch == "" { + return fmt.Errorf("a catalog revision is pinned by its watched branch and its number") + } + if half != HalfCatalog { + branch = "" + } + p := PinnedRevision{Revision: pinKey(half, branch, revision), Half: half, Branch: branch} if _, ok := r.Pinned[p.Revision]; !ok { seq, err := rs.Ledger.Apply(r.Instance, OpPin, p) if err != nil { @@ -95,11 +113,11 @@ func (rs *Records) Pin(r *Record, half string, revision int64) error { // Free writes that the record no longer points at a graph revision and says `freed` with why; a revision it does not // point at frees nothing and says nothing. -func (rs *Records) Free(r *Record, half string, revision int64, why string) error { +func (rs *Records) Free(r *Record, half, branch string, revision int64, why string) error { if err := checkHalf(half); err != nil { return err } - p, ok := r.Pinned[pinKey(half, revision)] + p, ok := r.Pinned[pinKey(half, branch, revision)] if !ok { return nil } diff --git a/modules/mesh-delivery-gate/cmd/mesh-delivery-gate/sourcetools.go b/modules/mesh-delivery-gate/cmd/mesh-delivery-gate/sourcetools.go index 2cebafb6c..68c982da0 100644 --- a/modules/mesh-delivery-gate/cmd/mesh-delivery-gate/sourcetools.go +++ b/modules/mesh-delivery-gate/cmd/mesh-delivery-gate/sourcetools.go @@ -11,7 +11,7 @@ import ( "strings" "git.novox.be/novox/mesh-contracts/health-and-repair/go/clients/meshhandactsclient" - seat "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshdelivery" + seat "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-contracts/go/seats/meshdelivery" stdio "git.novox.be/novox/mesh-sdk/go" "git.novox.be/novox/mesh-sdk/go/keys" ) diff --git a/modules/mesh-delivery-gate/cmd/mesh-delivery-gate/sourcetools_test.go b/modules/mesh-delivery-gate/cmd/mesh-delivery-gate/sourcetools_test.go index b5c142d9c..12204f773 100644 --- a/modules/mesh-delivery-gate/cmd/mesh-delivery-gate/sourcetools_test.go +++ b/modules/mesh-delivery-gate/cmd/mesh-delivery-gate/sourcetools_test.go @@ -2,7 +2,7 @@ package main import ( "errors" - seat "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshdelivery" + seat "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-contracts/go/seats/meshdelivery" "strings" "testing" "time" @@ -74,7 +74,7 @@ func TestTheGateStatesItsPinsThenSaysPinsStated(t *testing.T) { if err != nil { t.Fatal(err) } - if err := rs.Pin(r, HalfCatalog, 3); err != nil { + if err := rs.Pin(r, HalfCatalog, "main", 3); err != nil { t.Fatal(err) } events = nil diff --git a/modules/mesh-delivery-gate/module.json b/modules/mesh-delivery-gate/module.json index 0760ff7c9..194772ff1 100644 --- a/modules/mesh-delivery-gate/module.json +++ b/modules/mesh-delivery-gate/module.json @@ -98,6 +98,9 @@ "half": { "type": "string" }, + "branch": { + "type": "string" + }, "ended": { "type": "object" }, @@ -208,7 +211,8 @@ "effect": "set-entry", "fields": [ "revision", - "half" + "half", + "branch" ], "into": "pinned", "key": "revision"