29a1ceda1bfd

drop the forwarding kepr_host wrapper, the nil backend, the unused ask hook and the duplicated strip filter from steal and offer (3.0.105)

time
2026-09-28 04:55 UTC · 2 weeks ago
machine
Junes-MacBook-Pro.local
agent
claude-opus-5-5
session
fix a2 review findings
parent
934062214af8
8 files VERSION+1 −1src/cli/cmd_remote.v+1 −7src/cli/cmd_steal.v+14 −18src/exchange/exchange.v+15 −16src/exchange/exchange_test.v+4 −2src/exchange/steal.v+1 −6src/remote/web.v+2 −0v.mod+1 −1
VERSION+1 −1
@@ -1 +1 @@1-3.0.1041+3.0.105

view file

src/cli/cmd_remote.v+1 −7
@@ -139,17 +139,11 @@139139 	}140140 }141141 142-// kepr_host normalises a Kepr address: https unless the scheme says otherwise, and scheme, host143-// and port only. A path, a query, a fragment or credentials would send the code somewhere else.144-fn kepr_host(value string) !string {145-	return remote.kepr_host(value)146-}147-148142 fn set_kepr_remote(args Args) int {149143 	if args.positional.len != 1 {150144 		return usage_error('koh remote <host> accepts one Kepr host')151145 	}152-	host := kepr_host(args.positional_at(0)) or { return refuse(args.json(), err.msg()) }146+	host := remote.kepr_host(args.positional_at(0)) or { return refuse(args.json(), err.msg()) }153147 	connect_url := '${host}/connect?machine=${urllib.query_escape(machine.name())}'154148 	println('Open ${connect_url} and sign in')155149 	mut code := args.value('code')

view file

src/cli/cmd_steal.v+14 −18
@@ -57,12 +57,12 @@5757 	// A stolen repo has its own remote: the address it came from, until this machine signs in to5858 	// that Kepr, when the bucket takes over.5959 	source := exchange.source_of(store)60-	mut upgraded := source != '' && exchange.signed_in_to(exchange.source_host(source))61-	mut backend := remote.Backend(unsafe { nil })62-	if source != '' && !upgraded {63-		backend = exchange.read_only(source) or { return refuse(args.json(), err.msg()) }60+	upgraded := source != '' && exchange.signed_in_to(exchange.source_host(source))61+	read_only := source != '' && !upgraded62+	mut backend := if read_only {63+		exchange.read_only(source) or { return refuse(args.json(), err.msg()) }6464 	} else {65-		backend = this_machine_backend() or { return refuse(args.json(), err.msg()) }65+		this_machine_backend() or { return refuse(args.json(), err.msg()) }6666 	}6767 	mut options := exchange.StealOptions{6868 		mine:   args.flag('mine')@@ -81,7 +81,7 @@8181 	}8282 	// The remote should end up with whatever this machine just decided — unless there is no remote8383 	// to write to: a repo still riding its source stays read-only, and quietly so.84-	offered := if source != '' && !upgraded { true } else { offer_after_steal(store, mut backend) }84+	offer_failed := !read_only && !offer_after_steal(store, mut backend)8585 	if args.json() {8686 		emit_json(json2.encode(StolenJson{8787 			repo:          report.repo@@ -93,7 +93,7 @@9393 			replayed:      report.replayed9494 			kept_mine:     report.kept_mine9595 			head:          report.head96-			offer_failed:  !offered96+			offer_failed:  offer_failed9797 			flagged:       report.flagged.map(it.save_id)9898 			previous_head: report.previous_head9999 			kept_versions: report.kept_versions@@ -111,18 +111,14 @@111111 			ok('brought in ${report.fetched_saves} saves from "${report.repo}" — head is now [${report.head}]')112112 		}113113 	} else {114-		ok(if guided {115-			if source != '' && !upgraded {116-				'combined'117-			} else if offered {118-				'combined · offered to ${report.repo}'119-			} else {120-				'combined'121-			}122-		} else {114+		ok(if !guided {123115 			'brought in ${report.fetched_saves} saves from "${report.repo}"'116+		} else if read_only || offer_failed {117+			'combined'118+		} else {119+			'combined · offered to ${report.repo}'124120 		})125-		if source != '' && !upgraded {121+		if read_only {126122 			note('your saves stay on this machine (read-only source)')127123 		} else if !guided {128124 			note("replayed ${report.replayed} of this machine's saves onto the remote head")@@ -141,7 +137,7 @@141137 	if report.clashes.len > 0 {142138 		note('${report.clashes.len} changed on both machines: ${report.clashes.join(', ')}')143139 	}144-	if !offered {140+	if offer_failed {145141 		warn('the remote was not updated — run "koh offer" when it is reachable again')146142 	}147143 	return 0

view file

src/exchange/exchange.v+15 −16
@@ -290,14 +290,7 @@290290 		}291291 	}292292 	for record in fetched.saves {293-		if history.by_id(store, record.id) != none {294-			continue295-		}296-		files := to_files(record).filter(fn [strips, record] (file snapshot.FileEntry) bool {297-			return !strips.any(reaches(it, record.id, record.created_at)298-				&& covers(it.path, file.path))299-		})300-		history.import_save(store, to_save(record), files)!293+		import_arriving(store, record, strips)!301294 	}302295 	if strips.len > 0 {303296 		store.drop_unreferenced_objects()!@@ -310,16 +303,22 @@310303 	for hash, blob in fetched.repo_objects {311304 		store.put_object_blob(hash, blob)!312305 	}306+	strips := fetched.events_out.filter(it.kind == events.kind_strip)313307 	for record in fetched.saves {314-		if history.by_id(store, record.id) == none {315-			strips := fetched.events_out.filter(it.kind == events.kind_strip)316-			files := to_files(record).filter(fn [strips, record] (file snapshot.FileEntry) bool {317-				return !strips.any(reaches(it, record.id, record.created_at)318-					&& covers(it.path, file.path))319-			})320-			history.import_save(store, to_save(record), files)!321-		}308+		import_arriving(store, record, strips)!322309 	}310+}311+312+// import_arriving lands one save from the remote, unless it is already here, less every path a313+// strip that `reaches` it removed.314+fn import_arriving(store &storage.Store, record SaveRecord, strips []events.Event) ! {315+	if history.by_id(store, record.id) != none {316+		return317+	}318+	files := to_files(record).filter(fn [strips, record] (file snapshot.FileEntry) bool {319+		return !strips.any(reaches(it, record.id, record.created_at) && covers(it.path, file.path))320+	})321+	history.import_save(store, to_save(record), files)!323322 }324323 325324 // reaches reports whether a strip event applies to a save arriving from the

view file

src/exchange/exchange_test.v+4 −2
@@ -405,7 +405,7 @@405405 	root := b.root406406 	// An agent saves while the person is still choosing.407407 	if _ := steal(b, mut backend, StealOptions{408-		ask: fn [root] (clash Clash) Side {408+		guide: fn [root] (preview StealPreview) !map[string]Side {409409 			other := storage.open(root) or { panic(err) }410410 			defer {411411 				other.close()@@ -414,7 +414,9 @@414414 			history.save_worktree(other, root, snapshot.load(root), history.SaveInput{415415 				message: 'agent saves during the question'416416 			}) or { panic('steal held the write lock while asking: ${err}') }417-			return .mine417+			return {418+				preview.clashes[0].path: Side.mine419+			}418420 		}419421 	})420422 	{

view file

src/exchange/steal.v+1 −6
@@ -57,8 +57,7 @@5757 pub mut:5858 	mine   bool // resolve every clash as mine5959 	theirs bool // resolve every clash as theirs60-	// Asked once per clash when neither flag is set. Nil means "nobody to ask".61-	ask   fn (clash Clash) Side                      = unsafe { nil }60+	// Shown the whole combine and asked about every clash. Nil means "nobody to ask".6261 	guide fn (preview StealPreview) !map[string]Side = unsafe { nil }6362 	yes   bool6463 }@@ -515,10 +514,6 @@515514 		}516515 		if opts.theirs {517516 			sides[clash.path] = .theirs518-			continue519-		}520-		if opts.ask != unsafe { nil } {521-			sides[clash.path] = opts.ask(clash)522517 			continue523518 		}524519 		return ClashError{

view file

src/remote/web.v+2 −0
@@ -13,6 +13,8 @@1313 	return Web{ base: kepr_host(base)! }1414 }1515 16+/// kepr_host normalises a Kepr address: https unless the scheme says otherwise, and scheme, host17+/// and port only. A path, a query, a fragment or credentials would send the code somewhere else.1618 pub fn kepr_host(value string) !string {1719 	for letter in value {1820 		if letter <= ` ` {

view file

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

view file