2d53457a7f9e

wip: rewrite manual and tighten comments; full test results pending (3.8.4)

time
2026-10-06 22:28 UTC · 6 days ago
machine
Junes-MacBook-Pro.local
agent
gpt-6
session
clarify koh manual
parent
57a9c84b4678
13 files VERSION+1 −1build.sh+4 −12src/cli/cmd_back_test.v+2 −2src/cli/cmd_doctor.v+4 −12src/cli/cmd_strip.v+0 −1src/cli/prompt.v+4 −7src/exchange/strip.v+1 −1src/snapshot/ignore.v+4 −4src/storage/objects.v+2 −5src/view/view.v+0 −1src/view/view_test.v+0 −1tests/cli_test.v+9 −1v.mod+1 −1
VERSION+1 −1
@@ -1 +1 @@1-3.8.31+3.8.4

view file

build.sh+4 −12
@@ -1,15 +1,8 @@11 #!/bin/sh2-# Builds one Koh release binary into build/releases/koh-<target>.3-#4-#   KOH_TARGET=linux-x86_64 sh build.sh5-#6-# `koh build` sets KOH_TARGET for each platform in koh.build; run by hand, the7-# target defaults to the machine running the script. Runs on macOS and Linux.8-#9-# macOS targets need macOS: they link against the Apple SDK and are ad-hoc10-# signed with codesign. Linux targets use the host C compiler and static C runtime libraries.11-# Every target needs its own OS and CPU: the pinned V disables12-# garbage collection when cross-compiling. Linux VMs work from a Mac.2+# Build one native Koh release into build/releases/koh-<target>.3+# koh build supplies KOH_TARGET; otherwise use this host.4+# The pinned V disables GC when cross-compiling, so each target needs5+# its own OS and CPU. macOS binaries require ad-hoc signing.136 set -eu147 158 die() {@@ -17,7 +10,6 @@1710   exit 11811 }1912 20-# Paths below are relative to the repository, wherever the script is run from.2113 cd "$(dirname "$0")"2214 2315 # The pin is the only compiler Koh is verified against; see AGENTS.md.

view file

src/cli/cmd_back_test.v+2 −2
@@ -5,13 +5,13 @@55 import src.snapshot66 import src.storage77 8-// The history view's `l` key goes through restore_now, which a CLI test cannot9-// reach without a terminal, so it is checked here directly.108 // Global rules must not make source fixtures depend on the developer’s home.119 fn testsuite_begin() {1210 	os.setenv('HOME', os.join_path(os.temp_dir(), 'koh3-test-home-${os.getpid()}'), true)1311 }1412 13+// The history view's `l` key goes through restore_now, which a CLI test cannot14+// reach without a terminal, so it is checked here directly.1515 fn test_restore_now_refuses_to_overwrite_unsaved_work() {1616 	dir := os.join_path(os.temp_dir(), 'koh3_cli_restore_now_${os.getpid()}')1717 	os.rmdir_all(dir) or {}

view file

src/cli/cmd_doctor.v+4 −12
@@ -1,9 +1,7 @@1-//! `koh doctor` — integrity check and cleanup.1+//! Repository checks, remote verification and explicit maintenance.22 //!3-//! One source of truth means there is nothing to rebuild; what is left to check4-//! is that the file is sound and that every object a save names is really5-//! there. Doctor also tidies the things that are safe to lose: the file cache6-//! and, with --yes, old peek folders.3+//! Remote checks only read. Cache resets are automatic; --yes permits4+//! old-peek cleanup and --backup writes a consistent database copy.75 86 module cli97 @@ -67,7 +65,6 @@6765 	report.size_bytes = os.file_size(report.database)6866 	report.wal_bytes = os.file_size('${report.database}-wal')6967 	doctor_remote_config(mut report)70-	// The remote audit is nearly all of Doctor's time, so --fast skips exactly that.7168 	if args.flag('fast') {7269 		report.checks << DoctorCheck{'remote audit', 'skipped', '--fast requested; remote content not checked'}7370 	} else {@@ -179,7 +176,6 @@179176 	println('    ${fmt.dim('${label:-9}')}${value}')180177 }181178 182-// wrap_items joins items with a dot, breaking lines between items so none passes width.183179 fn wrap_items(items []string, width int) []string {184180 	mut lines := []string{}185181 	mut line := ''@@ -201,11 +197,8 @@201197 	return "'${text.replace("'", "'\\''")}'"202198 }203199 204-// peek_age is how old a peek folder must be before doctor clears it.205200 const peek_age = 86400206201 207-// sweep_peek_folders clears the scratch trees `koh peek` leaves in the system208-// temp directory, once they are a day old.209202 fn sweep_peek_folders() int {210203 	mut removed := 0211204 	for name in os.ls(os.temp_dir()) or { return 0 } {@@ -221,8 +214,7 @@221214 	return removed222215 }223216 224-// peek_is_old is true once a peek is more than a day old. A younger one may225-// still be open in somebody's editor.217+// A recent peek may still be open in an editor.226218 fn peek_is_old(modified i64, now i64) bool {227219 	return modified < now - peek_age228220 }

view file

src/cli/cmd_strip.v+0 −1
@@ -168,7 +168,6 @@168168 	skipped bool169169 }170170 171-// send_strip_to_remote tells this machine's remote about a strip.172171 fn send_strip_to_remote(store &storage.Store, path string) SendOutcome {173172 	if !machine.has_remote() {174173 		return SendOutcome{

view file

src/cli/prompt.v+4 −7
@@ -1,9 +1,7 @@1-//! The strict confirm prompt, shared by destructive commands.1+//! Confirmation shared by destructive commands.22 //!3-//! Before anything that rewrites the working tree, Koh shows what will happen4-//! and asks one question. Only `y` or `Y` confirms: `yes`, or a5-//! padded ` y ` all cancel. `--yes` skips the question; without a terminal and6-//! without `--yes` the command refuses rather than hanging.3+//! Only y or Y consents; --yes supplies consent without a prompt.4+//! Strip's exact-path check still requires a terminal.75 86 module cli97 @@ -27,8 +25,7 @@2725 	return answer.trim_right('\r') in ['y', 'Y']2826 }2927 30-// typed_confirm is confirm for a second, stricter question: the answer has to31-// match `expected` exactly. `strip` uses it for the typed path.28+/// Require an exact answer for confirmations that --yes must not bypass.3229 pub fn typed_confirm(prompt_text string, expected string) bool {3330 	done_working()3431 	eprint('  ${prompt_text} ')

view file

src/exchange/strip.v+1 −1
@@ -93,7 +93,7 @@9393 	return report9494 }9595 96-/// strip_local_batch commits every selected path together, or leaves history unchanged.96+/// Strip the batch in one transaction; failures leave history unchanged.9797 pub fn strip_local_batch(store &storage.Store, paths []string, only_save string) ![]LocalStrip {9898 	store.begin()!9999 	mut reports := []LocalStrip{}

view file

src/snapshot/ignore.v+4 −4
@@ -57,19 +57,19 @@5757 	patterns []Pattern5858 }5959 60-/// global_ignore_path is the machine-local rule file, outside repository history.60+/// Machine-wide ignore rules, excluded from repository history.6161 pub fn global_ignore_path() string {6262 	return os.join_path(os.home_dir(), ignore_file)6363 }6464 65-// Both scopes are additive, so a local rule never overrides a global one.65+/// Load both scopes; either can exclude a path.6666 pub fn load(root string) Ignore {6767 	local := os.read_file(os.join_path(root, ignore_file)) or { '' }6868 	global := os.read_file(global_ignore_path()) or { '' }6969 	return parse(global + '\n' + local)7070 }7171 72-/// matching_rules returns the lines that make the walk skip a literal path.72+/// Rules that exclude a literal path or an ancestor directory.7373 pub fn matching_rules(text string, rel string, is_dir bool) []string {7474 	mut rules := []string{}7575 	for raw in text.split_into_lines() {@@ -90,7 +90,7 @@9090 	return rules9191 }9292 93-/// ignore_rule escapes a literal path in the selected scope.93+/// Escape a literal path; anchor local rules and leave global rules unanchored.9494 pub fn ignore_rule(path string, is_dir bool, global bool) string {9595 	prefix := if global { '' } else { '/' }9696 	suffix := if is_dir { '/' } else { '' }

view file

src/storage/objects.v+2 −5
@@ -134,8 +134,7 @@134134 	return before - after135135 }136136 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.137+/// Verify every stored object's content hash and declared size.139138 pub fn (s &Store) check_objects() ![]string {140139 	mut problems := []string{}141140 	mut stmt := s.db.prepare('SELECT hash FROM objects ORDER BY hash')!@@ -158,9 +157,7 @@158157 	return problems159158 }160159 161-// check_save_files catches a save naming an object that is gone — the one162-// inconsistency the schema cannot prevent on its own, because objects are163-// shared between saves.160+/// Find missing objects, including damage from writes with foreign keys disabled.164161 pub fn (s &Store) check_save_files() ![]string {165162 	mut problems := []string{}166163 	mut stmt := s.db.prepare('SELECT DISTINCT f.save_id, f.hash FROM save_files f

view file

src/view/view.v+0 −1
@@ -312,7 +312,6 @@312312 	return false313313 }314314 315-// confirm_key is the strict confirm: only y or Y goes ahead.316315 fn confirm_key(mut session Session, store &storage.Store, key Key, act fn (store &storage.Store, action Action) string) bool {317316 	if key.kind == .rune && key.text in ['y', 'Y'] {318317 		session.screen.status = act(store, session.pending)

view file

src/view/view_test.v+0 −1
@@ -227,7 +227,6 @@227227 	assert session.pending.kind == .flag228228 	assert session.pending.reason == 'quick'229229 	assert plain(frame_lines(session)).last().contains('flag [aaa]? y yes · n/esc cancel')230-	// Enter cancels without invoking the action.231230 	handle_key(mut session, unsafe { nil }, Key{ kind: .enter }, never_act)232231 	assert session.mode == .list233232 	assert session.pending.kind == .none

view file

tests/cli_test.v+9 −1
@@ -1345,7 +1345,15 @@13451345 	assert !strip.out.contains('koh save ')13461346 	steal := run_home(dir, home, 'man', 'steal').out13471347 	assert steal.starts_with('    koh steal') && steal.contains('koh steal my-project')1348-	assert run_home(dir, home, 'man', 'init').out.trim_space() == "koh init\n        Create `.koh` here, and a starter `.kohignore` if there isn't one."1348+	for command in ['save', 'checkpoint', 'status', 'view', 'diff', 'undo', 'revert', 'load', 'pluck',1349+		'peek', 'flag', 'remote', 'offer', 'steal', 'ignore', 'strip', 'build', 'update', 'init',1350+		'doctor', 'migrate', 'man'] {1351+		entry := run_home(dir, home, 'man', command)1352+		assert entry.code == 0, command1353+		assert entry.out.starts_with('    koh ${command}'), command1354+	}1355+	assert run_home(dir, home, 'help', 'doctor').out == run_home(dir, home, 'man', 'doctor').out1356+	assert !whole.out.contains('--offline')13491357 	assert run_home(dir, home, 'man', 'nonsense').code == 213501358 	legacy := temp_repo('legacy')13511359 	defer { os.rmdir_all(legacy) or {} }

view file

v.mod+1 −1
@@ -1,6 +1,6 @@11 Module {22 	name: 'koh3'3-	version: '3.8.3'3+	version: '3.8.4'44 	description: 'One SQLite file per repo, one line of history.'55 	license: 'MIT'66 }

view file