1234567891011module sync1213import os14import src.events15import src.history16import src.remote17import src.snapshot18import src.storage192021const repo_state_key = 'remote_repo'22const offered_head_key = 'remote_head'2324pub const no_saves = 'this repo has no saves yet — nothing to offer'2526pub struct OfferReport {27pub mut:28 repo string29 saves int30 objects int31 events int32 head string33}34353637pub fn repo_name(store &storage.Store, mut backend remote.Backend) !string {38 if remembered := store.state(repo_state_key) {39 return remembered40 }41 name := os.base(store.root)42 existing := backend.get(head_key(name))!43 if existing.found {44 theirs := existing.body.bytestr().trim_space()45 if history.by_id(store, theirs) == none {46 return error('the remote already has "${name}" with a history this repo does not share — rename the folder, or use another remote')47 }48 }49 store.set_state(repo_state_key, name)!50 return name51}52535455pub fn offer(store &storage.Store, mut backend remote.Backend) !OfferReport {56 name := repo_name(store, mut backend)!57 local := history.head(store) or { return error(no_saves) }58 mut report := OfferReport{59 repo: name60 head: local.id61 }62 remote_head := backend.get(head_key(name))!63 if remote_head.found {64 theirs := remote_head.body.bytestr().trim_space()65 newer := saves_after(store, local.id, theirs) or {66 return error('remote has new saves — run koh steal')67 }68 for save in newer {69 upload_save(store, mut backend, name, save, mut report)!70 }71 } else {72 oldest_first := history.line(store)!73 for index := oldest_first.len - 1; index >= 0; index-- {74 upload_save(store, mut backend, name, oldest_first[index], mut report)!75 }76 }77 upload_events(store, mut backend, name, mut report)!78 if !backend.put_if_match(head_key(name), local.id.bytes(), remote_head.tag)! {79 return error('remote has new saves — run koh steal')80 }81 store.set_state(offered_head_key, local.id)!82 return report83}8485868788fn saves_after(store &storage.Store, ours string, theirs string) ![]history.Save {89 mut newer := []history.Save{}90 mut cursor := ours91 for cursor != '' && cursor != theirs {92 save := history.by_id(store, cursor) or { return error('save ${cursor} is missing') }93 newer << save94 cursor = save.parent95 }96 if cursor != theirs {97 return error('${theirs} is not on this line')98 }99 newer.reverse_in_place()100 return newer101}102103fn upload_save(store &storage.Store, mut backend remote.Backend, repo string, save history.Save, mut report OfferReport) ! {104 files := history.files_of(store, save.id)!105 for file in files {106 if backend.put_if_absent(object_key(repo, file.hash), store.object_blob(file.hash)!)! {107 report.objects++108 }109 }110 record := save_record(store, save)!111 if backend.put_if_absent(save_key(repo, save.id), encode_save(record).bytes())! {112 report.saves++113 }114}115116117118fn upload_events(store &storage.Store, mut backend remote.Backend, repo string, mut report OfferReport) ! {119 already := backend.list(events_prefix(repo))!120 mut already_sent := map[string]bool{}121 for key in already {122 already_sent[os.base(key)] = true123 }124 for event in events.all(store)! {125 if event.id in already_sent {126 continue127 }128 if backend.put_if_absent(event_key(repo, event.id), encode_event(event).bytes())! {129 report.events++130 }131 }132}133134135136137pub struct Fetched {138pub mut:139 repo_objects map[string][]u8140 saves []SaveRecord 141 events_out []events.Event142 head string143 base string 144 remote_save_ids map[string]bool145}146147pub fn fetch_back_to_known(store &storage.Store, mut backend remote.Backend, repo string) !Fetched {148 remote_head := backend.get(head_key(repo))!149 if !remote_head.found {150 return error('the remote has no "${repo}" yet — run koh offer here first')151 }152 mut fetched := Fetched{153 head: remote_head.body.bytestr().trim_space()154 remote_save_ids: map[string]bool{}155 }156 mut cursor := fetched.head157 for cursor != '' && history.by_id(store, cursor) == none {158 record_object := backend.get(save_key(repo, cursor))!159 if !record_object.found {160 return error('the remote is missing save ${cursor} — run koh offer there')161 }162 record := decode_save(record_object.body)!163 for file in record.files {164 if !store.has_object(file.hash)! && file.hash !in fetched.repo_objects {165 blob := backend.get(object_key(repo, file.hash))!166 if !blob.found {167 return error('the remote is missing object ${file.hash} that save ${record.id} needs')168 }169 fetched.repo_objects[file.hash] = blob.body170 }171 }172 fetched.remote_save_ids[record.id] = true173 fetched.saves << record174 cursor = record.parent175 }176 fetched.saves.reverse_in_place()177 fetched.base = cursor178 fetched.events_out = fetch_events(store, mut backend, repo)!179 return fetched180}181182fn fetch_events(store &storage.Store, mut backend remote.Backend, repo string) ![]events.Event {183 have := events.all(store)!184 mut known := map[string]bool{}185 for event in have {186 known[event.id] = true187 }188 mut incoming := []events.Event{}189 for key in backend.list(events_prefix(repo))! {190 id := os.base(key)191 if id in known {192 continue193 }194 body := backend.get(event_key(repo, id))!195 if !body.found {196 continue197 }198 incoming << decode_event(body.body)!199 }200 return incoming201}202203204205pub fn write_fetched(store &storage.Store, fetched Fetched) ! {206 for hash, blob in fetched.repo_objects {207 store.put_object_blob(hash, blob)!208 }209 for record in fetched.saves {210 history.import_save(store, to_save(record), to_files(record))!211 }212 for event in fetched.events_out {213 events.write(store, event)!214 }215}216217218219pub fn can_fast_forward(store &storage.Store, remote_head string, remote_ids map[string]bool) bool {220 mut cursor := remote_head221 for cursor != '' {222 if history.by_id(store, cursor) == none {223 return false224 }225 save := history.by_id(store, cursor) or { return false }226 if save.parent == '' {227 228 return true229 }230 cursor = save.parent231 }232 return false233}