cmd_offer.v

7.0 KB · raw

1//! `koh offer` — send the local saves to this machine's remote, and, with2//! `--builds`, publish a release built from them.34module cli56import json27import os8import src.build9import src.exchange10import src.fmt11import src.history12import src.machine13import src.release14import src.remote15import src.storage1617// cmd_offer sends new saves and events, and with --builds a release. A repo still on a read-only source18// refuses, and so does a clash met while upgrading it: "run koh steal".19pub fn cmd_offer(raw []string) int {20	args := parse('offer', raw, Options{21		values: ['notes']22		flags:  ['json', 'builds', 'yes']23	}) or { return usage_error(err.msg()) }24	// `--notes ""` is still `--notes`, so the flag's presence decides, not its25	// value; and offer takes no arguments at all.26	if args.positional.len > 0 {27		return usage_error('offer takes no arguments, not "${args.positional_at(0)}"')28	}29	if args.value_or_none('notes') != none && !args.flag('builds') {30		return usage_error('--notes belongs to --builds')31	}32	store := open_store() or { return refuse(args.json(), err.msg()) }33	defer {34		store.close()35	}36	// Saves that came in flagged while a stolen repo was upgraded, for the JSON to name as steal's does.37	mut flagged := []string{}38	source := exchange.source_of(store)39	if source != '' {40		host := exchange.source_host(source)41		if !exchange.signed_in_to(host) {42			return refuse(args.json(), 'this repo came from ${source}, which is read-only. Sign in with "koh remote ${host}" if it\'s yours.')43		}44		// Signed in since it was stolen: the bucket's saves come in first, by the steal a person45		// would run, so a clash is asked about on a terminal and refused without one.46		mut bucket := watched_backend('bringing in your bucket') or {47			return refuse(args.json(), err.msg())48		}49		mut options := exchange.StealOptions{50			yes:     args.flag('yes')51			json:    args.json()52			approve: approve_sync53		}54		if on_terminal(args.json()) {55			options.guide = guide_steal56		}57		report := exchange.steal(store, mut bucket, options) or {58			if err is exchange.ClashError {59				return refuse(args.json(), '${err.paths.join(', ')} changed on both machines — run "koh steal" to choose, then offer')60			}61			return refuse(args.json(), err.msg())62		}63		exchange.forget_source(store) or { return refuse(args.json(), err.msg()) }64		flagged = report.flagged.map(it.save_id)65		if !args.json() {66			ok('signed in to ${host}: this repo now syncs with your bucket')67			if report.previous_head != '' {68				note('to undo the combine: koh load ${report.previous_head}')69			}70			note_kept_versions(report)71			warn_flagged(report.flagged)72		}73	}74	mut backend := watched_backend('offering') or { return refuse(args.json(), err.msg()) }75	if args.flag('builds') {76		return offer_builds(args, store, mut backend, flagged)77	}78	report := exchange.offer(store, mut backend) or { return refuse(args.json(), err.msg()) }79	if args.json() {80		emit_json(json2.encode(OfferedJson{81			repo:    report.repo82			saves:   report.saves83			objects: report.objects84			events:  report.events85			head:    report.head86			flagged: flagged87		}))88		return 089	}90	if report.saves + report.objects + report.events == 0 {91		ok('the remote already has everything from [${report.head}]')92		return 093	}94	ok('offered ${fmt.plural(report.saves, 'save')} · ${fmt.plural(report.objects, 'object')} · ${fmt.plural(report.events, 'event')} to "${report.repo}"')95	note('remote head is now [${report.head}]')96	return 097}9899// offer_builds is `koh offer --builds`: the history goes up first, then a100// release built from the working tree. Unsaved changes are refused, so the101// published files always belong to a save that is on the remote.102fn offer_builds(args Args, store &storage.Store, mut backend remote.Backend, flagged []string) int {103	head := history.head(store) or { return refuse(args.json(), exchange.no_saves) }104	if exchange.dirty(store, head) or { true } {105		return refuse(args.json(), 'save or checkpoint first — a release is built from a save')106	}107	report := exchange.offer(store, mut backend) or { return refuse(args.json(), err.msg()) }108	config := build.read_config(store.root) or { return refuse(args.json(), err.msg()) }109	// Not `version`: that is the koh binary's own version, in this module.110	release_version := build.project_version(store.root) or {111		return refuse(args.json(), err.msg())112	}113	notes := args.value('notes')114	release.check_notes(notes) or { return refuse(args.json(), err.msg()) }115	published := release.publish(store, mut backend, report.repo, config, release_version, notes) or {116		return refuse(args.json(), err.msg())117	}118	if args.json() {119		emit_json(json2.encode(PublishedJson{120			repo:    report.repo121			version: published.version122			files:   published.files.map(PublishedFileJson{123				platform: it.platform124				name:     it.name125				size:     it.size126				blake3:   it.blake3127			})128			flagged: flagged129		}))130		return 0131	}132	for file in published.files {133		ok('${file.platform}  ${file.name}  (${fmt.human_bytes(file.size)})')134	}135	ok('published ${published.version} of "${report.repo}" from [${report.head}]')136	return 0137}138139// this_machine_backend builds the backend from the machine's saved remote.140pub fn this_machine_backend() !remote.Backend {141	settings := machine.load_remote()!142	if settings.kind == machine.kind_folder {143		// Only `koh remote` creates the folder. Missing later, it is usually a144		// disk or share that is not mounted, and a stand-in folder would take145		// saves the other machine never sees.146		if !os.is_dir(settings.path) {147			return error('the remote folder ${settings.path} is not there — is its disk mounted? ("koh remote" changes it)')148		}149		return remote.new_folder(settings.path)!150	}151	credentials := machine.load_credentials()!152	return remote.new_s3(settings.endpoint, settings.bucket, settings.region,153		credentials.access_key, credentials.secret_key)!154}155156// auto_offer runs after every save and checkpoint. A remote that is not set up,157// or not reachable, must never fail the save: it prints one line and stops.158// `quiet` is for `--json`, where that line would land inside the document.159pub fn auto_offer(store &storage.Store, quiet bool) {160	if !machine.has_remote() {161		return162	}163	// A stolen repo rides its source: a save in it is local work and nothing else. The upgrade is164	// the explicit `steal` or `offer`, which bring the bucket in *before* the source is dropped.165	if exchange.source_of(store) != '' {166		return167	}168	mut backend := watched_backend('offering') or {169		if !quiet {170			note('not offered: ${err.msg()}')171		}172		return173	}174	report := exchange.offer(store, mut backend) or {175		if !quiet {176			note('not offered: ${err.msg()}')177		}178		return179	}180	if !quiet && report.saves + report.objects + report.events > 0 {181		note('offered to "${report.repo}" — ${fmt.plural(report.saves, 'save')} · ${fmt.plural(report.objects, 'object')}')182	}183}184185struct OfferedJson {186	repo    string187	saves   int188	objects int189	events  int190	head    string191	flagged []string192}193194struct PublishedFileJson {195	platform string196	name     string197	size     i64198	blake3   string199}200201struct PublishedJson {202	repo    string203	version string204	files   []PublishedFileJson205	flagged []string206}