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
↓ koh-ea43d4f3.tar.gzatlas at this save
files changed9 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 −1README.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 insrc/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)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-}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+}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 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 //!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 == 0v.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 }