bba30a5a6637

follow Kepr's .kepr/renamed marker when the remembered repo name has no head, remembering the new name (v3.2.27)

time
2026-10-02 01:56 UTC · 1 week ago
machine
Junes-MacBook-Pro.local
agent
claude-opus-5-5
session
follow repo renames
parent
1d8f88f99d15
4 files VERSION+1 −1src/exchange/exchange.v+26 −1src/exchange/exchange_test.v+47 −0v.mod+1 −1
VERSION+1 −1
@@ -1 +1 @@1-3.2.261+3.2.27

view file

src/exchange/exchange.v+26 −1
@@ -75,7 +75,7 @@7575 // repo so renaming the folder later changes nothing.7676 pub fn repo_name(store &storage.Store, mut backend remote.Backend) !string {7777 	if remembered := store.state(repo_state_key) {78-		return remembered78+		return follow_rename(store, mut backend, remembered)!7979 	}8080 	name := os.base(store.root).to_lower()8181 	// Before the clash check below: a remote this koh cannot read is a "update@@ -89,6 +89,31 @@8989 		}9090 	}9191 	store.set_state(repo_state_key, name)!92+	return name93+}94+95+// follow_rename follows the marker Kepr leaves when it renames a repo, `.kepr/renamed/<old>`96+// holding the new name (§30, 2026-10-01), for as long as the remembered name has no `head`, and97+// remembers where it led. A few hops cover a repo renamed more than once.98+fn follow_rename(store &storage.Store, mut backend remote.Backend, remembered string) !string {99+	mut name := remembered100+	for _ in 0 .. 5 {101+		if backend.get(head_key(name))!.found {102+			break103+		}104+		marker := backend.get('.kepr/renamed/${name}')!105+		next := marker.body.bytestr().trim_space()106+		if !marker.found || next == '' || next.contains('/') || next.contains('\\') || next in [107+			'.',108+			'..',109+		] {110+			break111+		}112+		name = next113+	}114+	if name != remembered {115+		store.set_state(repo_state_key, name)!116+	}92117 	return name93118 }94119 

view file

src/exchange/exchange_test.v+47 −0
@@ -150,6 +150,53 @@150150 	assert backend.get(head_key('haven'))!.found151151 }152152 153+fn test_an_offer_follows_a_rename_kepr_marked_on_the_remote() {154+	base := scratch('renamed')!155+	defer {156+		os.rmdir_all(base) or {}157+	}158+	mut a := open_repo(machine('renamed_a')!)!159+	defer {160+		a.close()161+	}162+	put(a.root, 'a.txt', 'one\n')!163+	make_save(a, 'first')!164+	remote_dir := os.join_path(base, 'remote')165+	mut backend := remote.new_folder(remote_dir)!166+	offer_now(a, mut backend)!167+	// What Kepr's rename leaves: the history under the new name, a marker, no old name.168+	os.mv(os.join_path(remote_dir, 'proj'), os.join_path(remote_dir, 'tool'))!169+	os.mkdir_all(os.join_path(remote_dir, '.kepr', 'renamed'), os.MkdirParams{})!170+	os.write_file(os.join_path(remote_dir, '.kepr', 'renamed', 'proj'), 'tool\n')!171+172+	put(a.root, 'a.txt', 'two\n')!173+	make_save(a, 'second')!174+	assert offer_now(a, mut backend)!.repo == 'tool'175+	assert a.state('remote_repo') or { '' } == 'tool'176+	assert backend.get(head_key('tool'))!.body.bytestr() == head_of(a).id177+	assert !backend.get(head_key('proj'))!.found178+}179+180+fn test_a_rename_marker_naming_a_path_is_not_followed() {181+	base := scratch('badmarker')!182+	defer {183+		os.rmdir_all(base) or {}184+	}185+	mut a := open_repo(machine('badmarker_a')!)!186+	defer {187+		a.close()188+	}189+	put(a.root, 'a.txt', 'one\n')!190+	make_save(a, 'first')!191+	remote_dir := os.join_path(base, 'remote')192+	mut backend := remote.new_folder(remote_dir)!193+	offer_now(a, mut backend)!194+	os.rmdir_all(os.join_path(remote_dir, 'proj'))!195+	os.mkdir_all(os.join_path(remote_dir, '.kepr', 'renamed'), os.MkdirParams{})!196+	os.write_file(os.join_path(remote_dir, '.kepr', 'renamed', 'proj'), '../elsewhere\n')!197+	assert offer_now(a, mut backend)!.repo == 'proj'198+}199+153200 fn test_steal_fast_forwards_a_machine_that_has_nothing_of_its_own() {154201 	base := scratch('forward')!155202 	defer {

view file

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

view file