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, 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 ${fmt.plural(report.saves, 'save')} · ${fmt.plural(report.objects, 'object')} · ${fmt.plural(report.events, 'event')} 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, 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 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}130131132pub fn this_machine_backend() !remote.Backend {133 settings := machine.load_remote()!134 if settings.kind == machine.kind_folder {135 136 137 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}147148149150151pub fn auto_offer(store &storage.Store, quiet bool) {152 if !machine.has_remote() {153 return154 }155 156 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}" — ${fmt.plural(report.saves, 'save')} · ${fmt.plural(report.objects, 'object')}')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}