Artifact dc4eee3d1ca3b38544b9b97549c291d14563822a44d272d6f0401dd5dff09656:
- File
internal/checkout/vfile.go
— part of check-in
[f5744359d3]
at
2026-04-19 16:30:54
on branch trunk
— Initial open source release
libfossil is a pure-Go implementation of the Fossil SCM, reading and writing the same .fossil SQLite repository format as upstream Fossil.
This is the first public release of code that previously lived in a private repository. The module path is github.com/danmestas/libfossil and the binary is named libfossil to avoid $PATH collisions with upstream fossil.
Included: - Repo lifecycle (create, clone), checkout/checkin, timeline, merge, diff, annotate, shun, tag, branch, bisect, stash, undo/redo - Sync-over-HTTP protocol (client and handler) - Two SQLite drivers: modernc (pure Go, default) and ncruces (wasm-capable) - Observer interfaces (SyncObserver, CheckoutObserver) with an optional OpenTelemetry implementation in observer/otel (separate submodule) - Deterministic simulation testing harness with BUGGIFY fault injection - Kong-based embeddable CLI under cli/ and a standalone binary under cmd/libfossil/ (user: [email protected], size: 8102) [annotate] [blame] [check-ins using]
package checkout import ( "context" "fmt" "os" "path/filepath" "strings" libfossil "github.com/danmestas/libfossil/internal/fsltype" "github.com/danmestas/libfossil/internal/blob" "github.com/danmestas/libfossil/internal/hash" "github.com/danmestas/libfossil/internal/manifest" ) // LoadVFile populates vfile table with entries from the specified checkin manifest. // If clear=true, deletes all vfile rows for OTHER versions (keeps only vid=rid). // Returns the count of missing blobs (files whose content is not in the repo). // // Panics if c is nil (TigerStyle precondition). func (c *Checkout) LoadVFile(rid libfossil.FslID, clear bool) (missing uint32, err error) { if c == nil { panic("checkout.LoadVFile: nil *Checkout") } // Clear other versions if requested if clear { if _, err := c.db.Exec("DELETE FROM vfile WHERE vid != ?", int64(rid)); err != nil { return 0, fmt.Errorf("checkout.LoadVFile: clear: %w", err) } } // Get file list from manifest files, err := manifest.ListFiles(c.repo, rid) if err != nil { return 0, fmt.Errorf("checkout.LoadVFile: %w", err) } // Insert each file into vfile for _, file := range files { // Look up blob RID blobRID, exists := blob.Exists(c.repo.DB(), file.UUID) if !exists { // Blob not found - increment missing count missing++ // Insert with rid=0 to mark as missing blobRID = 0 } // Determine isexe flag isexe := 0 if strings.Contains(file.Perm, "x") { isexe = 1 } // Insert vfile row (INSERT OR IGNORE handles duplicates) _, err := c.db.Exec(` INSERT OR IGNORE INTO vfile(vid, pathname, rid, mrid, mhash, isexe, islink) VALUES(?, ?, ?, ?, ?, ?, ?)`, int64(rid), file.Name, int64(blobRID), int64(blobRID), file.UUID, isexe, 0, // islink - symlinks not tracked yet ) if err != nil { return 0, fmt.Errorf("checkout.LoadVFile: insert %s: %w", file.Name, err) } } return missing, nil } // UnloadVFile removes all vfile entries for the specified version. // // Panics if c is nil (TigerStyle precondition). func (c *Checkout) UnloadVFile(rid libfossil.FslID) error { if c == nil { panic("checkout.UnloadVFile: nil *Checkout") } _, err := c.db.Exec("DELETE FROM vfile WHERE vid = ?", int64(rid)) if err != nil { return fmt.Errorf("checkout.UnloadVFile: %w", err) } return nil } // scanVFileEntry holds a single vfile row for scan processing. type scanVFileEntry struct { id int64 pathname string blobRid int64 mhash string chnged int64 deleted int64 } // scanSingleEntry checks a single vfile entry against the file on disk, // updating vfile.chnged as needed. Returns (changed, missing) booleans. func (c *Checkout) scanSingleEntry( e scanVFileEntry, flags ScanFlags, ) (changed, missing bool, err error) { if e.deleted != 0 { return false, false, nil } fullPath, err := c.safePath(e.pathname) if err != nil { return false, false, fmt.Errorf( "checkout.ScanChanges: path traversal in %s: %w", e.pathname, err, ) } data, err := c.env.Storage.ReadFile(fullPath) if err != nil { if os.IsNotExist(err) { return false, true, nil } return false, false, fmt.Errorf( "checkout.ScanChanges: read %s: %w", fullPath, err, ) } if flags&ScanHash == 0 { return false, false, nil } diskHash := hash.ContentHash(data, e.mhash) if diskHash != e.mhash { if e.chnged == 0 { if _, err := c.db.Exec( "UPDATE vfile SET chnged = 1 WHERE id = ?", e.id, ); err != nil { return false, false, fmt.Errorf( "checkout.ScanChanges: update chnged for %s: %w", e.pathname, err, ) } return true, false, nil } } else { if e.chnged != 0 { if _, err := c.db.Exec( "UPDATE vfile SET chnged = 0 WHERE id = ?", e.id, ); err != nil { return false, false, fmt.Errorf( "checkout.ScanChanges: reset chnged for %s: %w", e.pathname, err, ) } } } return false, false, nil } // ScanChanges detects modified and missing files in the checkout. // Walks the vfile table, checks each file on disk, and updates vfile.chnged accordingly. // // If flags includes ScanHash, hashes file content and compares to vfile.mhash. // Otherwise, uses mtime-based detection (future enhancement). // // Panics if c is nil (TigerStyle precondition). func (c *Checkout) ScanChanges(flags ScanFlags) error { if c == nil { panic("checkout.ScanChanges: nil *Checkout") } ctx := c.obs.ScanStarted(context.Background()) var filesScanned, filesChanged, filesMissing, filesExtra int defer func() { c.obs.ScanCompleted(ctx, ScanEnd{ FilesScanned: filesScanned, FilesChanged: filesChanged, FilesMissing: filesMissing, FilesExtra: filesExtra, }) }() rid, _, err := c.Version() if err != nil { return fmt.Errorf("checkout.ScanChanges: %w", err) } rows, err := c.db.Query(` SELECT id, pathname, rid, mhash, chnged, deleted FROM vfile WHERE vid = ? `, int64(rid)) if err != nil { return fmt.Errorf("checkout.ScanChanges: query vfile: %w", err) } var entries []scanVFileEntry for rows.Next() { var e scanVFileEntry if err := rows.Scan(&e.id, &e.pathname, &e.blobRid, &e.mhash, &e.chnged, &e.deleted); err != nil { rows.Close() return fmt.Errorf("checkout.ScanChanges: scan vfile row: %w", err) } entries = append(entries, e) } rows.Close() if err := rows.Err(); err != nil { return fmt.Errorf("checkout.ScanChanges: iterate vfile rows: %w", err) } // Build a set of tracked pathnames for extra-file detection. tracked := make(map[string]bool, len(entries)) for _, e := range entries { tracked[e.pathname] = true filesScanned++ changed, missing, scanErr := c.scanSingleEntry(e, flags) if scanErr != nil { return scanErr } if changed { filesChanged++ } if missing { filesMissing++ c.obs.Error(ctx, fmt.Errorf( "checkout.ScanChanges: file missing: %s", e.pathname, )) } } // Walk the checkout directory to detect EXTRA files (on disk but // not in vfile). Errors are non-fatal: log via observer and continue. diskFiles, walkErr := c.walkDir(c.dir) if walkErr != nil { c.obs.Error(ctx, walkErr) } else { for relPath := range diskFiles { if !tracked[relPath] { filesExtra++ } } } return nil } // maxWalkDepth is the maximum directory nesting depth walkDir will traverse. const maxWalkDepth = 256 // walkDirEntry is a stack element for iterative directory traversal. type walkDirEntry struct { path string depth int } // walkDir iteratively lists all files under a directory via Storage.ReadDir. // Returns a map of relative paths (relative to c.dir) to true. // This helper prepares for detecting EXTRA files (files on disk not in vfile). // // Uses an explicit stack instead of recursion (TigerStyle: no recursion). // Enforces a maximum traversal depth of maxWalkDepth. // // Panics if c is nil (TigerStyle precondition). func (c *Checkout) walkDir(dir string) (map[string]bool, error) { if c == nil { panic("checkout.walkDir: nil *Checkout") } result := make(map[string]bool) stack := []walkDirEntry{{path: dir, depth: 0}} for len(stack) > 0 { // Pop from stack top := stack[len(stack)-1] stack = stack[:len(stack)-1] if top.depth > maxWalkDepth { return nil, fmt.Errorf("walkDir: exceeded max depth %d at %s", maxWalkDepth, top.path) } entries, err := c.env.Storage.ReadDir(top.path) if err != nil { if os.IsNotExist(err) { continue } return nil, fmt.Errorf("walkDir: read %s: %w", top.path, err) } for _, entry := range entries { name := entry.Name() // Skip checkout databases and VCS directories switch name { case ".fslckout", "_FOSSIL_", ".git", ".hg", ".svn": continue } fullPath := filepath.Join(top.path, name) if entry.IsDir() { stack = append(stack, walkDirEntry{path: fullPath, depth: top.depth + 1}) } else { relPath, err := filepath.Rel(c.dir, fullPath) if err != nil { return nil, fmt.Errorf("walkDir: compute relative path for %s: %w", fullPath, err) } result[relPath] = true } } } return result, nil }