1234567891011module cli1213import json214import src.fmt15import src.history16import src.snapshot17import src.storage181920pub fn cmd_undo(raw []string) int {21 args := parse('undo', raw, Options{22 flags: ['session', 'yes', 'json']23 }) or { return usage_error(err.msg()) }24 store := open_store() or { return refuse(args.json(), err.msg()) }25 defer {26 store.close()27 }28 effective := history.effective(store) or { return refuse(args.json(), err.msg()) }29 mut target := history.Save{}30 if args.flag('session') {31 start := history.session_start(store, effective.id) or {32 return refuse(args.json(), err.msg())33 }34 if start.parent == '' {35 return refuse(args.json(), 'there is nothing before session "${start.session}"')36 }37 target = history.by_id(store, start.parent) or { return refuse(args.json(), err.msg()) }38 } else {39 mut steps := 140 if args.positional.len > 0 {41 steps = args.positional_at(0).int()42 if steps < 1 {43 return usage_error('undo takes a positive number of saves')44 }45 }46 target = history.step_back(store, effective.id, steps) or {47 return refuse(args.json(), err.msg())48 }49 }50 return going_back(store, target, 'undo', args, false)51}52535455pub fn cmd_revert(raw []string) int {56 args := parse('revert', raw, Options{57 flags: ['yes', 'json']58 }) or { return usage_error(err.msg()) }59 store := open_store() or { return refuse(args.json(), err.msg()) }60 defer {61 store.close()62 }63 head := history.head(store) or {64 return refuse(args.json(), 'this repo has no saves yet — nothing to revert to')65 }66 changes := compare_with_head(store, head) or { return refuse(args.json(), err.msg()) }67 if !changes.dirty {68 69 snapshot.clear_restore_mark(store) or { return refuse(args.json(), err.msg()) }70 if args.json() {71 emit_json(json2.encode(RevertJson{72 reverted: false73 head_id: head.id74 }))75 return 076 }77 ok('working tree already matches [${head.id}]')78 return 079 }80 81 82 return going_back(store, head, 'revert', args, true)83}84858687pub fn cmd_load(raw []string) int {88 args := parse('load', raw, Options{89 flags: ['checkpoint', 'yes', 'json']90 }) or { return usage_error(err.msg()) }91 store := open_store() or { return refuse(args.json(), err.msg()) }92 defer {93 store.close()94 }95 mut target := history.Save{}96 if args.flag('checkpoint') {97 target = history.latest_checkpoint(store) or {98 return refuse(args.json(), 'this repo has no checkpoints')99 }100 } else {101 id := args.positional_at(0)102 if id == '' {103 return usage_error('load needs a save ID, or --checkpoint')104 }105 target = history.resolve(store, id) or { return refuse(args.json(), err.msg()) }106 }107 return going_back(store, target, 'load', args, false)108}109110111112fn going_back(store &storage.Store, target history.Save, verb string, args Args, discard_first bool) int {113 as_json := args.json()114 if as_json && !args.flag('yes') {115 return refuse(true, 'needs --yes when not interactive')116 }117 if !discard_first {118 require_clean(store) or { return refuse(as_json, err.msg()) }119 }120 preview := plan_restore(store, target) or { return refuse(as_json, err.msg()) }121 if !as_json {122 ok('${verb}: [${target.id}] ${target.message}')123 for delta in preview.deltas {124 println(' ${change_mark(delta.kind)} ${delta.path}')125 }126 note('${fmt.plural(preview.deltas.len, 'file')} change · ${fmt.plural(preview.remove.len, 'file')} removed')127 }128 approved := confirm(args.flag('yes')) or { return refuse(as_json, err.msg()) }129 if !approved {130 return 1131 }132 133 134 if !discard_first {135 require_clean(store) or { return refuse(as_json, err.msg()) }136 }137 plan := plan_restore(store, target) or { return refuse(as_json, err.msg()) }138 save := apply_restore(store, target, verb, plan, discard_first) or {139 return refuse(as_json, err.msg())140 }141 report := snapshot.restore_files(store, store.root, snapshot.load(store.root), plan.files,142 plan.remove) or { return refuse(as_json, err.msg()) }143 144 kept := if discard_first { save.parent } else { '' }145 if as_json {146 emit_json(json2.encode(BackJson{147 id: save.id148 restores: target.id149 written: report.written150 removed: report.removed151 discarded: discard_first152 checkpoint: kept153 }))154 return 0155 }156 ok('restored [${save.id}] ${save.message}')157 if kept != '' {158 note('the discarded changes are kept in [${kept}] — koh load ${kept} brings them back')159 }160 return 0161}162163164165166pub fn apply_restore(store &storage.Store, target history.Save, verb string, plan RestorePlan, discard_first bool) !history.Save {167 168 169 earlier := snapshot.restore_pending(store)170 snapshot.mark_restore(store)!171 return record_restore(store, target, verb, plan, discard_first) or {172 173 174 if !earlier {175 snapshot.clear_restore_mark(store) or {}176 }177 return err178 }179}180181fn record_restore(store &storage.Store, target history.Save, verb string, plan RestorePlan, discard_first bool) !history.Save {182 discarded := if discard_first {183 snapshot.walk(store, store.root, snapshot.load(store.root))!184 } else {185 []snapshot.FileEntry{}186 }187 188 189 190 store.begin()!191 save := record_both(store, target, verb, plan, discard_first, discarded) or {192 store.rollback()193 return err194 }195 store.end()!196 history.refresh_log_md(store)197 return save198}199200fn record_both(store &storage.Store, target history.Save, verb string, plan RestorePlan, discard_first bool, discarded []snapshot.FileEntry) !history.Save {201 if discard_first {202 203 204 history.record_save(store, store.root, discarded, history.SaveInput{205 message: 'revert: discarded changes'206 is_checkpoint: true207 })!208 }209 return history.record_save(store, '', plan.files, history.SaveInput{210 message: '${verb}: ${target.id}'211 restores: target.id212 })!213}214215216217pub fn restore_now(store &storage.Store, target history.Save, verb string) !history.Save {218 require_clean(store)!219 plan := plan_restore(store, target)!220 save := apply_restore(store, target, verb, plan, false)!221 snapshot.restore_files(store, store.root, snapshot.load(store.root), plan.files, plan.remove)!222 return save223}224225struct RestorePlan {226 files []snapshot.FileEntry227 remove []string228 deltas []snapshot.Delta229}230231const unsaved_changes = 'unsaved changes — save or checkpoint first, or discard them with koh revert'232233234235fn require_clean(store &storage.Store) ! {236 if snapshot.restore_pending(store) {237 return error(snapshot.restore_unfinished)238 }239 head := history.head(store) or { return }240 if compare_with_head(store, head)!.dirty {241 return error(unsaved_changes)242 }243}244245246247248fn plan_restore(store &storage.Store, target history.Save) !RestorePlan {249 files := history.files_of(store, target.id)!250 snapshot.check_links(store.root, files)!251 mut wanted := map[string]bool{}252 for file in files {253 wanted[file.path] = true254 }255 from := working_tree(store)!256 mut remove := []string{}257 for file in from.files {258 if file.path !in wanted {259 remove << file.path260 }261 }262 deltas := snapshot.changes(from.files, files)263 return RestorePlan{264 files: files265 remove: remove266 deltas: deltas267 }268}269270fn change_mark(kind string) string {271 if kind == 'added' {272 return fmt.green('+')273 }274 if kind == 'removed' {275 return fmt.red('-')276 }277 return fmt.amber('~')278}279280struct BackJson {281 id string282 restores string283 written int284 removed int285 discarded bool286 checkpoint string 287}288289struct RevertJson {290 reverted bool291 head_id string292}