ea43d4f3e936

remove the log command: view prints the whole line, checkpoints included (3.0.92)

time
2026-09-27 14:47 UTC · 2 weeks ago
machine
Junes-MacBook-Pro.local
agent
deepseek/deepseek-flash
session
koh3 help and history
parent
81c95eea13a7
9 files README.md+1 −1VERSION+1 −1src/cli/cli.v+0 −4src/cli/cmd_log.v+0 −284src/cli/cmd_view.v+267 −0src/history/log_md.v+1 −1src/view/view.v+2 −2tests/cli_test.v+40 −41v.mod+1 −1
README.md+1 −1
@@ -14,7 +14,7 @@1414 koh init1515 koh save "sketch the parser"1616 koh status17-koh log17+koh view1818 koh undo            # step back one save, without losing it1919 koh offer           # send your saves to your bucket2020 koh steal           # on the other machine: bring them in

view file

VERSION+1 −1
@@ -1 +1 @@1-3.0.911+3.0.92

view file

src/cli/cli.v+0 −4
@@ -19,7 +19,6 @@1919 			['save', 'snapshot your work'],2020 			['checkpoint', 'mark a place to come back to'],2121 			['status', 'what changed'],22-			['log', 'history'],2322 			['view', 'browse saves and checkpoints'],2423 			['diff', 'show changes'],2524 		],@@ -75,9 +74,6 @@7574 		}7675 		'status' {7776 			return cmd_status(rest)78-		}79-		'log' {80-			return cmd_log(rest)8177 		}8278 		'view' {8379 			return cmd_view(rest)

view file

src/cli/cmd_log.v+0 −284
@@ -1,284 +0,0 @@1-//! `koh log` and `koh view` — the same line, printed or browsed.2-//!3-//! On a terminal these open the history view on their tab; piped, or with4-//! `--json`, they print plain text, so an agent always gets something it can5-//! parse.6-7-module cli8-9-import json210-import os11-import time12-import src.events13-import src.fmt14-import src.history15-import src.storage16-import src.view17-18-pub fn cmd_log(raw []string) int {19-	return history_command(raw, .all, ['json'])20-}21-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'])34-}35-36-fn history_command(raw []string, tab view.Tab, option_flags []string) int {37-	verb := match tab {38-		.all { 'log' }39-		else { 'view' }40-	}41-42-	args := parse(verb, raw, Options{43-		values: ['since', 'session']44-		flags:  option_flags45-	}) 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-	}49-	store := open_store() or { return refuse(args.json(), err.msg()) }50-	defer {51-		store.close()52-	}53-	// The view has its own search but no date filter, so a filtered log prints54-	// plain text even on a terminal rather than quietly ignoring the filter.55-	filtered := args.value('since') != '' || args.value('session') != ''56-	if !args.json() && !filtered && on_a_terminal() {57-		return history_view(store, tab)58-	}59-	mut cutoff := i64(0)60-	if expr := args.value_or_none('since') {61-		cutoff = parse_since(expr) or { return refuse(args.json(), err.msg()) }62-	}63-	session := args.value('session')64-	head_id := history.head_id(store)65-	flags := events.latest_flags(store) or {66-		map[string]events.Flag{}67-	}68-	mut shown := []history.Save{}69-	for save in line_for_tab(store, tab) or { return refuse(args.json(), err.msg()) } {70-		if cutoff > 0 && save.created_at < cutoff {71-			continue72-		}73-		if session != '' && save.session != session {74-			continue75-		}76-		shown << save77-	}78-	if args.json() {79-		emit_json(json2.encode(LogJson{80-			head_id: head_id81-			saves:   shown.map(to_json(it))82-		}))83-		return 084-	}85-	if shown.len == 0 {86-		ok('no saves in that range')87-		return 088-	}89-	for save in shown {90-		println(log_line(save, save.id == head_id, flags))91-	}92-	return 093-}94-95-// line_for_tab picks the saves a tab shows: `log` is the whole line, `saves`96-// leaves checkpoints out, and `checkpoints` keeps only them, so the two lists97-// never overlap.98-fn line_for_tab(store &storage.Store, tab view.Tab) ![]history.Save {99-	line := history.line(store)!100-	if tab == .all {101-		return line102-	}103-	want_checkpoints := tab == .checkpoints104-	return line.filter(it.is_checkpoint == want_checkpoints)105-}106-107-fn history_view(store &storage.Store, tab view.Tab) int {108-	view.run(store, tab, history_view_action) or { return fail(err.msg()) }109-	return 0110-}111-112-// history_view_action is what the view calls when the user picks an action.113-// The view presents; this changes history.114-fn history_view_action(store &storage.Store, action view.Action) string {115-	match action.kind {116-		.load {117-			target := history.by_id(store, action.save_id) or {118-				return 'no such save: ${action.save_id}'119-			}120-			restore_now(store, target, 'load') or { return 'could not load: ${err.msg()}' }121-			return 'loaded [${action.save_id}]'122-		}123-		.flag {124-			events.flag(store, action.save_id, action.reason) or {125-				return 'could not flag: ${err.msg()}'126-			}127-			return 'flagged [${action.save_id}]'128-		}129-		.clear_flag {130-			events.clear_flag(store, action.save_id) or { return 'could not clear: ${err.msg()}' }131-			return 'cleared the flag on [${action.save_id}]'132-		}133-		.none {134-			return ''135-		}136-	}137-}138-139-fn on_a_terminal() bool {140-	return os.is_atty(0) != 0 && os.is_atty(1) != 0141-}142-143-struct SaveJson {144-	// Every field is always present, so a consumer never has to tell "absent"145-	// from "missing key". An empty string means the save recorded none.146-	id            string147-	parent        string148-	message       string149-	timestamp     i64150-	machine       string151-	agent         string152-	session       string153-	is_checkpoint bool154-	restores      string155-	replayed_from string156-}157-158-struct LogJson {159-	head_id string160-	saves   []SaveJson161-}162-163-fn to_json(save history.Save) SaveJson {164-	return SaveJson{165-		id:            save.id166-		parent:        save.parent167-		message:       save.message168-		timestamp:     save.created_at169-		machine:       save.machine170-		agent:         save.agent171-		session:       save.session172-		is_checkpoint: save.is_checkpoint173-		restores:      save.restores174-		replayed_from: save.replayed_from175-	}176-}177-178-fn log_line(save history.Save, is_head bool, flags map[string]events.Flag) string {179-	marker := if is_head { fmt.amber('▸') } else { ' ' }180-	mut line := '  ${marker} ${fmt.dim('[' + save.id + ']')} ${fmt.stamp(save.created_at)}  ${save.message}'181-	mut tags := []string{}182-	if save.agent != '' || save.session != '' {183-		mut inner := []string{}184-		if save.agent != '' {185-			inner << save.agent186-		}187-		if save.session != '' {188-			inner << save.session189-		}190-		tags << '[${inner.join(' · ')}]'191-	}192-	if save.is_checkpoint {193-		tags << '◐'194-	}195-	if (flags[save.id] or {196-		events.Flag{197-			save_id: save.id198-		}199-	}).flagged {200-		tags << '⚑'201-	}202-	if save.restores != '' {203-		tags << '↩ ${save.restores}'204-	}205-	if save.replayed_from != '' {206-		tags << 'replayed'207-	}208-	if tags.len > 0 {209-		line += '  ${fmt.dim(tags.join('  '))}'210-	}211-	return line212-}213-214-// parse_since turns the natural-language expressions Koh has always accepted215-// into a unix timestamp.216-pub fn parse_since(expr string) !i64 {217-	now := time.now().unix()218-	text := expr.trim_space().to_lower()219-	match text {220-		'today' { return start_of_day(now) }221-		'yesterday' { return start_of_day(now) - 86400 }222-		'last week' { return now - 7 * 86400 }223-		'last month' { return now - 30 * 86400 }224-		'last year' { return now - 365 * 86400 }225-		else {}226-	}227-228-	if date := parse_date(text) {229-		return date230-	}231-	parts := text.split(' ')232-	if parts.len >= 2 {233-		amount := parts[0].int()234-		if amount > 0 {235-			if unit := unit_seconds(parts[1]) {236-				return now - i64(amount) * unit237-			}238-		}239-		return error('cannot understand "--since ${expr}"')240-	}241-	days := text.int()242-	if days > 0 {243-		return now - i64(days) * 86400244-	}245-	return error('cannot understand "--since ${expr}"')246-}247-248-fn unit_seconds(unit string) ?i64 {249-	return match unit {250-		'm', 'min', 'mins', 'minute', 'minutes' { i64(60) }251-		'h', 'hr', 'hrs', 'hour', 'hours' { i64(3600) }252-		'd', 'day', 'days' { i64(86400) }253-		'w', 'week', 'weeks' { i64(7 * 86400) }254-		'month', 'months' { i64(30 * 86400) }255-		'y', 'year', 'years' { i64(365 * 86400) }256-		else { none }257-	}258-}259-260-fn parse_date(text string) ?i64 {261-	parts := text.split('-')262-	if parts.len != 3 || parts[0].len != 4 {263-		return none264-	}265-	if parts[0].int() == 0 || parts[1].int() == 0 || parts[2].int() == 0 {266-		return none267-	}268-	return midnight(parts[0].int(), parts[1].int(), parts[2].int())269-}270-271-fn start_of_day(unix_seconds i64) i64 {272-	moment := time.unix(unix_seconds).local()273-	return midnight(moment.year, moment.month, moment.day)274-}275-276-// midnight builds a local-time timestamp for a calendar day.277-fn midnight(year int, month int, day int) i64 {278-	return time.Time{279-		year:     year280-		month:    month281-		day:      day282-		is_local: true283-	}.unix()284-}

view file at the parent

src/cli/cmd_view.v+267 −0
@@ -0,0 +1,267 @@1+//! `koh view` — the history, browsed or printed.2+//!3+//! On a terminal it opens the history view on the saves tab, or on the4+//! checkpoints tab with `--checkpoints`; piped, filtered, or with `--json` it5+//! prints the line instead, so an agent always gets something it can parse.6+7+module cli8+9+import json210+import os11+import time12+import src.events13+import src.fmt14+import src.history15+import src.storage16+import src.view17+18+// cmd_view is the one way into the history. What it prints is the whole line,19+// checkpoints included; `--checkpoints` narrows both the view and the print to20+// the checkpoints.21+pub fn cmd_view(raw []string) int {22+	args := parse('view', raw, Options{23+		values: ['since', 'session']24+		flags:  ['checkpoints', 'json']25+	}) or { return usage_error(err.msg()) }26+	if args.positional.len > 0 {27+		return usage_error('view takes no arguments, not "${args.positional_at(0)}"')28+	}29+	checkpoints := args.flag('checkpoints')30+	view_tab := if checkpoints { view.Tab.checkpoints } else { view.Tab.saves }31+	print_tab := if checkpoints { view.Tab.checkpoints } else { view.Tab.all }32+	store := open_store() or { return refuse(args.json(), err.msg()) }33+	defer {34+		store.close()35+	}36+	// The view has its own search but no date filter, so a filtered list prints37+	// plain text even on a terminal rather than quietly ignoring the filter.38+	filtered := args.value('since') != '' || args.value('session') != ''39+	if !args.json() && !filtered && on_a_terminal() {40+		return history_view(store, view_tab)41+	}42+	mut cutoff := i64(0)43+	if expr := args.value_or_none('since') {44+		cutoff = parse_since(expr) or { return refuse(args.json(), err.msg()) }45+	}46+	session := args.value('session')47+	head_id := history.head_id(store)48+	flags := events.latest_flags(store) or {49+		map[string]events.Flag{}50+	}51+	mut shown := []history.Save{}52+	for save in line_for_tab(store, print_tab) or { return refuse(args.json(), err.msg()) } {53+		if cutoff > 0 && save.created_at < cutoff {54+			continue55+		}56+		if session != '' && save.session != session {57+			continue58+		}59+		shown << save60+	}61+	if args.json() {62+		emit_json(json2.encode(LogJson{63+			head_id: head_id64+			saves:   shown.map(to_json(it))65+		}))66+		return 067+	}68+	if shown.len == 0 {69+		ok('no saves in that range')70+		return 071+	}72+	for save in shown {73+		println(log_line(save, save.id == head_id, flags))74+	}75+	return 076+}77+78+// line_for_tab picks the saves a tab shows: `all` is the whole line, `saves`79+// leaves checkpoints out, and `checkpoints` keeps only them, so the two lists80+// never overlap.81+fn line_for_tab(store &storage.Store, tab view.Tab) ![]history.Save {82+	line := history.line(store)!83+	if tab == .all {84+		return line85+	}86+	want_checkpoints := tab == .checkpoints87+	return line.filter(it.is_checkpoint == want_checkpoints)88+}89+90+fn history_view(store &storage.Store, tab view.Tab) int {91+	view.run(store, tab, history_view_action) or { return fail(err.msg()) }92+	return 093+}94+95+// history_view_action is what the view calls when the user picks an action.96+// The view presents; this changes history.97+fn history_view_action(store &storage.Store, action view.Action) string {98+	match action.kind {99+		.load {100+			target := history.by_id(store, action.save_id) or {101+				return 'no such save: ${action.save_id}'102+			}103+			restore_now(store, target, 'load') or { return 'could not load: ${err.msg()}' }104+			return 'loaded [${action.save_id}]'105+		}106+		.flag {107+			events.flag(store, action.save_id, action.reason) or {108+				return 'could not flag: ${err.msg()}'109+			}110+			return 'flagged [${action.save_id}]'111+		}112+		.clear_flag {113+			events.clear_flag(store, action.save_id) or { return 'could not clear: ${err.msg()}' }114+			return 'cleared the flag on [${action.save_id}]'115+		}116+		.none {117+			return ''118+		}119+	}120+}121+122+fn on_a_terminal() bool {123+	return os.is_atty(0) != 0 && os.is_atty(1) != 0124+}125+126+struct SaveJson {127+	// Every field is always present, so a consumer never has to tell "absent"128+	// from "missing key". An empty string means the save recorded none.129+	id            string130+	parent        string131+	message       string132+	timestamp     i64133+	machine       string134+	agent         string135+	session       string136+	is_checkpoint bool137+	restores      string138+	replayed_from string139+}140+141+struct LogJson {142+	head_id string143+	saves   []SaveJson144+}145+146+fn to_json(save history.Save) SaveJson {147+	return SaveJson{148+		id:            save.id149+		parent:        save.parent150+		message:       save.message151+		timestamp:     save.created_at152+		machine:       save.machine153+		agent:         save.agent154+		session:       save.session155+		is_checkpoint: save.is_checkpoint156+		restores:      save.restores157+		replayed_from: save.replayed_from158+	}159+}160+161+fn log_line(save history.Save, is_head bool, flags map[string]events.Flag) string {162+	marker := if is_head { fmt.amber('▸') } else { ' ' }163+	mut line := '  ${marker} ${fmt.dim('[' + save.id + ']')} ${fmt.stamp(save.created_at)}  ${save.message}'164+	mut tags := []string{}165+	if save.agent != '' || save.session != '' {166+		mut inner := []string{}167+		if save.agent != '' {168+			inner << save.agent169+		}170+		if save.session != '' {171+			inner << save.session172+		}173+		tags << '[${inner.join(' · ')}]'174+	}175+	if save.is_checkpoint {176+		tags << '◐'177+	}178+	if (flags[save.id] or {179+		events.Flag{180+			save_id: save.id181+		}182+	}).flagged {183+		tags << '⚑'184+	}185+	if save.restores != '' {186+		tags << '↩ ${save.restores}'187+	}188+	if save.replayed_from != '' {189+		tags << 'replayed'190+	}191+	if tags.len > 0 {192+		line += '  ${fmt.dim(tags.join('  '))}'193+	}194+	return line195+}196+197+// parse_since turns the natural-language expressions Koh has always accepted198+// into a unix timestamp.199+pub fn parse_since(expr string) !i64 {200+	now := time.now().unix()201+	text := expr.trim_space().to_lower()202+	match text {203+		'today' { return start_of_day(now) }204+		'yesterday' { return start_of_day(now) - 86400 }205+		'last week' { return now - 7 * 86400 }206+		'last month' { return now - 30 * 86400 }207+		'last year' { return now - 365 * 86400 }208+		else {}209+	}210+211+	if date := parse_date(text) {212+		return date213+	}214+	parts := text.split(' ')215+	if parts.len >= 2 {216+		amount := parts[0].int()217+		if amount > 0 {218+			if unit := unit_seconds(parts[1]) {219+				return now - i64(amount) * unit220+			}221+		}222+		return error('cannot understand "--since ${expr}"')223+	}224+	days := text.int()225+	if days > 0 {226+		return now - i64(days) * 86400227+	}228+	return error('cannot understand "--since ${expr}"')229+}230+231+fn unit_seconds(unit string) ?i64 {232+	return match unit {233+		'm', 'min', 'mins', 'minute', 'minutes' { i64(60) }234+		'h', 'hr', 'hrs', 'hour', 'hours' { i64(3600) }235+		'd', 'day', 'days' { i64(86400) }236+		'w', 'week', 'weeks' { i64(7 * 86400) }237+		'month', 'months' { i64(30 * 86400) }238+		'y', 'year', 'years' { i64(365 * 86400) }239+		else { none }240+	}241+}242+243+fn parse_date(text string) ?i64 {244+	parts := text.split('-')245+	if parts.len != 3 || parts[0].len != 4 {246+		return none247+	}248+	if parts[0].int() == 0 || parts[1].int() == 0 || parts[2].int() == 0 {249+		return none250+	}251+	return midnight(parts[0].int(), parts[1].int(), parts[2].int())252+}253+254+fn start_of_day(unix_seconds i64) i64 {255+	moment := time.unix(unix_seconds).local()256+	return midnight(moment.year, moment.month, moment.day)257+}258+259+// midnight builds a local-time timestamp for a calendar day.260+fn midnight(year int, month int, day int) i64 {261+	return time.Time{262+		year:     year263+		month:    month264+		day:      day265+		is_local: true266+	}.unix()267+}

view file

src/history/log_md.v+1 −1
@@ -38,7 +38,7 @@3838 	if saves.len > shown {3939 		text += '\n… ${saves.len - shown} older saves\n'4040 	}41-	text += '\n*full history: koh log*\n'41+	text += '\n*full history: koh view*\n'4242 	os.write_file(path, text)!4343 }4444 

view file

src/view/view.v+2 −2
@@ -1,7 +1,7 @@11 //! The history view: the saves and checkpoints list, full screen.22 //!3-//! On a terminal `koh log` opens this instead of printing; `koh saves` and4-//! `koh checkpoints` open it on their tab. The look and the keys are Koh 2's3+//! On a terminal `koh view` opens this instead of printing; `--checkpoints`4+//! opens it on the checkpoints tab. The look and the keys are Koh 2's55 //! (`src/commands/history.zig`): a tab bar, evenly spaced entries between66 //! separators, and a status bar with the hints for the current mode.77 //!

view file

tests/cli_test.v+40 −41
@@ -233,7 +233,7 @@233233 	defer { os.rmdir_all(dir) or {} }234234 	help := run(dir, '--help')235235 	assert help.code == 0236-	for name in ['save', 'checkpoint', 'status', 'log', 'diff', 'undo', 'revert', 'load', 'offer',236+	for name in ['save', 'checkpoint', 'status', 'view', 'diff', 'undo', 'revert', 'load', 'offer',237237 		'steal'] {238238 		assert help.out.contains('  ${name} '), name239239 	}@@ -357,7 +357,7 @@357357 	assert r.code == 0358358 	assert r.out.starts_with('{')359359 	assert r.err.contains('LOG.md')360-	assert run(dir, 'log', '--json').out.contains('"message":"first"')360+	assert run(dir, 'view', '--json').out.contains('"message":"first"')361361 }362362 363363 // A folder of ignored files can sit where a restored file should go. Koh@@ -560,7 +560,7 @@560560 	assert status.out.contains('"modified":["run.sh"]')561561 }562562 563-fn test_log_and_diff() {563+fn test_view_and_diff() {564564 	dir := temp_repo('history')565565 	defer { os.rmdir_all(dir) or {} }566566 	init_repo(dir)@@ -572,16 +572,16 @@572572 	id2 := second.out.all_after('"id":"')[..12]573573 	cp := run(dir, 'checkpoint', 'checkpoint', '--json')574574 	assert cp.code == 0575-	log := run(dir, 'log')576-	assert log.code == 0577-	assert log.out.contains('▸')578-	assert log.out.contains('◐')579-	lj := run(dir, 'log', '--json')575+	printed := run(dir, 'view')576+	assert printed.code == 0577+	assert printed.out.contains('▸')578+	assert printed.out.contains('◐')579+	lj := run(dir, 'view', '--json')580580 	assert lj.code == 0581581 	assert lj.out.contains('"head_id"')582582 	assert lj.out.contains('"saves"')583-	assert run(dir, 'log', '--since', 'today').out.contains('two')584-	session := run(dir, 'log', '--session', 'alpha')583+	assert run(dir, 'view', '--since', 'today').out.contains('two')584+	session := run(dir, 'view', '--session', 'alpha')585585 	assert session.out.contains('one')586586 	assert !session.out.contains('two')587587 	write(dir, 'a.txt', 'three\n')@@ -622,13 +622,13 @@622622 	// Unsaved work — here a file never saved — blocks every going-back but623623 	// revert, as in Koh 2: a restore must never overwrite what only the disk has.624624 	write(dir, 'never.txt', 'keep me\n')625-	log_before := run(dir, 'log').out625+	view_before := run(dir, 'view').out626626 	for args in [['undo', '--yes'], ['load', id1, '--yes'], ['pluck', id2, '--yes']] {627627 		blocked := run(dir, ...args)628628 		assert blocked.code == 1629629 		assert blocked.err.contains('unsaved changes')630630 	}631-	assert run(dir, 'log').out == log_before631+	assert run(dir, 'view').out == view_before632632 	os.rm(os.join_path(dir, 'never.txt')) or { panic(err) }633633 	write(dir, 'a.txt', 'edited, never saved\n')634634 	assert run(dir, 'undo', '--yes').err.contains('unsaved changes')@@ -650,11 +650,11 @@650650 	assert run(dir, 'load', id2, '--yes').code == 0651651 	assert run(dir, 'undo', '99', '--yes').err.contains('nothing before')652652 	write(dir, 'a.txt', 'dirty\n')653-	before := run(dir, 'log')653+	before := run(dir, 'view')654654 	refuse := run(dir, 'revert')655655 	assert refuse.code == 1656656 	assert refuse.err.contains('needs --yes when not interactive')657-	assert run(dir, 'log').out == before.out657+	assert run(dir, 'view').out == before.out658658 	assert os.read_file(os.join_path(dir, 'a.txt'))! == 'dirty\n'659659 	write(dir, 'untracked.txt', 'remove\n')660660 	rv := run(dir, 'revert', '--yes', '--json')@@ -676,7 +676,7 @@676676 	assert run(dir, 'load', 'unknown-id', '--yes').code == 1677677 }678678 679-fn test_flag_and_flagged_log() {679+fn test_flag_and_flagged_view() {680680 	dir := temp_repo('flag')681681 	defer { os.rmdir_all(dir) or {} }682682 	init_repo(dir)@@ -691,7 +691,7 @@691691 	assert list.code == 0692692 	assert list.out.contains(id)693693 	assert list.out.contains('unsafe')694-	assert run(dir, 'log').out.contains('⚑')694+	assert run(dir, 'view').out.contains('⚑')695695 	assert run(dir, 'flag', '--clear', id).code == 0696696 	assert run(dir, 'flag').out.contains('nothing is flagged')697697 	assert run(dir, 'flag', 'unknown-id').code == 1@@ -733,15 +733,15 @@733733 	init_repo(dir)734734 	write(dir, 'peek.txt', 'peek me\n')735735 	assert run(dir, 'save', 'peekable').code == 0736-	id := run(dir, 'log', '--json').out.all_after('"id":"')[..12]737-	before_log := run(dir, 'log', '--json').out736+	id := run(dir, 'view', '--json').out.all_after('"id":"')[..12]737+	before_view := run(dir, 'view', '--json').out738738 	peek := run(dir, 'peek', id, '--json')739739 	assert peek.code == 0740740 	peek_path := peek.out.all_after('"path":"').before('"')741741 	assert peek.out.contains('"id":"${id}"')742742 	assert os.read_file(os.join_path(peek_path, 'peek.txt'))! == 'peek me\n'743743 	assert run(dir, 'status').out.contains('no changes')744-	assert run(dir, 'log', '--json').out == before_log744+	assert run(dir, 'view', '--json').out == before_view745745 	assert run(dir, 'peek').code == 2746746 	assert run(dir, 'peek', 'unknown-id').code == 1747747 	backup := os.join_path(dir, 'doctor.backup')@@ -773,45 +773,44 @@773773 	init_repo(dir)774774 	write(dir, 'secret.txt', 'keep\n')775775 	assert run(dir, 'save', 'secret').code == 0776-	before_log := run(dir, 'log', '--json').out776+	before_view := run(dir, 'view', '--json').out777777 	refused := run(dir, 'strip', 'secret.txt')778778 	assert refused.code == 1779779 	assert refused.err.contains('needs a terminal')780-	assert run(dir, 'log', '--json').out == before_log780+	assert run(dir, 'view', '--json').out == before_view781781 	assert os.read_file(os.join_path(dir, 'secret.txt'))! == 'keep\n'782-	assert run(dir, 'log', '--json').out.contains('secret')782+	assert run(dir, 'view', '--json').out.contains('secret')783783 }784784 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() {785+// `koh view` is the one way into the history: the whole line by default,786+// checkpoints alone with the flag, and the two lists never overlap.787+fn test_view_shows_the_line_and_checkpoints_apart() {788788 	dir := temp_repo('view')789789 	defer { os.rmdir_all(dir) or {} }790790 	init_repo(dir)791791 	write(dir, 'view.txt', 'one\n')792792 	assert run(dir, 'save', 'plain save').code == 0793793 	assert run(dir, 'checkpoint', 'checkpoint save').code == 0794-	saves := run(dir, 'view')795-	assert saves.code == 0796-	assert saves.out.contains('plain save')797-	assert !saves.out.contains('checkpoint save')794+	line := run(dir, 'view')795+	assert line.code == 0796+	assert line.out.contains('plain save')797+	assert line.out.contains('checkpoint save')798798 	checkpoints := run(dir, 'view', '--checkpoints')799799 	assert checkpoints.code == 0800800 	assert checkpoints.out.contains('checkpoint save')801801 	assert !checkpoints.out.contains('plain save')802-	json_saves := run(dir, 'view', '--json')803-	assert json_saves.code == 0804-	assert json_saves.out.contains('"is_checkpoint":false')805-	assert !json_saves.out.contains('"is_checkpoint":true')802+	json_line := run(dir, 'view', '--json')803+	assert json_line.code == 0804+	assert json_line.out.contains('"is_checkpoint":false')805+	assert json_line.out.contains('"is_checkpoint":true')806806 	json_checkpoints := run(dir, 'view', '--checkpoints', '--json')807807 	assert json_checkpoints.code == 0808808 	assert json_checkpoints.out.contains('"is_checkpoint":true')809809 	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.812-	assert run(dir, 'log').out.contains('checkpoint save')810+	// `view` takes no arguments, so a stray one is a usage error, and `log` is811+	// gone rather than hidden.813812 	assert run(dir, 'view', 'extra').code == 2814-	// The two aliases `view` replaced are gone, not hidden.813+	assert run(dir, 'log').code == 2815814 	assert run(dir, 'saves').code == 2816815 	assert run(dir, 'checkpoints').code == 2817816 }@@ -995,7 +994,7 @@995994 	assert run_home(b, home, 'save', 'B second clash').code == 0996995 	assert run_home(b, home, 'steal', '--mine').code == 0997996 	assert os.read_file(os.join_path(b, 'clash.txt'))! == 'B second\n'998-	assert run_home(b, home, 'log').out.contains('combine: kept mine for')997+	assert run_home(b, home, 'view').out.contains('combine: kept mine for')999998 }1000999 10011000 // A clone brings in the whole history, flags included; both output forms must@@ -1176,10 +1175,10 @@11761175 	assert !os.exists(os.join_path(dir, '.koh-migrate'))11771176 11781177 	// Every save kept its original ID, and the new repo is sound.1179-	logged := run_home(dir, home, 'log', '--json')1180-	assert logged.code == 01178+	listed := run_home(dir, home, 'view', '--json')1179+	assert listed.code == 011811180 	for id in original {1182-		assert logged.out.contains(id)1181+		assert listed.out.contains(id)11831182 	}11841183 	assert run_home(dir, home, 'doctor').code == 011851184 	assert run_home(dir, home, 'status').code == 0

view file

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

view file