cmd_offer.v

4.9 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	mut backend := this_machine_backend() or { return refuse(args.json(), err.msg()) }35	if args.flag('builds') {36		return offer_builds(args, store, mut backend)37	}38	report := exchange.offer(store, mut backend) or { return refuse(args.json(), err.msg()) }39	if args.json() {40		emit_json(json2.encode(OfferedJson{41			repo:    report.repo42			saves:   report.saves43			objects: report.objects44			events:  report.events45			head:    report.head46		}))47		return 048	}49	if report.saves + report.objects + report.events == 0 {50		ok('the remote already has everything from [${report.head}]')51		return 052	}53	ok('offered ${report.saves} saves · ${report.objects} objects · ${report.events} events to "${report.repo}"')54	note('remote head is now [${report.head}]')55	return 056}5758// offer_builds is `koh offer --builds`: the history goes up first, then a59// release built from the working tree. Unsaved changes are refused, so the60// published files always belong to a save that is on the remote.61fn offer_builds(args Args, store &storage.Store, mut backend remote.Backend) int {62	head := history.head(store) or { return refuse(args.json(), exchange.no_saves) }63	if exchange.dirty(store, head) or { true } {64		return refuse(args.json(), 'save or checkpoint first — a release is built from a save')65	}66	report := exchange.offer(store, mut backend) or { return refuse(args.json(), err.msg()) }67	config := build.read_config(store.root) or { return refuse(args.json(), err.msg()) }68	// Not `version`: that is the koh binary's own version, in this module.69	release_version := build.project_version(store.root) or {70		return refuse(args.json(), err.msg())71	}72	notes := args.value('notes')73	release.check_notes(notes) or { return refuse(args.json(), err.msg()) }74	published := release.publish(store, mut backend, report.repo, config, release_version, notes) or {75		return refuse(args.json(), err.msg())76	}77	if args.json() {78		emit_json(json2.encode(PublishedJson{79			repo:    report.repo80			version: published.version81			files:   published.files.map(PublishedFileJson{82				platform: it.platform83				name:     it.name84				size:     it.size85				blake3:   it.blake386			})87		}))88		return 089	}90	for file in published.files {91		ok('${file.platform}  ${file.name}  (${fmt.human_bytes(file.size)})')92	}93	ok('published ${published.version} of "${report.repo}" from [${report.head}]')94	return 095}9697// this_machine_backend builds the backend from the machine's saved remote.98pub fn this_machine_backend() !remote.Backend {99	settings := machine.load_remote()!100	if settings.kind == machine.kind_folder {101		// Only `koh remote` creates the folder. Missing later, it is usually a102		// disk or share that is not mounted, and a stand-in folder would take103		// saves the other machine never sees.104		if !os.is_dir(settings.path) {105			return error('the remote folder ${settings.path} is not there — is its disk mounted? ("koh remote" changes it)')106		}107		return remote.new_folder(settings.path)!108	}109	credentials := machine.load_credentials()!110	return remote.new_s3(settings.endpoint, settings.bucket, settings.region,111		credentials.access_key, credentials.secret_key)!112}113114// auto_offer runs after every save and checkpoint. A remote that is not set up,115// or not reachable, must never fail the save: it prints one line and stops.116// `quiet` is for `--json`, where that line would land inside the document.117pub fn auto_offer(store &storage.Store, quiet bool) {118	if !machine.has_remote() {119		return120	}121	mut backend := this_machine_backend() or {122		if !quiet {123			note('not offered: ${err.msg()}')124		}125		return126	}127	report := exchange.offer(store, mut backend) or {128		if !quiet {129			note('not offered: ${err.msg()}')130		}131		return132	}133	if !quiet && report.saves + report.objects + report.events > 0 {134		note('offered to "${report.repo}" — ${report.saves} saves · ${report.objects} objects')135	}136}137138struct OfferedJson {139	repo    string140	saves   int141	objects int142	events  int143	head    string144}145146struct PublishedFileJson {147	platform string148	name     string149	size     i64150	blake3   string151}152153struct PublishedJson {154	repo    string155	version string156	files   []PublishedFileJson157}