4fedb4de22e5

explain the sync, strip, save and restore code (3.2.18)

time
2026-09-29 02:40 UTC · 2 weeks ago
machine
Junes-MacBook-Pro.local
agent
claude-sonnet-5-5
session
comment and tidy pass
parent
17c48991c43c
10 files VERSION+1 −1src/exchange/exchange.v+6 −0src/exchange/records.v+5 −0src/exchange/steal.v+2 −0src/exchange/strip.v+8 −1src/history/save.v+3 −0src/remote/s3.v+6 −0src/snapshot/restore.v+3 −0src/snapshot/tree.v+4 −0v.mod+1 −1
VERSION+1 −1
@@ -1 +1 @@1-3.2.171+3.2.18

view file

src/exchange/exchange.v+6 −0
@@ -181,6 +181,9 @@181181 	}182182 }183183 184+// upload_save puts a save's new objects, then its record, so a record on the remote always finds its185+// objects there. Both are put_if_absent, so a retry after a partial offer is safe; `uploaded` keeps186+// what this call created, which is all take_back may delete.184187 fn upload_save(store &storage.Store, mut backend remote.Backend, repo string, save history.Save, mut report OfferReport, mut uploaded Uploaded) ! {185188 	files := history.files_of(store, save.id)!186189 	// Saves go up oldest first and head moves only once all of them landed, so@@ -240,6 +243,9 @@240243 	unsafe_paths []string241244 }242245 246+// fetch_back_to_known walks from the remote head along parents until it meets a save on this line247+// (`base`), reading only the objects this repo lacks. Nothing is written here: write_fetched lands it.248+// A save held locally but off the line, from an earlier steal, is taken from the local copy.243249 pub fn fetch_back_to_known(store &storage.Store, mut backend remote.Backend, repo string) !Fetched {244250 	check_format(mut backend, repo)!245251 	remote_head := backend.get(head_key(repo))!

view file

src/exchange/records.v+5 −0
@@ -20,6 +20,7 @@2020 import src.storage2121 import strconv2222 23+// One spelling for each key of the layout above (spec §9.2), so no caller builds a path by hand.2324 pub fn format_key(repo string) string {2425 	return '${repo}/format'2526 }@@ -40,6 +41,7 @@4041 	return '${repo}/head'4142 }4243 44+// events_prefix lists a repo's events; event_key names one.4345 pub fn events_prefix(repo string) string {4446 	return '${repo}/events'4547 }@@ -100,6 +102,8 @@100102 	return json2.encode(record)101103 }102104 105+// decode_save reads a record the remote holds. A lone surrogate escape fails only its path,106+// not the whole record (spec §5.5).103107 pub fn decode_save(body []u8) !SaveRecord {104108 	return json2.decode[SaveRecord](lone_surrogates_as_bytes(body).bytestr())!105109 }@@ -218,6 +222,7 @@218222 	return json2.encode(event_record(event))219223 }220224 225+// decode_event reads an event the way decode_save reads a save.221226 pub fn decode_event(body []u8) !events.Event {222227 	record := json2.decode[EventRecord](lone_surrogates_as_bytes(body).bytestr())!223228 	return events.Event{

view file

src/exchange/steal.v+2 −0
@@ -445,6 +445,8 @@445445 	return saves.last()446446 }447447 448+// combine_preview counts what a combine would do to the paths either side changed since `base`:449+// `identical` were changed the same way on both sides, `clashes` need a choice, the rest merge alone.448450 fn combine_preview(store &storage.Store, name string, fetched Fetched, local []history.Save, clashes []Clash, yes bool, all_side string) !StealPreview {449451 	base := history.files_of(store, fetched.base)!450452 	mine := history.files_of(store, local.last().id)!

view file

src/exchange/strip.v+8 −1
@@ -53,6 +53,7 @@5353 	return json2.decode[Pending](text) or { Pending{} }5454 }5555 56+// strip_pending is true while the remote half of a strip has not finished.5657 pub fn strip_pending(store &storage.Store, path string) bool {5758 	return store.state(pending_key(path)) != none5859 }@@ -63,6 +64,7 @@6364 	return file == path || file.starts_with('${path.trim_right('/')}/')6465 }6566 67+// strip_scope counts what strip_rows will delete, using the same match, so the warning is exact.6668 pub fn strip_scope(store &storage.Store, path string, only_save string) !Scope {6769 	mut stmt := store.db.prepare('SELECT COUNT(*), COUNT(DISTINCT save_id), COUNT(DISTINCT hash)6870 		FROM save_files WHERE ${match_clause(only_save)}')!@@ -90,6 +92,8 @@9092 	return report9193 }9294 95+// strip_rows is the body of strip_local's transaction. Its event has no save id for a whole-history96+// strip (spec §10.2), and that empty id is what bans the path from every later walk.9397 fn strip_rows(store &storage.Store, path string, only_save string) !LocalStrip {9498 	mut hashes := []string{}9599 	mut stmt :=@@ -175,7 +179,8 @@175179 }176180 177181 // unapplied_strips are the strip events on the remote this repo has not178-// received yet.182+// received yet. offer asks before uploading and again after, because a strip that lands183+// in between would otherwise leave the stripped path in the bucket.179184 fn unapplied_strips(store &storage.Store, mut backend remote.Backend, repo string) ![]events.Event {180185 	mut known := map[string]bool{}181186 	for event in events.all(store)! {@@ -206,6 +211,7 @@206211 	}207212 }208213 214+// remove_path deletes the rows and counts them; the objects go afterwards, once nothing references them.209215 fn remove_path(store &storage.Store, path string, only_save string) !int {210216 	mut stmt := store.db.prepare('DELETE FROM save_files WHERE ${match_clause(only_save)}')!211217 	defer {@@ -223,6 +229,7 @@223229 	return '(path = ?1 OR substr(path, 1, length(?2)) = ?2)${scope}'224230 }225231 232+// bind_match fills what match_clause names: ?1 the path, ?2 its folder prefix, ?3 the save.226233 fn bind_match(stmt &storage.Stmt, path string, only_save string) ! {227234 	stmt.bind_text(1, path)!228235 	stmt.bind_text(2, '${path.trim_right('/')}/')!

view file

src/history/save.v+3 −0
@@ -99,6 +99,8 @@9999 	return write_save(store, files, input)!100100 }101101 102+// write_save mints the ID, inserts the save and then moves head, inside the caller's transaction, so103+// the save's rows and head change together or not at all. The objects were stored before (record_save).102104 fn write_save(store &storage.Store, files []snapshot.FileEntry, input SaveInput) !Save {103105 	created_at := if input.created_at > 0 { input.created_at } else { time.now().unix() }104106 	mut save := Save{@@ -141,6 +143,7 @@141143 	store.set_state(storage.head_key, id)!142144 }143145 146+// insert_save writes the save row, then one row per file. Its callers hold the transaction.144147 fn insert_save(store &storage.Store, save Save, files []snapshot.FileEntry) ! {145148 	mut stmt := store.db.prepare('INSERT INTO saves (id, parent, message, created_at, machine,146149 		is_checkpoint, size_bytes, agent, session, restores, replayed_from)

view file

src/remote/s3.v+6 −0
@@ -53,6 +53,7 @@5353 	}5454 }5555 56+// get answers 404 as not found, not an error. The ETag comes back as `tag` for put_if_match.5657 pub fn (mut s S3) get(key string) !Object {5758 	response := s.send('GET', key, '', map[string]string{}, []u8{})!5859 	match response.status_code {@@ -74,6 +75,7 @@7475 	}7576 }7677 78+// list follows ListObjectsV2 continuation tokens until the listing is no longer truncated.7779 pub fn (mut s S3) list(prefix string) ![]string {7880 	mut keys := []string{}7981 	mut token := ''@@ -97,6 +99,7 @@9799 	return keys98100 }99101 102+// put_if_absent sends If-None-Match: *. A 412 means the key exists and nothing was written: false, not an error.100103 pub fn (mut s S3) put_if_absent(key string, body []u8) !bool {101104 	response := s.send('PUT', key, '', {102105 		'If-None-Match': '*'@@ -110,6 +113,8 @@110113 	return true111114 }112115 116+// put_if_match is the only overwrite: If-Match with the tag from get, or If-None-Match: * when there was no117+// head yet (tag ). A 412 means another machine moved the key first: false, not an error.113118 pub fn (mut s S3) put_if_match(key string, body []u8, tag string) !bool {114119 	mut headers := map[string]string{}115120 	if tag == '' {@@ -128,6 +133,7 @@128133 	return true129134 }130135 136+// delete counts a missing key as done.131137 pub fn (mut s S3) delete(key string) ! {132138 	response := s.send('DELETE', key, '', map[string]string{}, []u8{})!133139 	if !success(response.status_code) && response.status_code != 404 {

view file

src/snapshot/restore.v+3 −0
@@ -59,6 +59,9 @@5959 	return report6060 }6161 62+// write_tree checks containment first, before any removal or write. Removals run before the folder check63+// because they may empty the folder standing where a saved file belongs; one still holding ignored files64+// refuses the restore before any file is written. A file that already matches is not rewritten.6265 fn write_tree(store &storage.Store, root string, ig Ignore, target []FileEntry, remove []string) !RestoreReport {6366 	check_containment(root, target, remove)!6467 	mut wanted := map[string]bool{}

view file

src/snapshot/tree.v+4 −0
@@ -126,6 +126,10 @@126126 	return entries127127 }128128 129+// walk_dir lists one folder and recurses, adding hashed files to `out`. `cache` holds each path's last130+// size and stamps, so an unchanged file is not read again; `fresh` gathers rows for files read now, for131+// walk_report to store. A name breaking spec §5.5 is skipped and named in `unsafe_paths`, not an error:132+// the walk goes on and status and save report it.129133 fn walk_dir(root string, rel string, ig Ignore, cache map[string]storage.CacheRow, mut out []FileEntry, mut fresh []storage.CacheRow, mut unsafe_paths []string, mut hashing Hashing) ! {130134 	dir := if rel == '' { root } else { os.join_path(root, rel) }131135 	for name in os.ls(dir)! {

view file

v.mod+1 −1
@@ -1,6 +1,6 @@11 Module {22 	name: 'koh3'3-	version: '3.2.17'3+	version: '3.2.18'44 	description: 'One SQLite file per repo, one line of history.'55 	license: 'MIT'66 }

view file