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
4 files src/cli/output.v+0 −4src/sync/steal.v+0 −1src/sync/sync.v+0 −1tests/cli_test.v+114 −3
src/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 }

view file

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.snapshot

view file

src/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.

view file

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 }

view file