b3e8bdf8a104
leave files that already match alone during a restore (3.0.24, codex review pending)
- time
- 2026-09-23 22:35 UTC · 2 weeks ago
- machine
- koh2
- agent
- claude-opus-5-5
- session
- koh 3 review fixes
- parent
- 71d55a793226
↓ koh-b3e8bdf8.tar.gzatlas at this save
files changed5 files
VERSION+1 −1src/snapshot/restore.v+24 −0src/snapshot/snapshot_test.v+3 −1tests/cli_test.v+18 −0v.mod+1 −1src/snapshot/restore.v+24 −0
@@ -49,10 +49,16 @@4949 }5050 }5151 mut written := 052+ cache := store.load_cache()!5253 for entry in target {5354 if ig.matches(entry.path, false) && os.exists(os.join_path(root, entry.path)) {5455 continue5556 }57+ // Rewriting a file that already matches would only reset its mtime,58+ // and make the next status hash it again.59+ if already_there(root, entry, cache) {60+ continue61+ }5662 data := store.get_object(entry.hash)!5763 write_file(os.join_path(root, entry.path), data, entry.executable)!5864 written++@@ -61,6 +67,24 @@6167 written: written6268 removed: removed6369 }70+}71+72+// already_there reports whether the file on disk has the target's content and73+// executable bit. The file cache answers without reading when the file is as74+// it was when last hashed.75+fn already_there(root string, entry FileEntry, cache map[string]storage.CacheRow) bool {76+ full := os.join_path(root, entry.path)77+ st := lstat(full) or { return false }78+ if st.kind != .file || st.size != entry.size || is_executable(st.mode) != entry.executable {79+ return false80+ }81+ if row := cache[entry.path] {82+ if row.size == st.size && row.mtime_ns == st.mtime_ns && row.ctime_ns == st.ctime_ns {83+ return row.hash == entry.hash84+ }85+ }86+ data := os.read_bytes(full) or { return false }87+ return storage.hash_hex(data) == entry.hash6488 }6589 6690 fn write_file(path string, data []u8, executable bool) ! {src/snapshot/snapshot_test.v+3 −1
@@ -147,7 +147,9 @@147147 report := restore_files(store, root, load(root), target,148148 ['extra.txt', '.kohignore', 'noise.log'])!149149 150- assert report.written == target.len150+ // The clobbered and the deleted file are written; the unchanged151+ // .kohignore is left as it is.152+ assert report.written == 2151153 assert report.removed == 1152154 assert os.read_file(full_path(root, 'keep.txt'))! == 'recorded\n'153155 assert os.read_file(full_path(root, 'gone/inside.txt'))! == 'also recorded\n'tests/cli_test.v+18 −0
@@ -268,6 +268,24 @@268268 assert !os.exists(marker)269269 }270270 271+// A restore rewrites only what differs. Rewriting every file resets every272+// mtime, so the next status has to hash the whole tree again.273+fn test_a_restore_leaves_unchanged_files_alone() {274+ dir := temp_repo('restore-skip')275+ defer { os.rmdir_all(dir) or {} }276+ init_repo(dir)277+ write(dir, 'a.txt', 'one\n')278+ write(dir, 'same.txt', 'never changes\n')279+ assert run(dir, 'save', 'one').code == 0280+ write(dir, 'a.txt', 'two\n')281+ assert run(dir, 'save', 'two').code == 0282+ long_ago := i64(1700000000)283+ os.utime(os.join_path(dir, 'same.txt'), long_ago, long_ago) or { panic(err) }284+ assert run(dir, 'undo', '--yes').code == 0285+ assert os.read_file(os.join_path(dir, 'a.txt'))! == 'one\n'286+ assert os.file_last_mod_unix(os.join_path(dir, 'same.txt')) == long_ago287+}288+271289 // `save` records the executable bit, so `status` has to see it too, or a272290 // chmod would be invisible until it turned up in a save.273291 fn test_status_sees_an_executable_bit_change() {v.mod+1 −1
@@ -1,6 +1,6 @@11 Module {22 name: 'koh3'3- version: '3.0.23'3+ version: '3.0.24'44 description: 'One SQLite file per repo, one line of history.'55 license: 'MIT'66 }