cmd_steal.v

5.7 KB · raw

1//! `koh steal` — bring another machine's saves in.2//!3//! Inside a repo this pulls the remote's new saves and combines when both4//! machines moved. Outside one it fetches a whole repo into a new folder.5//! `--list` shows what the remote holds.67module 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	// The remote should end up with whatever this machine just decided.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}8687// offer_after_steal pushes what this machine just decided. A failure here does88// not undo the steal; it is reported.89fn offer_after_steal(store &storage.Store, mut backend remote.Backend) bool {90	if _ := sync.offer(store, mut backend) {91		return true92	}93	return false94}9596// ask_clash is the per-file question a terminal gets. Only a lowercase y-style97// answer is needed here: `m` for mine, `t` for theirs.98fn 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}135136// clone_repo fetches a whole repo into ./<name>, for a machine that does not137// have it yet.138fn 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	// SQLite creates the file, not the folder holding it.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}