55fa35690dab
leave files the remote changed out of combine's replay, so an undone local edit no longer blocks every steal (3.0.27, codex review pending)
- time
- 2026-09-23 22:42 UTC · 2 weeks ago
- machine
- koh2
- agent
- claude-opus-5-5
- session
- koh 3 reliability campaign
- parent
- 84f2455205b5
↓ koh-55fa3569.tar.gzatlas at this save
files changed4 files
VERSION+1 −1src/sync/steal.v+27 −2src/sync/sync_test.v+44 −0v.mod+1 −1src/sync/steal.v+27 −2
@@ -179,6 +179,13 @@179179 mine_files = history.files_of(store, our_head.id)!180180 }181181 182+ // A file the remote changed is a clash, decided below, or one this machine183+ // left as it was or changed the same way, so the remote's version stands.184+ // Replaying this machine's steps on it could only conflict: an edit it185+ // later undid would no longer apply.186+ base_files := history.files_of(store, fetched.base)!187+ settled := changed_paths(base_files, remote_files)188+182189 // Take the remote's line first, so the replayed saves chain onto it.183190 history.set_head(store, fetched.head)!184191 mut current := remote_files.clone()@@ -189,12 +196,12 @@189196 []snapshot.FileEntry{}190197 }191198 target_files := history.files_of(store, save.id)!192- plan := apply.three_way(current, parent_files, target_files, [], clashes.map(it.path))199+ plan := apply.three_way(current, parent_files, target_files, [], settled)193200 if apply.conflicts(plan) {194201 return error('combine could not apply ${plan.conflict} — it is not a clash file')195202 }196203 if plan.applied.len == 0 {197- // Everything this save touched was a clash resolved as theirs.204+ // Everything this save touched was a file the remote changed.198205 continue199206 }200207 history.record_save(store, '', plan.files, history.SaveInput{@@ -277,6 +284,24 @@277284 }278285 }279286 return clashes287+}288+289+// changed_paths lists the paths whose entry differs between two trees.290+fn changed_paths(before []snapshot.FileEntry, after []snapshot.FileEntry) []string {291+ was := snapshot.index_files(before)292+ now := snapshot.index_files(after)293+ mut paths := []string{}294+ for path, entry in now {295+ if !same(entry, was[path] or { snapshot.FileEntry{} }) {296+ paths << path297+ }298+ }299+ for path, _ in was {300+ if path !in now {301+ paths << path302+ }303+ }304+ return paths280305 }281306 282307 // combine_clashes finds the files both lines changed, with the diff stat eachsrc/sync/sync_test.v+44 −0
@@ -264,6 +264,50 @@264264 assert line_head == history.head(b)!.id265265 }266266 267+// An agent that edits a file and then undoes the edit leaves no net change, so268+// the file is not a clash when the other machine changed it. Replaying the269+// edit itself must not stop the steal: the remote's version simply stands.270+// Likewise a file both machines ended up changing the same way.271+fn test_a_local_change_undone_does_not_block_a_combine() {272+ base := scratch('undone')!273+ defer {274+ os.rmdir_all(base) or {}275+ }276+ mut backend := remote.new_folder(os.join_path(base, 'remote'))!277+ mut a := open_repo(machine('undone_a')!)!278+ defer {279+ a.close()280+ }281+ mut b := open_repo(machine('undone_b')!)!282+ defer {283+ b.close()284+ }285+ put(a.root, 'f.txt', 'base\n')!286+ put(a.root, 'g.txt', 'base\n')!287+ commit(a, 'base')!288+ offer_now(a, mut backend)!289+ join(b, mut backend)!290+291+ put(a.root, 'f.txt', 'from A\n')!292+ put(a.root, 'g.txt', 'same\n')!293+ commit(a, 'A edits both')!294+ offer_now(a, mut backend)!295+296+ put(b.root, 'f.txt', 'tried on B\n')!297+ put(b.root, 'g.txt', 'first try\n')!298+ commit(b, 'B tries')!299+ put(b.root, 'f.txt', 'base\n')!300+ put(b.root, 'g.txt', 'same\n')!301+ put(b.root, 'h.txt', 'B\n')!302+ commit(b, 'B undoes f, lands on the same g, adds h')!303+304+ report := steal_now(b, mut backend)!305+ assert report.clashes.len == 0306+ assert os.read_file(os.join_path(b.root, 'f.txt'))! == 'from A\n'307+ assert os.read_file(os.join_path(b.root, 'g.txt'))! == 'same\n'308+ assert os.read_file(os.join_path(b.root, 'h.txt'))! == 'B\n'309+}310+267311 // A person deciding a clash may take minutes; the question must come before268312 // the write lock, or every agent's save on this repo waits and then fails.269313 fn test_clash_questions_are_asked_before_the_write_lock() {v.mod+1 −1
@@ -1,6 +1,6 @@11 Module {22 name: 'koh3'3- version: '3.0.26'3+ version: '3.0.27'44 description: 'One SQLite file per repo, one line of history.'55 license: 'MIT'66 }