1234567891011module exchange1213import os14import src.events15import src.history16import src.remote17import src.snapshot18import src.storage192021const repo_state_key = 'remote_repo'22232425const remote_head_key = 'remote_head'2627pub const no_saves = 'this repo has no saves yet — nothing to offer'28293031const format_value = 'koh-remote 1'3233343536fn check_format(mut backend remote.Backend, repo string) !string {37 existing := backend.get(format_key(repo))!38 if !existing.found {39 return ''40 }41 value := existing.body.bytestr().trim_space()42 if value != format_value {43 return error('the remote "${repo}" is "${value}" — this koh speaks "${format_value}"; update koh')44 }45 return value46}4748pub struct OfferReport {49pub mut:50 repo string51 saves int52 objects int53 events int54 head string55}56575859pub fn remember_repo(store &storage.Store, name string) ! {60 store.set_state(repo_state_key, name)!61}62636465pub fn repo_name(store &storage.Store, mut backend remote.Backend) !string {66 if remembered := store.state(repo_state_key) {67 return remembered68 }69 name := os.base(store.root)70 existing := backend.get(head_key(name))!71 if existing.found {72 theirs := existing.body.bytestr().trim_space()73 if history.by_id(store, theirs) == none {74 return error('the remote already has "${name}" with a history this repo does not share — rename the folder, or use another remote')75 }76 }77 store.set_state(repo_state_key, name)!78 return name79}80818283pub fn offer(store &storage.Store, mut backend remote.Backend) !OfferReport {84 name := repo_name(store, mut backend)!85 local := history.head(store) or { return error(no_saves) }86 mut report := OfferReport{87 repo: name88 head: local.id89 }90 if check_format(mut backend, name)! == '' {91 92 93 backend.put_if_absent(format_key(name), format_value.bytes())!94 }95 remote_head := backend.get(head_key(name))!96 97 98 99 100 if unapplied_strip(store, mut backend, name)! {101 return error('the remote has a strip this repo has not applied — run koh steal')102 }103 if remote_head.found {104 theirs := remote_head.body.bytestr().trim_space()105 newer := saves_after(store, local.id, theirs) or {106 return error('remote has new saves — run koh steal')107 }108 for save in newer {109 upload_save(store, mut backend, name, save, mut report)!110 }111 } else {112 oldest_first := history.line(store)!113 for index := oldest_first.len - 1; index >= 0; index-- {114 upload_save(store, mut backend, name, oldest_first[index], mut report)!115 }116 }117 upload_events(store, mut backend, name, mut report)!118 if !backend.put_if_match(head_key(name), local.id.bytes(), remote_head.tag)! {119 return error('remote has new saves — run koh steal')120 }121 store.set_state(remote_head_key, local.id)!122 return report123}124125126127128fn saves_after(store &storage.Store, ours string, theirs string) ![]history.Save {129 mut newer := []history.Save{}130 mut cursor := ours131 for cursor != '' && cursor != theirs {132 save := history.by_id(store, cursor) or { return error('save ${cursor} is missing') }133 newer << save134 cursor = save.parent135 }136 if cursor != theirs {137 return error('${theirs} is not on this line')138 }139 newer.reverse_in_place()140 return newer141}142143fn upload_save(store &storage.Store, mut backend remote.Backend, repo string, save history.Save, mut report OfferReport) ! {144 files := history.files_of(store, save.id)!145 146 147 148 mut sent := map[string]bool{}149 if save.parent != '' {150 for file in history.files_of(store, save.parent)! {151 sent[file.hash] = true152 }153 }154 for file in files {155 if file.hash in sent {156 continue157 }158 sent[file.hash] = true159 if backend.put_if_absent(object_key(repo, file.hash), store.object_blob(file.hash)!)! {160 report.objects++161 }162 }163 record := save_record(store, save)!164 if backend.put_if_absent(save_key(repo, save.id), encode_save(record).bytes())! {165 report.saves++166 }167}168169170171fn upload_events(store &storage.Store, mut backend remote.Backend, repo string, mut report OfferReport) ! {172 already := backend.list(events_prefix(repo))!173 mut already_sent := map[string]bool{}174 for key in already {175 already_sent[os.base(key)] = true176 }177 for event in events.all(store)! {178 if event.id in already_sent {179 continue180 }181 if backend.put_if_absent(event_key(repo, event.id), encode_event(event).bytes())! {182 report.events++183 }184 }185}186187188189190pub struct Fetched {191pub mut:192 repo_objects map[string][]u8193 saves []SaveRecord 194 events_out []events.Event195 head string196 base string 197}198199pub fn fetch_back_to_known(store &storage.Store, mut backend remote.Backend, repo string) !Fetched {200 check_format(mut backend, repo)!201 remote_head := backend.get(head_key(repo))!202 if !remote_head.found {203 return error('the remote has no "${repo}" yet — run koh offer here first')204 }205 mut fetched := Fetched{206 head: remote_head.body.bytestr().trim_space()207 }208 mut cursor := fetched.head209 for cursor != '' && history.by_id(store, cursor) == none {210 record_object := backend.get(save_key(repo, cursor))!211 if !record_object.found {212 return error('the remote is missing save ${cursor} — run koh offer there')213 }214 record := decode_save(record_object.body)!215 for file in record.files {216 if !store.has_object(file.hash)! && file.hash !in fetched.repo_objects {217 blob := backend.get(object_key(repo, file.hash))!218 if !blob.found {219 return error('the remote is missing object ${file.hash} that save ${record.id} needs')220 }221 fetched.repo_objects[file.hash] = blob.body222 }223 }224 fetched.saves << record225 cursor = record.parent226 }227 fetched.saves.reverse_in_place()228 fetched.base = cursor229 fetched.events_out = fetch_events(store, mut backend, repo)!230 return fetched231}232233fn fetch_events(store &storage.Store, mut backend remote.Backend, repo string) ![]events.Event {234 have := events.all(store)!235 mut known := map[string]bool{}236 for event in have {237 known[event.id] = true238 }239 mut incoming := []events.Event{}240 for key in backend.list(events_prefix(repo))! {241 id := os.base(key)242 if id in known {243 continue244 }245 body := backend.get(event_key(repo, id))!246 if !body.found {247 continue248 }249 incoming << decode_event(body.body)!250 }251 return incoming252}253254255256257258pub fn write_fetched(store &storage.Store, fetched Fetched) ! {259 for hash, blob in fetched.repo_objects {260 store.put_object_blob(hash, blob)!261 }262 mut strips := []events.Event{}263 for event in fetched.events_out {264 if !events.receive(store, event)! {265 continue266 }267 if event.kind == events.kind_strip {268 269 270 remove_path(store, event.path, event.save_id)!271 strips << event272 }273 }274 for record in fetched.saves {275 files := to_files(record).filter(fn [strips, record] (file snapshot.FileEntry) bool {276 return !strips.any(reaches(it, record.id, record.created_at)277 && covers(it.path, file.path))278 })279 history.import_save(store, to_save(record), files)!280 }281 if strips.len > 0 {282 store.drop_unreferenced_objects()!283 }284}285286287288fn reaches(strip events.Event, save_id string, created_at i64) bool {289 if strip.save_id != '' {290 return strip.save_id == save_id291 }292 return created_at <= strip.created_at293}