1234567891011module cli1213import json14import src.history15import src.snapshot16import src.storage1718pub fn cmd_undo(raw []string) int {19 args := parse('undo', raw, Options{20 flags: ['session', 'yes', 'json']21 }) or { return usage_error(err.msg()) }22 store := open_store() or { return refuse(args.json(), err.msg()) }23 defer {24 store.close()25 }26 effective := history.effective(store) or { return refuse(args.json(), err.msg()) }27 mut target := history.Save{}28 if args.flag('session') {29 start := history.session_start(store, effective.id) or {30 return refuse(args.json(), err.msg())31 }32 if start.parent == '' {33 return refuse(args.json(), 'there is nothing before session "${start.session}"')34 }35 target = history.by_id(store, start.parent) or { return refuse(args.json(), err.msg()) }36 } else {37 mut steps := 138 if args.positional.len > 0 {39 steps = args.positional_at(0).int()40 if steps < 1 {41 return usage_error('undo takes a positive number of saves')42 }43 }44 target = history.step_back(store, effective.id, steps) or {45 return refuse(args.json(), err.msg())46 }47 }48 return going_back(store, target, 'undo', args, false)49}5051pub fn cmd_revert(raw []string) int {52 args := parse('revert', raw, Options{53 flags: ['yes', 'json']54 }) or { return usage_error(err.msg()) }55 store := open_store() or { return refuse(args.json(), err.msg()) }56 defer {57 store.close()58 }59 head := history.head(store) or {60 return refuse(args.json(), 'this repo has no saves yet — nothing to revert to')61 }62 changes := compare_with_head(store, head) or { return refuse(args.json(), err.msg()) }63 if !changes.dirty {64 if args.json() {65 emit_json(json.encode(RevertJson{66 reverted: false67 head_id: head.id68 }))69 return 070 }71 ok('working tree already matches [${head.id}]')72 return 073 }74 75 76 return going_back(store, head, 'revert', args, true)77}7879pub fn cmd_load(raw []string) int {80 args := parse('load', raw, Options{81 flags: ['checkpoint', 'yes', 'json']82 }) or { return usage_error(err.msg()) }83 store := open_store() or { return refuse(args.json(), err.msg()) }84 defer {85 store.close()86 }87 mut target := history.Save{}88 if args.flag('checkpoint') {89 target = history.latest_checkpoint(store) or {90 return refuse(args.json(), 'this repo has no checkpoints')91 }92 } else {93 id := args.positional_at(0)94 if id == '' {95 return usage_error('load needs a save ID, or --checkpoint')96 }97 target = history.resolve(store, id) or { return refuse(args.json(), err.msg()) }98 }99 return going_back(store, target, 'load', args, false)100}101102103104fn going_back(store &storage.Store, target history.Save, verb string, args Args, discard_first bool) int {105 as_json := args.json()106 if as_json && !args.flag('yes') {107 return refuse(true, 'needs --yes when not interactive')108 }109 plan := plan_restore(store, target, discard_first) or { return refuse(as_json, err.msg()) }110 if !as_json {111 ok('${verb}: [${target.id}] ${target.message}')112 for delta in plan.deltas {113 println(' ${change_mark(delta.kind)} ${delta.path}')114 }115 note('${plural(plan.deltas.len, 'file')} change · ${plural(plan.remove.len, 'file')} removed')116 }117 approved := confirm(args.flag('yes')) or { return refuse(as_json, err.msg()) }118 if !approved {119 return 1120 }121 if discard_first {122 history.commit_worktree(store, store.root, snapshot.load(store.root), history.SaveInput{123 message: 'revert: discarded changes'124 is_checkpoint: true125 }) or { return refuse(as_json, err.msg()) }126 }127 save := history.commit_files(store, '', plan.files, history.SaveInput{128 message: '${verb}: ${target.id}'129 restores: target.id130 }) or { return refuse(as_json, err.msg()) }131 report := snapshot.restore_files(store, store.root, snapshot.load(store.root), plan.files,132 plan.remove) or { return refuse(as_json, err.msg()) }133 if as_json {134 emit_json(json.encode(BackJson{135 id: save.id136 restores: target.id137 written: report.written138 removed: report.removed139 discarded: discard_first140 }))141 return 0142 }143 ok('restored [${save.id}] ${save.message}')144 return 0145}146147struct RestorePlan {148 files []snapshot.FileEntry149 remove []string150 deltas []snapshot.Delta151}152153154155156157158159fn plan_restore(store &storage.Store, target history.Save, prune_untracked bool) !RestorePlan {160 files := history.files_of(store, target.id)!161 mut wanted := map[string]bool{}162 for file in files {163 wanted[file.path] = true164 }165 from := working_tree(store)!166 mut remove := []string{}167 if prune_untracked {168 for file in from.files {169 if file.path !in wanted {170 remove << file.path171 }172 }173 } else if head := history.head(store) {174 for file in history.files_of(store, head.id) or { []snapshot.FileEntry{} } {175 if file.path !in wanted {176 remove << file.path177 }178 }179 }180 deltas := snapshot.diff_trees(store, from, snapshot.Tree{181 files: files182 })!183 return RestorePlan{184 files: files185 remove: remove186 deltas: deltas187 }188}189190fn change_mark(kind string) string {191 if kind == 'added' {192 return green('+')193 }194 if kind == 'removed' {195 return red('-')196 }197 return amber('~')198}199200struct BackJson {201 id string202 restores string203 written int204 removed int205 discarded bool206}207208struct RevertJson {209 reverted bool210 head_id string211}