Artifact f8bce6d63ab8ae762ad3adde35612086424f66e7bee3ee01afe6f706d6dd16a7:
- File
internal/checkout/setmtime.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: 3482) [annotate] [blame] [check-ins using] [more...]
package checkout import ( "fmt" "time" libfossil "github.com/danmestas/go-libfossil/internal/fsltype" ) // fileCheckinTimesQuery is fossil's mtime_of_manifest_file (src/descendants.c) // for every file at once: for each artifact, the time of the newest check-in // that recorded it among vid and vid's primary ancestors (compute_ancestors // in direct mode), in seconds since 1970. The seconds are computed and cast // as fossil computes and reads them, so a time is truncated, not rounded. const fileCheckinTimesQuery = ` WITH RECURSIVE ancestor(rid, mtime) AS ( SELECT ?, mtime FROM event WHERE objid=? UNION SELECT plink.pid, event.mtime FROM ancestor, plink, event WHERE plink.cid=ancestor.rid AND event.objid=plink.pid AND plink.isPrim ORDER BY mtime DESC LIMIT 100000000 ) SELECT mlink.fid, CAST((max(event.mtime)-2440587.5)*86400 AS INTEGER) FROM mlink, event WHERE mlink.mid=event.objid AND mlink.mid IN (SELECT rid FROM ancestor) GROUP BY mlink.fid` // fileCheckinTimes returns, by file artifact, the seconds fossil's --setmtime // gives a file of version vid (fileCheckinTimesQuery). func (c *Checkout) fileCheckinTimes(vid libfossil.FslID) (map[int64]int64, error) { rows, err := c.repo.DB().Query(fileCheckinTimesQuery, int64(vid), int64(vid)) if err != nil { return nil, fmt.Errorf("file check-in times: %w", err) } defer rows.Close() out := map[int64]int64{} for rows.Next() { var fid, secs int64 if err := rows.Scan(&fid, &secs); err != nil { return nil, fmt.Errorf("file check-in times: %w", err) } out[fid] = secs } if err := rows.Err(); err != nil { return nil, fmt.Errorf("file check-in times: %w", err) } return out, nil } // setFileMTimes is fossil's --setmtime, vfile_check_signature with // CKSIG_SETMTIME (src/vfile.c): after the files of vid are brought up to date // by hash, every one still unchanged (chnged 0, 2 or 4) and present on disk // takes the time fileCheckinTimes gives its artifact. As in fossil it is // best-effort: a file whose time cannot be set keeps the one it has. func (c *Checkout) setFileMTimes(vid libfossil.FslID) error { if vid <= 0 { panic("checkout.setFileMTimes: vid must be positive") } if err := c.refreshChanged(vid); err != nil { return err } times, err := c.fileCheckinTimes(vid) if err != nil { return err } rows, err := c.db.Query(`SELECT pathname, rid FROM vfile WHERE vid=? AND NOT deleted AND rid>0 AND chnged IN (0, 2, 4)`, int64(vid)) if err != nil { return fmt.Errorf("set mtimes: %w", err) } defer rows.Close() set := map[string]int64{} for rows.Next() { var name string var rid int64 if err := rows.Scan(&name, &rid); err != nil { return fmt.Errorf("set mtimes: %w", err) } // An artifact no check-in in the ancestry recorded is absent from // times, and gets 0: fossil's max() over no rows is NULL, which it // reads as 0, setting the file to 1970. secs := times[rid] fullPath, err := c.safePath(name) if err != nil { continue } info, err := c.env.Storage.Stat(fullPath) if err != nil || !info.Mode().IsRegular() { continue } when := time.Unix(secs, 0) if c.env.Storage.Chtimes(fullPath, when, when) == nil { set[name] = recordableMTime(when, c.env.Clock.Now().Unix()) } } if err := rows.Err(); err != nil { return fmt.Errorf("set mtimes: %w", err) } // As fossil's scan, the time given a file is the one vfile records. return c.setRowMTimes(vid, set) }