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
5 files VERSION+1 −1src/snapshot/restore.v+24 −0src/snapshot/snapshot_test.v+3 −1tests/cli_test.v+18 −0v.mod+1 −1
VERSION+1 −1
@@ -1 +1 @@1-3.0.231+3.0.24

view file

src/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) ! {

view file

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'

view file

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() {

view file

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 }

view file