8aa3154c04f0

make status and steal see executable-bit changes the way save does (3.0.1)

time
2026-09-23 20:54 UTC · 2 weeks ago
machine
koh2
agent
claude-opus-5-5
session
koh 3 review fixes
parent
b09a48226a5b
6 files VERSION+1 −1src/cli/cmd_status.v+2 −1src/sync/steal.v+2 −13src/sync/sync_test.v+22 −0tests/cli_test.v+14 −0v.mod+1 −1
VERSION+1 −1
@@ -1 +1 @@1-3.0.01+3.0.1

view file

src/cli/cmd_status.v+2 −1
@@ -83,7 +83,8 @@8383 	for path in after.keys() {8484 		if path !in before {8585 			added << path86-		} else if before[path].hash != after[path].hash {86+		} else if before[path].hash != after[path].hash87+			|| before[path].executable != after[path].executable {8788 			modified << path8889 		}8990 	}

view file

src/sync/steal.v+2 −13
@@ -304,19 +304,8 @@304304 // dirty reports whether the working tree differs from `head` in a way a steal305305 // would overwrite.306306 fn dirty(store &storage.Store, head history.Save) !bool {307-	before := snapshot.index_files(history.files_of(store, head.id)!)308-	after := snapshot.index_files(snapshot.walk(store, store.root, snapshot.load(store.root))!)309-	for path in after.keys() {310-		if path !in before || before[path].hash != after[path].hash {311-			return true312-		}313-	}314-	for path in before.keys() {315-		if path !in after {316-			return true317-		}318-	}319-	return false307+	return !snapshot.same_tree(history.files_of(store, head.id)!, snapshot.walk(store, store.root,308+		snapshot.load(store.root))!)320309 }321310 322311 // fetch_repo pulls a whole repo — head, saves, objects and events — for

view file

src/sync/sync_test.v+22 −0
@@ -135,6 +135,28 @@135135 	assert history.head(b)!.id == fetched.head136136 }137137 138+// A chmod is unsaved work too: a steal would restore the old mode over it.139+fn test_steal_refuses_an_unsaved_executable_bit_change() {140+	base := scratch('chmod')!141+	defer {142+		os.rmdir_all(base) or {}143+	}144+	mut backend := remote.new_folder(os.join_path(base, 'remote'))!145+	mut a := open_repo(machine('chmod_a')!)!146+	defer {147+		a.close()148+	}149+	put(a.root, 'run.sh', 'echo hi\n')!150+	commit(a, 'script')!151+	offer_now(a, mut backend)!152+	os.chmod(os.join_path(a.root, 'run.sh'), 0o755)!153+	if _ := steal_now(a, mut backend) {154+		assert false, 'a chmod-only change should make the tree dirty'155+	} else {156+		assert err.msg() == dirty_tree157+	}158+}159+138160 fn test_divergence_without_clashes_keeps_both_machines_work() {139161 	base := scratch('diverge')!140162 	defer {

view file

tests/cli_test.v+14 −0
@@ -155,6 +155,20 @@155155 	assert !run(dir, 'status').out.contains('LOG.md')156156 }157157 158+// `save` records the executable bit, so `status` has to see it too, or a159+// chmod would be invisible until it turned up in a save.160+fn test_status_sees_an_executable_bit_change() {161+	dir := temp_repo('chmod')162+	defer { os.rmdir_all(dir) or {} }163+	init_repo(dir)164+	write(dir, 'run.sh', 'echo hi\n')165+	assert run(dir, 'save', 'script').code == 0166+	os.chmod(os.join_path(dir, 'run.sh'), 0o755) or { panic(err) }167+	status := run(dir, 'status', '--json')168+	assert status.out.contains('"dirty":true')169+	assert status.out.contains('"modified":["run.sh"]')170+}171+158172 fn test_log_and_diff() {159173 	dir := temp_repo('history')160174 	defer { os.rmdir_all(dir) or {} }

view file

v.mod+1 −1
@@ -1,6 +1,6 @@11 Module {22 	name: 'koh3'3-	version: '3.0.0'3+	version: '3.0.1'44 	description: 'One SQLite file per repo, one line of history.'55 	license: 'MIT'66 }

view file