Artifact 3fd4aa24c63243b07ebe4fcd0dba834cadcb0580ae62d172e54c45ddbb873656:
- File
internal/checkout/checkout.go
— part of check-in
[3672f3d4fb]
at
2026-10-18 18:26:58
on branch feat/submanifest-reader
— feat(checkout): the sparse worktree of draft-fossil-sparse-cone-00 §4
A checkout created with a cone materializes, scans and tracks only the files it covers; vfile stays the full record, so fossil still reads the checkout. Commits carry uncovered files from the baseline by hash, update applies uncovered changes to the record only, and tracking, renaming, reverting or merging across the cone is refused naming it. SetCone widens and narrows. (user: [email protected], size: 11864) [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" "github.com/danmestas/go-libfossil/internal/cone" 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 cone *cone.Cone // nil for a dense checkout 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) } if err := writeCone(ckdb, opts.Cone); err != nil { ckdb.Close() return nil, fmt.Errorf("checkout.Create: %w", err) } c := &Checkout{ db: ckdb, repo: r, env: env, obs: obs, dir: dir, cone: opts.Cone, } 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") } sparse, err := readCone(ckdb) if err != nil { ckdb.Close() return nil, fmt.Errorf("checkout.Open: %w", err) } c := &Checkout{ db: ckdb, repo: r, env: env, obs: obs, dir: filepath.Dir(dbPath), cone: sparse, } 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_"} }