cmd_offer.v

6.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.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	source := exchange.source_of(store)35	if source != '' {36		host := exchange.source_host(source)37		if !exchange.signed_in_to(host) {38			return refuse(args.json(), 'this repo came from ${source}, which is read-only. Sign in with "koh remote ${host}" if it\'s yours.')39		}40		// Signed in since it was stolen: take whatever the bucket has first, then this is an41		// ordinary repo. This machine's own saves replay on top, so nothing is lost.42		mut bucket := this_machine_backend() or { return refuse(args.json(), err.msg()) }43		if _ := exchange.steal(store, mut bucket, exchange.StealOptions{44			theirs: true45		}) {46			exchange.forget_source(store) or { return refuse(args.json(), err.msg()) }47			if !args.json() {48				ok('signed in to ${host}: this repo now syncs with your bucket')49			}50		} else {51			return refuse(args.json(), err.msg())52		}53	}54	mut backend := this_machine_backend() or { return refuse(args.json(), err.msg()) }55	if args.flag('builds') {56		return offer_builds(args, store, mut backend)57	}58	report := exchange.offer(store, mut backend) or { return refuse(args.json(), err.msg()) }59	if args.json() {60		emit_json(json2.encode(OfferedJson{61			repo:    report.repo62			saves:   report.saves63			objects: report.objects64			events:  report.events65			head:    report.head66		}))67		return 068	}69	if report.saves + report.objects + report.events == 0 {70		ok('the remote already has everything from [${report.head}]')71		return 072	}73	ok('offered ${report.saves} saves · ${report.objects} objects · ${report.events} events to "${report.repo}"')74	note('remote head is now [${report.head}]')75	return 076}7778// offer_builds is `koh offer --builds`: the history goes up first, then a79// release built from the working tree. Unsaved changes are refused, so the80// published files always belong to a save that is on the remote.81fn offer_builds(args Args, store &storage.Store, mut backend remote.Backend) int {82	head := history.head(store) or { return refuse(args.json(), exchange.no_saves) }83	if exchange.dirty(store, head) or { true } {84		return refuse(args.json(), 'save or checkpoint first — a release is built from a save')85	}86	report := exchange.offer(store, mut backend) or { return refuse(args.json(), err.msg()) }87	config := build.read_config(store.root) or { return refuse(args.json(), err.msg()) }88	// Not `version`: that is the koh binary's own version, in this module.89	release_version := build.project_version(store.root) or {90		return refuse(args.json(), err.msg())91	}92	notes := args.value('notes')93	release.check_notes(notes) or { return refuse(args.json(), err.msg()) }94	published := release.publish(store, mut backend, report.repo, config, release_version, notes) or {95		return refuse(args.json(), err.msg())96	}97	if args.json() {98		emit_json(json2.encode(PublishedJson{99			repo:    report.repo100			version: published.version101			files:   published.files.map(PublishedFileJson{102				platform: it.platform103				name:     it.name104				size:     it.size105				blake3:   it.blake3106			})107		}))108		return 0109	}110	for file in published.files {111		ok('${file.platform}  ${file.name}  (${fmt.human_bytes(file.size)})')112	}113	ok('published ${published.version} of "${report.repo}" from [${report.head}]')114	return 0115}116117// this_machine_backend builds the backend from the machine's saved remote.118pub fn this_machine_backend() !remote.Backend {119	settings := machine.load_remote()!120	if settings.kind == machine.kind_folder {121		// Only `koh remote` creates the folder. Missing later, it is usually a122		// disk or share that is not mounted, and a stand-in folder would take123		// saves the other machine never sees.124		if !os.is_dir(settings.path) {125			return error('the remote folder ${settings.path} is not there — is its disk mounted? ("koh remote" changes it)')126		}127		return remote.new_folder(settings.path)!128	}129	credentials := machine.load_credentials()!130	return remote.new_s3(settings.endpoint, settings.bucket, settings.region,131		credentials.access_key, credentials.secret_key)!132}133134// auto_offer runs after every save and checkpoint. A remote that is not set up,135// or not reachable, must never fail the save: it prints one line and stops.136// `quiet` is for `--json`, where that line would land inside the document.137pub fn auto_offer(store &storage.Store, quiet bool) {138	if !machine.has_remote() {139		return140	}141	// A stolen repo rides its source: a save in it is local work and nothing else. The upgrade is142	// the explicit `steal` or `offer`, which bring the bucket in *before* the source is dropped.143	if exchange.source_of(store) != '' {144		return145	}146	mut backend := this_machine_backend() or {147		if !quiet {148			note('not offered: ${err.msg()}')149		}150		return151	}152	report := exchange.offer(store, mut backend) or {153		if !quiet {154			note('not offered: ${err.msg()}')155		}156		return157	}158	if !quiet && report.saves + report.objects + report.events > 0 {159		note('offered to "${report.repo}" — ${report.saves} saves · ${report.objects} objects')160	}161}162163struct OfferedJson {164	repo    string165	saves   int166	objects int167	events  int168	head    string169}170171struct PublishedFileJson {172	platform string173	name     string174	size     i64175	blake3   string176}177178struct PublishedJson {179	repo    string180	version string181	files   []PublishedFileJson182}