1234567891011module cli1213import json14import src.fmt15import src.history16import src.snapshot17import src.storage1819pub fn cmd_undo(raw []string) int {20 args := parse('undo', raw, Options{21 flags: ['session', 'yes', 'json']22 }) or { return usage_error(err.msg()) }23 store := open_store() or { return refuse(args.json(), err.msg()) }24 defer {25 store.close()26 }27 effective := history.effective(store) or { return refuse(args.json(), err.msg()) }28 mut target := history.Save{}29 if args.flag('session') {30 start := history.session_start(store, effective.id) or {31 return refuse(args.json(), err.msg())32 }33 if start.parent == '' {34 return refuse(args.json(), 'there is nothing before session "${start.session}"')35 }36 target = history.by_id(store, start.parent) or { return refuse(args.json(), err.msg()) }37 } else {38 mut steps := 139 if args.positional.len > 0 {40 steps = args.positional_at(0).int()41 if steps < 1 {42 return usage_error('undo takes a positive number of saves')43 }44 }45 target = history.step_back(store, effective.id, steps) or {46 return refuse(args.json(), err.msg())47 }48 }49 return going_back(store, target, 'undo', args, false)50}5152pub fn cmd_revert(raw []string) int {53 args := parse('revert', raw, Options{54 flags: ['yes', 'json']55 }) or { return usage_error(err.msg()) }56 store := open_store() or { return refuse(args.json(), err.msg()) }57 defer {58 store.close()59 }60 head := history.head(store) or {61 return refuse(args.json(), 'this repo has no saves yet — nothing to revert to')62 }63 changes := compare_with_head(store, head) or { return refuse(args.json(), err.msg()) }64 if !changes.dirty {65 if args.json() {66 emit_json(json.encode(RevertJson{67 reverted: false68 head_id: head.id69 }))70 return 071 }72 ok('working tree already matches [${head.id}]')73 return 074 }75 76 77 return going_back(store, head, 'revert', args, true)78}7980pub fn cmd_load(raw []string) int {81 args := parse('load', raw, Options{82 flags: ['checkpoint', 'yes', 'json']83 }) or { return usage_error(err.msg()) }84 store := open_store() or { return refuse(args.json(), err.msg()) }85 defer {86 store.close()87 }88 mut target := history.Save{}89 if args.flag('checkpoint') {90 target = history.latest_checkpoint(store) or {91 return refuse(args.json(), 'this repo has no checkpoints')92 }93 } else {94 id := args.positional_at(0)95 if id == '' {96 return usage_error('load needs a save ID, or --checkpoint')97 }98 target = history.resolve(store, id) or { return refuse(args.json(), err.msg()) }99 }100 return going_back(store, target, 'load', args, false)101}102103104105fn going_back(store &storage.Store, target history.Save, verb string, args Args, discard_first bool) int {106 as_json := args.json()107 if as_json && !args.flag('yes') {108 return refuse(true, 'needs --yes when not interactive')109 }110 if !discard_first {111 require_clean(store) or { return refuse(as_json, err.msg()) }112 }113 preview := plan_restore(store, target) or { return refuse(as_json, err.msg()) }114 if !as_json {115 ok('${verb}: [${target.id}] ${target.message}')116 for delta in preview.deltas {117 println(' ${change_mark(delta.kind)} ${delta.path}')118 }119 note('${fmt.plural(preview.deltas.len, 'file')} change · ${fmt.plural(preview.remove.len,120 'file')} removed')121 }122 approved := confirm(args.flag('yes')) or { return refuse(as_json, err.msg()) }123 if !approved {124 return 1125 }126 127 128 if !discard_first {129 require_clean(store) or { return refuse(as_json, err.msg()) }130 }131 plan := plan_restore(store, target) or { return refuse(as_json, err.msg()) }132 save := apply_restore(store, target, verb, plan, discard_first) or {133 return refuse(as_json, err.msg())134 }135 report := snapshot.restore_files(store, store.root, snapshot.load(store.root), plan.files,136 plan.remove) or { return refuse(as_json, err.msg()) }137 if as_json {138 emit_json(json.encode(BackJson{139 id: save.id140 restores: target.id141 written: report.written142 removed: report.removed143 discarded: discard_first144 }))145 return 0146 }147 ok('restored [${save.id}] ${save.message}')148 return 0149}150151152153154pub fn apply_restore(store &storage.Store, target history.Save, verb string, plan RestorePlan, discard_first bool) !history.Save {155 if discard_first {156 history.commit_worktree(store, store.root, snapshot.load(store.root), history.SaveInput{157 message: 'revert: discarded changes'158 is_checkpoint: true159 })!160 }161 return history.commit_files(store, '', plan.files, history.SaveInput{162 message: '${verb}: ${target.id}'163 restores: target.id164 })!165}166167168169pub fn restore_now(store &storage.Store, target history.Save, verb string) !history.Save {170 require_clean(store)!171 plan := plan_restore(store, target)!172 save := apply_restore(store, target, verb, plan, false)!173 snapshot.restore_files(store, store.root, snapshot.load(store.root), plan.files, plan.remove)!174 return save175}176177struct RestorePlan {178 files []snapshot.FileEntry179 remove []string180 deltas []snapshot.Delta181}182183const unsaved_changes = 'unsaved changes — save or checkpoint first, or discard them with koh revert'184185186187fn require_clean(store &storage.Store) ! {188 head := history.head(store) or { return }189 if compare_with_head(store, head)!.dirty {190 return error(unsaved_changes)191 }192}193194195196197fn plan_restore(store &storage.Store, target history.Save) !RestorePlan {198 files := history.files_of(store, target.id)!199 mut wanted := map[string]bool{}200 for file in files {201 wanted[file.path] = true202 }203 from := working_tree(store)!204 mut remove := []string{}205 for file in from.files {206 if file.path !in wanted {207 remove << file.path208 }209 }210 deltas := snapshot.diff_trees(store, from, snapshot.Tree{211 files: files212 })!213 return RestorePlan{214 files: files215 remove: remove216 deltas: deltas217 }218}219220fn change_mark(kind string) string {221 if kind == 'added' {222 return fmt.green('+')223 }224 if kind == 'removed' {225 return fmt.red('-')226 }227 return fmt.amber('~')228}229230struct BackJson {231 id string232 restores string233 written int234 removed int235 discarded bool236}237238struct RevertJson {239 reverted bool240 head_id string241}