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
5 files VERSION+1 −1src/cli/cmd_back.v+1 −3src/snapshot/diff.v+46 −24tests/cli_test.v+26 −0v.mod+1 −1
VERSION+1 −1
@@ -1 +1 @@1-3.0.221+3.0.23

view file

src/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: remove

view file

src/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 }

view file

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

view file

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 }

view file