71d55a793226
draw going-back previews from hashes instead of diff(1) per file (3.0.23, codex review pending)
- time
- 2026-09-23 22:34 UTC · 2 weeks ago
- machine
- koh2
- agent
- claude-opus-5-5
- session
- koh 3 review fixes
- parent
- 9323833c98e8
↓ koh-71d55a79.tar.gzatlas at this save
files changed5 files
VERSION+1 −1src/cli/cmd_back.v+1 −3src/snapshot/diff.v+46 −24tests/cli_test.v+26 −0v.mod+1 −1src/cli/cmd_back.v+1 −3
@@ -207,9 +207,7 @@207207 remove << file.path208208 }209209 }210- deltas := snapshot.diff_trees(store, from, snapshot.Tree{211- files: files212- })!210+ deltas := snapshot.changes(from.files, files)213211 return RestorePlan{214212 files: files215213 remove: removesrc/snapshot/diff.v+46 −24
@@ -32,14 +32,13 @@3232 3333 pub const binary_notice = 'Binary files differ'3434 35-// diff_trees reports every path whose content differs, in path order.36-pub fn diff_trees(store &storage.Store, from Tree, to Tree) ![]Delta {37- from_map := index_files(from.files)38- to_map := index_files(to.files)39- mut paths := []string{}40- for path in from_map.keys() {41- paths << path42- }35+// changes lists the paths that differ between two trees and how, in path36+// order, from the hashes alone: no file is read and diff(1) never runs. A37+// preview needs no more than this.38+pub fn changes(from []FileEntry, to []FileEntry) []Delta {39+ from_map := index_files(from)40+ to_map := index_files(to)41+ mut paths := from_map.keys()4342 for path in to_map.keys() {4443 if path !in from_map {4544 paths << path@@ -47,32 +46,55 @@4746 }4847 paths.sort()4948 mut deltas := []Delta{}49+ for path in paths {50+ before := from_map[path] or {51+ deltas << Delta{52+ path: path53+ kind: 'added'54+ }55+ continue56+ }57+ after := to_map[path] or {58+ deltas << Delta{59+ path: path60+ kind: 'removed'61+ }62+ continue63+ }64+ if before.hash != after.hash || before.executable != after.executable {65+ deltas << Delta{66+ path: path67+ kind: 'modified'68+ }69+ }70+ }71+ return deltas72+}73+74+// diff_trees reports every path whose content differs, in path order, with75+// its unified diff.76+pub fn diff_trees(store &storage.Store, from Tree, to Tree) ![]Delta {77+ from_map := index_files(from.files)78+ to_map := index_files(to.files)79+ mut deltas := []Delta{}5080 work := temp_workspace()!5181 defer {5282 os.rmdir_all(work) or {}5383 }54- for path in paths {55- has_before := path in from_map56- has_after := path in to_map57- if has_before && has_after && from_map[path].hash == to_map[path].hash {84+ for change in changes(from.files, to.files) {85+ // A change of the executable bit alone has no text to show.86+ if change.kind == 'modified' && from_map[change.path].hash == to_map[change.path].hash {5887 continue5988 }60- kind := if !has_before {61- 'added'62- } else if !has_after {63- 'removed'64- } else {65- 'modified'66- }6789 mut from_bytes := []u8{}68- if has_before {69- from_bytes = bytes_of(store, from, from_map[path])!90+ if change.kind != 'added' {91+ from_bytes = bytes_of(store, from, from_map[change.path])!7092 }7193 mut to_bytes := []u8{}72- if has_after {73- to_bytes = bytes_of(store, to, to_map[path])!94+ if change.kind != 'removed' {95+ to_bytes = bytes_of(store, to, to_map[change.path])!7496 }75- deltas << diff_in(work, path, kind, from_bytes, to_bytes)!97+ deltas << diff_in(work, change.path, change.kind, from_bytes, to_bytes)!7698 }7799 return deltas78100 }tests/cli_test.v+26 −0
@@ -242,6 +242,32 @@242242 assert os.read_file(os.join_path(other, '.env'))! == 'KEY=saved long ago\n'243243 }244244 245+// A going-back preview lists paths only. Running diff(1) per file to draw it246+// cost 27 s for 5,000 changed files, so it must not run diff at all.247+fn test_a_going_back_preview_does_not_run_diff() {248+ dir := temp_repo('preview-nodiff')249+ home := temp_repo('preview-nodiff-home')250+ defer {251+ os.rmdir_all(dir) or {}252+ os.rmdir_all(home) or {}253+ }254+ init_repo(dir)255+ write(dir, 'a.txt', 'one\n')256+ assert run_home(dir, home, 'save', 'one').code == 0257+ write(dir, 'a.txt', 'two\n')258+ assert run_home(dir, home, 'save', 'two').code == 0259+ fake := os.join_path(home, 'bin')260+ os.mkdir_all(fake) or { panic(err) }261+ marker := os.join_path(home, 'diff-ran')262+ os.write_file(os.join_path(fake, 'diff'), '#!/bin/sh\ntouch ${q(marker)}\nexit 2\n') or {263+ panic(err)264+ }265+ os.chmod(os.join_path(fake, 'diff'), 0o755) or { panic(err) }266+ assert run_home_with_path(dir, home, fake, 'undo', '--yes').code == 0267+ assert os.read_file(os.join_path(dir, 'a.txt'))! == 'one\n'268+ assert !os.exists(marker)269+}270+245271 // `save` records the executable bit, so `status` has to see it too, or a246272 // chmod would be invisible until it turned up in a save.247273 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.22'3+ version: '3.0.23'44 description: 'One SQLite file per repo, one line of history.'55 license: 'MIT'66 }