3a0d4b911f90

close the public steal review: a sourced repo is never auto-offered, and a failed clone leaves nothing (3.0.98)

time
2026-09-27 16:12 UTC · 2 weeks ago
machine
Junes-MacBook-Pro.local
agent
deepseek/deepseek-flash
session
koh3 help and history
parent
2b1e04f61099
7 files VERSION+1 −1src/cli/cmd_offer.v+3 −4src/cli/cmd_remote.v+12 −9src/cli/cmd_steal.v+20 −6src/remote/web.v+2 −1tests/cli_test.v+15 −0v.mod+1 −1
VERSION+1 −1
@@ -1 +1 @@1-3.0.971+3.0.98

view file

src/cli/cmd_offer.v+3 −4
@@ -138,10 +138,9 @@138138 	if !machine.has_remote() {139139 		return140140 	}141-	// A stolen repo has no bucket of this machine's yet: it rides its source, and a save in it is142-	// simply local. Once the machine is signed in there, this returns to offering as usual.143-	source := exchange.source_of(store)144-	if source != '' && !exchange.signed_in_to(exchange.source_host(source)) {141+	// A stolen repo rides its source: a save in it is local work and nothing else. The upgrade is142+	// the explicit `steal` or `offer`, which bring the bucket in *before* the source is dropped.143+	if exchange.source_of(store) != '' {145144 		return146145 	}147146 	mut backend := this_machine_backend() or {

view file

src/cli/cmd_remote.v+12 −9
@@ -24,15 +24,18 @@2424 		values: ['code']2525 		flags:  ['json', 'forget']2626 	}) or { return usage_error(err.msg()) }27-	// A repo stolen from a Kepr has an address of its own, and it belongs above the machine's.28-	if _ := storage.find_root(os.getwd()) {29-		store := open_store() or { return refuse(args.json(), err.msg()) }30-		defer {31-			store.close()32-		}33-		source := exchange.source_of(store)34-		if source != '' {35-			println('  ${fmt.muted('source:')} ${source} ${fmt.muted('(read-only)')}')27+	// A repo stolen from a Kepr has an address of its own, and it belongs above the machine's — as28+	// a line for a person: `--json` stays one document.29+	if !args.json() {30+		if _ := storage.find_root(os.getwd()) {31+			store := open_store() or { return refuse(args.json(), err.msg()) }32+			defer {33+				store.close()34+			}35+			source := exchange.source_of(store)36+			if source != '' {37+				println('  ${fmt.muted('source:')} ${source} ${fmt.muted('(read-only)')}')38+			}3639 		}3740 	}3841 	if args.value('code') != '' && args.positional.len == 0 {

view file

src/cli/cmd_steal.v+20 −6
@@ -21,6 +21,9 @@2121 		flags: ['list', 'mine', 'theirs', 'json']2222 	}) or { return usage_error(err.msg()) }2323 	if args.flag('list') {24+		if args.positional.len > 0 {25+			return usage_error('steal --list takes no arguments')26+		}2427 		return list_repos(args)2528 	}2629 	if storage.find_root(os.getwd()) == none {@@ -28,7 +31,11 @@2831 		if name == '' {2932 			return usage_error('steal needs a repo name outside a repo, e.g. koh steal myproject')3033 		}31-		if address := web_address(name) {34+		if args.positional.len > 1 {35+			return usage_error('steal takes one repo: koh steal myproject, or koh steal kepr.uk/nina')36+		}37+		address := web_address(name) or { return refuse(args.json(), err.msg()) }38+		if address.len > 0 {3239 			return clone_web(address[0], address[1], args)3340 		}3441 		return clone_repo(name, args)@@ -158,15 +165,16 @@158165 }159166 160167 // web_address splits a `<host>/<repo>` argument out of a steal name: a slash makes it an address161-// (`kepr.uk/nina`, `http://127.0.0.1:3917/shipped`), a bare name is this machine's own remote.162-fn web_address(value string) ?[]string {168+// (`kepr.uk/nina`, `http://127.0.0.1:3917/shipped`), a bare name is this machine's own remote. An169+// addressed value that is missing either half is an error, never a name.170+fn web_address(value string) ![]string {163171 	if !value.contains('/') {164-		return none172+		return []string{}165173 	}166174 	repo := value.all_after_last('/')167175 	host := value.all_before_last('/')168176 	if repo == '' || host == '' {169-		return none177+		return error('"${value}" is not <host>/<repo>')170178 	}171179 	return [host, repo]172180 }@@ -200,12 +208,17 @@200208 	}201209 	fetched := exchange.fetch_repo(mut backend, name) or { return refuse(args.json(), err.msg()) }202210 	store := land_clone(target, name, fetched, source) or {203-		// A folder left behind would make the retry refuse as "already exists".204211 		os.rmdir_all(target) or {}205212 		return refuse(args.json(), err.msg())206213 	}214+	// Anything that goes wrong from here leaves the folder half-made, and a retry would refuse it215+	// as "already exists", so the whole clone goes.216+	mut finished := false207217 	defer {208218 		store.close()219+		if !finished {220+			os.rmdir_all(target) or {}221+		}209222 	}210223 	files := history.files_of(store, fetched.head) or { return refuse(args.json(), err.msg()) }211224 	snapshot.restore_files(store, target, snapshot.Ignore{}, files, []) or {@@ -218,6 +231,7 @@218231 		}219232 	}220233 	flagged.sort(a.save_id < b.save_id)234+	finished = true221235 	if args.json() {222236 		emit_json(json2.encode(StolenJson{223237 			repo:    name

view file

src/remote/web.v+2 −1
@@ -31,7 +31,8 @@3131 	if text.all_after('//').contains('@') {3232 		return error('"${value}" is not a Kepr address — try kepr.uk')3333 	}34-	return '${parsed.scheme}://${parsed.host}'34+	// A hostname is case-insensitive, so the stored form is the lower-case one.35+	return '${parsed.scheme}://${parsed.host.to_lower()}'3536 }3637 3738 pub fn (mut w Web) get(key string) !Object {

view file

tests/cli_test.v+15 −0
@@ -1367,6 +1367,21 @@13671367 	return error('no free fake Kepr port')13681368 }13691369 1370+// A slash makes an argument an address, so half an address is an error and never a repo name.1371+fn test_steal_refuses_an_address_that_is_not_one() {1372+	outside := temp_repo('steal-address')1373+	home := temp_repo('steal-address-home')1374+	defer { os.rmdir_all(outside) or {} }1375+	defer { os.rmdir_all(home) or {} }1376+	for bad in ['kepr.uk/', '/shipped', 'kepr.uk/'] {1377+		refused := run_home(outside, home, 'steal', bad)1378+		assert refused.code == 1, bad1379+		assert refused.err.contains('is not <host>/<repo>'), '${bad}: ${refused.err}'1380+	}1381+	assert run_home(outside, home, 'steal', 'one', 'two').code == 21382+	assert run_home(outside, home, 'steal', '--list', 'extra').code == 21383+}1384+13701385 fn test_remote_kepr_saves_and_refuses_without_changes() {13711386 	repo := temp_repo('kepr')13721387 	home := temp_repo('kepr-home')

view file

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

view file