Artifact 5e18d93153fec367cbff55425b9af9ee8f43785b1b696a1ac58b47a38c0ee6ab:
- File
repo_ticket.go
— part of check-in
[29b53e75d2]
at
2026-10-26 03:24:05
on branch main
— fix: timelineSince means arrived after, not stamped after
A sync handler asks what a push brought in. Filtering on timestamps missed an artifact made on a machine whose clock runs behind the given check-in's. TimelineSince now keeps entries whose artifacts were stored after the given one: a later receipt (blob.rcvid, which a phantom fill updates), then a higher rid. Any held artifact hash is accepted; an unknown one is an error.
Tracked in #504. (user: [email protected], size: 6426) [annotate] [blame] [check-ins using] [more...]
package libfossil import ( "fmt" "strings" "time" "github.com/danmestas/go-libfossil/internal/fsltype" "github.com/danmestas/go-libfossil/internal/manifest" ) // TicketOpts is one change to a ticket. type TicketOpts struct { // Fields maps a ticket field (a column of the repository's ticket // schema: title, status, type, resolution, comment, ...) to its new // value. A "+name" key appends the value to the field instead. A field // with an empty value is left out, as `fossil ticket` leaves it out. Fields map[string]string // User is the change's author, its U card. User string // Comment, when not empty, is a remark added with the change, stored as // the stock ticket forms store one: the icomment field, with login and // username set to User and mimetype text/plain. Comment string // Time is the change's D card; zero means now. Time time.Time } // Ticket is one ticket as the ticket table holds it after every change. type Ticket struct { UUID string `json:"uuid"` Status string `json:"status"` // Changed is the time of the ticket's latest change. Changed time.Time `json:"changed"` // Fields holds every ticket-table field that has a value. Fields map[string]string `json:"fields"` } // TicketChange is one change artifact of a ticket. type TicketChange struct { // Artifact is the change artifact's hash. Artifact string `json:"artifact"` User string `json:"user"` Time time.Time `json:"time"` // Comment is the remark the change added, or "". Comment string `json:"comment"` // Fields holds the change's fields exactly as written, the remark's // included: "+name" for an append. Fields map[string]string `json:"fields"` } // remarkFields are the fields TicketOpts.Comment writes. var remarkFields = []string{"icomment", "login", "username", "mimetype"} // TicketAdd creates a ticket, as `fossil ticket add` does, and returns its // new id and the hash of the artifact that created it. The ticket table // reflects it when TicketAdd returns, and the artifact is queued to sync. func (r *Repo) TicketAdd(opts TicketOpts) (ticketUUID, artifact string, err error) { ticketUUID, err = manifest.NewTicketUUID(r.inner.DB()) if err != nil { return "", "", fmt.Errorf("libfossil: ticket add: %w", err) } artifact, err = r.writeTicket(ticketUUID, opts) if err != nil { return "", "", fmt.Errorf("libfossil: ticket add: %w", err) } return ticketUUID, artifact, nil } // TicketSet changes the ticket whose id is or starts with ticket, as `fossil // ticket set` does, and returns the hash of the change artifact. func (r *Repo) TicketSet(ticket string, opts TicketOpts) (artifact string, err error) { full, err := manifest.ResolveTicket(r.inner.DB(), ticket) if err != nil { return "", fmt.Errorf("libfossil: ticket set: %w", err) } artifact, err = r.writeTicket(full, opts) if err != nil { return "", fmt.Errorf("libfossil: ticket set: %w", err) } return artifact, nil } func (r *Repo) writeTicket(ticketUUID string, opts TicketOpts) (string, error) { if strings.TrimSpace(opts.User) == "" { return "", fmt.Errorf("a ticket change needs a user") } fields := make(map[string]string, len(opts.Fields)+len(remarkFields)) for name, value := range opts.Fields { fields[name] = value } if opts.Comment != "" { for _, name := range remarkFields { if fields[name] != "" || fields["+"+name] != "" { return "", fmt.Errorf("field %q is written by the comment", name) } } fields["icomment"] = opts.Comment fields["login"] = opts.User fields["username"] = opts.User fields["mimetype"] = "text/plain" } _, artifact, err := manifest.WriteTicket(r.inner, manifest.TicketWrite{ UUID: ticketUUID, Fields: fields, User: opts.User, Time: opts.Time, }) return artifact, err } // TicketList returns every ticket, the most recently changed first. func (r *Repo) TicketList() ([]Ticket, error) { rows, err := manifest.ListTickets(r.inner.DB()) if err != nil { return nil, fmt.Errorf("libfossil: ticket list: %w", err) } tickets := make([]Ticket, len(rows)) for i, t := range rows { tickets[i] = Ticket{ UUID: t.UUID, Status: t.Fields["status"], Changed: t.Changed, Fields: t.Fields, } } return tickets, nil } // TicketChanges returns the changes of the ticket whose id is or starts with // ticket, oldest first: the order fossil applies them in, as `fossil ticket // history` lists them. func (r *Repo) TicketChanges(ticket string) ([]TicketChange, error) { full, err := manifest.ResolveTicket(r.inner.DB(), ticket) if err != nil { return nil, fmt.Errorf("libfossil: ticket changes: %w", err) } rows, err := manifest.TicketChanges(r.inner.DB(), full) if err != nil { return nil, fmt.Errorf("libfossil: ticket changes: %w", err) } changes := make([]TicketChange, len(rows)) for i, c := range rows { comment := c.Fields["icomment"] if comment == "" { comment = c.Fields["+icomment"] } changes[i] = TicketChange{ Artifact: c.Artifact, User: c.User, Time: c.Time, Comment: comment, Fields: c.Fields, } } return changes, nil } // TimelineSince returns the timeline entries whose artifacts arrived in this // repository after the artifact with full hash hash: stored by a later // receipt (a later push, pull or local write), or later within the same // one. kinds, when given, keeps only entries of those kinds. The entries are // in Timeline's order, newest first; an empty result is an empty slice, not // nil. A hash the repository does not hold is an error. // // Arrival, not timestamp, is what "after" means, because the question this // answers is what a push brought in: an artifact made on a machine whose // clock runs behind the given artifact's still arrived after it. Arrival // order is this repository's own, so two clones can answer differently. func (r *Repo) TimelineSince(hash string, kinds ...EventKind) ([]TimelineEntry, error) { for _, k := range kinds { if k == "" || !k.Valid() { return nil, fmt.Errorf("libfossil: timeline since: unknown event kind %q", k) } } rid, err := manifest.ArtifactRID(r.inner.DB(), hash) if err != nil { return nil, fmt.Errorf("libfossil: timeline since: %w", err) } entries, err := manifest.Timeline(r.inner, manifest.TimelineOpts{ Kinds: []fsltype.EventKind(kinds), ArrivedAfter: rid, }) if err != nil { return nil, fmt.Errorf("libfossil: timeline since: %w", err) } return timelineEntries(entries), nil }