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
↓ koh-bba30a5a.tar.gzatlas at this save
files changed4 files
VERSION+1 −1src/exchange/exchange.v+26 −1src/exchange/exchange_test.v+47 −0v.mod+1 −1src/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 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 {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 }