package api
import (
"log"
"sort"
"strconv"
"strings"
"sync"
"craftdesign.group/quarry/internal/index"
)
// LandingNumberStore is the forge's landing numbering
// (oltp/landingnumber.go): !N, one sequence per repository, apart from
// the tickets'.
type LandingNumberStore interface {
LandingNumbersAssign(repo string, ids []string) error
LandingNumbers(repo string) (map[string]int, error)
LandingNumbered(repo string, n int) (string, bool, error)
}
// landingNumberLocks serializes numbering per repository within this
// process; the store's immediate transaction does it across processes.
var landingNumberLocks sync.Map
// landingNumbers is a repository's landing numbers, id → number, after
// numbering every landing that has none yet, oldest opened first. A
// landing opened here is numbered as landing.open returns; one pushed
// from a clone, when the forge first reads it. So the first read after
// this ships backfills every landing in the order it was opened. all is
// the repository's whole landing list when the caller already holds it,
// nil to read it. Nil when the forge keeps no numbers or they cannot be
// read: a number is decoration and must never fail a page.
func (d Deps) landingNumbers(name string, repo *Repo, all []index.LandingSummary) map[string]int {
if d.LandingNumbers == nil {
return nil
}
if repo == nil && d.Repos != nil {
repo, _ = d.Repos.Get(name)
}
have, err := d.LandingNumbers.LandingNumbers(name)
if err != nil {
log.Printf("landings: %s: reading numbers: %v", name, err)
return nil
}
if all == nil && repo != nil && repo.Index != nil {
all, _ = repo.Index.Landings("", 0)
}
if !missingNumber(have, all) {
return have
}
mu, _ := landingNumberLocks.LoadOrStore(name, &sync.Mutex{})
mu.(*sync.Mutex).Lock()
defer mu.(*sync.Mutex).Unlock()
if have, err = d.LandingNumbers.LandingNumbers(name); err != nil {
return nil
}
var missing []index.LandingSummary
for _, l := range all {
if _, ok := have[l.ID]; !ok {
missing = append(missing, l)
}
}
if len(missing) == 0 {
return have
}
sort.SliceStable(missing, func(i, j int) bool {
a, b := openedKey(missing[i].Opened), openedKey(missing[j].Opened)
if a != b {
return a < b
}
return missing[i].ID < missing[j].ID
})
ids := make([]string, len(missing))
for i, l := range missing {
ids[i] = l.ID
}
if err := d.LandingNumbers.LandingNumbersAssign(name, ids); err != nil {
log.Printf("landings: %s: numbering %d: %v", name, len(ids), err)
return have
}
have, err = d.LandingNumbers.LandingNumbers(name)
if err != nil {
return nil
}
return have
}
func missingNumber(have map[string]int, all []index.LandingSummary) bool {
for _, l := range all {
if _, ok := have[l.ID]; !ok {
return true
}
}
return false
}
// openedKey reads an opened time to the second, however it is spelled:
// the index and a caller may write the same instant differently.
func openedKey(s string) string {
s = strings.Replace(s, "T", " ", 1)
if len(s) > 19 {
s = s[:19]
}
return s
}
// numberLandings writes each landing's number onto it.
func (d Deps) numberLandings(name string, repo *Repo, ls []index.LandingSummary) {
if len(ls) == 0 {
return
}
nums := d.landingNumbers(name, repo, nil)
for i := range ls {
ls[i].Number = nums[ls[i].ID]
}
}
// landingNumberOf is one landing's number, numbering it first if it is
// new. 0 when the forge keeps none.
func (d Deps) landingNumberOf(name string, repo *Repo, id string) int {
if d.LandingNumbers == nil || id == "" {
return 0
}
if have, err := d.LandingNumbers.LandingNumbers(name); err == nil {
if n, ok := have[id]; ok {
return n
}
}
return d.landingNumbers(name, repo, nil)[id]
}
// landingNumberIn reads a landing number as a caller may write it: !12,
// or 12 alone. ok is false for anything else — a hash prefix.
func landingNumberIn(ref string) (int, bool) {
ref = strings.TrimPrefix(ref, "!")
if !isDigits(ref) || ref[0] == '0' {
return 0, false
}
n, err := strconv.Atoi(ref)
return n, err == nil
}
// landingRef turns what a caller typed into a landing id: !12 or an
// all-digit 12 names a NUMBER first (landing 12, not whichever hash
// starts with 12), and 12 falls back to being an id prefix when this
// repository has no such number. Anything else is returned as given.
func (d Deps) landingRef(name string, repo *Repo, ref string) string {
n, ok := landingNumberIn(ref)
if !ok || d.LandingNumbers == nil {
return ref
}
d.landingNumbers(name, repo, nil) // a number read before any list was shown still resolves
if id, found, err := d.LandingNumbers.LandingNumbered(name, n); err == nil && found {
return id
}
return ref
}
// landingName is how a sentence names a landing: its number first, then
// the branch it proposes, or the branch alone where there is no number.
func landingName(n int, branch string) string {
if n > 0 {
return "!" + strconv.Itoa(n) + " " + branch
}
return branch
}