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', 'yes']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 yes: args.flag('yes')51 json: args.json()52 approve: approve_sync53 }54 if on_terminal(args.json()) {55 options.guide = guide_steal56 }57 report := exchange.steal(store, mut bucket, options) or {58 if err is exchange.ClashError {59 return refuse(args.json(), '${err.paths.join(', ')} changed on both machines — run "koh steal" to choose, then offer')60 }61 return refuse(args.json(), err.msg())62 }63 exchange.forget_source(store) or { return refuse(args.json(), err.msg()) }64 flagged = report.flagged.map(it.save_id)65 if !args.json() {66 ok('signed in to ${host}: this repo now syncs with your bucket')67 if report.previous_head != '' {68 note('to undo the combine: koh load ${report.previous_head}')69 }70 note_kept_versions(report)71 warn_flagged(report.flagged)72 }73 }74 mut backend := watched_backend('offering') or { return refuse(args.json(), err.msg()) }75 if args.flag('builds') {76 return offer_builds(args, store, mut backend, flagged)77 }78 report := exchange.offer(store, mut backend) or { return refuse(args.json(), err.msg()) }79 if args.json() {80 emit_json(json2.encode(OfferedJson{81 repo: report.repo82 saves: report.saves83 objects: report.objects84 events: report.events85 head: report.head86 flagged: flagged87 }))88 return 089 }90 if report.saves + report.objects + report.events == 0 {91 ok('the remote already has everything from [${report.head}]')92 return 093 }94 ok('offered ${fmt.plural(report.saves, 'save')} · ${fmt.plural(report.objects, 'object')} · ${fmt.plural(report.events, 'event')} to "${report.repo}"')95 note('remote head is now [${report.head}]')96 return 097}9899100101102fn offer_builds(args Args, store &storage.Store, mut backend remote.Backend, flagged []string) int {103 head := history.head(store) or { return refuse(args.json(), exchange.no_saves) }104 if exchange.dirty(store, head) or { true } {105 return refuse(args.json(), 'save or checkpoint first — a release is built from a save')106 }107 report := exchange.offer(store, mut backend) or { return refuse(args.json(), err.msg()) }108 config := build.read_config(store.root) or { return refuse(args.json(), err.msg()) }109 110 release_version := build.project_version(store.root) or {111 return refuse(args.json(), err.msg())112 }113 notes := args.value('notes')114 release.check_notes(notes) or { return refuse(args.json(), err.msg()) }115 published := release.publish(store, mut backend, report.repo, config, release_version, notes) or {116 return refuse(args.json(), err.msg())117 }118 if args.json() {119 emit_json(json2.encode(PublishedJson{120 repo: report.repo121 version: published.version122 files: published.files.map(PublishedFileJson{123 platform: it.platform124 name: it.name125 size: it.size126 blake3: it.blake3127 })128 flagged: flagged129 }))130 return 0131 }132 for file in published.files {133 ok('${file.platform} ${file.name} (${fmt.human_bytes(file.size)})')134 }135 ok('published ${published.version} of "${report.repo}" from [${report.head}]')136 return 0137}138139140pub fn this_machine_backend() !remote.Backend {141 settings := machine.load_remote()!142 if settings.kind == machine.kind_folder {143 144 145 146 if !os.is_dir(settings.path) {147 return error('the remote folder ${settings.path} is not there — is its disk mounted? ("koh remote" changes it)')148 }149 return remote.new_folder(settings.path)!150 }151 credentials := machine.load_credentials()!152 return remote.new_s3(settings.endpoint, settings.bucket, settings.region,153 credentials.access_key, credentials.secret_key)!154}155156157158159pub fn auto_offer(store &storage.Store, quiet bool) {160 if !machine.has_remote() {161 return162 }163 164 165 if exchange.source_of(store) != '' {166 return167 }168 mut backend := watched_backend('offering') or {169 if !quiet {170 note('not offered: ${err.msg()}')171 }172 return173 }174 report := exchange.offer(store, mut backend) or {175 if !quiet {176 note('not offered: ${err.msg()}')177 }178 return179 }180 if !quiet && report.saves + report.objects + report.events > 0 {181 note('offered to "${report.repo}" — ${fmt.plural(report.saves, 'save')} · ${fmt.plural(report.objects, 'object')}')182 }183}184185struct OfferedJson {186 repo string187 saves int188 objects int189 events int190 head string191 flagged []string192}193194struct PublishedFileJson {195 platform string196 name string197 size i64198 blake3 string199}200201struct PublishedJson {202 repo string203 version string204 files []PublishedFileJson205 flagged []string206}