f053194e7074
remove dead sync and history code, move doctor SQL into storage, drop forwarding colour wrappers (3.0.22, codex review pending)
- time
- 2026-09-23 22:32 UTC · 2 weeks ago
- machine
- koh2
- agent
- claude-opus-5-5
- session
- koh 3 review fixes
- parent
- 65f2423754f1
↓ koh-f053194e.tar.gzatlas at this save
files changed15 files
src/cli/cmd_back.v+3 −3src/cli/cmd_diff.v+2 −2src/cli/cmd_doctor.v+2 −37src/cli/cmd_flag.v+3 −3src/cli/cmd_pluck.v+3 −3src/cli/cmd_remote.v+3 −2src/cli/cmd_status.v+4 −3src/cli/cmd_steal.v+2 −2src/cli/cmd_strip.v+4 −4src/cli/output.v+0 −16src/history/history.v+0 −14src/storage/objects.v+34 −0src/sync/steal.v+1 −3src/sync/sync.v+7 −30src/sync/sync_test.v+3 −3src/cli/cmd_back.v+3 −3
@@ -219,12 +219,12 @@219219 220220 fn change_mark(kind string) string {221221 if kind == 'added' {222- return green('+')222+ return fmt.green('+')223223 }224224 if kind == 'removed' {225- return red('-')225+ return fmt.red('-')226226 }227- return amber('~')227+ return fmt.amber('~')228228 }229229 230230 struct BackJson {src/cli/cmd_diff.v+2 −2
@@ -46,8 +46,8 @@4646 added += delta.added4747 removed += delta.removed4848 }49- note('${fmt.plural(deltas.len, 'file')} changed · ${green('+' + added.str())} ${red('-' +50- removed.str())}')49+ note('${fmt.plural(deltas.len, 'file')} changed · ${fmt.green('+' + added.str())} ${fmt.red(50+ '-' + removed.str())}')5151 return 05252 }5353 src/cli/cmd_doctor.v+2 −37
@@ -11,7 +11,6 @@1111 import os1212 import time1313 import src.history14-import src.storage1514 1615 pub fn cmd_doctor(raw []string) int {1716 args := parse('doctor', raw, Options{@@ -27,8 +26,8 @@2726 }2827 2928 report.problems << store.integrity_check() or { return refuse(args.json(), err.msg()) }30- report.problems << check_objects(store) or { return refuse(args.json(), err.msg()) }31- report.problems << check_save_files(store) or { return refuse(args.json(), err.msg()) }29+ report.problems << store.check_objects() or { return refuse(args.json(), err.msg()) }30+ report.problems << store.check_save_files() or { return refuse(args.json(), err.msg()) }3231 3332 report.objects = int(store.object_count() or { 0 })3433 report.saves = int(history.save_count(store) or { 0 })@@ -64,40 +63,6 @@6463 return 16564 }6665 return 067-}68-69-// check_objects reads every object and hashes it back. Nothing else proves the70-// store is intact; the hash is the point of the store.71-fn check_objects(store &storage.Store) ![]string {72- mut problems := []string{}73- mut stmt := store.db.prepare('SELECT hash FROM objects ORDER BY hash')!74- defer {75- stmt.finalize()76- }77- mut hashes := []string{}78- for stmt.step()! {79- hashes << stmt.text(0)80- }81- for hash in hashes {82- store.get_object(hash) or { problems << 'object ${hash}: ${err.msg()}' }83- }84- return problems85-}86-87-// check_save_files catches a save naming an object that is gone — the one88-// inconsistency the schema cannot prevent on its own, because objects are89-// shared between saves.90-fn check_save_files(store &storage.Store) ![]string {91- mut problems := []string{}92- mut stmt := store.db.prepare('SELECT DISTINCT f.save_id, f.hash FROM save_files f93- LEFT JOIN objects o ON o.hash = f.hash WHERE o.hash IS NULL')!94- defer {95- stmt.finalize()96- }97- for stmt.step()! {98- problems << 'save ${stmt.text(0)} needs missing object ${stmt.text(1)}'99- }100- return problems10166 }10267 10368 // peek_age is how old a peek folder must be before doctor clears it.src/cli/cmd_flag.v+3 −3
@@ -7,8 +7,8 @@77 module cli88 99 import json10-import src.fmt1110 import src.events11+import src.fmt1212 import src.history1313 import src.storage1414 @@ -82,9 +82,9 @@8282 }8383 for entry in flagged {8484 save := history.by_id(store, entry.save_id) or { continue }85- mut line := ' ${amber('⚑')} ${dim('[' + save.id + ']')} ${fmt.stamp(save.created_at)} ${save.message}'85+ mut line := ' ${fmt.amber('⚑')} ${fmt.dim('[' + save.id + ']')} ${fmt.stamp(save.created_at)} ${save.message}'8686 if entry.reason != '' {87- line += ' ${dim(entry.reason)}'87+ line += ' ${fmt.dim(entry.reason)}'8888 }8989 println(line)9090 }src/cli/cmd_pluck.v+3 −3
@@ -8,8 +8,8 @@88 module cli99 1010 import json11-import src.fmt1211 import src.apply12+import src.fmt1313 import src.history1414 import src.snapshot1515 import src.storage@@ -59,7 +59,7 @@5959 }6060 ok('${verb} [${source.id}] would change:')6161 for path in plan.applied {62- println(' ${amber('~')} ${path}')62+ println(' ${fmt.amber('~')} ${path}')6363 }6464 note('${fmt.plural(plan.applied.len, 'file')} — run again without --dry-run to apply')6565 return 0@@ -67,7 +67,7 @@6767 6868 ok('${verb}: [${source.id}] ${source.message}')6969 for path in plan.applied {70- println(' ${amber('~')} ${path}')70+ println(' ${fmt.amber('~')} ${path}')7171 }7272 note('${fmt.plural(plan.applied.len, 'file')} change')7373 approved := confirm(args.flag('yes')) or { return refuse(args.json(), err.msg()) }src/cli/cmd_remote.v+3 −2
@@ -10,6 +10,7 @@1010 1111 import json1212 import os13+import src.fmt1314 import src.machine1415 import src.remote1516 @@ -136,8 +137,8 @@136137 return fail('setting up a remote needs a terminal, or use: koh remote folder <path>, koh remote s3 <endpoint> <bucket> <region>')137138 }138139 println(' choose a remote:')139- println(' ${amber('1')} folder a directory, on a disk or a NAS')140- println(' ${amber('2')} s3 an S3-compatible bucket')140+ println(' ${fmt.amber('1')} folder a directory, on a disk or a NAS')141+ println(' ${fmt.amber('2')} s3 an S3-compatible bucket')141142 print(' which? [1] ')142143 if os.get_line().trim_space() == '2' {143144 return wizard_s3()src/cli/cmd_status.v+4 −3
@@ -3,6 +3,7 @@33 module cli44 55 import json6+import src.fmt67 import src.history78 import src.snapshot89 import src.storage@@ -40,13 +41,13 @@4041 return 04142 }4243 for path in changes.added {43- println(' ${green('+')} ${path}')44+ println(' ${fmt.green('+')} ${path}')4445 }4546 for path in changes.modified {46- println(' ${amber('~')} ${path}')47+ println(' ${fmt.amber('~')} ${path}')4748 }4849 for path in changes.removed {49- println(' ${red('-')} ${path}')50+ println(' ${fmt.red('-')} ${path}')5051 }5152 note('${changes.added.len} added · ${changes.modified.len} modified · ${changes.removed.len} removed')5253 if changes.ignore_changed {src/cli/cmd_steal.v+2 −2
@@ -105,8 +105,8 @@105105 // ask_clash is the per-file question a terminal gets. Only a lowercase y-style106106 // answer is needed here: `m` for mine, `t` for theirs.107107 fn ask_clash(clash sync.Clash) sync.Side {108- println(' ${amber('both changed')} ${clash.path} ${clash.stat}')109- print(' ${amber('m')}ine or ${amber('t')}heirs? [t] ')108+ println(' ${fmt.amber('both changed')} ${clash.path} ${clash.stat}')109+ print(' ${fmt.amber('m')}ine or ${fmt.amber('t')}heirs? [t] ')110110 answer := os.get_line().trim_space()111111 if answer == 'm' {112112 return .minesrc/cli/cmd_strip.v+4 −4
@@ -15,9 +15,9 @@1515 import src.fmt1616 import src.history1717 import src.machine18-import src.sync1918 import src.snapshot2019 import src.storage20+import src.sync2121 2222 pub fn cmd_strip(raw []string) int {2323 args := parse('strip', raw, Options{@@ -53,10 +53,10 @@5353 note('${path} is already stripped here — finishing the remote')5454 } else {5555 where := if only_save == '' { 'every save' } else { 'save ${only_save}' }56- println(' ${red(fmt.glyph_fail)} this cannot be undone — strip is the only command that destroys information')57- println(' ${amber('!')} ${fmt.plural(scope.files, 'file')} in ${fmt.plural(scope.saves,56+ println(' ${fmt.red(fmt.glyph_fail)} this cannot be undone — strip is the only command that destroys information')57+ println(' ${fmt.amber('!')} ${fmt.plural(scope.files, 'file')} in ${fmt.plural(scope.saves,5858 'save')}, ${fmt.plural(scope.objects, 'object')} — ${where}')59- println(' ${dim('strip removes the path from history; the file on disk is left alone')}')59+ println(' ${fmt.dim('strip removes the path from history; the file on disk is left alone')}')6060 approved := confirm(false) or { return refuse(args.json(), err.msg()) }6161 if !approved {6262 note('nothing was stripped')src/cli/output.v+0 −16
@@ -11,22 +11,6 @@1111 import json1212 import src.fmt1313 14-pub fn amber(text string) string {15- return fmt.amber(text)16-}17-18-pub fn red(text string) string {19- return fmt.red(text)20-}21-22-pub fn green(text string) string {23- return fmt.green(text)24-}25-26-pub fn dim(text string) string {27- return fmt.dim(text)28-}29-3014 pub fn ok(message string) {3115 println(' ${fmt.amber(fmt.glyph_ok)} ${message}')3216 }src/history/history.v+0 −14
@@ -222,17 +222,3 @@222222 }223223 return stmt.int(0)224224 }225-226-// is_on_line reports whether `id` is reachable from head, so combine can tell227-// replayed-away saves from live ones.228-pub fn is_on_line(store &storage.Store, id string) bool {229- mut cursor := head_id(store)230- for cursor != '' {231- if cursor == id {232- return true233- }234- save := by_id(store, cursor) or { return false }235- cursor = save.parent236- }237- return false238-}src/storage/objects.v+34 −0
@@ -133,3 +133,37 @@133133 after := s.object_count()!134134 return before - after135135 }136+137+// check_objects reads every object and hashes it back. Nothing else proves the138+// store is intact; the hash is the point of the store.139+pub fn (s &Store) check_objects() ![]string {140+ mut problems := []string{}141+ mut stmt := s.db.prepare('SELECT hash FROM objects ORDER BY hash')!142+ defer {143+ stmt.finalize()144+ }145+ mut hashes := []string{}146+ for stmt.step()! {147+ hashes << stmt.text(0)148+ }149+ for hash in hashes {150+ s.get_object(hash) or { problems << 'object ${hash}: ${err.msg()}' }151+ }152+ return problems153+}154+155+// check_save_files catches a save naming an object that is gone — the one156+// inconsistency the schema cannot prevent on its own, because objects are157+// shared between saves.158+pub fn (s &Store) check_save_files() ![]string {159+ mut problems := []string{}160+ mut stmt := s.db.prepare('SELECT DISTINCT f.save_id, f.hash FROM save_files f161+ LEFT JOIN objects o ON o.hash = f.hash WHERE o.hash IS NULL')!162+ defer {163+ stmt.finalize()164+ }165+ for stmt.step()! {166+ problems << 'save ${stmt.text(0)} needs missing object ${stmt.text(1)}'167+ }168+ return problems169+}src/sync/steal.v+1 −3
@@ -393,8 +393,7 @@393393 object_keys := backend.list('${name}/objects')!394394 event_keys := backend.list('${name}/events')!395395 mut fetched := Fetched{396- head: remote_head.body.bytestr().trim_space()397- remote_save_ids: map[string]bool{}396+ head: remote_head.body.bytestr().trim_space()398397 }399398 for key in object_keys {400399 blob := backend.get(key)!@@ -408,7 +407,6 @@408407 continue409408 }410409 record := decode_save(body.body)!411- fetched.remote_save_ids[record.id] = true412410 fetched.saves << record413411 }414412 fetched.saves = parent_first(fetched.saves)!src/sync/sync.v+7 −30
@@ -16,9 +16,8 @@1616 import src.remote1717 import src.storage1818 19-// Where the repo's name on the remote and the last offered head are remembered.19+// Where the repo's name on the remote is remembered.2020 const repo_state_key = 'remote_repo'21-const offered_head_key = 'remote_head'2221 2322 pub const no_saves = 'this repo has no saves yet — nothing to offer'2423 @@ -90,7 +89,6 @@9089 if !backend.put_if_match(head_key(name), local.id.bytes(), remote_head.tag)! {9190 return error('remote has new saves — run koh steal')9291 }93- store.set_state(offered_head_key, local.id)!9492 return report9593 }9694 @@ -148,12 +146,11 @@148146 // transaction, so a failure leaves nothing half-applied.149147 pub struct Fetched {150148 pub mut:151- repo_objects map[string][]u8152- saves []SaveRecord // oldest first153- events_out []events.Event154- head string155- base string // the newest save both sides already shared, '' when none156- remote_save_ids map[string]bool149+ repo_objects map[string][]u8150+ saves []SaveRecord // oldest first151+ events_out []events.Event152+ head string153+ base string // the newest save both sides already shared, '' when none157154 }158155 159156 pub fn fetch_back_to_known(store &storage.Store, mut backend remote.Backend, repo string) !Fetched {@@ -162,8 +159,7 @@162159 return error('the remote has no "${repo}" yet — run koh offer here first')163160 }164161 mut fetched := Fetched{165- head: remote_head.body.bytestr().trim_space()166- remote_save_ids: map[string]bool{}162+ head: remote_head.body.bytestr().trim_space()167163 }168164 mut cursor := fetched.head169165 for cursor != '' && history.by_id(store, cursor) == none {@@ -181,7 +177,6 @@181177 fetched.repo_objects[file.hash] = blob.body182178 }183179 }184- fetched.remote_save_ids[record.id] = true185180 fetched.saves << record186181 cursor = record.parent187182 }@@ -231,22 +226,4 @@231226 apply_strip(store, event)!232227 }233228 }234-}235-236-// can_fast_forward reports whether our line sits behind the remote's, which is237-// the case when every local save is an ancestor of the remote head.238-pub fn can_fast_forward(store &storage.Store, remote_head string, remote_ids map[string]bool) bool {239- mut cursor := remote_head240- for cursor != '' {241- if history.by_id(store, cursor) == none {242- return false243- }244- save := history.by_id(store, cursor) or { return false }245- if save.parent == '' {246- // Reached the first save without leaving the line we have.247- return true248- }249- cursor = save.parent250- }251- return false252229 }src/sync/sync_test.v+3 −3
@@ -82,7 +82,7 @@8282 assert again.saves == 08383 }8484 85-fn test_the_repo_name_and_offered_head_are_remembered() {85+fn test_the_repo_name_is_remembered_and_head_offered() {8686 base := scratch('remember')!8787 defer {8888 os.rmdir_all(base) or {}@@ -96,14 +96,14 @@9696 mut backend := remote.new_folder(os.join_path(base, 'remote'))!9797 offer_now(a, mut backend)!9898 assert a.state('remote_repo') or { '' } == 'proj'99- assert a.state('remote_head') or { '' } == history.head(a)!.id99+ assert backend.get(head_key('proj'))!.body.bytestr() == history.head(a)!.id100100 101101 // A second offer, with the remote already holding our head, must not102102 // refuse: the name is remembered, so the remote is ours.103103 put(a.root, 'a.txt', 'two\n')!104104 commit(a, 'second')!105105 offer_now(a, mut backend)!106- assert a.state('remote_head') or { '' } == history.head(a)!.id106+ assert backend.get(head_key('proj'))!.body.bytestr() == history.head(a)!.id107107 }108108 109109 fn test_steal_fast_forwards_a_machine_that_has_nothing_of_its_own() {