1234567891011module sync1213import os14import src.events15import src.history16import src.remote17import src.storage181920const repo_state_key = 'remote_repo'21const offered_head_key = 'remote_head'2223pub const no_saves = 'this repo has no saves yet — nothing to offer'2425pub struct OfferReport {26pub mut:27 repo string28 saves int29 objects int30 events int31 head string32}33343536pub fn remember_repo(store &storage.Store, name string) ! {37 store.set_state(repo_state_key, name)!38}39404142pub fn repo_name(store &storage.Store, mut backend remote.Backend) !string {43 if remembered := store.state(repo_state_key) {44 return remembered45 }46 name := os.base(store.root)47 existing := backend.get(head_key(name))!48 if existing.found {49 theirs := existing.body.bytestr().trim_space()50 if history.by_id(store, theirs) == none {51 return error('the remote already has "${name}" with a history this repo does not share — rename the folder, or use another remote')52 }53 }54 store.set_state(repo_state_key, name)!55 return name56}57585960pub fn offer(store &storage.Store, mut backend remote.Backend) !OfferReport {61 name := repo_name(store, mut backend)!62 local := history.head(store) or { return error(no_saves) }63 mut report := OfferReport{64 repo: name65 head: local.id66 }67 remote_head := backend.get(head_key(name))!68 69 70 71 72 if unapplied_strip(store, mut backend, name)! {73 return error('the remote has a strip this repo has not applied — run koh steal')74 }75 if remote_head.found {76 theirs := remote_head.body.bytestr().trim_space()77 newer := saves_after(store, local.id, theirs) or {78 return error('remote has new saves — run koh steal')79 }80 for save in newer {81 upload_save(store, mut backend, name, save, mut report)!82 }83 } else {84 oldest_first := history.line(store)!85 for index := oldest_first.len - 1; index >= 0; index-- {86 upload_save(store, mut backend, name, oldest_first[index], mut report)!87 }88 }89 upload_events(store, mut backend, name, mut report)!90 if !backend.put_if_match(head_key(name), local.id.bytes(), remote_head.tag)! {91 return error('remote has new saves — run koh steal')92 }93 store.set_state(offered_head_key, local.id)!94 return report95}96979899100fn saves_after(store &storage.Store, ours string, theirs string) ![]history.Save {101 mut newer := []history.Save{}102 mut cursor := ours103 for cursor != '' && cursor != theirs {104 save := history.by_id(store, cursor) or { return error('save ${cursor} is missing') }105 newer << save106 cursor = save.parent107 }108 if cursor != theirs {109 return error('${theirs} is not on this line')110 }111 newer.reverse_in_place()112 return newer113}114115fn upload_save(store &storage.Store, mut backend remote.Backend, repo string, save history.Save, mut report OfferReport) ! {116 files := history.files_of(store, save.id)!117 for file in files {118 if backend.put_if_absent(object_key(repo, file.hash), store.object_blob(file.hash)!)! {119 report.objects++120 }121 }122 record := save_record(store, save)!123 if backend.put_if_absent(save_key(repo, save.id), encode_save(record).bytes())! {124 report.saves++125 }126}127128129130fn upload_events(store &storage.Store, mut backend remote.Backend, repo string, mut report OfferReport) ! {131 already := backend.list(events_prefix(repo))!132 mut already_sent := map[string]bool{}133 for key in already {134 already_sent[os.base(key)] = true135 }136 for event in events.all(store)! {137 if event.id in already_sent {138 continue139 }140 if backend.put_if_absent(event_key(repo, event.id), encode_event(event).bytes())! {141 report.events++142 }143 }144}145146147148149pub struct Fetched {150pub mut:151 repo_objects map[string][]u8152 saves []SaveRecord 153 events_out []events.Event154 head string155 base string 156 remote_save_ids map[string]bool157}158159pub fn fetch_back_to_known(store &storage.Store, mut backend remote.Backend, repo string) !Fetched {160 remote_head := backend.get(head_key(repo))!161 if !remote_head.found {162 return error('the remote has no "${repo}" yet — run koh offer here first')163 }164 mut fetched := Fetched{165 head: remote_head.body.bytestr().trim_space()166 remote_save_ids: map[string]bool{}167 }168 mut cursor := fetched.head169 for cursor != '' && history.by_id(store, cursor) == none {170 record_object := backend.get(save_key(repo, cursor))!171 if !record_object.found {172 return error('the remote is missing save ${cursor} — run koh offer there')173 }174 record := decode_save(record_object.body)!175 for file in record.files {176 if !store.has_object(file.hash)! && file.hash !in fetched.repo_objects {177 blob := backend.get(object_key(repo, file.hash))!178 if !blob.found {179 return error('the remote is missing object ${file.hash} that save ${record.id} needs')180 }181 fetched.repo_objects[file.hash] = blob.body182 }183 }184 fetched.remote_save_ids[record.id] = true185 fetched.saves << record186 cursor = record.parent187 }188 fetched.saves.reverse_in_place()189 fetched.base = cursor190 fetched.events_out = fetch_events(store, mut backend, repo)!191 return fetched192}193194fn fetch_events(store &storage.Store, mut backend remote.Backend, repo string) ![]events.Event {195 have := events.all(store)!196 mut known := map[string]bool{}197 for event in have {198 known[event.id] = true199 }200 mut incoming := []events.Event{}201 for key in backend.list(events_prefix(repo))! {202 id := os.base(key)203 if id in known {204 continue205 }206 body := backend.get(event_key(repo, id))!207 if !body.found {208 continue209 }210 incoming << decode_event(body.body)!211 }212 return incoming213}214215216217pub fn write_fetched(store &storage.Store, fetched Fetched) ! {218 for hash, blob in fetched.repo_objects {219 store.put_object_blob(hash, blob)!220 }221 for record in fetched.saves {222 history.import_save(store, to_save(record), to_files(record))!223 }224 for event in fetched.events_out {225 if !events.receive(store, event)! {226 continue227 }228 if event.kind == events.kind_strip {229 230 231 apply_strip(store, event)!232 }233 }234}235236237238pub fn can_fast_forward(store &storage.Store, remote_head string, remote_ids map[string]bool) bool {239 mut cursor := remote_head240 for cursor != '' {241 if history.by_id(store, cursor) == none {242 return false243 }244 save := history.by_id(store, cursor) or { return false }245 if save.parent == '' {246 247 return true248 }249 cursor = save.parent250 }251 return false252}