cmd_offer.v

6.8 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.storage1617pub fn cmd_offer(raw []string) int {18	args := parse('offer', raw, Options{19		values: ['notes']20		flags:  ['json', 'builds']21	}) or { return usage_error(err.msg()) }22	// `--notes ""` is still `--notes`, so the flag's presence decides, not its23	// value; and offer takes no arguments at all.24	if args.positional.len > 0 {25		return usage_error('offer takes no arguments, not "${args.positional_at(0)}"')26	}27	if args.value_or_none('notes') != none && !args.flag('builds') {28		return usage_error('--notes belongs to --builds')29	}30	store := open_store() or { return refuse(args.json(), err.msg()) }31	defer {32		store.close()33	}34	// Saves that came in flagged while a stolen repo was upgraded, for the JSON to name as steal's does.35	mut flagged := []string{}36	source := exchange.source_of(store)37	if source != '' {38		host := exchange.source_host(source)39		if !exchange.signed_in_to(host) {40			return refuse(args.json(), 'this repo came from ${source}, which is read-only. Sign in with "koh remote ${host}" if it\'s yours.')41		}42		// Signed in since it was stolen: the bucket's saves come in first, by the steal a person43		// would run, so a clash is asked about on a terminal and refused without one.44		mut bucket := watched_backend('bringing in your bucket') or {45			return refuse(args.json(), err.msg())46		}47		mut options := exchange.StealOptions{}48		if on_terminal(args.json()) {49			options.guide = guide_steal50		}51		report := exchange.steal(store, mut bucket, options) or {52			if err is exchange.ClashError {53				return refuse(args.json(), '${err.paths.join(', ')} changed on both machines — run "koh steal" to choose, then offer')54			}55			return refuse(args.json(), err.msg())56		}57		exchange.forget_source(store) or { return refuse(args.json(), err.msg()) }58		flagged = report.flagged.map(it.save_id)59		if !args.json() {60			ok('signed in to ${host}: this repo now syncs with your bucket')61			if report.previous_head != '' {62				note('to undo the combine: koh load ${report.previous_head}')63			}64			note_kept_versions(report)65			warn_flagged(report.flagged)66		}67	}68	mut backend := watched_backend('offering') or { return refuse(args.json(), err.msg()) }69	if args.flag('builds') {70		return offer_builds(args, store, mut backend, flagged)71	}72	report := exchange.offer(store, mut backend) or { return refuse(args.json(), err.msg()) }73	if args.json() {74		emit_json(json2.encode(OfferedJson{75			repo:    report.repo76			saves:   report.saves77			objects: report.objects78			events:  report.events79			head:    report.head80			flagged: flagged81		}))82		return 083	}84	if report.saves + report.objects + report.events == 0 {85		ok('the remote already has everything from [${report.head}]')86		return 087	}88	ok('offered ${fmt.plural(report.saves, 'save')} · ${fmt.plural(report.objects, 'object')} · ${fmt.plural(report.events, 'event')} to "${report.repo}"')89	note('remote head is now [${report.head}]')90	return 091}9293// offer_builds is `koh offer --builds`: the history goes up first, then a94// release built from the working tree. Unsaved changes are refused, so the95// published files always belong to a save that is on the remote.96fn offer_builds(args Args, store &storage.Store, mut backend remote.Backend, flagged []string) int {97	head := history.head(store) or { return refuse(args.json(), exchange.no_saves) }98	if exchange.dirty(store, head) or { true } {99		return refuse(args.json(), 'save or checkpoint first — a release is built from a save')100	}101	report := exchange.offer(store, mut backend) or { return refuse(args.json(), err.msg()) }102	config := build.read_config(store.root) or { return refuse(args.json(), err.msg()) }103	// Not `version`: that is the koh binary's own version, in this module.104	release_version := build.project_version(store.root) or {105		return refuse(args.json(), err.msg())106	}107	notes := args.value('notes')108	release.check_notes(notes) or { return refuse(args.json(), err.msg()) }109	published := release.publish(store, mut backend, report.repo, config, release_version, notes) or {110		return refuse(args.json(), err.msg())111	}112	if args.json() {113		emit_json(json2.encode(PublishedJson{114			repo:    report.repo115			version: published.version116			files:   published.files.map(PublishedFileJson{117				platform: it.platform118				name:     it.name119				size:     it.size120				blake3:   it.blake3121			})122			flagged: flagged123		}))124		return 0125	}126	for file in published.files {127		ok('${file.platform}  ${file.name}  (${fmt.human_bytes(file.size)})')128	}129	ok('published ${published.version} of "${report.repo}" from [${report.head}]')130	return 0131}132133// this_machine_backend builds the backend from the machine's saved remote.134pub fn this_machine_backend() !remote.Backend {135	settings := machine.load_remote()!136	if settings.kind == machine.kind_folder {137		// Only `koh remote` creates the folder. Missing later, it is usually a138		// disk or share that is not mounted, and a stand-in folder would take139		// saves the other machine never sees.140		if !os.is_dir(settings.path) {141			return error('the remote folder ${settings.path} is not there — is its disk mounted? ("koh remote" changes it)')142		}143		return remote.new_folder(settings.path)!144	}145	credentials := machine.load_credentials()!146	return remote.new_s3(settings.endpoint, settings.bucket, settings.region,147		credentials.access_key, credentials.secret_key)!148}149150// auto_offer runs after every save and checkpoint. A remote that is not set up,151// or not reachable, must never fail the save: it prints one line and stops.152// `quiet` is for `--json`, where that line would land inside the document.153pub fn auto_offer(store &storage.Store, quiet bool) {154	if !machine.has_remote() {155		return156	}157	// A stolen repo rides its source: a save in it is local work and nothing else. The upgrade is158	// the explicit `steal` or `offer`, which bring the bucket in *before* the source is dropped.159	if exchange.source_of(store) != '' {160		return161	}162	mut backend := watched_backend('offering') or {163		if !quiet {164			note('not offered: ${err.msg()}')165		}166		return167	}168	report := exchange.offer(store, mut backend) or {169		if !quiet {170			note('not offered: ${err.msg()}')171		}172		return173	}174	if !quiet && report.saves + report.objects + report.events > 0 {175		note('offered to "${report.repo}" — ${fmt.plural(report.saves, 'save')} · ${fmt.plural(report.objects, 'object')}')176	}177}178179struct OfferedJson {180	repo    string181	saves   int182	objects int183	events  int184	head    string185	flagged []string186}187188struct PublishedFileJson {189	platform string190	name     string191	size     i64192	blake3   string193}194195struct PublishedJson {196	repo    string197	version string198	files   []PublishedFileJson199	flagged []string200}