Artifact c6f84eda26a9b8b3a59d4f77aa74fcdf103ba6e9276a4e9eefbf948210dda768:
- File
internal/checkout/mtime.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: 3904) [annotate] [blame] [check-ins using]
package checkout import ( "fmt" "time" libfossil "github.com/danmestas/go-libfossil/internal/fsltype" ) // recordableMTime is the vfile.mtime a scan or a write stores for a file // whose mtime is mtime, in fossil's unit: whole seconds since 1970. // // This is the one deliberate difference from fossil. Fossil's // vfile_check_signature (src/vfile.c) stores the mtime as it is and later // skips hashing while size and mtime match, so it misses an edit that keeps // the size within the second it recorded. Here an mtime not older than now's // second is stored as 0, which makes the next scan, fossil's or ours, hash // the file once; a stored 0 is never trusted (scanSingleEntry). func recordableMTime(mtime time.Time, now int64) int64 { secs := mtime.Unix() if secs >= now { return 0 } return secs } // writtenMTime returns the vfile.mtime to record for the file at fullPath, // statting it right after it was written, as fossil's vfile_to_disk does. // now is read before the stat, so a write after the stat cannot slip past // the guard with an mtime equal to the one recorded. func (c *Checkout) writtenMTime(fullPath string) (int64, error) { now := c.env.Clock.Now().Unix() info, err := c.env.Storage.Stat(fullPath) if err != nil { return 0, fmt.Errorf("stat %s: %w", fullPath, err) } return recordableMTime(info.ModTime(), now), nil } // recordMTime stores mtime, from writtenMTime, on the row of version vid // named pathname when the row is based on artifact rid, the content that was // written. A row based on another artifact keeps its mtime, so a row only // ever vouches for the content it names. func (c *Checkout) recordMTime(vid libfossil.FslID, pathname string, rid, mtime int64) error { if rid <= 0 { panic("checkout.recordMTime: rid must be positive") } if _, err := c.db.Exec( "UPDATE vfile SET mtime = ? WHERE vid = ? AND pathname = ? AND mrid = ?", mtime, int64(vid), pathname, rid, ); err != nil { return fmt.Errorf("record mtime for %s: %w", pathname, err) } return nil } // inheritMTimes gives each row of version vid with no recorded mtime the one // a row of another version recorded for the same file, where that row vouches // for the same artifact: unchanged (chnged=0), not removed, and with the same // mrid. It is fossil's update moving an unchanged file's mtime to the target // version's row, and its commit keeping vfile rows in place. func (c *Checkout) inheritMTimes(vid libfossil.FslID) error { if vid <= 0 { panic("checkout.inheritMTimes: vid must be positive") } _, err := c.db.Exec(` UPDATE vfile SET mtime = old.mtime FROM vfile AS old WHERE vfile.vid = ? AND coalesce(vfile.mtime, 0) = 0 AND old.vid != vfile.vid AND old.pathname = vfile.pathname AND old.chnged = 0 AND NOT old.deleted AND old.mrid = vfile.mrid AND old.mtime != 0`, int64(vid)) if err != nil { return fmt.Errorf("inherit mtimes: %w", err) } return nil } // rowMTimes returns the recorded mtimes of version vid's rows named in names. func (c *Checkout) rowMTimes(vid libfossil.FslID, names []string) (map[string]int64, error) { out := make(map[string]int64, len(names)) for _, name := range names { var mtime int64 err := c.db.QueryRow( "SELECT CAST(coalesce(mtime, 0) AS INTEGER) FROM vfile WHERE vid = ? AND pathname = ?", int64(vid), name, ).Scan(&mtime) if err != nil { return nil, fmt.Errorf("read mtime of %s: %w", name, err) } if mtime != 0 { out[name] = mtime } } return out, nil } // setRowMTimes stores mtimes on version vid's rows, by pathname. func (c *Checkout) setRowMTimes(vid libfossil.FslID, mtimes map[string]int64) error { for name, mtime := range mtimes { if _, err := c.db.Exec( "UPDATE vfile SET mtime = ? WHERE vid = ? AND pathname = ?", mtime, int64(vid), name, ); err != nil { return fmt.Errorf("set mtime of %s: %w", name, err) } } return nil }