package api
import (
"log"
"sort"
"strconv"
"sync"
)
// TicketNumberStore is the forge's ticket numbering (oltp/ticketnumber.go).
type TicketNumberStore interface {
TicketNumbersAssign(repo string, ids []string) error
TicketNumbers(repo string) (map[string]int, error)
TicketNumbered(repo string, n int) (string, bool, error)
TicketNumberRekey(repo, from, to string) error
}
// numberLocks serializes numbering per repository, so two filings at
// once cannot both read the same "next number".
var numberLocks sync.Map
// numbers says whether this forge numbers a repository's tickets: one it
// owns, never a mirror — a mirror's tickets are numbered, if at all, by
// the forge that owns them (54c5175f).
func (d Deps) numbers(repo string) bool {
if d.Numbers == nil {
return false
}
m, _ := d.posture(repo)
return !m.Tracking
}
// ticketNumbers is a repository's ticket numbers, id → number, after
// numbering anything that has none yet: the store's tickets and the
// local tier's, together, oldest first. So the first read after this
// ships backfills the tracker in creation order, and a ticket that
// arrives later — filed here, or pushed from a clone — takes the next
// number when it is first seen. Nil for a repository the forge does not
// number, or when the store cannot be read; a number is decoration on
// every page that shows one and must never fail the page.
func (d Deps) ticketNumbers(name string, repo *Repo, also ...string) map[string]int {
if !d.numbers(name) {
return nil
}
mu, _ := numberLocks.LoadOrStore(name, &sync.Mutex{})
mu.(*sync.Mutex).Lock()
defer mu.(*sync.Mutex).Unlock()
have, err := d.Numbers.TicketNumbers(name)
if err != nil {
log.Printf("tickets: %s: reading numbers: %v", name, err)
return nil
}
type born struct{ id, at string }
var missing []born
seen := map[string]bool{}
note := func(id, at string) {
if id == "" || seen[id] {
return
}
seen[id] = true
if _, ok := have[id]; !ok {
if len(at) > 19 {
at = at[:19] // index and local tier write the same instant differently
}
missing = append(missing, born{id, at})
}
}
if repo != nil && repo.Index != nil {
if all, err := repo.Index.TicketCreation(); err == nil {
for _, t := range all {
note(t.ID, t.Created)
}
}
}
if d.Local != nil {
if locals, err := d.Local.LocalTickets(name); err == nil {
for _, lt := range locals {
note(lt.ID, lt.Created)
}
}
}
// A ticket just filed that no reader has folded yet: last.
for _, id := range also {
note(id, "9999")
}
if len(missing) == 0 {
return have
}
sort.SliceStable(missing, func(i, j int) bool { return missing[i].at < missing[j].at })
ids := make([]string, len(missing))
for i, m := range missing {
ids[i] = m.id
}
if err := d.Numbers.TicketNumbersAssign(name, ids); err != nil {
log.Printf("tickets: %s: numbering %d: %v", name, len(ids), err)
return have
}
if have, err = d.Numbers.TicketNumbers(name); err != nil {
return nil
}
return have
}
// ticketRef turns what a caller typed into a ticket id: an all-digit
// token names a NUMBER first (#12 is ticket 12, not whichever hex id
// starts with 12), and falls back to being an id prefix when this
// repository has no such number. Anything else is returned as given.
func (d Deps) ticketRef(name string, repo *Repo, ref string) string {
n, err := strconv.Atoi(ref)
if err != nil || n <= 0 || !d.numbers(name) {
return ref
}
d.ticketNumbers(name, repo) // a number read before any list was shown still resolves
if id, ok, err := d.Numbers.TicketNumbered(name, n); err == nil && ok {
return id
}
return ref
}