1234567module cli89import json10import os11import src.fmt12import src.history13import src.remote14import src.snapshot15import src.storage16import src.sync1718pub fn cmd_steal(raw []string) int {19 args := parse('steal', raw, Options{20 flags: ['list', 'mine', 'theirs', 'json']21 }) or { return usage_error(err.msg()) }22 if args.flag('list') {23 return list_repos(args)24 }25 if storage.find_root(os.getwd()) == none {26 name := args.positional_at(0)27 if name == '' {28 return usage_error('steal needs a repo name outside a repo, e.g. koh steal myproject')29 }30 return clone_repo(name, args)31 }32 store := open_store() or { return refuse(args.json(), err.msg()) }33 defer {34 store.close()35 }36 if args.positional.len > 0 {37 return usage_error('steal takes no name inside a repo — run it outside to fetch a whole repo')38 }39 mut backend := this_machine_backend() or { return refuse(args.json(), err.msg()) }40 mut options := sync.StealOptions{41 mine: args.flag('mine')42 theirs: args.flag('theirs')43 }44 if !options.mine && !options.theirs && interactive() && os.is_atty(1) != 0 {45 options.ask = ask_clash46 }47 report := sync.steal(store, mut backend, options) or { return refuse(args.json(), err.msg()) }48 49 offered := offer_after_steal(store, mut backend)50 if args.json() {51 emit_json(json.encode(StolenJson{52 repo: report.repo53 forward: report.forward54 saves: report.fetched_saves55 objects: report.fetched_objects56 events: report.fetched_events57 clashes: report.clashes58 replayed: report.replayed59 kept_mine: report.kept_mine60 head: report.head61 offer_failed: !offered62 }))63 return 064 }65 if report.fetched_saves == 0 {66 ok('already up to date with "${report.repo}" — head is [${report.head}]')67 return 068 }69 if report.forward {70 ok('brought in ${report.fetched_saves} saves from "${report.repo}" — head is now [${report.head}]')71 } else {72 ok('brought in ${report.fetched_saves} saves from "${report.repo}"')73 note('replayed ${report.replayed} of this machine\'s saves onto the remote head')74 if report.kept_mine > 0 {75 note('kept mine for ${fmt.plural(report.kept_mine, 'file')}')76 }77 }78 if report.clashes.len > 0 {79 note('${report.clashes.len} changed on both machines: ${report.clashes.join(', ')}')80 }81 if !offered {82 warn('the remote was not updated — run "koh offer" when it is reachable again')83 }84 return 085}86878889fn offer_after_steal(store &storage.Store, mut backend remote.Backend) bool {90 if _ := sync.offer(store, mut backend) {91 return true92 }93 return false94}95969798fn ask_clash(clash sync.Clash) sync.Side {99 println(' ${amber('both changed')} ${clash.path} ${clash.stat}')100 print(' ${amber('m')}ine or ${amber('t')}heirs? [t] ')101 answer := os.get_line().trim_space()102 if answer == 'm' {103 return .mine104 }105 return .theirs106}107108fn list_repos(args Args) int {109 mut backend := this_machine_backend() or { return refuse(args.json(), err.msg()) }110 keys := backend.list('') or { return refuse(args.json(), err.msg()) }111 mut names := map[string]bool{}112 for key in keys {113 parts := key.trim_left('/').split('/')114 if parts.len > 0 && parts[0] != '' {115 names[parts[0]] = true116 }117 }118 mut sorted := names.keys()119 sorted.sort()120 if args.json() {121 emit_json(json.encode(ReposJson{122 repos: sorted123 }))124 return 0125 }126 if sorted.len == 0 {127 ok('the remote has no repos yet')128 return 0129 }130 for name in sorted {131 println(' ${name}')132 }133 return 0134}135136137138fn clone_repo(name string, args Args) int {139 target := os.join_path(os.getwd(), name)140 if os.exists(target) {141 return refuse(args.json(), '${name} already exists here — run steal inside it instead')142 }143 mut backend := this_machine_backend() or { return refuse(args.json(), err.msg()) }144 fetched := sync.fetch_repo(mut backend, name) or { return refuse(args.json(), err.msg()) }145 146 os.mkdir_all(target, os.MkdirParams{}) or { return refuse(args.json(), err.msg()) }147 mut store := storage.create(target) or { return refuse(args.json(), err.msg()) }148 defer {149 store.close()150 }151 store.begin() or { return refuse(args.json(), err.msg()) }152 sync.write_fetched(store, fetched) or {153 store.rollback()154 return refuse(args.json(), err.msg())155 }156 history.set_head(store, fetched.head) or {157 store.rollback()158 return refuse(args.json(), err.msg())159 }160 store.commit() or { return refuse(args.json(), err.msg()) }161 files := history.files_of(store, fetched.head) or { return refuse(args.json(), err.msg()) }162 snapshot.restore_files(store, target, snapshot.Ignore{}, files, []) or {163 return refuse(args.json(), err.msg())164 }165 if args.json() {166 emit_json(json.encode(StolenJson{167 repo: name168 forward: true169 saves: fetched.saves.len170 objects: fetched.repo_objects.len171 events: fetched.events_out.len172 head: fetched.head173 }))174 return 0175 }176 ok('fetched "${name}" into ${target}')177 note('${fetched.saves.len} saves at [${fetched.head}]')178 return 0179}180181struct ReposJson {182 repos []string183}184185struct StolenJson {186 repo string187 forward bool188 saves int189 objects int190 events int191 clashes []string192 replayed int193 kept_mine int194 head string195 offer_failed bool196}