1234567891011module cli1213import json214import 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 66 snapshot.clear_restore_mark(store) or { return refuse(args.json(), err.msg()) }67 if args.json() {68 emit_json(json2.encode(RevertJson{69 reverted: false70 head_id: head.id71 }))72 return 073 }74 ok('working tree already matches [${head.id}]')75 return 076 }77 78 79 return going_back(store, head, 'revert', args, true)80}8182pub fn cmd_load(raw []string) int {83 args := parse('load', raw, Options{84 flags: ['checkpoint', 'yes', 'json']85 }) or { return usage_error(err.msg()) }86 store := open_store() or { return refuse(args.json(), err.msg()) }87 defer {88 store.close()89 }90 mut target := history.Save{}91 if args.flag('checkpoint') {92 target = history.latest_checkpoint(store) or {93 return refuse(args.json(), 'this repo has no checkpoints')94 }95 } else {96 id := args.positional_at(0)97 if id == '' {98 return usage_error('load needs a save ID, or --checkpoint')99 }100 target = history.resolve(store, id) or { return refuse(args.json(), err.msg()) }101 }102 return going_back(store, target, 'load', args, false)103}104105106107fn going_back(store &storage.Store, target history.Save, verb string, args Args, discard_first bool) int {108 as_json := args.json()109 if as_json && !args.flag('yes') {110 return refuse(true, 'needs --yes when not interactive')111 }112 if !discard_first {113 require_clean(store) or { return refuse(as_json, err.msg()) }114 }115 preview := plan_restore(store, target) or { return refuse(as_json, err.msg()) }116 if !as_json {117 ok('${verb}: [${target.id}] ${target.message}')118 for delta in preview.deltas {119 println(' ${change_mark(delta.kind)} ${delta.path}')120 }121 note('${fmt.plural(preview.deltas.len, 'file')} change · ${fmt.plural(preview.remove.len,122 'file')} removed')123 }124 approved := confirm(args.flag('yes')) or { return refuse(as_json, err.msg()) }125 if !approved {126 return 1127 }128 129 130 if !discard_first {131 require_clean(store) or { return refuse(as_json, err.msg()) }132 }133 plan := plan_restore(store, target) or { return refuse(as_json, err.msg()) }134 save := apply_restore(store, target, verb, plan, discard_first) or {135 return refuse(as_json, err.msg())136 }137 report := snapshot.restore_files(store, store.root, snapshot.load(store.root), plan.files,138 plan.remove) or { return refuse(as_json, err.msg()) }139 140 kept := if discard_first { save.parent } else { '' }141 if as_json {142 emit_json(json2.encode(BackJson{143 id: save.id144 restores: target.id145 written: report.written146 removed: report.removed147 discarded: discard_first148 checkpoint: kept149 }))150 return 0151 }152 ok('restored [${save.id}] ${save.message}')153 if kept != '' {154 note('the discarded changes are kept in [${kept}] — koh load ${kept} brings them back')155 }156 return 0157}158159160161162pub fn apply_restore(store &storage.Store, target history.Save, verb string, plan RestorePlan, discard_first bool) !history.Save {163 164 165 earlier := snapshot.restore_pending(store)166 snapshot.mark_restore(store)!167 return record_restore(store, target, verb, plan, discard_first) or {168 169 170 if !earlier {171 snapshot.clear_restore_mark(store) or {}172 }173 return err174 }175}176177fn record_restore(store &storage.Store, target history.Save, verb string, plan RestorePlan, discard_first bool) !history.Save {178 discarded := if discard_first {179 snapshot.walk(store, store.root, snapshot.load(store.root))!180 } else {181 []snapshot.FileEntry{}182 }183 184 185 186 store.begin()!187 save := record_both(store, target, verb, plan, discard_first, discarded) or {188 store.rollback()189 return err190 }191 store.commit()!192 history.refresh_log_md(store)193 return save194}195196fn record_both(store &storage.Store, target history.Save, verb string, plan RestorePlan, discard_first bool, discarded []snapshot.FileEntry) !history.Save {197 if discard_first {198 199 200 history.record_save(store, store.root, discarded, history.SaveInput{201 message: 'revert: discarded changes'202 is_checkpoint: true203 })!204 }205 return history.record_save(store, '', plan.files, history.SaveInput{206 message: '${verb}: ${target.id}'207 restores: target.id208 })!209}210211212213pub fn restore_now(store &storage.Store, target history.Save, verb string) !history.Save {214 require_clean(store)!215 plan := plan_restore(store, target)!216 save := apply_restore(store, target, verb, plan, false)!217 snapshot.restore_files(store, store.root, snapshot.load(store.root), plan.files, plan.remove)!218 return save219}220221struct RestorePlan {222 files []snapshot.FileEntry223 remove []string224 deltas []snapshot.Delta225}226227const unsaved_changes = 'unsaved changes — save or checkpoint first, or discard them with koh revert'228229230231fn require_clean(store &storage.Store) ! {232 if snapshot.restore_pending(store) {233 return error(snapshot.restore_unfinished)234 }235 head := history.head(store) or { return }236 if compare_with_head(store, head)!.dirty {237 return error(unsaved_changes)238 }239}240241242243244fn plan_restore(store &storage.Store, target history.Save) !RestorePlan {245 files := history.files_of(store, target.id)!246 snapshot.check_links(store.root, files)!247 mut wanted := map[string]bool{}248 for file in files {249 wanted[file.path] = true250 }251 from := working_tree(store)!252 mut remove := []string{}253 for file in from.files {254 if file.path !in wanted {255 remove << file.path256 }257 }258 deltas := snapshot.changes(from.files, files)259 return RestorePlan{260 files: files261 remove: remove262 deltas: deltas263 }264}265266fn change_mark(kind string) string {267 if kind == 'added' {268 return fmt.green('+')269 }270 if kind == 'removed' {271 return fmt.red('-')272 }273 return fmt.amber('~')274}275276struct BackJson {277 id string278 restores string279 written int280 removed int281 discarded bool282 checkpoint string 283}284285struct RevertJson {286 reverted bool287 head_id string288}