Store mtime in nanoseconds so a same-second rewrite is re-hashed (closes #12)
check / check (push) Waiting to run

scan recorded mtime in whole seconds, so a file rewritten in place at
the same size within the same second as its recorded mtime was classed
unchanged and kept its old hashes. The walk, a file given as an operand,
and the content phase's recheck now all use ModTime().UnixNano(); the
change rule itself is unchanged. The column stays INTEGER and
PRAGMA user_version stays 1, per the owner's ruling. A database written
by an earlier build holds seconds, so its files count as changed once,
at the next scan that covers them. README states the new resolution.

Model: opus-5-5
This commit is contained in:
2026-10-07 21:21:38 +00:00
parent 0064eba542
commit afb404b1c1
4 changed files with 59 additions and 7 deletions
+48 -3
View File
@@ -1285,8 +1285,53 @@ func TestSyncScanMtimeBump(t *testing.T) {
t.Fatalf("mtime-bump stats = %+v, want 1 updated", st)
}
if r := recordByPath(t, dbRecords(t, db), a); r.mtime != future.Unix() {
t.Fatalf("mtime = %d, want %d", r.mtime, future.Unix())
if r := recordByPath(t, dbRecords(t, db), a); r.mtime != future.UnixNano() {
t.Fatalf("mtime = %d, want %d", r.mtime, future.UnixNano())
}
}
// TestSyncScanSameSecondRewrite rewrites a file in place at the same
// size with an mtime later in the same second as the recorded one: the
// next scan must notice the change and re-hash the file.
func TestSyncScanSameSecondRewrite(t *testing.T) {
t.Parallel()
dir := t.TempDir()
db := openTestDB(t)
a := writeFile(t, dir, "a.bin", pattern(1, 500))
// b.bin shares the size of a.bin, so a.bin is hashed.
writeFile(t, dir, "b.bin", pattern(2, 500))
first := time.Date(2026, 1, 2, 3, 4, 5, 100_000_000, time.UTC)
err := os.Chtimes(a, first, first)
if err != nil {
t.Fatal(err)
}
syncTree(t, db, dir)
rewritten := pattern(3, 500)
writeFile(t, dir, "a.bin", rewritten)
later := first.Add(500 * time.Millisecond)
err = os.Chtimes(a, later, later)
if err != nil {
t.Fatal(err)
}
st := syncTree(t, db, dir)
if st != (scanStats{walked: 2, updated: 1, unchanged: 1}) {
t.Fatalf("rescan stats = %+v, want 1 updated 1 unchanged", st)
}
r := recordByPath(t, dbRecords(t, db), a)
if want := hexSum(rewritten); r.head != want || r.tail != want ||
r.content != want {
t.Fatalf("head, tail, content = %q, %q, %q, want %q for each",
r.head, r.tail, r.content, want)
}
}
@@ -1335,7 +1380,7 @@ func TestSyncScanSizeChange(t *testing.T) {
writeFile(t, dir, "f", pattern(1, 200))
mt := time.Unix(old.mtime, 0)
mt := time.Unix(0, old.mtime)
err := os.Chtimes(p, mt, mt)
if err != nil {