ab6e63d2bc60

never overwrite an ignored file on disk during a restore (3.0.19, codex review pending)

time
2026-09-23 22:30 UTC · 2 weeks ago
machine
koh2
agent
claude-opus-5-5
session
koh 3 review fixes
parent
340f37f875a6
4 files VERSION+1 −1src/snapshot/restore.v+16 −10tests/cli_test.v+28 −0v.mod+1 −1
VERSION+1 −1
@@ -1 +1 @@1-3.0.181+3.0.19

view file

src/snapshot/restore.v+16 −10
@@ -1,9 +1,9 @@11 //! Writing a tree back onto disk.22 //!3-//! A restore writes exactly the target's files and deletes the tracked files4-//! the target does not have. Ignored paths are never touched, and every file5-//! lands through a temp file and a rename, so an interrupted restore leaves a6-//! readable tree rather than half-written files.3+//! A restore writes the target's files and deletes the tracked files the4+//! target does not have. An ignored file already on disk is never overwritten5+//! or deleted, and every file lands through a temp file and a rename, so an6+//! interrupted restore leaves a readable tree rather than half-written files.77 88 module snapshot99 @@ -16,11 +16,12 @@1616 	removed int1717 }1818 19-// restore_files writes exactly `target` into the working tree and deletes the20-// paths in `remove` (tracked files the target does not have). Ignored paths are21-// never touched — not written, not deleted — even if a caller asks. Every file22-// lands through a temp file and a rename, so an interrupted restore leaves a23-// readable tree rather than half-written files.19+// restore_files writes `target` into the working tree and deletes the paths in20+// `remove` (tracked files the target does not have). An ignored file already21+// on disk is left as it is, even when the target saved an older copy: it may22+// hold work no save has. A missing one is written. Every file lands through a23+// temp file and a rename, so an interrupted restore leaves a readable tree24+// rather than half-written files.2425 pub fn restore_files(store &storage.Store, root string, ig Ignore, target []FileEntry, remove []string) !RestoreReport {2526 	mut wanted := map[string]bool{}2627 	for entry in target {@@ -47,12 +48,17 @@4748 			}4849 		}4950 	}51+	mut written := 05052 	for entry in target {53+		if ig.matches(entry.path, false) && os.exists(os.join_path(root, entry.path)) {54+			continue55+		}5156 		data := store.get_object(entry.hash)!5257 		write_file(os.join_path(root, entry.path), data, entry.executable)!58+		written++5359 	}5460 	return RestoreReport{55-		written: target.len61+		written: written5662 		removed: removed5763 	}5864 }

view file

tests/cli_test.v+28 −0
@@ -214,6 +214,34 @@214214 	assert !os.exists(old)215215 }216216 217+// A file saved long ago and ignored since holds work no save has. Loading the218+// old save must not write over it; the document says a restore never touches219+// ignored files.220+fn test_a_restore_never_overwrites_an_ignored_file() {221+	dir := temp_repo('ignored-restore')222+	defer { os.rmdir_all(dir) or {} }223+	init_repo(dir)224+	write(dir, '.env', 'KEY=saved long ago\n')225+	first := run(dir, 'save', 'with env', '--json').out.all_after('"id":"')[..12]226+	write(dir, '.kohignore', '.env\n')227+	assert run(dir, 'save', 'ignore env').code == 0228+	write(dir, '.env', 'KEY=never saved\n')229+	assert run(dir, 'load', first, '--yes').code == 0230+	assert os.read_file(os.join_path(dir, '.env'))! == 'KEY=never saved\n'231+232+	// Where the ignored file is missing, the restore still brings it back.233+	other := temp_repo('ignored-restore-missing')234+	defer { os.rmdir_all(other) or {} }235+	init_repo(other)236+	write(other, '.env', 'KEY=saved long ago\n')237+	again := run(other, 'save', 'with env', '--json').out.all_after('"id":"')[..12]238+	write(other, '.kohignore', '.env\n')239+	assert run(other, 'save', 'ignore env').code == 0240+	os.rm(os.join_path(other, '.env')) or { panic(err) }241+	assert run(other, 'load', again, '--yes').code == 0242+	assert os.read_file(os.join_path(other, '.env'))! == 'KEY=saved long ago\n'243+}244+217245 // `save` records the executable bit, so `status` has to see it too, or a218246 // chmod would be invisible until it turned up in a save.219247 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.18'3+	version: '3.0.19'44 	description: 'One SQLite file per repo, one line of history.'55 	license: 'MIT'66 }

view file