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 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 43 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}9293949596fn 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 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}132133134pub fn this_machine_backend() !remote.Backend {135 settings := machine.load_remote()!136 if settings.kind == machine.kind_folder {137 138 139 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}149150151152153pub fn auto_offer(store &storage.Store, quiet bool) {154 if !machine.has_remote() {155 return156 }157 158 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}