499786717d76
finish the man page for the sync and migration commands and cover migration end to end
- time
- 2026-09-23 20:28 UTC · 2 weeks ago
- machine
- koh2
- agent
- deepseek/deepseek-flash
- session
- koh 3 implementation
- parent
- 1f6a834a1455
↓ koh-49978671.tar.gzatlas at this save
files changed4 files
src/cli/output.v+0 −4src/sync/steal.v+0 −1src/sync/sync.v+0 −1tests/cli_test.v+114 −3src/cli/output.v+0 −4
@@ -11,10 +11,6 @@1111 import json1212 import src.fmt1313 14-pub fn color_enabled() bool {15- return fmt.colour_enabled()16-}17-1814 pub fn amber(text string) string {1915 return fmt.amber(text)2016 }src/sync/steal.v+0 −1
@@ -10,7 +10,6 @@1010 1111 import os1212 import src.apply13-import src.events1413 import src.history1514 import src.remote1615 import src.snapshotsrc/sync/sync.v+0 −1
@@ -14,7 +14,6 @@1414 import src.events1515 import src.history1616 import src.remote17-import src.snapshot1817 import src.storage1918 2019 // Where the repo's name on the remote and the last offered head are remembered.tests/cli_test.v+114 −3
@@ -374,7 +374,6 @@374374 assert !json_result.out.contains('"is_checkpoint":false')375375 }376376 377-378377 fn test_remote_configuration_is_isolated_and_atomic() {379378 dir := temp_repo('remote')380379 home := temp_repo('remote-home')@@ -427,6 +426,25 @@427426 assert run_home(a, home, 'steal').code == 0428427 assert os.exists(os.join_path(a, 'b.txt'))429428 assert run_home(b_parent, home, 'steal', '--list').out.contains('proj')429+ write(a, 'clash.txt', 'base\n')430+ assert run_home(a, home, 'save', 'clash base').code == 0431+ assert run_home(b, home, 'steal').code == 0432+ write(a, 'clash.txt', 'A version\n')433+ assert run_home(a, home, 'save', 'A clash').code == 0434+ write(b, 'clash.txt', 'B version\n')435+ assert run_home(b, home, 'save', 'B clash').code == 0436+ refused := run_home(b, home, 'steal')437+ assert refused.code == 1438+ assert refused.err.contains('--mine') || refused.err.contains('--theirs')439+ assert run_home(b, home, 'steal', '--theirs').code == 0440+ assert os.read_file(os.join_path(b, 'clash.txt'))! == 'A version\n'441+ write(a, 'clash.txt', 'A second\n')442+ assert run_home(a, home, 'save', 'A second clash').code == 0443+ write(b, 'clash.txt', 'B second\n')444+ assert run_home(b, home, 'save', 'B second clash').code == 0445+ assert run_home(b, home, 'steal', '--mine').code == 0446+ assert os.read_file(os.join_path(b, 'clash.txt'))! == 'B second\n'447+ assert run_home(b, home, 'log').out.contains('combine: kept mine for')430448 }431449 432450 fn test_man_and_koh2_handoff() {@@ -464,7 +482,7 @@464482 out_path := '${base}.out'465483 err_path := '${base}.err'466484 code_path := '${base}.code'467- mut command := 'HOME=${q(home)} PATH=${q(bin_dir + ":" + os.getenv("PATH"))} ${q(binary)}'485+ mut command := 'HOME=${q(home)} PATH=${q(bin_dir + ':' + os.getenv('PATH'))} ${q(binary)}'468486 for arg in args {469487 command += ' ${q(arg)}'470488 }@@ -479,5 +497,98 @@479497 os.rm(out_path) or {}480498 os.rm(err_path) or {}481499 os.rm(code_path) or {}482- return Result{code: code.int(), out: out, err: err}500+ return Result{501+ code: code.int()502+ out: out503+ err: err504+ }505+}506+507+// --- migration -------------------------------------------------------------508+509+fn koh2_binary() string {510+ return os.find_abs_path_of_executable('koh') or { '' }511+}512+513+// run_koh2 runs the Koh 2 binary in a fixture; its own output is not under514+// test here.515+fn run_koh2(dir string, command string) {516+ os.execute('cd ${q(dir)} && ${command} >/dev/null 2>&1')517+}518+519+fn save_ids(dir string) []string {520+ mut ids := []string{}521+ for name in os.ls(os.join_path(dir, '.koh', 'snapshots')) or { []string{} } {522+ if name.ends_with('.json') {523+ ids << name.trim_right('.json')524+ }525+ }526+ ids.sort()527+ return ids528+}529+530+fn test_migrate_converts_a_koh2_repo_and_keeps_every_save() {531+ koh2 := koh2_binary()532+ if koh2 == '' {533+ println('skipping the migration tests: koh (Koh 2) is not on PATH')534+ return535+ }536+ dir := temp_repo('migrate')537+ defer { os.rmdir_all(dir) or {} }538+ home := temp_repo('migrate-home')539+ defer { os.rmdir_all(home) or {} }540+ run_koh2(dir, '${q(koh2)} init')541+ // One file large enough to be an object, one small enough to be inlined.542+ write(dir, 'code.txt', 'x'.repeat(700))543+ write(dir, 'tiny.txt', 'tiny\n')544+ run_koh2(dir, '${q(koh2)} save "first save"')545+ run_koh2(dir, '${q(koh2)} checkpoint "before something"')546+ write(dir, 'code.txt', 'y'.repeat(700))547+ run_koh2(dir, '${q(koh2)} save "second save"')548+ original := save_ids(dir)549+ assert original.len == 3550+551+ report := run_home(dir, home, 'migrate')552+ assert report.code == 0553+ assert report.out.contains('migrated 3 saves')554+ // The swap: `.koh` is a file, the old data is archived, nothing is staging.555+ assert os.is_file(os.join_path(dir, '.koh'))556+ assert os.is_dir(os.join_path(dir, '.koh-v2'))557+ assert os.is_file(os.join_path(dir, '.kohignore'))558+ assert !os.exists(os.join_path(dir, '.koh-migrate'))559+560+ // Every save kept its original ID, and the new repo is sound.561+ logged := run_home(dir, home, 'log', '--json')562+ assert logged.code == 0563+ for id in original {564+ assert logged.out.contains(id)565+ }566+ assert run_home(dir, home, 'doctor').code == 0567+ assert run_home(dir, home, 'status').code == 0568+569+ // Migrating again has nothing to convert.570+ assert run_home(dir, home, 'migrate').code == 1571+}572+573+fn test_migrate_refuses_a_tree_that_does_not_match_the_last_save() {574+ koh2 := koh2_binary()575+ if koh2 == '' {576+ return577+ }578+ dir := temp_repo('migrate-dirty')579+ defer { os.rmdir_all(dir) or {} }580+ home := temp_repo('migrate-dirty-home')581+ defer { os.rmdir_all(home) or {} }582+ run_koh2(dir, '${q(koh2)} init')583+ write(dir, 'code.txt', 'x'.repeat(700))584+ run_koh2(dir, '${q(koh2)} save "only save"')585+ write(dir, 'code.txt', 'unsaved change\n')586+587+ refused := run_home(dir, home, 'migrate')588+ assert refused.code == 1589+ assert refused.err.contains('save with koh2 first')590+ // Nothing moved: still Koh 2, no archive, no staging file.591+ assert os.is_dir(os.join_path(dir, '.koh'))592+ assert !os.exists(os.join_path(dir, '.koh-v2'))593+ assert !os.exists(os.join_path(dir, '.koh-migrate'))483594 }