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 if args.positional.len > 0 {25 return usage_error('steal --list takes no arguments')26 }27 return list_repos(args)28 }29 if storage.find_root(os.getwd()) == none {30 name := args.positional_at(0)31 if name == '' {32 return usage_error('steal needs a repo name outside a repo, e.g. koh steal myproject')33 }34 if args.positional.len > 1 {35 return usage_error('steal takes one repo: koh steal myproject, or koh steal kepr.uk/nina')36 }37 address := web_address(name) or { return refuse(args.json(), err.msg()) }38 if address.len > 0 {39 return clone_web(address[0], address[1], args)40 }41 return clone_repo(name, args)42 }43 store := open_store() or { return refuse(args.json(), err.msg()) }44 defer {45 store.close()46 }47 if args.positional.len > 0 {48 return usage_error('steal takes no name inside a repo — run it outside to fetch a whole repo')49 }50 51 52 source := exchange.source_of(store)53 mut upgraded := source != '' && exchange.signed_in_to(exchange.source_host(source))54 mut backend := remote.Backend(unsafe { nil })55 if source != '' && !upgraded {56 backend = exchange.read_only(source) or { return refuse(args.json(), err.msg()) }57 } else {58 backend = this_machine_backend() or { return refuse(args.json(), err.msg()) }59 }60 mut options := exchange.StealOptions{61 mine: args.flag('mine')62 theirs: args.flag('theirs')63 }64 if !options.mine && !options.theirs && interactive() && os.is_atty(1) != 0 {65 options.ask = ask_clash66 }67 report := exchange.steal(store, mut backend, options) or { return refuse(args.json(), err.msg()) }68 if upgraded {69 exchange.forget_source(store) or { return refuse(args.json(), err.msg()) }70 if !args.json() {71 ok('signed in to ${exchange.source_host(source)}: this repo now syncs with your bucket')72 }73 }74 75 76 offered := if source != '' && !upgraded { true } else { offer_after_steal(store, mut backend) }77 if args.json() {78 emit_json(json2.encode(StolenJson{79 repo: report.repo80 forward: report.forward81 saves: report.fetched_saves82 objects: report.fetched_objects83 events: report.fetched_events84 clashes: report.clashes85 replayed: report.replayed86 kept_mine: report.kept_mine87 head: report.head88 offer_failed: !offered89 flagged: report.flagged.map(it.save_id)90 previous_head: report.previous_head91 }))92 return 093 }94 if report.fetched_saves == 0 {95 ok('already up to date with "${report.repo}" — head is [${report.head}]')96 return 097 }98 if report.forward {99 ok('brought in ${report.fetched_saves} saves from "${report.repo}" — head is now [${report.head}]')100 } else {101 ok('brought in ${report.fetched_saves} saves from "${report.repo}"')102 note("replayed ${report.replayed} of this machine's saves onto the remote head")103 if report.kept_mine > 0 {104 note('kept mine for ${fmt.plural(report.kept_mine, 'file')}')105 }106 note('the tree from before is kept — koh load ${report.previous_head} brings it back, less anything stripped')107 }108 warn_flagged(report.flagged)109 if report.clashes.len > 0 {110 note('${report.clashes.len} changed on both machines: ${report.clashes.join(', ')}')111 }112 if !offered {113 warn('the remote was not updated — run "koh offer" when it is reachable again')114 }115 return 0116}117118119120fn offer_after_steal(store &storage.Store, mut backend remote.Backend) bool {121 if _ := exchange.offer(store, mut backend) {122 return true123 }124 return false125}126127128129fn ask_clash(clash exchange.Clash) exchange.Side {130 println(' ${fmt.amber('both changed')} ${clash.path} ${clash.stat}')131 print(' ${fmt.amber('m')}ine or ${fmt.amber('t')}heirs? [t] ')132 answer := os.get_line().trim_space()133 if answer == 'm' {134 return .mine135 }136 return .theirs137}138139fn list_repos(args Args) int {140 mut backend := this_machine_backend() or { return refuse(args.json(), err.msg()) }141 keys := backend.list('') or { return refuse(args.json(), err.msg()) }142 mut names := map[string]bool{}143 for key in keys {144 parts := key.trim_left('/').split('/')145 if parts.len > 0 && parts[0] != '' {146 names[parts[0]] = true147 }148 }149 mut sorted := names.keys()150 sorted.sort()151 if args.json() {152 emit_json(json2.encode(ReposJson{153 repos: sorted154 }))155 return 0156 }157 if sorted.len == 0 {158 ok('the remote has no repos yet')159 return 0160 }161 for name in sorted {162 println(' ${name}')163 }164 return 0165}166167168169170fn web_address(value string) ![]string {171 if !value.contains('/') {172 return []string{}173 }174 repo := value.all_after_last('/')175 host := value.all_before_last('/')176 if repo == '' || host == '' {177 return error('"${value}" is not <host>/<repo>')178 }179 return [host, repo]180}181182183184185fn clone_web(host string, repo string, args Args) int {186 base := remote.kepr_host(host) or { return refuse(args.json(), err.msg()) }187 if exchange.signed_in_to(base) {188 return clone_repo(repo, args)189 }190 mut backend := exchange.read_only('${base}/${repo}') or { return refuse(args.json(), err.msg()) }191 return clone_with(repo, args, mut backend, '${base}/${repo}')192}193194195196fn clone_repo(name string, args Args) int {197 mut backend := this_machine_backend() or { return refuse(args.json(), err.msg()) }198 return clone_with(name, args, mut backend, '')199}200201202203204fn clone_with(name string, args Args, mut backend remote.Backend, source string) int {205 target := os.join_path(os.getwd(), name)206 if os.exists(target) {207 return refuse(args.json(), '${name} already exists here — run steal inside it instead')208 }209 fetched := exchange.fetch_repo(mut backend, name) or { return refuse(args.json(), err.msg()) }210 store := land_clone(target, name, fetched, source) or {211 os.rmdir_all(target) or {}212 return refuse(args.json(), err.msg())213 }214 215 216 mut finished := false217 defer {218 store.close()219 if !finished {220 os.rmdir_all(target) or {}221 }222 }223 files := history.files_of(store, fetched.head) or { return refuse(args.json(), err.msg()) }224 snapshot.restore_files(store, target, snapshot.Ignore{}, files, []) or {225 return refuse(args.json(), err.msg())226 }227 mut flagged := []events.Flag{}228 for _, flag in events.latest_flags(store) or { return refuse(args.json(), err.msg()) } {229 if flag.flagged {230 flagged << flag231 }232 }233 flagged.sort(a.save_id < b.save_id)234 finished = true235 if args.json() {236 emit_json(json2.encode(StolenJson{237 repo: name238 forward: true239 saves: fetched.saves.len240 objects: fetched.repo_objects.len241 events: fetched.events_out.len242 head: fetched.head243 flagged: flagged.map(it.save_id)244 }))245 return 0246 }247 ok('fetched "${name}" into ${target}')248 note('${fetched.saves.len} saves at [${fetched.head}]')249 if source != '' {250 note('read-only: this clone keeps up with ${source} — sign in with "koh remote ${exchange.source_host(source)}" to make it yours')251 }252 warn_flagged(flagged)253 return 0254}255256257258fn land_clone(target string, name string, fetched exchange.Fetched, source string) !storage.Store {259 260 os.mkdir_all(target, os.MkdirParams{})!261 mut store := storage.create(target)!262 fill_clone(store, name, fetched, source) or {263 store.rollback()264 store.close()265 return err266 }267 return store268}269270fn fill_clone(store &storage.Store, name string, fetched exchange.Fetched, source string) ! {271 store.begin()!272 exchange.write_fetched(store, fetched)!273 history.set_head(store, fetched.head)!274 exchange.remember_repo(store, name)!275 if source != '' {276 exchange.remember_source(store, source)!277 }278 snapshot.mark_restore(store)!279 store.commit()!280}281282283fn warn_flagged(flags []events.Flag) {284 for flag in flags {285 reason := if flag.reason != '' { ' — ${flag.reason}' } else { '' }286 warn('[${flag.save_id}] came in flagged${reason}')287 }288}289290struct ReposJson {291 repos []string292}293294struct StolenJson {295 repo string296 forward bool297 saves int298 objects int299 events int300 clashes []string301 replayed int302 kept_mine int303 head string304 offer_failed bool305 flagged []string306 previous_head string307}