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 }))65 return 066 }67 if report.fetched_saves == 0 {68 ok('already up to date with "${report.repo}" — head is [${report.head}]')69 return 070 }71 if report.forward {72 ok('brought in ${report.fetched_saves} saves from "${report.repo}" — head is now [${report.head}]')73 } else {74 ok('brought in ${report.fetched_saves} saves from "${report.repo}"')75 note('replayed ${report.replayed} of this machine\'s saves onto the remote head')76 if report.kept_mine > 0 {77 note('kept mine for ${fmt.plural(report.kept_mine, 'file')}')78 }79 }80 warn_flagged(report.flagged)81 if report.clashes.len > 0 {82 note('${report.clashes.len} changed on both machines: ${report.clashes.join(', ')}')83 }84 if !offered {85 warn('the remote was not updated — run "koh offer" when it is reachable again')86 }87 return 088}89909192fn offer_after_steal(store &storage.Store, mut backend remote.Backend) bool {93 if _ := sync.offer(store, mut backend) {94 return true95 }96 return false97}9899100101fn ask_clash(clash sync.Clash) sync.Side {102 println(' ${fmt.amber('both changed')} ${clash.path} ${clash.stat}')103 print(' ${fmt.amber('m')}ine or ${fmt.amber('t')}heirs? [t] ')104 answer := os.get_line().trim_space()105 if answer == 'm' {106 return .mine107 }108 return .theirs109}110111fn list_repos(args Args) int {112 mut backend := this_machine_backend() or { return refuse(args.json(), err.msg()) }113 keys := backend.list('') or { return refuse(args.json(), err.msg()) }114 mut names := map[string]bool{}115 for key in keys {116 parts := key.trim_left('/').split('/')117 if parts.len > 0 && parts[0] != '' {118 names[parts[0]] = true119 }120 }121 mut sorted := names.keys()122 sorted.sort()123 if args.json() {124 emit_json(json.encode(ReposJson{125 repos: sorted126 }))127 return 0128 }129 if sorted.len == 0 {130 ok('the remote has no repos yet')131 return 0132 }133 for name in sorted {134 println(' ${name}')135 }136 return 0137}138139140141fn clone_repo(name string, args Args) int {142 target := os.join_path(os.getwd(), name)143 if os.exists(target) {144 return refuse(args.json(), '${name} already exists here — run steal inside it instead')145 }146 mut backend := this_machine_backend() or { return refuse(args.json(), err.msg()) }147 fetched := sync.fetch_repo(mut backend, name) or { return refuse(args.json(), err.msg()) }148 149 os.mkdir_all(target, os.MkdirParams{}) or { return refuse(args.json(), err.msg()) }150 mut store := storage.create(target) or { return refuse(args.json(), err.msg()) }151 defer {152 store.close()153 }154 store.begin() or { return refuse(args.json(), err.msg()) }155 sync.write_fetched(store, fetched) or {156 store.rollback()157 return refuse(args.json(), err.msg())158 }159 history.set_head(store, fetched.head) or {160 store.rollback()161 return refuse(args.json(), err.msg())162 }163 sync.remember_repo(store, name) or {164 store.rollback()165 return refuse(args.json(), err.msg())166 }167 store.commit() or { return refuse(args.json(), err.msg()) }168 files := history.files_of(store, fetched.head) or { return refuse(args.json(), err.msg()) }169 snapshot.restore_files(store, target, snapshot.Ignore{}, files, []) or {170 return refuse(args.json(), err.msg())171 }172 mut flagged := []events.Flag{}173 for _, flag in events.latest_flags(store) or { return refuse(args.json(), err.msg()) } {174 if flag.flagged {175 flagged << flag176 }177 }178 flagged.sort(a.save_id < b.save_id)179 if args.json() {180 emit_json(json.encode(StolenJson{181 repo: name182 forward: true183 saves: fetched.saves.len184 objects: fetched.repo_objects.len185 events: fetched.events_out.len186 head: fetched.head187 flagged: flagged.map(it.save_id)188 }))189 return 0190 }191 ok('fetched "${name}" into ${target}')192 note('${fetched.saves.len} saves at [${fetched.head}]')193 warn_flagged(flagged)194 return 0195}196197198fn warn_flagged(flags []events.Flag) {199 for flag in flags {200 reason := if flag.reason != '' { ' — ${flag.reason}' } else { '' }201 warn('[${flag.save_id}] came in flagged${reason}')202 }203}204205struct ReposJson {206 repos []string207}208209struct StolenJson {210 repo string211 forward bool212 saves int213 objects int214 events int215 clashes []string216 replayed int217 kept_mine int218 head string219 offer_failed bool220 flagged []string221}