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 := 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)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}9091929394fn offer_builds(args Args, store &storage.Store, mut backend remote.Backend) 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 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 }))121 return 0122 }123 for file in published.files {124 ok('${file.platform} ${file.name} (${fmt.human_bytes(file.size)})')125 }126 ok('published ${published.version} of "${report.repo}" from [${report.head}]')127 return 0128}129130131pub fn this_machine_backend() !remote.Backend {132 settings := machine.load_remote()!133 if settings.kind == machine.kind_folder {134 135 136 137 if !os.is_dir(settings.path) {138 return error('the remote folder ${settings.path} is not there — is its disk mounted? ("koh remote" changes it)')139 }140 return remote.new_folder(settings.path)!141 }142 credentials := machine.load_credentials()!143 return remote.new_s3(settings.endpoint, settings.bucket, settings.region,144 credentials.access_key, credentials.secret_key)!145}146147148149150pub fn auto_offer(store &storage.Store, quiet bool) {151 if !machine.has_remote() {152 return153 }154 155 156 if exchange.source_of(store) != '' {157 return158 }159 mut backend := this_machine_backend() or {160 if !quiet {161 note('not offered: ${err.msg()}')162 }163 return164 }165 report := exchange.offer(store, mut backend) or {166 if !quiet {167 note('not offered: ${err.msg()}')168 }169 return170 }171 if !quiet && report.saves + report.objects + report.events > 0 {172 note('offered to "${report.repo}" — ${report.saves} saves · ${report.objects} objects')173 }174}175176struct OfferedJson {177 repo string178 saves int179 objects int180 events int181 head string182 flagged []string183}184185struct PublishedFileJson {186 platform string187 name string188 size i64189 blake3 string190}191192struct PublishedJson {193 repo string194 version string195 files []PublishedFileJson196}