cmd_steal.v

6.5 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.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	// The remote should end up with whatever this machine just decided.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}8990// offer_after_steal pushes what this machine just decided. A failure here does91// not undo the steal; it is reported.92fn offer_after_steal(store &storage.Store, mut backend remote.Backend) bool {93	if _ := sync.offer(store, mut backend) {94		return true95	}96	return false97}9899// ask_clash is the per-file question a terminal gets. Only a lowercase y-style100// answer is needed here: `m` for mine, `t` for theirs.101fn 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}138139// clone_repo fetches a whole repo into ./<name>, for a machine that does not140// have it yet.141fn 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	// SQLite creates the file, not the folder holding it.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}196197// warn_flagged prints the line the document asks for, per incoming flagged save.198fn 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}