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
↓ koh-2d53457a.tar.gzatlas at this save
files changed13 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 −1build.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.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 {}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 }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{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} ')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{}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 { '' }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 fsrc/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)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 == .nonetests/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 {} }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 }