Artifact 9c4052baf9eca1aecb4d98ef6f8ac4480386231cbc9245faaa495e75a93df939:
- File
cli/repo_timeline.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: 1050) [annotate] [blame] [check-ins using]
package cli import ( "fmt" libfossil "github.com/danmestas/libfossil" ) // RepoTimelineCmd shows repository timeline/history. type RepoTimelineCmd struct { Limit int `short:"n" default:"20" help:"Number of entries"` } func (c *RepoTimelineCmd) Run(g *Globals) error { r, err := g.OpenRepo() if err != nil { return err } defer r.Close() entries, err := r.Timeline(libfossil.TimelineOpts{Limit: c.Limit}) if err != nil { return err } for _, e := range entries { uuid := e.UUID if len(uuid) > 10 { uuid = uuid[:10] } // A check-in with no recorded user (event.user IS NULL) comes back // from the library as "" — TimelineEntry.User never leaks a nullable type. // Canonical fossil renders that case as "?" on the TTY // (coalesce(euser,user,'?') in timeline_query_for_tty()); match it // here at the presentation boundary rather than in the library. user := e.User if user == "" { user = "?" } fmt.Printf("%s %s %s %s\n", uuid, e.Time.Format("2006-01-02 15:04"), user, e.Comment) } return nil }