Skip to content
quarry Artifact [ee707fe53f]
Login

Artifact [ee707fe53f]

Artifact ee707fe53f1f795f810c2e874432703f9719b918b3f0e7833d50d917763a37d4:


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
}

// 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
}

// numberTimeline writes the !N of the landing each entry names: the one
// a merge landed, or the landing an event row is about.
func (d Deps) numberTimeline(name string, repo *Repo, entries []index.TimelineEntry) {
	var nums map[string]int
	for i := range entries {
		id := entries[i].Landing
		if id == "" && (entries[i].Kind == "l" || entries[i].Kind == "lc") {
			id = entries[i].Ref
		}
		if id == "" {
			continue
		}
		if nums == nil {
			if nums = d.landingNumbers(name, repo, nil); nums == nil {
				return
			}
		}
		entries[i].LandingNumber = nums[id]
	}
}