Artifact 74dd3154ae8ffa2630d82f31b366362fdc9c5f2b4560992fcb24c6deb41242c5:
- File
types.go
— part of check-in
[ad09ba3d97]
at
2026-07-19 18:27:06
on branch trunk
— fix: give Timeline its own result type so an Ancestry cursor cannot compile into it (#78)
* fix: give Timeline its own result type so an Ancestry cursor cannot compile into it
LogEntry (Ancestry's result type) carried a Cursor field that was always the zero value on Ancestry entries — structurally identical to a legitimate Timeline first-page call. Feeding it into TimelineOpts.After silently paginated from page one forever with no error; documenting the field as invalid on Ancestry entries did nothing to stop it, because the zero cursor cannot be told apart from a valid first-call state even in principle.
Split the shared type instead of adding a runtime guard: LogEntry loses Cursor entirely and remains Ancestry's result type, while a new TimelineEntry (embeds LogEntry, adds Cursor) becomes Timeline's result type. An Ancestry entry's cursor can no longer be obtained at all, so passing one to Timeline is now a compile error rather than a documented hazard. Applied the same split to the internal manifest.LogEntry / manifest.TimelineEntry pair Timeline and Ancestry are built on.
Breaking change; CHANGELOG documents the migration.
* doc: warn against hand-assembling TimelineEntry from an Ancestry LogEntry
Review found the one remaining escape hatch: TimelineEntry{LogEntry: someAncestryEntry} compiles with a zero-value Cursor, which Timeline reads as "start from newest" rather than an error. No library path produces that value — it requires deliberately embedding an Ancestry LogEntry into a hand-built TimelineEntry — but flag it at the type so whoever does that gets the warning where they're typing. (user: [email protected], size: 2243) [annotate] [blame] [check-ins using]
package libfossil import "github.com/danmestas/libfossil/internal/fsltype" // FslID is a row-id in the blob table (content-addressed artifacts). type FslID = fsltype.FslID // FslSize represents a blob size; negative values indicate phantom blobs. type FslSize = fsltype.FslSize const ( PhantomSize FslSize = fsltype.PhantomSize FossilApplicationID int32 = fsltype.FossilApplicationID ) // EventKind is the type discriminator on a Timeline entry: one of the // single-letter codes fossil assigns per event.type row. The zero value // means "all kinds" — the default Timeline uses when no Type is given. type EventKind = fsltype.EventKind const ( // EventKindCheckin is a check-in ('ci'). EventKindCheckin EventKind = fsltype.EventKindCheckin // EventKindTechnote is a technote / event artifact ('e'). EventKindTechnote EventKind = fsltype.EventKindTechnote // EventKindForum is a forum post ('f'). EventKindForum EventKind = fsltype.EventKindForum // EventKindTag is a tag/control artifact ('g'). EventKindTag EventKind = fsltype.EventKindTag // EventKindTicket is a ticket change ('t'). EventKindTicket EventKind = fsltype.EventKindTicket // EventKindWiki is a wiki page edit ('w'). EventKindWiki EventKind = fsltype.EventKindWiki ) // Cursor is an opaque pagination token for Timeline. Obtain one from a // returned TimelineEntry's Cursor field and pass it back as the next // TimelineOpts.After to resume enumeration immediately after that entry. // The zero Cursor means "start from the newest event". // // Cursor's representation is deliberately hidden: it cannot be // constructed from a timestamp and a rid by calling code, only obtained // from a TimelineEntry the library already produced. See fsltype.Cursor's // doc comment for why — a hand-built cursor derived from a rounded // time.Time is not guaranteed to match the row it claims to follow // exactly, which is what silently reintroduces skipped or duplicated rows // at a page boundary. LogEntry, Ancestry's result type, has no Cursor // field at all: Ancestry's pagination is by Start/Limit, so there is no // valid cursor to obtain from it, and the type reflects that instead of // documenting it as a hazard. type Cursor = fsltype.Cursor