1234567module cli89import json10import os11import src.events12import src.fmt13import src.history14import src.remote15import src.snapshot16import src.storage17import src.sync1819pub fn cmd_steal(raw []string) int {20 args := parse('steal', raw, Options{21 flags: ['list', 'mine', 'theirs', 'json']22 }) or { return usage_error(err.msg()) }23 if args.flag('list') {24 return list_repos(args)25 }26 if storage.find_root(os.getwd()) == none {27 name := args.positional_at(0)28 if name == '' {29 return usage_error('steal needs a repo name outside a repo, e.g. koh steal myproject')30 }31 return clone_repo(name, args)32 }33 store := open_store() or { return refuse(args.json(), err.msg()) }34 defer {35 store.close()36 }37 if args.positional.len > 0 {38 return usage_error('steal takes no name inside a repo — run it outside to fetch a whole repo')39 }40 mut backend := this_machine_backend() or { return refuse(args.json(), err.msg()) }41 mut options := sync.StealOptions{42 mine: args.flag('mine')43 theirs: args.flag('theirs')44 }45 if !options.mine && !options.theirs && interactive() && os.is_atty(1) != 0 {46 options.ask = ask_clash47 }48 report := sync.steal(store, mut backend, options) or { return refuse(args.json(), err.msg()) }49 50 offered := offer_after_steal(store, mut backend)51 if args.json() {52 emit_json(json.encode(StolenJson{53 repo: report.repo54 forward: report.forward55 saves: report.fetched_saves56 objects: report.fetched_objects57 events: report.fetched_events58 clashes: report.clashes59 replayed: report.replayed60 kept_mine: report.kept_mine61 head: report.head62 offer_failed: !offered63 flagged: report.flagged.map(it.save_id)64 previous_head: report.previous_head65 }))66 return 067 }68 if report.fetched_saves == 0 {69 ok('already up to date with "${report.repo}" — head is [${report.head}]')70 return 071 }72 if report.forward {73 ok('brought in ${report.fetched_saves} saves from "${report.repo}" — head is now [${report.head}]')74 } else {75 ok('brought in ${report.fetched_saves} saves from "${report.repo}"')76 note('replayed ${report.replayed} of this machine\'s saves onto the remote head')77 if report.kept_mine > 0 {78 note('kept mine for ${fmt.plural(report.kept_mine, 'file')}')79 }80 note('the tree from before is kept — koh load ${report.previous_head} brings it back, less anything stripped')81 }82 warn_flagged(report.flagged)83 if report.clashes.len > 0 {84 note('${report.clashes.len} changed on both machines: ${report.clashes.join(', ')}')85 }86 if !offered {87 warn('the remote was not updated — run "koh offer" when it is reachable again')88 }89 return 090}91929394fn offer_after_steal(store &storage.Store, mut backend remote.Backend) bool {95 if _ := sync.offer(store, mut backend) {96 return true97 }98 return false99}100101102103fn ask_clash(clash sync.Clash) sync.Side {104 println(' ${fmt.amber('both changed')} ${clash.path} ${clash.stat}')105 print(' ${fmt.amber('m')}ine or ${fmt.amber('t')}heirs? [t] ')106 answer := os.get_line().trim_space()107 if answer == 'm' {108 return .mine109 }110 return .theirs111}112113fn list_repos(args Args) int {114 mut backend := this_machine_backend() or { return refuse(args.json(), err.msg()) }115 keys := backend.list('') or { return refuse(args.json(), err.msg()) }116 mut names := map[string]bool{}117 for key in keys {118 parts := key.trim_left('/').split('/')119 if parts.len > 0 && parts[0] != '' {120 names[parts[0]] = true121 }122 }123 mut sorted := names.keys()124 sorted.sort()125 if args.json() {126 emit_json(json.encode(ReposJson{127 repos: sorted128 }))129 return 0130 }131 if sorted.len == 0 {132 ok('the remote has no repos yet')133 return 0134 }135 for name in sorted {136 println(' ${name}')137 }138 return 0139}140141142143fn clone_repo(name string, args Args) int {144 target := os.join_path(os.getwd(), name)145 if os.exists(target) {146 return refuse(args.json(), '${name} already exists here — run steal inside it instead')147 }148 mut backend := this_machine_backend() or { return refuse(args.json(), err.msg()) }149 fetched := sync.fetch_repo(mut backend, name) or { return refuse(args.json(), err.msg()) }150 151 os.mkdir_all(target, os.MkdirParams{}) or { return refuse(args.json(), err.msg()) }152 mut store := storage.create(target) or { return refuse(args.json(), err.msg()) }153 defer {154 store.close()155 }156 store.begin() or { return refuse(args.json(), err.msg()) }157 sync.write_fetched(store, fetched) or {158 store.rollback()159 return refuse(args.json(), err.msg())160 }161 history.set_head(store, fetched.head) or {162 store.rollback()163 return refuse(args.json(), err.msg())164 }165 sync.remember_repo(store, name) or {166 store.rollback()167 return refuse(args.json(), err.msg())168 }169 snapshot.mark_restore(store) or {170 store.rollback()171 return refuse(args.json(), err.msg())172 }173 store.commit() or { return refuse(args.json(), err.msg()) }174 files := history.files_of(store, fetched.head) or { return refuse(args.json(), err.msg()) }175 snapshot.restore_files(store, target, snapshot.Ignore{}, files, []) or {176 return refuse(args.json(), err.msg())177 }178 mut flagged := []events.Flag{}179 for _, flag in events.latest_flags(store) or { return refuse(args.json(), err.msg()) } {180 if flag.flagged {181 flagged << flag182 }183 }184 flagged.sort(a.save_id < b.save_id)185 if args.json() {186 emit_json(json.encode(StolenJson{187 repo: name188 forward: true189 saves: fetched.saves.len190 objects: fetched.repo_objects.len191 events: fetched.events_out.len192 head: fetched.head193 flagged: flagged.map(it.save_id)194 }))195 return 0196 }197 ok('fetched "${name}" into ${target}')198 note('${fetched.saves.len} saves at [${fetched.head}]')199 warn_flagged(flagged)200 return 0201}202203204fn warn_flagged(flags []events.Flag) {205 for flag in flags {206 reason := if flag.reason != '' { ' — ${flag.reason}' } else { '' }207 warn('[${flag.save_id}] came in flagged${reason}')208 }209}210211struct ReposJson {212 repos []string213}214215struct StolenJson {216 repo string217 forward bool218 saves int219 objects int220 events int221 clashes []string222 replayed int223 kept_mine int224 head string225 offer_failed bool226 flagged []string227 previous_head string228}