Skip to content
go-libfossil Artifact [050eb6d0fe]
Login

Artifact [050eb6d0fe]

Artifact 050eb6d0fe395f70e9e2200057d44b813f91faad40726fbae26cf788e20dd96f:


package horizon

import (
	"database/sql"
	"errors"
	"fmt"
	"strings"

	"github.com/danmestas/go-libfossil/db"
	libfossil "github.com/danmestas/go-libfossil/internal/fsltype"
)

// ErrNameNotFound is returned when a horizon name resolves to no check-in.
var ErrNameNotFound = errors.New("horizon: name resolves to no check-in")

// NameTip is the name of the most recent check-in.
const NameTip = "tip"

// Resolved is the check-in a horizon name resolved to and how many steps
// back along primary parents were taken to reach it.
type Resolved struct {
	Hash  string
	Steps uint32
}

// ResolveName resolves a horizon name over q's check-ins as
// draft-fossil-horizon-00 ยง6.2.7 does: tip is the most recent check-in, a
// full hash names its check-in, and any other name, or a full hash naming no
// check-in, is the most recent check-in carrying an active sym-NAME tag,
// propagated or not. A tie on mtime goes to the lexicographically lowest
// full hash, never a local rid, so every origin resolves a name alike. From there it steps back up
// to depth times along primary parents, stopping early at a check-in with
// none, in one query bounded by the check-ins the repository holds.
func ResolveName(q db.Querier, name string, depth uint32) (Resolved, error) {
	if q == nil {
		panic("horizon.ResolveName: nil querier")
	}
	rid, err := namedCheckin(q, name)
	if err != nil {
		return Resolved{}, err
	}
	// One query walks back, at most depth steps and never more than there
	// are check-ins, so a corrupt cycle of primary parents ends too.
	var steps uint32
	err = q.QueryRow(`WITH RECURSIVE back(rid, n) AS (
			SELECT ?1, 0
			UNION ALL
			SELECT p.pid, back.n+1 FROM back
			JOIN plink p ON p.cid=back.rid AND p.isprim=1
			JOIN event e ON e.objid=p.pid AND e.type='ci'
			WHERE back.n < min(?2, (SELECT count(*) FROM event WHERE type='ci')))
		SELECT rid, n FROM back ORDER BY n DESC LIMIT 1`, rid, int64(depth)).Scan(&rid, &steps)
	if err != nil {
		return Resolved{}, fmt.Errorf("horizon: primary parents: %w", err)
	}
	if steps > depth {
		panic("horizon.ResolveName: walked past the depth")
	}
	var uuid string
	if err := q.QueryRow("SELECT uuid FROM blob WHERE rid=?", rid).Scan(&uuid); err != nil {
		return Resolved{}, fmt.Errorf("horizon: resolved check-in: %w", err)
	}
	return Resolved{Hash: uuid, Steps: steps}, nil
}

// namedCheckin is the check-in name names before any step back.
func namedCheckin(q db.Querier, name string) (libfossil.FslID, error) {
	if name == NameTip {
		return firstCheckin(q, `SELECT e.objid FROM event e JOIN blob b ON b.rid=e.objid
			WHERE e.type='ci' ORDER BY e.mtime DESC, b.uuid ASC LIMIT 1`)
	}
	if isFullHash(name) {
		rid, err := firstCheckin(q, `SELECT e.objid FROM blob b JOIN event e ON e.objid=b.rid AND e.type='ci'
			WHERE b.uuid=?`, strings.ToLower(name))
		if !errors.Is(err, ErrNameNotFound) {
			return rid, err
		}
	}
	return firstCheckin(q, `SELECT e.objid FROM tagxref x JOIN tag t ON t.tagid=x.tagid
		JOIN event e ON e.objid=x.rid AND e.type='ci' JOIN blob b ON b.rid=e.objid
		WHERE t.tagname=? AND x.tagtype>0 ORDER BY e.mtime DESC, b.uuid ASC LIMIT 1`, "sym-"+name)
}

// firstCheckin is the rid query's first row names, ErrNameNotFound for none.
func firstCheckin(q db.Querier, query string, args ...any) (libfossil.FslID, error) {
	var rid libfossil.FslID
	err := q.QueryRow(query, args...).Scan(&rid)
	if errors.Is(err, sql.ErrNoRows) {
		return 0, ErrNameNotFound
	}
	if err != nil {
		return 0, fmt.Errorf("horizon: resolve name: %w", err)
	}
	return rid, nil
}

// isFullHash reports a 40 or 64 digit hex string, of either case.
func isFullHash(s string) bool {
	if len(s) != 40 && len(s) != 64 {
		return false
	}
	for i := 0; i < len(s); i++ {
		c := s[i]
		if !(c >= '0' && c <= '9' || c >= 'a' && c <= 'f' || c >= 'A' && c <= 'F') {
			return false
		}
	}
	return true
}