1234567module cli89import json210import os11import src.events12import src.fmt13import src.history14import src.remote15import src.snapshot16import src.storage17import src.exchange1819pub 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 if address := web_address(name) {32 return clone_web(address[0], address[1], args)33 }34 return clone_repo(name, args)35 }36 store := open_store() or { return refuse(args.json(), err.msg()) }37 defer {38 store.close()39 }40 if args.positional.len > 0 {41 return usage_error('steal takes no name inside a repo — run it outside to fetch a whole repo')42 }43 44 45 source := exchange.source_of(store)46 mut upgraded := source != '' && exchange.signed_in_to(exchange.source_host(source))47 mut backend := remote.Backend(unsafe { nil })48 if source != '' && !upgraded {49 backend = exchange.read_only(source) or { return refuse(args.json(), err.msg()) }50 } else {51 backend = this_machine_backend() or { return refuse(args.json(), err.msg()) }52 }53 mut options := exchange.StealOptions{54 mine: args.flag('mine')55 theirs: args.flag('theirs')56 }57 if !options.mine && !options.theirs && interactive() && os.is_atty(1) != 0 {58 options.ask = ask_clash59 }60 report := exchange.steal(store, mut backend, options) or { return refuse(args.json(), err.msg()) }61 if upgraded {62 exchange.forget_source(store) or { return refuse(args.json(), err.msg()) }63 if !args.json() {64 ok('signed in to ${exchange.source_host(source)}: this repo now syncs with your bucket')65 }66 }67 68 69 offered := if source != '' && !upgraded { true } else { offer_after_steal(store, mut backend) }70 if args.json() {71 emit_json(json2.encode(StolenJson{72 repo: report.repo73 forward: report.forward74 saves: report.fetched_saves75 objects: report.fetched_objects76 events: report.fetched_events77 clashes: report.clashes78 replayed: report.replayed79 kept_mine: report.kept_mine80 head: report.head81 offer_failed: !offered82 flagged: report.flagged.map(it.save_id)83 previous_head: report.previous_head84 }))85 return 086 }87 if report.fetched_saves == 0 {88 ok('already up to date with "${report.repo}" — head is [${report.head}]')89 return 090 }91 if report.forward {92 ok('brought in ${report.fetched_saves} saves from "${report.repo}" — head is now [${report.head}]')93 } else {94 ok('brought in ${report.fetched_saves} saves from "${report.repo}"')95 note("replayed ${report.replayed} of this machine's saves onto the remote head")96 if report.kept_mine > 0 {97 note('kept mine for ${fmt.plural(report.kept_mine, 'file')}')98 }99 note('the tree from before is kept — koh load ${report.previous_head} brings it back, less anything stripped')100 }101 warn_flagged(report.flagged)102 if report.clashes.len > 0 {103 note('${report.clashes.len} changed on both machines: ${report.clashes.join(', ')}')104 }105 if !offered {106 warn('the remote was not updated — run "koh offer" when it is reachable again')107 }108 return 0109}110111112113fn offer_after_steal(store &storage.Store, mut backend remote.Backend) bool {114 if _ := exchange.offer(store, mut backend) {115 return true116 }117 return false118}119120121122fn ask_clash(clash exchange.Clash) exchange.Side {123 println(' ${fmt.amber('both changed')} ${clash.path} ${clash.stat}')124 print(' ${fmt.amber('m')}ine or ${fmt.amber('t')}heirs? [t] ')125 answer := os.get_line().trim_space()126 if answer == 'm' {127 return .mine128 }129 return .theirs130}131132fn list_repos(args Args) int {133 mut backend := this_machine_backend() or { return refuse(args.json(), err.msg()) }134 keys := backend.list('') or { return refuse(args.json(), err.msg()) }135 mut names := map[string]bool{}136 for key in keys {137 parts := key.trim_left('/').split('/')138 if parts.len > 0 && parts[0] != '' {139 names[parts[0]] = true140 }141 }142 mut sorted := names.keys()143 sorted.sort()144 if args.json() {145 emit_json(json2.encode(ReposJson{146 repos: sorted147 }))148 return 0149 }150 if sorted.len == 0 {151 ok('the remote has no repos yet')152 return 0153 }154 for name in sorted {155 println(' ${name}')156 }157 return 0158}159160161162fn web_address(value string) ?[]string {163 if !value.contains('/') {164 return none165 }166 repo := value.all_after_last('/')167 host := value.all_before_last('/')168 if repo == '' || host == '' {169 return none170 }171 return [host, repo]172}173174175176177fn clone_web(host string, repo string, args Args) int {178 base := remote.kepr_host(host) or { return refuse(args.json(), err.msg()) }179 if exchange.signed_in_to(base) {180 return clone_repo(repo, args)181 }182 mut backend := exchange.read_only('${base}/${repo}') or { return refuse(args.json(), err.msg()) }183 return clone_with(repo, args, mut backend, '${base}/${repo}')184}185186187188fn clone_repo(name string, args Args) int {189 mut backend := this_machine_backend() or { return refuse(args.json(), err.msg()) }190 return clone_with(name, args, mut backend, '')191}192193194195196fn clone_with(name string, args Args, mut backend remote.Backend, source string) int {197 target := os.join_path(os.getwd(), name)198 if os.exists(target) {199 return refuse(args.json(), '${name} already exists here — run steal inside it instead')200 }201 fetched := exchange.fetch_repo(mut backend, name) or { return refuse(args.json(), err.msg()) }202 store := land_clone(target, name, fetched, source) or {203 204 os.rmdir_all(target) or {}205 return refuse(args.json(), err.msg())206 }207 defer {208 store.close()209 }210 files := history.files_of(store, fetched.head) or { return refuse(args.json(), err.msg()) }211 snapshot.restore_files(store, target, snapshot.Ignore{}, files, []) or {212 return refuse(args.json(), err.msg())213 }214 mut flagged := []events.Flag{}215 for _, flag in events.latest_flags(store) or { return refuse(args.json(), err.msg()) } {216 if flag.flagged {217 flagged << flag218 }219 }220 flagged.sort(a.save_id < b.save_id)221 if args.json() {222 emit_json(json2.encode(StolenJson{223 repo: name224 forward: true225 saves: fetched.saves.len226 objects: fetched.repo_objects.len227 events: fetched.events_out.len228 head: fetched.head229 flagged: flagged.map(it.save_id)230 }))231 return 0232 }233 ok('fetched "${name}" into ${target}')234 note('${fetched.saves.len} saves at [${fetched.head}]')235 if source != '' {236 note('read-only: this clone keeps up with ${source} — sign in with "koh remote ${exchange.source_host(source)}" to make it yours')237 }238 warn_flagged(flagged)239 return 0240}241242243244fn land_clone(target string, name string, fetched exchange.Fetched, source string) !storage.Store {245 246 os.mkdir_all(target, os.MkdirParams{})!247 mut store := storage.create(target)!248 fill_clone(store, name, fetched, source) or {249 store.rollback()250 store.close()251 return err252 }253 return store254}255256fn fill_clone(store &storage.Store, name string, fetched exchange.Fetched, source string) ! {257 store.begin()!258 exchange.write_fetched(store, fetched)!259 history.set_head(store, fetched.head)!260 exchange.remember_repo(store, name)!261 if source != '' {262 exchange.remember_source(store, source)!263 }264 snapshot.mark_restore(store)!265 store.commit()!266}267268269fn warn_flagged(flags []events.Flag) {270 for flag in flags {271 reason := if flag.reason != '' { ' — ${flag.reason}' } else { '' }272 warn('[${flag.save_id}] came in flagged${reason}')273 }274}275276struct ReposJson {277 repos []string278}279280struct StolenJson {281 repo string282 forward bool283 saves int284 objects int285 events int286 clashes []string287 replayed int288 kept_mine int289 head string290 offer_failed bool291 flagged []string292 previous_head string293}