Artifact 900a23bddc0c396870cdf5c8e89aa6139d8effe13059aa355dd277627f971d26:
- File
internal/checkout/checkout.go
— part of check-in
[cba2d0c823]
at
2026-10-13 11:05:50
on branch main
— feat(resolve): resolve checkout-relative names and branch-type requests (#409)
* feat(resolve): resolve checkout-relative names and branch-type requests
Checkout.ResolveVersion resolves current, prev/previous and next relative to the open checkout, after tip and before the other forms, as fossil's symbolic_name_to_rid does; a keyword that names nothing falls through. Repo.ResolveBranchStart is the branch-type request: it resolves a name and walks back to the first check-in of its branch. Section 8.3 walks back from every result; fossil only from tag:NAME and plain names, and this follows the draft.
The repostate.8.3 checkout and branch-type cases run against the new entry points, with new cases for a keyword shadowing a tag, prev at a root, an untyped ckout, root:current and a branch-type hash. Tracked in #386.
* fix(resolve): match check-ins only for branch-type requests, merge via checkout
A branch-type request is rewritten to a check-in request (repo-state-01 Section 8.3), so a technote tagged with the branch name cannot answer it. A checkout with no recorded version resolves the ordinary forms, and merge resolves its version through the open checkout. Tracked in #386.
* fix(resolve): wrap a long test line (user: [email protected], size: 11531) [annotate] [blame] [check-ins using]
// Package checkout provides working directory management for Fossil repositories. // It implements file extraction, change tracking, staging, and commit operations, // ported from libfossil's checkout API. All filesystem operations go through // simio.Env for platform-agnostic operation (native, WASI, browser WASM). // // A Checkout is not safe for concurrent use. Callers must serialize // access to a single Checkout instance. package checkout import ( "context" "database/sql" "errors" "fmt" "path/filepath" "runtime" "strconv" "github.com/danmestas/go-libfossil/db" libfossil "github.com/danmestas/go-libfossil/internal/fsltype" "github.com/danmestas/go-libfossil/internal/repo" "github.com/danmestas/go-libfossil/simio" ) // Checkout represents an opened or created checkout database. // A Checkout owns the checkout DB (*sql.DB) but NOT the repo — the caller // owns the repo lifecycle. type Checkout struct { db *sql.DB repo *repo.Repo env *simio.Env obs Observer dir string checkinQueue map[string]bool // in-memory staging queue (session-scoped) } // initCheckoutVersion finds the tip checkin from the repo and sets // vvar checkout/checkout-hash in the checkout DB. Empty repos use the // unborn checkout state: checkout=0 with empty checkout-hash. func initCheckoutVersion(ckdb *sql.DB, r *repo.Repo) error { var tipRID int64 var tipUUID string err := r.DB().QueryRow(` SELECT l.rid, b.uuid FROM leaf l JOIN event e ON e.objid=l.rid JOIN blob b ON b.rid=l.rid WHERE e.type='ci' ORDER BY e.mtime DESC LIMIT 1 `).Scan(&tipRID, &tipUUID) if err != nil { if err == sql.ErrNoRows { return initUnbornCheckoutVersion(ckdb) } return fmt.Errorf("checkout.Create: query tip: %w", err) } if err := setVVar(ckdb, "checkout", strconv.FormatInt(tipRID, 10)); err != nil { return fmt.Errorf("checkout.Create: %w", err) } if err := setVVar(ckdb, "checkout-hash", tipUUID); err != nil { return fmt.Errorf("checkout.Create: %w", err) } return nil } func initUnbornCheckoutVersion(ckdb *sql.DB) error { if err := setVVar(ckdb, "checkout", "0"); err != nil { return fmt.Errorf("checkout.Create: %w", err) } if err := setVVar(ckdb, "checkout-hash", ""); err != nil { return fmt.Errorf("checkout.Create: %w", err) } return nil } func initUndoState(ckdb *sql.DB) error { if err := setVVar(ckdb, "undo_available", "0"); err != nil { return fmt.Errorf("checkout.Create: %w", err) } if err := setVVar(ckdb, "undo_checkout", "0"); err != nil { return fmt.Errorf("checkout.Create: %w", err) } return nil } // Create creates a new checkout database at dir/.fslckout (or dir/_FOSSIL_ on Windows), // finds the tip checkin from the repo, and sets vvar checkout/checkout-hash. // // Panics if r or dir is nil/empty (TigerStyle precondition). func Create(r *repo.Repo, dir string, opts CreateOpts) (*Checkout, error) { if r == nil { panic("checkout.Create: nil *repo.Repo") } if dir == "" { panic("checkout.Create: empty dir") } env := resolveEnv(opts.Env) obs := resolveObserver(opts.Observer) // Verify a SQLite driver is registered before creating the checkout DB. if db.RegisteredDriver() == nil { return nil, fmt.Errorf("checkout.Create: no SQLite driver registered") } // Determine DB path dbPath := filepath.Join(dir, PreferredDBName()) // Create parent directory if needed if err := env.Storage.MkdirAll(dir, 0o755); err != nil { return nil, fmt.Errorf("checkout.Create: mkdir: %w", err) } // Open the checkout database ckdb, err := db.OpenSQL(dbPath, db.OpenConfig{}, nil) if err != nil { return nil, fmt.Errorf("checkout.Create: sql.Open: %w", err) } // Ensure tables exist if err := EnsureTables(ckdb); err != nil { ckdb.Close() return nil, fmt.Errorf("checkout.Create: %w", err) } // Find the tip checkin and set vvar checkout/checkout-hash if err := initCheckoutVersion(ckdb, r); err != nil { ckdb.Close() return nil, err } if err := initUndoState(ckdb); err != nil { ckdb.Close() return nil, err } // Set the repository vvar so tools can find the repo. if err := setVVar(ckdb, "repository", r.Path()); err != nil { ckdb.Close() return nil, fmt.Errorf("checkout.Create: %w", err) } c := &Checkout{ db: ckdb, repo: r, env: env, obs: obs, dir: dir, } if err := c.loadInitialVFile(); err != nil { ckdb.Close() return nil, err } return c, nil } // loadInitialVFile loads vfile for the version Create recorded. A tip that a // partial sync has not finished is no reason to fail: the caller may be about // to extract an older, complete version. vfile then stays empty rather than // claiming files the repo cannot produce. func (c *Checkout) loadInitialVFile() error { if c == nil { panic("checkout.loadInitialVFile: nil *Checkout") } if c.obs == nil { panic("checkout.loadInitialVFile: nil observer") } rid, _, err := c.Version() if err != nil { return err } if rid == 0 { return nil // Empty repository: nothing to load. } err = c.LoadVFile(rid, true) if errors.Is(err, errMissingContent) { // Tolerated, but not silently: observers learn why vfile is empty. c.obs.Error(context.Background(), fmt.Errorf("checkout.Create: tip not loaded: %w", err)) return nil } return err } // resolveEnv fills in the defaults Create and Open share: the real // environment when none is given, and OS storage when it has none. func resolveEnv(env *simio.Env) *simio.Env { if env == nil { env = simio.RealEnv() } if env.Storage == nil { env.Storage = simio.OSStorage{} } return env } // Open opens an existing checkout database. If opts.SearchParents is true, // it searches parent directories for the checkout DB using FindCheckoutDB. // // Panics if r or dir is nil/empty (TigerStyle precondition). func Open(r *repo.Repo, dir string, opts OpenOpts) (*Checkout, error) { if r == nil { panic("checkout.Open: nil *repo.Repo") } if dir == "" { panic("checkout.Open: empty dir") } env := resolveEnv(opts.Env) obs := resolveObserver(opts.Observer) // Verify a SQLite driver is registered before opening the checkout DB. if db.RegisteredDriver() == nil { return nil, fmt.Errorf("checkout.Open: no SQLite driver registered") } // Find the checkout DB dbPath, err := FindCheckoutDB(env.Storage, dir, opts.SearchParents) if err != nil { return nil, fmt.Errorf("checkout.Open: %w", err) } // Open the checkout database ckdb, err := db.OpenSQL(dbPath, db.OpenConfig{}, nil) if err != nil { return nil, fmt.Errorf("checkout.Open: sql.Open: %w", err) } // Ping to validate the connection if err := ckdb.Ping(); err != nil { ckdb.Close() return nil, fmt.Errorf("checkout.Open: ping: %w", err) } // Verify vvar checkout exists checkoutVal, err := getVVar(ckdb, "checkout") if err != nil { ckdb.Close() return nil, fmt.Errorf("checkout.Open: %w", err) } if checkoutVal == "" { ckdb.Close() return nil, fmt.Errorf("checkout.Open: vvar checkout not found") } c := &Checkout{ db: ckdb, repo: r, env: env, obs: obs, dir: filepath.Dir(dbPath), } if err := c.ValidateFingerprint(); err != nil { ckdb.Close() return nil, fmt.Errorf("checkout.Open: %w", err) } return c, nil } // Close closes the checkout database. Does NOT close the repo. func (c *Checkout) Close() error { if c == nil { panic("checkout.Close: nil *Checkout") } if c.db == nil { panic("checkout.Close: nil *sql.DB (already closed?)") } return c.db.Close() } // Dir returns the checkout directory. func (c *Checkout) Dir() string { if c == nil { panic("checkout.Dir: nil *Checkout") } return c.dir } // Repo returns the associated repo. func (c *Checkout) Repo() *repo.Repo { if c == nil { panic("checkout.Repo: nil *Checkout") } return c.repo } // ErrNoVersion is returned (wrapped) by Version when the checkout database // records no checked-out version, as after an open with nothing to check out. var ErrNoVersion = errors.New("checkout: no checkout version") // Version returns the current checkout version (RID and UUID). func (c *Checkout) Version() (libfossil.FslID, string, error) { if c == nil { panic("checkout.Version: nil *Checkout") } ridStr, err := getVVar(c.db, "checkout") if err != nil { return 0, "", fmt.Errorf("checkout.Version: %w", err) } if ridStr == "" { return 0, "", fmt.Errorf("checkout.Version: vvar checkout not set: %w", ErrNoVersion) } rid64, err := strconv.ParseInt(ridStr, 10, 64) if err != nil { return 0, "", fmt.Errorf("checkout.Version: parse RID: %w", err) } if rid64 < 0 { return 0, "", fmt.Errorf("checkout.Version: negative checkout RID %d", rid64) } uuid, err := getVVar(c.db, "checkout-hash") if err != nil { return 0, "", fmt.Errorf("checkout.Version: %w", err) } if rid64 != 0 && uuid == "" { return 0, "", fmt.Errorf("checkout.Version: vvar checkout-hash not set") } if rid64 == 0 && uuid != "" { return 0, "", fmt.Errorf("checkout.Version: unborn checkout has non-empty checkout-hash") } return libfossil.FslID(rid64), uuid, nil } // ValidateFingerprint verifies that the checkout-hash vvar matches the blob uuid in the repo. func (c *Checkout) ValidateFingerprint() error { if c == nil { panic("checkout.ValidateFingerprint: nil *Checkout") } rid, uuid, err := c.Version() if err != nil { return fmt.Errorf("checkout.ValidateFingerprint: %w", err) } if rid == 0 { if uuid != "" { return fmt.Errorf("checkout.ValidateFingerprint: unborn checkout has non-empty checkout-hash") } return nil } // Query the repo for the blob uuid var repoUUID string err = c.repo.DB().QueryRow("SELECT uuid FROM blob WHERE rid = ?", rid).Scan(&repoUUID) if err != nil { return fmt.Errorf("checkout.ValidateFingerprint: query blob: %w", err) } if repoUUID != uuid { return fmt.Errorf( "checkout.ValidateFingerprint: mismatch (checkout: %s, repo: %s)", uuid, repoUUID, ) } return nil } // maxParentSearchDepth is the maximum number of parent directories to search. const maxParentSearchDepth = 256 // FindCheckoutDB searches for a checkout database (.fslckout or _FOSSIL_) in dir, // and optionally in parent directories if searchParents is true. // Returns the full path to the DB file. func FindCheckoutDB(storage simio.Storage, dir string, searchParents bool) (string, error) { if storage == nil { panic("checkout.FindCheckoutDB: nil Storage") } if dir == "" { panic("checkout.FindCheckoutDB: empty dir") } currentDir := dir for depth := 0; depth <= maxParentSearchDepth; depth++ { // Try each DB name for _, name := range DBNames() { dbPath := filepath.Join(currentDir, name) if _, err := storage.Stat(dbPath); err == nil { return dbPath, nil } } if !searchParents { break } // Move to parent directory parentDir := filepath.Dir(currentDir) if parentDir == currentDir { // Reached the root break } currentDir = parentDir } return "", fmt.Errorf("checkout.FindCheckoutDB: no checkout DB found in %s", dir) } // PreferredDBName returns the preferred checkout DB name based on the OS. // Returns "_FOSSIL_" on Windows, ".fslckout" elsewhere. func PreferredDBName() string { if runtime.GOOS == "windows" { return "_FOSSIL_" } return ".fslckout" } // DBNames returns all possible checkout DB names, with the preferred name first. func DBNames() []string { if runtime.GOOS == "windows" { return []string{"_FOSSIL_", ".fslckout"} } return []string{".fslckout", "_FOSSIL_"} }