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
↓ koh-ab6e63d2.tar.gzatlas at this save
files changed4 files
VERSION+1 −1src/snapshot/restore.v+16 −10tests/cli_test.v+28 −0v.mod+1 −1src/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 }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() {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 }