1234module 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 23 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 41 42 mut bucket := this_machine_backend() or { return refuse(args.json(), err.msg()) }43 mut options := exchange.StealOptions{}44 if on_terminal(args.json()) {45 options.guide = guide_steal46 }47 report := exchange.steal(store, mut bucket, options) or {48 if err is exchange.ClashError {49 return refuse(args.json(), '${err.paths.join(', ')} changed on both machines — run "koh steal" to choose, then offer')50 }51 return refuse(args.json(), err.msg())52 }53 exchange.forget_source(store) or { return refuse(args.json(), err.msg()) }54 if !args.json() {55 ok('signed in to ${host}: this repo now syncs with your bucket')56 if report.previous_head != '' {57 note('to undo the combine: koh load ${report.previous_head}')58 }59 note_kept_versions(report)60 warn_flagged(report.flagged)61 }62 }63 mut backend := this_machine_backend() or { return refuse(args.json(), err.msg()) }64 if args.flag('builds') {65 return offer_builds(args, store, mut backend)66 }67 report := exchange.offer(store, mut backend) or { return refuse(args.json(), err.msg()) }68 if args.json() {69 emit_json(json2.encode(OfferedJson{70 repo: report.repo71 saves: report.saves72 objects: report.objects73 events: report.events74 head: report.head75 }))76 return 077 }78 if report.saves + report.objects + report.events == 0 {79 ok('the remote already has everything from [${report.head}]')80 return 081 }82 ok('offered ${report.saves} saves · ${report.objects} objects · ${report.events} events to "${report.repo}"')83 note('remote head is now [${report.head}]')84 return 085}8687888990fn offer_builds(args Args, store &storage.Store, mut backend remote.Backend) int {91 head := history.head(store) or { return refuse(args.json(), exchange.no_saves) }92 if exchange.dirty(store, head) or { true } {93 return refuse(args.json(), 'save or checkpoint first — a release is built from a save')94 }95 report := exchange.offer(store, mut backend) or { return refuse(args.json(), err.msg()) }96 config := build.read_config(store.root) or { return refuse(args.json(), err.msg()) }97 98 release_version := build.project_version(store.root) or {99 return refuse(args.json(), err.msg())100 }101 notes := args.value('notes')102 release.check_notes(notes) or { return refuse(args.json(), err.msg()) }103 published := release.publish(store, mut backend, report.repo, config, release_version, notes) or {104 return refuse(args.json(), err.msg())105 }106 if args.json() {107 emit_json(json2.encode(PublishedJson{108 repo: report.repo109 version: published.version110 files: published.files.map(PublishedFileJson{111 platform: it.platform112 name: it.name113 size: it.size114 blake3: it.blake3115 })116 }))117 return 0118 }119 for file in published.files {120 ok('${file.platform} ${file.name} (${fmt.human_bytes(file.size)})')121 }122 ok('published ${published.version} of "${report.repo}" from [${report.head}]')123 return 0124}125126127pub fn this_machine_backend() !remote.Backend {128 settings := machine.load_remote()!129 if settings.kind == machine.kind_folder {130 131 132 133 if !os.is_dir(settings.path) {134 return error('the remote folder ${settings.path} is not there — is its disk mounted? ("koh remote" changes it)')135 }136 return remote.new_folder(settings.path)!137 }138 credentials := machine.load_credentials()!139 return remote.new_s3(settings.endpoint, settings.bucket, settings.region,140 credentials.access_key, credentials.secret_key)!141}142143144145146pub fn auto_offer(store &storage.Store, quiet bool) {147 if !machine.has_remote() {148 return149 }150 151 152 if exchange.source_of(store) != '' {153 return154 }155 mut backend := this_machine_backend() or {156 if !quiet {157 note('not offered: ${err.msg()}')158 }159 return160 }161 report := exchange.offer(store, mut backend) or {162 if !quiet {163 note('not offered: ${err.msg()}')164 }165 return166 }167 if !quiet && report.saves + report.objects + report.events > 0 {168 note('offered to "${report.repo}" — ${report.saves} saves · ${report.objects} objects')169 }170}171172struct OfferedJson {173 repo string174 saves int175 objects int176 events int177 head string178}179180struct PublishedFileJson {181 platform string182 name string183 size i64184 blake3 string185}186187struct PublishedJson {188 repo string189 version string190 files []PublishedFileJson191}