1234module cli56import json27import os8import src.build9import src.exchange10import src.fmt11import src.history12import src.machine13import src.release14import src.remote15import src.storage16171819pub fn cmd_offer(raw []string) int {20 args := parse('offer', raw, Options{21 values: ['notes']22 flags: ['json', 'builds']23 }) or { return usage_error(err.msg()) }24 25 26 if args.positional.len > 0 {27 return usage_error('offer takes no arguments, not "${args.positional_at(0)}"')28 }29 if args.value_or_none('notes') != none && !args.flag('builds') {30 return usage_error('--notes belongs to --builds')31 }32 store := open_store() or { return refuse(args.json(), err.msg()) }33 defer {34 store.close()35 }36 37 mut flagged := []string{}38 source := exchange.source_of(store)39 if source != '' {40 host := exchange.source_host(source)41 if !exchange.signed_in_to(host) {42 return refuse(args.json(), 'this repo came from ${source}, which is read-only. Sign in with "koh remote ${host}" if it\'s yours.')43 }44 45 46 mut bucket := watched_backend('bringing in your bucket') or {47 return refuse(args.json(), err.msg())48 }49 mut options := exchange.StealOptions{}50 if on_terminal(args.json()) {51 options.guide = guide_steal52 }53 report := exchange.steal(store, mut bucket, options) or {54 if err is exchange.ClashError {55 return refuse(args.json(), '${err.paths.join(', ')} changed on both machines — run "koh steal" to choose, then offer')56 }57 return refuse(args.json(), err.msg())58 }59 exchange.forget_source(store) or { return refuse(args.json(), err.msg()) }60 flagged = report.flagged.map(it.save_id)61 if !args.json() {62 ok('signed in to ${host}: this repo now syncs with your bucket')63 if report.previous_head != '' {64 note('to undo the combine: koh load ${report.previous_head}')65 }66 note_kept_versions(report)67 warn_flagged(report.flagged)68 }69 }70 mut backend := watched_backend('offering') or { return refuse(args.json(), err.msg()) }71 if args.flag('builds') {72 return offer_builds(args, store, mut backend, flagged)73 }74 report := exchange.offer(store, mut backend) or { return refuse(args.json(), err.msg()) }75 if args.json() {76 emit_json(json2.encode(OfferedJson{77 repo: report.repo78 saves: report.saves79 objects: report.objects80 events: report.events81 head: report.head82 flagged: flagged83 }))84 return 085 }86 if report.saves + report.objects + report.events == 0 {87 ok('the remote already has everything from [${report.head}]')88 return 089 }90 ok('offered ${fmt.plural(report.saves, 'save')} · ${fmt.plural(report.objects, 'object')} · ${fmt.plural(report.events, 'event')} to "${report.repo}"')91 note('remote head is now [${report.head}]')92 return 093}9495969798fn offer_builds(args Args, store &storage.Store, mut backend remote.Backend, flagged []string) int {99 head := history.head(store) or { return refuse(args.json(), exchange.no_saves) }100 if exchange.dirty(store, head) or { true } {101 return refuse(args.json(), 'save or checkpoint first — a release is built from a save')102 }103 report := exchange.offer(store, mut backend) or { return refuse(args.json(), err.msg()) }104 config := build.read_config(store.root) or { return refuse(args.json(), err.msg()) }105 106 release_version := build.project_version(store.root) or {107 return refuse(args.json(), err.msg())108 }109 notes := args.value('notes')110 release.check_notes(notes) or { return refuse(args.json(), err.msg()) }111 published := release.publish(store, mut backend, report.repo, config, release_version, notes) or {112 return refuse(args.json(), err.msg())113 }114 if args.json() {115 emit_json(json2.encode(PublishedJson{116 repo: report.repo117 version: published.version118 files: published.files.map(PublishedFileJson{119 platform: it.platform120 name: it.name121 size: it.size122 blake3: it.blake3123 })124 flagged: flagged125 }))126 return 0127 }128 for file in published.files {129 ok('${file.platform} ${file.name} (${fmt.human_bytes(file.size)})')130 }131 ok('published ${published.version} of "${report.repo}" from [${report.head}]')132 return 0133}134135136pub fn this_machine_backend() !remote.Backend {137 settings := machine.load_remote()!138 if settings.kind == machine.kind_folder {139 140 141 142 if !os.is_dir(settings.path) {143 return error('the remote folder ${settings.path} is not there — is its disk mounted? ("koh remote" changes it)')144 }145 return remote.new_folder(settings.path)!146 }147 credentials := machine.load_credentials()!148 return remote.new_s3(settings.endpoint, settings.bucket, settings.region,149 credentials.access_key, credentials.secret_key)!150}151152153154155pub fn auto_offer(store &storage.Store, quiet bool) {156 if !machine.has_remote() {157 return158 }159 160 161 if exchange.source_of(store) != '' {162 return163 }164 mut backend := watched_backend('offering') or {165 if !quiet {166 note('not offered: ${err.msg()}')167 }168 return169 }170 report := exchange.offer(store, mut backend) or {171 if !quiet {172 note('not offered: ${err.msg()}')173 }174 return175 }176 if !quiet && report.saves + report.objects + report.events > 0 {177 note('offered to "${report.repo}" — ${fmt.plural(report.saves, 'save')} · ${fmt.plural(report.objects, 'object')}')178 }179}180181struct OfferedJson {182 repo string183 saves int184 objects int185 events int186 head string187 flagged []string188}189190struct PublishedFileJson {191 platform string192 name string193 size i64194 blake3 string195}196197struct PublishedJson {198 repo string199 version string200 files []PublishedFileJson201 flagged []string202}