84f2455205b5
match strip paths exactly, not with case-blind LIKE wildcards (3.0.26, codex review pending)
- time
- 2026-09-23 22:41 UTC · 2 weeks ago
- machine
- koh2
- agent
- claude-opus-5-5
- session
- koh 3 reliability campaign
- parent
- 4fdcae077240
↓ koh-84f24552.tar.gzatlas at this save
files changed4 files
VERSION+1 −1src/sync/strip.v+4 −6src/sync/sync_test.v+26 −0v.mod+1 −1src/sync/strip.v+4 −6
@@ -222,18 +222,16 @@222222 return store.db.changes()223223 }224224 225-// match_clause is `covers` in SQL. LIKE's own wildcards are escaped by226-// bind_match, so a path containing `%` or `_` matches itself and nothing else.225+// match_clause is `covers` in SQL. LIKE would ignore ASCII case and treat `_`226+// and `%` as wildcards; a strip destroys, so the prefix is compared exactly.227227 fn match_clause(only_save string) string {228228 scope := if only_save == '' { '' } else { ' AND save_id = ?3' }229- return '(path = ?1 OR path LIKE ?2 ESCAPE \'\\\')${scope}'229+ return '(path = ?1 OR substr(path, 1, length(?2)) = ?2)${scope}'230230 }231231 232232 fn bind_match(stmt &storage.Stmt, path string, only_save string) ! {233- folder := path.trim_right('/')234- escaped := folder.replace('\\', '\\\\').replace('%', '\\%').replace('_', '\\_')235233 stmt.bind_text(1, path)!236- stmt.bind_text(2, '${escaped}/%')!234+ stmt.bind_text(2, '${path.trim_right('/')}/')!237235 if only_save != '' {238236 stmt.bind_text(3, only_save)!239237 }src/sync/sync_test.v+26 −0
@@ -616,6 +616,32 @@616616 assert events.strip_events(a)!.len == 1617617 }618618 619+// SQLite's LIKE ignores ASCII case, so a strip of `docs` must not reach into620+// `Docs/`, and `a_b` must not reach `aXb`: strip destroys, so it matches exactly.621+// The tree is built by hand: on macOS `Docs` and `docs` are one folder on disk,622+// but a save made on Linux can hold both.623+fn test_a_strip_matches_the_path_exactly() {624+ mut a := open_repo(machine('strip_exact')!)!625+ defer {626+ a.close()627+ }628+ hash := a.put_object('x\n'.bytes())!629+ mut files := []snapshot.FileEntry{}630+ for path in ['DOCS.txt', 'Docs/keep.txt', 'a_b/x.txt', 'aXb/y.txt', 'docs/secret.txt'] {631+ files << snapshot.FileEntry{632+ path: path633+ hash: hash634+ }635+ }636+ save := history.commit_files(a, '', files, history.SaveInput{637+ message: 'add all'638+ })!639+ assert strip_scope(a, 'docs', '')!.files == 1640+ strip_local(a, 'docs', '')!641+ strip_local(a, 'a_b', '')!642+ assert history.files_of(a, save.id)!.map(it.path) == ['DOCS.txt', 'Docs/keep.txt', 'aXb/y.txt']643+}644+619645 fn test_a_strip_reaches_the_remote_and_other_machines() {620646 base := scratch('strip_sync')!621647 defer {v.mod+1 −1
@@ -1,6 +1,6 @@11 Module {22 name: 'koh3'3- version: '3.0.25'3+ version: '3.0.26'44 description: 'One SQLite file per repo, one line of history.'55 license: 'MIT'66 }