259ca814c682

import cloned saves parent-first so a combined repo can be stolen fresh (3.0.5)

time
2026-09-23 21:21 UTC · 2 weeks ago
machine
koh2
agent
claude-opus-5-5
session
koh 3 review fixes
parent
19c02504e67a
4 files VERSION+1 −1src/sync/steal.v+31 −1src/sync/sync_test.v+27 −0v.mod+1 −1
VERSION+1 −1
@@ -1 +1 @@1-3.0.41+3.0.5

view file

src/sync/steal.v+31 −1
@@ -351,7 +351,7 @@351351 		fetched.remote_save_ids[record.id] = true352352 		fetched.saves << record353353 	}354-	fetched.saves.sort(a.created_at < b.created_at)354+	fetched.saves = parent_first(fetched.saves)!355355 	for key in event_keys {356356 		body := backend.get(key)!357357 		if body.found {@@ -359,4 +359,34 @@359359 		}360360 	}361361 	return fetched362+}363+364+// parent_first orders saves so every parent lands before its children, as the365+// foreign key on `parent` demands. Time cannot do it: a replayed save keeps its366+// original, older time but sits on a newer parent.367+fn parent_first(records []SaveRecord) ![]SaveRecord {368+	mut by_id := map[string]SaveRecord{}369+	for record in records {370+		by_id[record.id] = record371+	}372+	mut placed := map[string]bool{}373+	mut ordered := []SaveRecord{}374+	for record in records {375+		mut chain := []SaveRecord{}376+		mut cursor := record.id377+		for cursor != '' && cursor !in placed {378+			if chain.len > records.len {379+				return error('the save records on the remote form a loop')380+			}381+			current := by_id[cursor] or { return error('the remote is missing save ${cursor}') }382+			chain << current383+			cursor = current.parent384+		}385+		chain.reverse_in_place()386+		for current in chain {387+			placed[current.id] = true388+			ordered << current389+		}390+	}391+	return ordered362392 }

view file

src/sync/sync_test.v+27 −0
@@ -160,6 +160,33 @@160160 	assert backend.get(head_key('proj'))!.body.bytestr() == ahead.id161161 }162162 163+// A replayed save keeps its original time but sits on a newer parent, so a164+// clone cannot order saves by time.165+fn test_a_clone_imports_parents_before_children() {166+	base := scratch('clone_order')!167+	defer {168+		os.rmdir_all(base) or {}169+	}170+	mut backend := remote.new_folder(os.join_path(base, 'remote'))!171+	mut a := open_repo(machine('order_a')!)!172+	defer {173+		a.close()174+	}175+	put(a.root, 'a.txt', 'one\n')!176+	parent := commit(a, 'parent')!177+	child := history.commit_files(a, '', history.files_of(a, parent.id)!, history.SaveInput{178+		message:    'replayed, older than its parent'179+		created_at: parent.created_at - 3600180+	})!181+	offer_now(a, mut backend)!182+	mut b := open_repo(machine('order_b')!)!183+	defer {184+		b.close()185+	}186+	join(b, mut backend)!187+	assert history.head(b)!.id == child.id188+}189+163190 // A chmod is unsaved work too: a steal would restore the old mode over it.164191 fn test_steal_refuses_an_unsaved_executable_bit_change() {165192 	base := scratch('chmod')!

view file

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

view file