cmd_offer.v

6.7 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 := this_machine_backend() or { return refuse(args.json(), err.msg()) }45		mut options := exchange.StealOptions{}46		if on_terminal(args.json()) {47			options.guide = guide_steal48		}49		report := exchange.steal(store, mut bucket, options) or {50			if err is exchange.ClashError {51				return refuse(args.json(), '${err.paths.join(', ')} changed on both machines — run "koh steal" to choose, then offer')52			}53			return refuse(args.json(), err.msg())54		}55		exchange.forget_source(store) or { return refuse(args.json(), err.msg()) }56		flagged = report.flagged.map(it.save_id)57		if !args.json() {58			ok('signed in to ${host}: this repo now syncs with your bucket')59			if report.previous_head != '' {60				note('to undo the combine: koh load ${report.previous_head}')61			}62			note_kept_versions(report)63			warn_flagged(report.flagged)64		}65	}66	mut backend := this_machine_backend() or { return refuse(args.json(), err.msg()) }67	if args.flag('builds') {68		return offer_builds(args, store, mut backend, flagged)69	}70	report := exchange.offer(store, mut backend) or { return refuse(args.json(), err.msg()) }71	if args.json() {72		emit_json(json2.encode(OfferedJson{73			repo:    report.repo74			saves:   report.saves75			objects: report.objects76			events:  report.events77			head:    report.head78			flagged: flagged79		}))80		return 081	}82	if report.saves + report.objects + report.events == 0 {83		ok('the remote already has everything from [${report.head}]')84		return 085	}86	ok('offered ${report.saves} saves · ${report.objects} objects · ${report.events} events to "${report.repo}"')87	note('remote head is now [${report.head}]')88	return 089}9091// offer_builds is `koh offer --builds`: the history goes up first, then a92// release built from the working tree. Unsaved changes are refused, so the93// published files always belong to a save that is on the remote.94fn offer_builds(args Args, store &storage.Store, mut backend remote.Backend, flagged []string) int {95	head := history.head(store) or { return refuse(args.json(), exchange.no_saves) }96	if exchange.dirty(store, head) or { true } {97		return refuse(args.json(), 'save or checkpoint first — a release is built from a save')98	}99	report := exchange.offer(store, mut backend) or { return refuse(args.json(), err.msg()) }100	config := build.read_config(store.root) or { return refuse(args.json(), err.msg()) }101	// Not `version`: that is the koh binary's own version, in this module.102	release_version := build.project_version(store.root) or {103		return refuse(args.json(), err.msg())104	}105	notes := args.value('notes')106	release.check_notes(notes) or { return refuse(args.json(), err.msg()) }107	published := release.publish(store, mut backend, report.repo, config, release_version, notes) or {108		return refuse(args.json(), err.msg())109	}110	if args.json() {111		emit_json(json2.encode(PublishedJson{112			repo:    report.repo113			version: published.version114			files:   published.files.map(PublishedFileJson{115				platform: it.platform116				name:     it.name117				size:     it.size118				blake3:   it.blake3119			})120			flagged: flagged121		}))122		return 0123	}124	for file in published.files {125		ok('${file.platform}  ${file.name}  (${fmt.human_bytes(file.size)})')126	}127	ok('published ${published.version} of "${report.repo}" from [${report.head}]')128	return 0129}130131// this_machine_backend builds the backend from the machine's saved remote.132pub fn this_machine_backend() !remote.Backend {133	settings := machine.load_remote()!134	if settings.kind == machine.kind_folder {135		// Only `koh remote` creates the folder. Missing later, it is usually a136		// disk or share that is not mounted, and a stand-in folder would take137		// saves the other machine never sees.138		if !os.is_dir(settings.path) {139			return error('the remote folder ${settings.path} is not there — is its disk mounted? ("koh remote" changes it)')140		}141		return remote.new_folder(settings.path)!142	}143	credentials := machine.load_credentials()!144	return remote.new_s3(settings.endpoint, settings.bucket, settings.region,145		credentials.access_key, credentials.secret_key)!146}147148// auto_offer runs after every save and checkpoint. A remote that is not set up,149// or not reachable, must never fail the save: it prints one line and stops.150// `quiet` is for `--json`, where that line would land inside the document.151pub fn auto_offer(store &storage.Store, quiet bool) {152	if !machine.has_remote() {153		return154	}155	// A stolen repo rides its source: a save in it is local work and nothing else. The upgrade is156	// the explicit `steal` or `offer`, which bring the bucket in *before* the source is dropped.157	if exchange.source_of(store) != '' {158		return159	}160	mut backend := this_machine_backend() or {161		if !quiet {162			note('not offered: ${err.msg()}')163		}164		return165	}166	report := exchange.offer(store, mut backend) or {167		if !quiet {168			note('not offered: ${err.msg()}')169		}170		return171	}172	if !quiet && report.saves + report.objects + report.events > 0 {173		note('offered to "${report.repo}" — ${report.saves} saves · ${report.objects} objects')174	}175}176177struct OfferedJson {178	repo    string179	saves   int180	objects int181	events  int182	head    string183	flagged []string184}185186struct PublishedFileJson {187	platform string188	name     string189	size     i64190	blake3   string191}192193struct PublishedJson {194	repo    string195	version string196	files   []PublishedFileJson197	flagged []string198}