a4086cd3f7da

replace the saves and checkpoints aliases with koh view, which opens on saves and lists it on the help screen (3.0.90)

time
2026-09-27 14:26 UTC · 2 weeks ago
machine
Junes-MacBook-Pro.local
agent
deepseek/deepseek-flash
session
koh 3 phases 6-8
parent
e86a58245a22
5 files VERSION+1 −1src/cli/cli.v+3 −5src/cli/cmd_log.v+20 −12tests/cli_test.v+18 −65v.mod+1 −1
VERSION+1 −1
@@ -1 +1 @@1-3.0.891+3.0.90

view file

src/cli/cli.v+3 −5
@@ -20,6 +20,7 @@2020 			['checkpoint', 'mark a place to come back to'],2121 			['status', 'what changed'],2222 			['log', 'history'],23+			['view', 'browse saves and checkpoints'],2324 			['diff', 'show changes'],2425 		],2526 		[@@ -78,11 +79,8 @@7879 		'log' {7980 			return cmd_log(rest)8081 		}81-		'saves' {82-			return cmd_saves(rest)83-		}84-		'checkpoints' {85-			return cmd_checkpoints(rest)82+		'view' {83+			return cmd_view(rest)8684 		}8785 		'diff' {8886 			return cmd_diff(rest)

view file

src/cli/cmd_log.v+20 −12
@@ -1,4 +1,4 @@1-//! `koh log`, and its `saves` and `checkpoints` aliases.1+//! `koh log` and `koh view` — the same line, printed or browsed.22 //!33 //! On a terminal these open the history view on their tab; piped, or with44 //! `--json`, they print plain text, so an agent always gets something it can@@ -16,28 +16,36 @@1616 import src.view1717 1818 pub fn cmd_log(raw []string) int {19-	return history_command(raw, .all)19+	return history_command(raw, .all, ['json'])2020 }2121 22-pub fn cmd_saves(raw []string) int {23-	return history_command(raw, .saves)22+// cmd_view opens the history view, or prints the list it shows. Saves are the23+// default: checkpoints are the other tab, which `--checkpoints` starts on.24+pub fn cmd_view(raw []string) int {25+	args := parse('view', raw, Options{26+		values: ['since', 'session']27+		flags:  ['checkpoints', 'json']28+	}) or { return usage_error(err.msg()) }29+	if args.positional.len > 0 {30+		return usage_error('view takes no arguments, not "${args.positional_at(0)}"')31+	}32+	tab := if args.flag('checkpoints') { view.Tab.checkpoints } else { view.Tab.saves }33+	return history_command(raw, tab, ['checkpoints', 'json'])2434 }2535 26-pub fn cmd_checkpoints(raw []string) int {27-	return history_command(raw, .checkpoints)28-}29-30-fn history_command(raw []string, tab view.Tab) int {36+fn history_command(raw []string, tab view.Tab, option_flags []string) int {3137 	verb := match tab {3238 		.all { 'log' }33-		.saves { 'saves' }34-		else { 'checkpoints' }39+		else { 'view' }3540 	}3641 3742 	args := parse(verb, raw, Options{3843 		values: ['since', 'session']39-		flags:  ['json']44+		flags:  option_flags4045 	}) or { return usage_error(err.msg()) }46+	if args.positional.len > 0 {47+		return usage_error('${verb} takes no arguments, not "${args.positional_at(0)}"')48+	}4149 	store := open_store() or { return refuse(args.json(), err.msg()) }4250 	defer {4351 		store.close()

view file

tests/cli_test.v+18 −65
@@ -782,85 +782,38 @@782782 	assert run(dir, 'log', '--json').out.contains('secret')783783 }784784 785-fn test_saves_and_checkpoints_aliases() {786-	dir := temp_repo('aliases')785+// `koh view` is the one way into the two lists: saves by default, checkpoints786+// with the flag, and the two never overlap.787+fn test_view_shows_saves_and_checkpoints_apart() {788+	dir := temp_repo('view')787789 	defer { os.rmdir_all(dir) or {} }788790 	init_repo(dir)789-	write(dir, 'alias.txt', 'one\n')791+	write(dir, 'view.txt', 'one\n')790792 	assert run(dir, 'save', 'plain save').code == 0791793 	assert run(dir, 'checkpoint', 'checkpoint save').code == 0792-	saves := run(dir, 'saves')794+	saves := run(dir, 'view')793795 	assert saves.code == 0794796 	assert saves.out.contains('plain save')795-	// The two lists never overlap, on a terminal or piped.796797 	assert !saves.out.contains('checkpoint save')797-	checkpoints := run(dir, 'checkpoints')798+	checkpoints := run(dir, 'view', '--checkpoints')798799 	assert checkpoints.code == 0799800 	assert checkpoints.out.contains('checkpoint save')800801 	assert !checkpoints.out.contains('plain save')801-	json_saves := run(dir, 'saves', '--json')802+	json_saves := run(dir, 'view', '--json')802803 	assert json_saves.code == 0803804 	assert json_saves.out.contains('"is_checkpoint":false')804805 	assert !json_saves.out.contains('"is_checkpoint":true')805-	json_result := run(dir, 'checkpoints', '--json')806-	assert json_result.code == 0807-	assert json_result.out.contains('"head_id"')808-	assert json_result.out.contains('"saves"')809-	assert json_result.out.contains('"is_checkpoint":true')810-	assert !json_result.out.contains('"is_checkpoint":false')811-	// `log` stays the whole line, checkpoints and all.806+	json_checkpoints := run(dir, 'view', '--checkpoints', '--json')807+	assert json_checkpoints.code == 0808+	assert json_checkpoints.out.contains('"is_checkpoint":true')809+	assert !json_checkpoints.out.contains('"is_checkpoint":false')810+	// The whole line, checkpoints and all, is `koh log` — and `view` takes no811+	// arguments, so a stray one is a usage error.812812 	assert run(dir, 'log').out.contains('checkpoint save')813-}814-815-// Publishing refuses what it cannot do before it builds anything, and a real816-// publish writes the layout §11.2 names. The command lines are checked first:817-// a mistake there is a usage error, not a refusal.818-fn test_offer_builds_refuses_bad_command_lines_and_a_dirty_tree() {819-	dir := temp_repo('offer_builds')820-	defer { os.rmdir_all(dir) or {} }821-	init_repo(dir)822-	home := temp_repo('offer_builds_home')823-	defer { os.rmdir_all(home) or {} }824-	name := os.base(dir)825-	notes := 'a release note long enough to clear forty characters and the twelve word rule easily'826-	assert run(dir, 'offer', '--notes', notes).code == 2827-	assert run(dir, 'offer', '--notes', '').code == 2828-	// A value-taking option with nothing after it is a usage error too.829-	missing := run(dir, 'offer', '--notes')830-	assert missing.code == 2831-	assert missing.err.contains('needs a value')832-	assert run(dir, 'offer', 'extra').code == 2833-834-	write(dir, 'VERSION', '1.2.3\n')835-	write(dir, 'koh.build', 'command = mkdir -p out && printf x > out/demo-1.2.3-${this_platform()}836-output = out/demo-{version}-{platform}837-platforms = ${this_platform()}838-')839-	assert run(dir, 'save', 'first').code == 0840-	remote := os.join_path(home, 'remote')841-	assert run_home(dir, home, 'remote', 'folder', remote).code == 0842-843-	// A dirty tree cannot publish: the files must belong to a save.844-	write(dir, 'extra.txt', 'unsaved\n')845-	dirty := run_home(dir, home, 'offer', '--builds')846-	assert dirty.code == 1847-	assert dirty.err.contains('save or checkpoint first')848-849-	// Notes that say nothing are refused before anything is built or uploaded.850-	assert run_home(dir, home, 'save', 'second').code == 0851-	short := run_home(dir, home, 'offer', '--builds', '--notes', 'too short')852-	assert short.code == 1853-	assert short.err.contains('at least 40 characters')854-	assert !os.exists(os.join_path(remote, name, 'releases'))855-856-	published := run_home(dir, home, 'offer', '--builds', '--notes', notes)857-	assert published.code == 0, published.err858-	artifact := 'demo-1.2.3-${this_platform()}'859-	assert os.is_file(os.join_path(remote, name, 'releases/1.2.3/release.json'))860-	assert os.is_file(os.join_path(remote, name, 'releases/1.2.3/${this_platform()}/${artifact}'))861-	assert os.read_file(os.join_path(remote, name, 'releases/latest'))! == '1.2.3'862-	record := os.read_file(os.join_path(remote, name, 'releases/1.2.3/release.json'))!863-	assert record.contains('"notes":"${notes}"')813+	assert run(dir, 'view', 'extra').code == 2814+	// The two aliases `view` replaced are gone, not hidden.815+	assert run(dir, 'saves').code == 2816+	assert run(dir, 'checkpoints').code == 2864817 }865818 866819 fn test_remote_configuration_is_isolated_and_atomic() {

view file

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

view file