Artifact e65ef6e0e23f42f0a961f9971822844c0649167e2f2b828b4d1a015b2e0b6d60:
- File
simio/storage_mem.go
— part of check-in
[a71bf662ee]
at
2026-10-18 20:59:23
on branch main
— fix(checkout): record and trust vfile.mtime as fossil does (#444)
* fix(checkout): record and trust vfile.mtime as fossil does
go-libfossil wrote vfile.mtime = 0 everywhere and hashed every file on every scan, so it and fossil rehashed a go-libfossil checkout on every command.
The scan is now fossil's vfile_check_signature: a file whose size matches its artifact (blob.size of mrid) and whose mtime matches vfile.mtime is taken as unchanged without being read, unless ScanHash is given or the mtime-changes setting is off; the scan stores the file's mtime in whole seconds and runs its updates in one transaction. Status, commit, merge, revert and extract use the default scan, as fossil's commands do.
Mtimes are recorded where fossil records them: extract and update store the mtime of each file written with its artifact's content, an unchanged file keeps its mtime through LoadVFile (fossil's update transfer and in-place commit), committed files keep theirs, revert stores the restored file's, and SetMTime stores the time it gives. Undoing a rename drops the mtime, since the file was not checked.
One deliberate difference: an mtime not older than the current second is stored as 0, so the next scan (ours or fossil's) hashes the file and an edit that keeps the size within that second is still seen. A stored 0 is never trusted.
MemStorage now stamps each write with a new mtime from its own clock, as a filesystem does, so in-memory checkouts see same-size edits.
Status on the sqlite repository's 2,220-file checkout: about 250 ms before, about 30 ms after.
* test(checkout): pin the same-second guard against fossil
A same-size edit in the second a file was scanned is committed, and fossil 2.28 reading a row the guard stored as 0 reports the edit. Both tests fail with the guard removed. The guard's comment and the CHANGELOG name it as the one deliberate difference from vfile_check_signature.
* fix(checkout): record the mtime of each write as it happens
Extract and update statted their files only after every file was written and vfile reloaded, so an edit in between was recorded as the written content. They now stat each file right after writing it, as fossil's vfile_to_disk does, and carry that mtime to the row. Revert and the new writtenMTime read the clock before the stat, as the scan does.
A caller's Env without a Clock now gets the real clock, as Storage defaults to the OS; scans read it. The mtime-changes setting is read from the repository only: go-libfossil reads no global fossil settings, which the comment and CHANGELOG now say.
New tests: an edit after extract's or update's write is seen, a stored 0 is never trusted, and a simulated clock at a realistic time drives the mtime fast path. (user: [email protected], size: 6459) [annotate] [blame] [check-ins using] [more...]
package simio import ( "fmt" "io/fs" "os" "path/filepath" "sort" "strings" "time" ) // MemStorage is an in-memory Storage implementation for tests and browser fallback. type MemStorage struct { files map[string][]byte dirs map[string]bool mtimes map[string]time.Time writes int64 // stamps each write: the nth write is dated n seconds after 1970 } // NewMemStorage creates a new in-memory storage. func NewMemStorage() *MemStorage { return &MemStorage{ files: make(map[string][]byte), dirs: make(map[string]bool), mtimes: make(map[string]time.Time), } } // Stat returns file info for the given path. func (m *MemStorage) Stat(path string) (os.FileInfo, error) { if path == "" { panic("MemStorage.Stat: path must not be empty") } // Check files first if data, ok := m.files[path]; ok { return &memFileInfo{ name: filepath.Base(path), size: int64(len(data)), isDir: false, mtime: m.mtimes[path], }, nil } // Check directories if m.dirs[path] { return &memFileInfo{ name: filepath.Base(path), size: 0, isDir: true, }, nil } return nil, fmt.Errorf("stat %s: %w", path, os.ErrNotExist) } // Remove deletes the file or empty directory at the given path. func (m *MemStorage) Remove(path string) error { if path == "" { panic("MemStorage.Remove: path must not be empty") } if _, ok := m.files[path]; ok { delete(m.files, path) return nil } if m.dirs[path] { delete(m.dirs, path) return nil } return os.ErrNotExist } // MkdirAll creates the directory and all parent directories. func (m *MemStorage) MkdirAll(path string, perm os.FileMode) error { // Mark all parent paths as directories parts := strings.Split(path, string(filepath.Separator)) current := "" for _, part := range parts { if part == "" { continue } if current == "" { current = string(filepath.Separator) + part } else { current = filepath.Join(current, part) } m.dirs[current] = true } return nil } // ReadFile reads the file at the given path. Returns a copy of the data. func (m *MemStorage) ReadFile(path string) ([]byte, error) { if path == "" { panic("MemStorage.ReadFile: path must not be empty") } data, ok := m.files[path] if !ok { return nil, os.ErrNotExist } // Return a copy to prevent external modification result := make([]byte, len(data)) copy(result, data) return result, nil } // WriteFile writes data to the file at the given path. Stores a copy. func (m *MemStorage) WriteFile(path string, data []byte, perm os.FileMode) error { if path == "" { panic("MemStorage.WriteFile: path must not be empty") } if data == nil { panic("MemStorage.WriteFile: data must not be nil") } // Store a copy to prevent external modification stored := make([]byte, len(data)) copy(stored, data) m.files[path] = stored // A write moves the file's mtime, as on a filesystem; a clock of its own // keeps that deterministic and never repeats a time. m.writes++ m.mtimes[path] = time.Unix(m.writes, 0) return nil } // ReadDir returns directory entries for the given path. func (m *MemStorage) ReadDir(path string) ([]fs.DirEntry, error) { if path == "" { panic("MemStorage.ReadDir: path must not be empty") } // Build prefix for scanning prefix := path if !strings.HasSuffix(prefix, string(filepath.Separator)) { prefix += string(filepath.Separator) } // Track unique direct children (files and subdirectories) children := make(map[string]bool) childIsDir := make(map[string]bool) // Scan files for filePath := range m.files { if strings.HasPrefix(filePath, prefix) { remainder := strings.TrimPrefix(filePath, prefix) if remainder == "" { continue // Skip exact match } // Extract first path component sepIdx := strings.Index(remainder, string(filepath.Separator)) var childName string if sepIdx == -1 { // Direct child file childName = remainder childIsDir[childName] = false } else { // File in subdirectory childName = remainder[:sepIdx] childIsDir[childName] = true } children[childName] = true } } // Scan dirs for empty subdirectories for dirPath := range m.dirs { if strings.HasPrefix(dirPath, prefix) && dirPath != path { remainder := strings.TrimPrefix(dirPath, prefix) if remainder == "" { continue } sepIdx := strings.Index(remainder, string(filepath.Separator)) var childName string if sepIdx == -1 { childName = remainder } else { childName = remainder[:sepIdx] } children[childName] = true childIsDir[childName] = true } } // Check if path exists (either as dir or has children) if len(children) == 0 && !m.dirs[path] { return nil, os.ErrNotExist } // Build sorted entry list entries := make([]fs.DirEntry, 0, len(children)) for name := range children { entries = append(entries, &memDirEntry{ name: name, isDir: childIsDir[name], }) } // Sort by name to match os.ReadDir behavior sort.Slice(entries, func(i, j int) bool { return entries[i].Name() < entries[j].Name() }) return entries, nil } // Chtimes sets the access and modification times of the named file. func (m *MemStorage) Chtimes(path string, atime, mtime time.Time) error { if path == "" { panic("MemStorage.Chtimes: path must not be empty") } if _, ok := m.files[path]; !ok { return fmt.Errorf("chtimes %s: %w", path, os.ErrNotExist) } m.mtimes[path] = mtime return nil } // memDirEntry implements fs.DirEntry for in-memory directory entries. type memDirEntry struct { name string isDir bool } func (e *memDirEntry) Name() string { return e.name } func (e *memDirEntry) IsDir() bool { return e.isDir } func (e *memDirEntry) Type() fs.FileMode { if e.isDir { return fs.ModeDir } return 0 } func (e *memDirEntry) Info() (fs.FileInfo, error) { return &memFileInfo{ name: e.name, size: 0, isDir: e.isDir, }, nil } // memFileInfo implements os.FileInfo for in-memory files. type memFileInfo struct { name string size int64 isDir bool mtime time.Time } func (f *memFileInfo) Name() string { return f.name } func (f *memFileInfo) Size() int64 { return f.size } func (f *memFileInfo) Mode() os.FileMode { if f.isDir { return os.ModeDir | 0755 } return 0644 } func (f *memFileInfo) ModTime() time.Time { return f.mtime } func (f *memFileInfo) IsDir() bool { return f.isDir } func (f *memFileInfo) Sys() any { return nil }