Skip to content
quarry Artifact [93db23e20d]
Login

Artifact [93db23e20d]

Artifact 93db23e20d1068abf8929a273f0e0bd428287fb89362a786359319a85580218a:


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
}