From 3778d4e9c1993ca602bc4e473b1f79b30451be1b Mon Sep 17 00:00:00 2001 From: Lewis Date: Tue, 28 Jul 2026 17:51:29 +0000 Subject: [PATCH] knot2/index: resolve clone path by display name when no rkey Lewis: May this revision serve well! --- knot2/crates/knot-index/src/intern.rs | 30 +++++++++++++++++++----------- knot2/crates/knot-index/src/lib.rs | 11 ++++++++++- knot2/crates/knot-index/src/projections.rs | 132 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------------- knot2/crates/knot-index/tests/projections.rs | 510 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-- knot2/crates/knot-pack/src/lib.rs | 36 ++++++++---------------------------- knot2/crates/knot-pack/tests/git_client.rs | 12 ++++++++---- knot2/crates/knot-pack/tests/h3_conformance.rs | 2 +- knot2/crates/knot-pack/tests/handle_owner.rs | 4 +++- knot2/crates/knot-pack/tests/serving.rs | 2 +- knot2/crates/knot-pack/tests/soak.rs | 2 +- knot2/crates/knot-server/src/main.rs | 7 ++++--- knot2/crates/knot-sim/src/harness.rs | 4 ++-- knot2/crates/knot-sim/tests/h3.rs | 2 +- knot2/crates/knot-sim/tests/lfs_roundtrip.rs | 4 ++-- knot2/crates/knot-ssh/src/exec.rs | 34 ++++++++++++++++------------------ knot2/crates/knot-ssh/tests/ssh_push.rs | 73 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-- knot2/crates/knot-types/src/ids.rs | 82 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------------- knot2/crates/knot-types/src/lib.rs | 6 +++--- knot2/crates/knot-xrpc/src/lib.rs | 22 +++++++++++----------- knot2/crates/knot-index/tests/common/mod.rs | 16 ++++++++++++++++ knot2/crates/knot-pack/tests/common/mod.rs | 2 +- 21 file(s) changed, 872 insertion(s)(+), 121 deletion(s)(-) diff --git a/knot2/crates/knot-index/src/intern.rs b/knot2/crates/knot-index/src/intern.rs --- a/knot2/crates/knot-index/src/intern.rs +++ b/knot2/crates/knot-index/src/intern.rs @@ -1,6 +1,6 @@ use std::sync::Arc; -use knot_types::{AccountDid, OwnerDid, RepoDid, RepoRkey}; +use knot_types::{AccountDid, OwnerDid, RepoDid, RepoName, RepoRkey}; use lasso::{Spur, ThreadedRodeo}; #[derive(Debug, Clone, Default)] @@ -13,11 +13,7 @@ } macro_rules! interned { - ($( - $key:ident of $value:ty { - $intern:ident, $get:ident, $resolve:ident, $label:literal - } - )+) => {$( + (@lookup $key:ident of $value:ty { $intern:ident, $get:ident }) => { #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)] pub(crate) struct $key(Spur); @@ -29,12 +25,23 @@ pub(crate) fn $get(&self, value: &$value) -> Option<$key> { self.0.get(value.as_str()).map($key) } - - pub(crate) fn $resolve(&self, key: $key) -> $value { - <$value>::new(self.0.resolve(&key.0)) - .expect(concat!("interned ", $label, " is valid ", $label)) - } } + }; + ($( + $key:ident of $value:ty { + $intern:ident, $get:ident $(, $resolve:ident, $label:literal)? + } + )+) => {$( + interned!(@lookup $key of $value { $intern, $get }); + + $( + impl Interner { + pub(crate) fn $resolve(&self, key: $key) -> $value { + <$value>::new(self.0.resolve(&key.0)) + .expect(concat!("interned ", $label, " is valid ", $label)) + } + } + )? )+}; } @@ -43,4 +50,5 @@ RepoKey of RepoDid { intern_repo, repo, resolve_repo, "repo DID" } OwnerKey of OwnerDid { intern_owner, owner, resolve_owner, "owner DID" } RkeyKey of RepoRkey { intern_rkey, rkey, resolve_rkey, "rkey" } + NameKey of RepoName { intern_name, name } } diff --git a/knot2/crates/knot-index/src/lib.rs b/knot2/crates/knot-index/src/lib.rs --- a/knot2/crates/knot-index/src/lib.rs +++ b/knot2/crates/knot-index/src/lib.rs @@ -16,7 +16,7 @@ MembersCob, RegistryChange, RepoRegistryCob, }; use knot_git::{Layout, Repo}; -use knot_types::{AccountDid, OwnerDid, RepoDid, RepoRkey}; +use knot_types::{AccountDid, ClonePath, OwnerDid, RepoDid, RepoRkey}; use intern::Interner; use projections::{CollaboratorsProjection, GrantSetProjection, KeyProjection, RegistryProjection}; @@ -197,6 +197,15 @@ pub fn resolve_repo(&self, owner: &OwnerDid, rkey: &RepoRkey) -> Resolved> { self.registry.resolve(&self.interner, owner, rkey) + } + + pub fn resolve_clone_path( + &self, + owner: &OwnerDid, + path: &ClonePath, + ) -> Resolved> { + self.registry + .resolve_clone_path(&self.interner, owner, path) } pub fn owner_of(&self, repo: &RepoDid) -> Resolved> { diff --git a/knot2/crates/knot-index/src/projections.rs b/knot2/crates/knot-index/src/projections.rs --- a/knot2/crates/knot-index/src/projections.rs +++ b/knot2/crates/knot-index/src/projections.rs @@ -9,11 +9,11 @@ CollaboratorsChange, CollaboratorsCob, Grant, GrantChange, Registration, Registry, RegistryChange, Rename, RepoRef, RepoRegistryCob, Roster, }; -use knot_types::{AccountDid, OfferedKey, OwnerDid, RepoDid, RepoRkey, UnixSeconds}; +use knot_types::{AccountDid, ClonePath, OfferedKey, OwnerDid, RepoDid, RepoRkey, UnixSeconds}; use crate::coverage::{Coverage, CoverageCell, Resolved}; use crate::error::IndexError; -use crate::intern::{AccountKey, Interner, OwnerKey, RepoKey, RkeyKey}; +use crate::intern::{AccountKey, Interner, NameKey, OwnerKey, RepoKey, RkeyKey}; const KEY_CACHE_CAPACITY: usize = 16_384; @@ -414,10 +414,13 @@ struct RecordSlot { owner: OwnerKey, rkey: RkeyKey, + name: NameKey, + created_at: UnixSeconds, } pub(crate) struct RegistryProjection { aliases: scc::HashMap<(OwnerKey, RkeyKey), RepoKey>, + names: scc::HashMap<(OwnerKey, NameKey), BTreeSet<(UnixSeconds, RepoKey)>>, records: scc::HashMap, coverage: CoverageCell, tip: Mutex>, @@ -427,6 +430,7 @@ pub(crate) fn new() -> Self { Self { aliases: scc::HashMap::new(), + names: scc::HashMap::new(), records: scc::HashMap::new(), coverage: CoverageCell::new(Coverage::Warming), tip: Mutex::new(None), @@ -444,6 +448,7 @@ .unwrap_or_else(|poisoned| poisoned.into_inner()); let evacuated = self.hosted_repos(interner); self.aliases.clear_sync(); + self.names.clear_sync(); self.records.clear_sync(); *tip = None; self.coverage.set(Coverage::Ready); @@ -466,6 +471,54 @@ .map(|repo| interner.resolve_repo(repo)), ), } + } + + pub(crate) fn resolve_clone_path( + &self, + interner: &Interner, + owner: &OwnerDid, + path: &ClonePath, + ) -> Resolved> { + if self.coverage.get() == Coverage::Warming { + return Resolved::Warming; + } + let Some(owner) = interner.owner(owner) else { + return Resolved::Ready(None); + }; + let by_rkey = path + .rkeys() + .filter_map(|rkey| interner.rkey(rkey)) + .find_map(|rkey| self.aliases.read_sync(&(owner, rkey), |_, repo| *repo)); + if let Some(repo) = by_rkey { + return Resolved::Ready(Some(interner.resolve_repo(repo))); + } + Resolved::Ready( + path.names() + .filter_map(|name| interner.name(name)) + .find_map(|name| self.oldest_registration_for_name(interner, owner, name)), + ) + } + + fn oldest_registration_for_name( + &self, + interner: &Interner, + owner: OwnerKey, + name: NameKey, + ) -> Option { + // Notice how set keys sort by whichever string the interner saw first, + // and a cold rebuild will see them in a different order than live replay, + // so 2 entries with the same timestamp will settle by comparing + // DIDs instead. + self.names + .read_sync(&(owner, name), |_, registered| { + let earliest = registered.first()?.0; + registered + .iter() + .take_while(|(created_at, _)| *created_at == earliest) + .map(|(_, repo)| interner.resolve_repo(*repo)) + .min() + }) + .flatten() } pub(crate) fn owner_of( @@ -549,13 +602,20 @@ fn seed(&self, interner: &Interner, registry: &Registry) { self.aliases.clear_sync(); + self.names.clear_sync(); self.records.clear_sync(); registry.records().for_each(|(repo, record)| { + let repo = interner.intern_repo(repo); + let owner = interner.intern_owner(&record.owner); + let name = interner.intern_name(&record.name); self.upsert_record( - interner.intern_repo(repo), - interner.intern_owner(&record.owner), + repo, + owner, interner.intern_rkey(&record.rkey), + name, + record.created_at, ); + self.bind_name(owner, name, record.created_at, repo); }); registry.aliases().for_each(|(owner, rkey, repo)| { self.upsert_alias( @@ -612,13 +672,15 @@ let repo = interner.intern_repo(®istration.repo); let owner = interner.intern_owner(®istration.owner); let rkey = interner.intern_rkey(®istration.rkey); + let name = interner.intern_name(®istration.name); if self.records.contains_sync(&repo) { self.drop_record(repo); displaced.push(repo); } displaced.extend(self.steal_alias(owner, rkey, repo)); - self.upsert_record(repo, owner, rkey); + self.upsert_record(repo, owner, rkey, name, registration.created_at); self.upsert_alias(owner, rkey, repo); + self.bind_name(owner, name, registration.created_at, repo); displaced } @@ -631,16 +693,23 @@ let repo = interner.intern_repo(&rename.repo); let owner = interner.intern_owner(&rename.owner); let rkey = interner.intern_rkey(&rename.rkey); + let name = interner.intern_name(&rename.name); let held = self .records - .read_sync(&repo, |_, slot| slot.owner == owner) - .unwrap_or(false); - if !held { + .read_sync(&repo, |_, slot| { + (slot.owner == owner).then_some((slot.created_at, slot.name)) + }) + .flatten(); + let Some((created_at, previous)) = held else { return displaced; - } + }; displaced.extend(self.steal_alias(owner, rkey, repo)); - self.upsert_record(repo, owner, rkey); + self.upsert_record(repo, owner, rkey, name, created_at); self.upsert_alias(owner, rkey, repo); + self.bind_name(owner, name, created_at, repo); + if previous != name { + self.unbind_name(owner, previous, created_at, repo); + } displaced } @@ -683,20 +752,57 @@ } fn drop_record(&self, repo: RepoKey) { - let _ = self.records.remove_sync(&repo); + if let Some((_, slot)) = self.records.remove_sync(&repo) { + self.unbind_name(slot.owner, slot.name, slot.created_at, repo); + } self.aliases.retain_sync(|_, holder| *holder != repo); } - fn upsert_record(&self, repo: RepoKey, owner: OwnerKey, rkey: RkeyKey) { + fn bind_name(&self, owner: OwnerKey, name: NameKey, created_at: UnixSeconds, repo: RepoKey) { + match self.names.entry_sync((owner, name)) { + scc::hash_map::Entry::Occupied(mut occupied) => { + occupied.get_mut().insert((created_at, repo)); + } + scc::hash_map::Entry::Vacant(vacant) => { + vacant.insert_entry(BTreeSet::from([(created_at, repo)])); + } + } + } + + fn unbind_name(&self, owner: OwnerKey, name: NameKey, created_at: UnixSeconds, repo: RepoKey) { + let _ = self.names.remove_if_sync(&(owner, name), |registered| { + registered.remove(&(created_at, repo)); + registered.is_empty() + }); + } + + fn upsert_record( + &self, + repo: RepoKey, + owner: OwnerKey, + rkey: RkeyKey, + name: NameKey, + created_at: UnixSeconds, + ) { if self .records .update_sync(&repo, |_, slot| { slot.owner = owner; slot.rkey = rkey; + slot.name = name; + slot.created_at = created_at; }) .is_none() { - let _ = self.records.insert_sync(repo, RecordSlot { owner, rkey }); + let _ = self.records.insert_sync( + repo, + RecordSlot { + owner, + rkey, + name, + created_at, + }, + ); } } diff --git a/knot2/crates/knot-index/tests/projections.rs b/knot2/crates/knot-index/tests/projections.rs --- a/knot2/crates/knot-index/tests/projections.rs +++ b/knot2/crates/knot-index/tests/projections.rs @@ -5,11 +5,13 @@ use knot_cobs::{CollaboratorsChange, MembersChange, RegistryChange, Removal, Rename, RepoRef}; use knot_git::{RefUpdate, Repo}; use knot_index::{Coverage, IndexError, OfferedKey, Resolved}; -use knot_types::{RefName, RepoName}; +use knot_types::{ClonePath, RefName, RepoName}; use serde::{Deserialize, Serialize}; mod common; -use common::{World, acc, at, grant, meta_home, own, registration, repo_did, rkey}; +use common::{ + World, acc, at, grant, meta_home, named_registration, own, registration, repo_did, rkey, +}; #[derive(Serialize, Deserialize)] #[serde(tag = "op", content = "data", rename_all = "snake_case")] @@ -563,5 +565,509 @@ assert_eq!( index.owner_of_key(&key(CAP)), Resolved::Ready(Some(acc("nel"))) + ); +} + +fn path(raw: &str) -> ClonePath { + ClonePath::parse(raw).unwrap() +} + +#[test] +fn a_clone_path_resolves_by_name_when_the_record_key_is_a_tid() { + let world = World::new(); + let repo = repo_did("squid"); + + let meta = Repo::open(&world.meta_path).unwrap(); + let store = CobStore::new(&meta); + store + .create( + &meta_home(), + &RegistryChange::Register(named_registration( + "nel", + "3mizfnpxii522", + "substratum.cloud", + &repo, + 1, + )), + &world.signer, + at(1), + ) + .unwrap(); + + let index = world.index(); + index.rebuild().unwrap(); + + assert_eq!( + index.resolve_clone_path(&own("nel"), &path("substratum.cloud")), + Resolved::Ready(Some(repo.clone())), + "a PDS-native record key leaves the display name as the only human clone path" + ); + assert_eq!( + index.resolve_clone_path(&own("nel"), &path("3mizfnpxii522")), + Resolved::Ready(Some(repo.clone())), + "the record key still resolves" + ); + assert_eq!( + index.resolve_clone_path(&own("nel"), &path("substratum.cloud.git")), + Resolved::Ready(Some(repo)), + "the conventional .git suffix strips before the name lookup" + ); + assert_eq!( + index.resolve_clone_path(&own("nel"), &path("periwinkle")), + Resolved::Ready(None) + ); +} + +#[test] +fn a_record_key_outranks_another_repos_name() { + let world = World::new(); + let by_name = repo_did("limpet"); + let by_rkey = repo_did("mussel"); + + let meta = Repo::open(&world.meta_path).unwrap(); + let store = CobStore::new(&meta); + let object = store + .create( + &meta_home(), + &RegistryChange::Register(named_registration("nel", "limpet", "mussel", &by_name, 1)), + &world.signer, + at(1), + ) + .unwrap() + .object; + store + .update( + &meta_home(), + object, + &RegistryChange::Register(named_registration("nel", "mussel", "scallop", &by_rkey, 2)), + &world.signer, + at(2), + ) + .unwrap(); + + let index = world.index(); + index.rebuild().unwrap(); + + assert_eq!( + index.resolve_clone_path(&own("nel"), &path("mussel")), + Resolved::Ready(Some(by_rkey)), + "a record key match wins over another repo holding that string as its name" + ); + assert_eq!( + index.resolve_clone_path(&own("nel"), &path("scallop")), + Resolved::Ready(Some(repo_did("mussel"))) + ); +} + +#[test] +fn a_name_two_repos_share_resolves_to_the_older_registration() { + let world = World::new(); + let first = repo_did("whelk"); + let second = repo_did("conch"); + + let meta = Repo::open(&world.meta_path).unwrap(); + let store = CobStore::new(&meta); + let object = store + .create( + &meta_home(), + &RegistryChange::Register(named_registration( + "nel", + "3lubrptx57d22", + "kelp", + &first, + 1, + )), + &world.signer, + at(1), + ) + .unwrap() + .object; + store + .update( + &meta_home(), + object, + &RegistryChange::Register(named_registration( + "nel", + "3mqydma3re27z", + "kelp", + &second, + 2, + )), + &world.signer, + at(2), + ) + .unwrap(); + + let index = world.index(); + index.rebuild().unwrap(); + + assert_eq!( + index.resolve_clone_path(&own("nel"), &path("kelp")), + Resolved::Ready(Some(first.clone())), + "a contested name resolves to whichever repo registered first, ordered by \ + created_at so a cold seed and an incremental replay agree" + ); + assert_eq!( + index.resolve_clone_path(&own("nel"), &path("3lubrptx57d22")), + Resolved::Ready(Some(first)), + "each record key stays unambiguous" + ); + assert_eq!( + index.resolve_clone_path(&own("nel"), &path("3mqydma3re27z")), + Resolved::Ready(Some(second)), + "the repo that lost the name is still reachable by its record key" + ); +} + +#[test] +fn a_repo_registered_later_never_takes_a_contested_name_by_renaming_onto_it() { + let world = World::new(); + let holder = repo_did("whelk"); + let latecomer = repo_did("conch"); + + let meta = Repo::open(&world.meta_path).unwrap(); + let store = CobStore::new(&meta); + let object = store + .create( + &meta_home(), + &RegistryChange::Register(named_registration( + "nel", + "3lubrptx57d22", + "kelp", + &holder, + 1, + )), + &world.signer, + at(1), + ) + .unwrap() + .object; + store + .update( + &meta_home(), + object, + &RegistryChange::Register(named_registration( + "nel", + "3mqydma3re27z", + "uni", + &latecomer, + 2, + )), + &world.signer, + at(2), + ) + .unwrap(); + + let index = world.index(); + index.rebuild().unwrap(); + + store + .update( + &meta_home(), + object, + &RegistryChange::Rename(Rename { + owner: own("nel"), + rkey: rkey("3mqydma3re27z"), + name: RepoName::new("kelp").unwrap(), + repo: latecomer.clone(), + }), + &world.signer, + at(3), + ) + .unwrap(); + index.refresh_registry().unwrap(); + + assert_eq!( + index.resolve_clone_path(&own("nel"), &path("kelp")), + Resolved::Ready(Some(holder)), + "a rename keeps the repo's original created_at, so renaming onto a name \ + another repo registered earlier cannot take it" + ); + assert_eq!( + index.resolve_clone_path(&own("nel"), &path("uni")), + Resolved::Ready(None), + "the renamed repo's previous name stops resolving" + ); + assert_eq!( + index.resolve_clone_path(&own("nel"), &path("3mqydma3re27z")), + Resolved::Ready(Some(latecomer)) + ); +} + +#[test] +fn deregistering_the_older_registration_moves_a_shared_name_to_the_survivor() { + let world = World::new(); + let first = repo_did("whelk"); + let survivor = repo_did("conch"); + + let meta = Repo::open(&world.meta_path).unwrap(); + let store = CobStore::new(&meta); + let object = store + .create( + &meta_home(), + &RegistryChange::Register(named_registration( + "nel", + "3lubrptx57d22", + "kelp", + &first, + 1, + )), + &world.signer, + at(1), + ) + .unwrap() + .object; + store + .update( + &meta_home(), + object, + &RegistryChange::Register(named_registration( + "nel", + "3mqydma3re27z", + "kelp", + &survivor, + 2, + )), + &world.signer, + at(2), + ) + .unwrap(); + + let index = world.index(); + index.rebuild().unwrap(); + assert_eq!( + index.resolve_clone_path(&own("nel"), &path("kelp")), + Resolved::Ready(Some(first)), + "the name resolves to the older registration while both exist" + ); + + store + .update( + &meta_home(), + object, + &RegistryChange::Deregister(RepoRef { + owner: own("nel"), + rkey: rkey("3lubrptx57d22"), + }), + &world.signer, + at(3), + ) + .unwrap(); + index.refresh_registry().unwrap(); + + assert_eq!( + index.resolve_clone_path(&own("nel"), &path("kelp")), + Resolved::Ready(Some(survivor)), + "the name resolves to the remaining repo once the older one is deregistered" + ); +} + +#[test] +fn a_rename_moves_name_resolution_off_the_old_name() { + let world = World::new(); + let repo = repo_did("squid"); + + let meta = Repo::open(&world.meta_path).unwrap(); + let store = CobStore::new(&meta); + let object = store + .create( + &meta_home(), + &RegistryChange::Register(named_registration( + "nel", + "3mizfnpxii522", + "anemone", + &repo, + 1, + )), + &world.signer, + at(1), + ) + .unwrap() + .object; + + let index = world.index(); + index.rebuild().unwrap(); + assert_eq!( + index.resolve_clone_path(&own("nel"), &path("anemone")), + Resolved::Ready(Some(repo.clone())) + ); + + store + .update( + &meta_home(), + object, + &RegistryChange::Rename(Rename { + owner: own("nel"), + rkey: rkey("3mizfnpxii522"), + name: RepoName::new("barnacle").unwrap(), + repo: repo.clone(), + }), + &world.signer, + at(2), + ) + .unwrap(); + index.refresh_registry().unwrap(); + + assert_eq!( + index.resolve_clone_path(&own("nel"), &path("barnacle")), + Resolved::Ready(Some(repo.clone())), + "the new name resolves after a rename that keeps the record key" + ); + assert_eq!( + index.resolve_clone_path(&own("nel"), &path("anemone")), + Resolved::Ready(None), + "the superseded name stops resolving" + ); + assert_eq!( + index.resolve_clone_path(&own("nel"), &path("3mizfnpxii522")), + Resolved::Ready(Some(repo)) + ); +} + +#[test] +fn a_rename_that_changes_only_the_record_key_keeps_the_name_resolving() { + let world = World::new(); + let repo = repo_did("squid"); + + let meta = Repo::open(&world.meta_path).unwrap(); + let store = CobStore::new(&meta); + let object = store + .create( + &meta_home(), + &RegistryChange::Register(named_registration("nel", "3lubrptx57d22", "kelp", &repo, 1)), + &world.signer, + at(1), + ) + .unwrap() + .object; + + let index = world.index(); + index.rebuild().unwrap(); + + store + .update( + &meta_home(), + object, + &RegistryChange::Rename(Rename { + owner: own("nel"), + rkey: rkey("3mqydma3re27z"), + name: RepoName::new("kelp").unwrap(), + repo: repo.clone(), + }), + &world.signer, + at(2), + ) + .unwrap(); + index.refresh_registry().unwrap(); + + assert_eq!( + index.resolve_clone_path(&own("nel"), &path("kelp")), + Resolved::Ready(Some(repo.clone())), + "the unchanged name survives a rename that swaps the record key" + ); + assert_eq!( + index.resolve_clone_path(&own("nel"), &path("3mqydma3re27z")), + Resolved::Ready(Some(repo.clone())) + ); + assert_eq!( + index.resolve_clone_path(&own("nel"), &path("3lubrptx57d22")), + Resolved::Ready(Some(repo)), + "the superseded record key keeps resolving through its retained alias" + ); +} + +#[test] +fn seeding_and_replaying_agree_on_name_resolution() { + let world = World::new(); + let first = repo_did("whelk"); + let second = repo_did("conch"); + + let meta = Repo::open(&world.meta_path).unwrap(); + let store = CobStore::new(&meta); + let object = store + .create( + &meta_home(), + &RegistryChange::Register(named_registration( + "nel", + "3lubrptx57d22", + "kelp", + &first, + 1, + )), + &world.signer, + at(1), + ) + .unwrap() + .object; + + let replayed = world.index(); + replayed.rebuild().unwrap(); + + store + .update( + &meta_home(), + object, + &RegistryChange::Register(named_registration( + "nel", + "3mqydma3re27z", + "uni", + &second, + 2, + )), + &world.signer, + at(2), + ) + .unwrap(); + replayed.refresh_registry().unwrap(); + + let seeded = world.index(); + seeded.rebuild().unwrap(); + + ["kelp", "uni", "3lubrptx57d22", "3mqydma3re27z", "nautilus"] + .into_iter() + .for_each(|segment| { + assert_eq!( + replayed.resolve_clone_path(&own("nel"), &path(segment)), + seeded.resolve_clone_path(&own("nel"), &path(segment)), + "incremental replay and a cold seed disagree on {segment}" + ); + }); +} + +#[test] +fn a_name_differing_from_its_record_key_only_by_case_resolves() { + let world = World::new(); + let repo = repo_did("squid"); + + let meta = Repo::open(&world.meta_path).unwrap(); + let store = CobStore::new(&meta); + store + .create( + &meta_home(), + &RegistryChange::Register(named_registration( + "nel", + "runic_lang", + "Runic_lang", + &repo, + 1, + )), + &world.signer, + at(1), + ) + .unwrap(); + + let index = world.index(); + index.rebuild().unwrap(); + + assert_eq!( + index.resolve_clone_path(&own("nel"), &path("Runic_lang")), + Resolved::Ready(Some(repo.clone())), + "the appview lowercases the record key but keeps the display name's case, \ + so the mixed-case path resolves by name" + ); + assert_eq!( + index.resolve_clone_path(&own("nel"), &path("runic_lang")), + Resolved::Ready(Some(repo)), + "the lowercased record key still resolves" ); } diff --git a/knot2/crates/knot-pack/src/lib.rs b/knot2/crates/knot-pack/src/lib.rs --- a/knot2/crates/knot-pack/src/lib.rs +++ b/knot2/crates/knot-pack/src/lib.rs @@ -30,7 +30,7 @@ use knot_resource::{PackSlots, SlotPermit}; use knot_runtime::Clock; use knot_types::{ - AccountDid, Handle, KnotHostname, OwnerDid, OwnerRef, ParseError, RepoDid, RepoRkey, + AccountDid, ClonePath, Handle, KnotHostname, OwnerDid, OwnerRef, ParseError, RepoDid, }; use std::sync::Arc; use tokio::sync::mpsc; @@ -276,7 +276,7 @@ #[derive(Debug, Clone, PartialEq, Eq)] pub enum RepoTarget { Did(RepoDid), - OwnerRkey(OwnerDid, RepoRkey), + OwnerPath(OwnerDid, ClonePath), } #[derive(Debug, Clone, PartialEq, Eq)] @@ -287,24 +287,6 @@ } impl RepoLookup { - pub fn or_else(self, next: impl FnOnce() -> RepoLookup) -> RepoLookup { - match self { - RepoLookup::Unhosted => next(), - decided => decided, - } - } - - pub fn first( - candidates: impl IntoIterator, - resolve: impl Fn(RepoRkey) -> RepoLookup, - ) -> RepoLookup { - candidates - .into_iter() - .fold(RepoLookup::Unhosted, |acc, rkey| { - acc.or_else(|| resolve(rkey)) - }) - } - pub fn from_resolved( resolved: knot_index::Resolved>, found: impl FnOnce(T) -> RepoDid, @@ -531,14 +513,12 @@ owner: &OwnerDid, name: &str, ) -> Result { - lookup_did(RepoLookup::first( - RepoRkey::clone_path_candidates(name), - |rkey| { - state - .resolver - .resolve(&RepoTarget::OwnerRkey(owner.clone(), rkey)) - }, - )) + let path = ClonePath::parse(name).ok_or(PackError::NotFound)?; + lookup_did( + state + .resolver + .resolve(&RepoTarget::OwnerPath(owner.clone(), path)), + ) } fn resolve_did_did(state: &PackState, did: &str) -> Result { diff --git a/knot2/crates/knot-pack/tests/git_client.rs b/knot2/crates/knot-pack/tests/git_client.rs --- a/knot2/crates/knot-pack/tests/git_client.rs +++ b/knot2/crates/knot-pack/tests/git_client.rs @@ -120,11 +120,15 @@ let plain_did = plain_did.clone(); let literal_did = literal_did.clone(); Arc::new(move |target: &RepoTarget| match target { - RepoTarget::OwnerRkey(o, n) if *o == owner && n.as_str() == "anemone" => { - RepoLookup::Hosted(plain_did.clone()) - } - RepoTarget::OwnerRkey(o, n) if *o == owner && n.as_str() == "barnacle.git" => { + RepoTarget::OwnerPath(o, p) + if *o == owner && p.rkeys().any(|rkey| rkey.as_str() == "barnacle.git") => + { RepoLookup::Hosted(literal_did.clone()) + } + RepoTarget::OwnerPath(o, p) + if *o == owner && p.rkeys().any(|rkey| rkey.as_str() == "anemone") => + { + RepoLookup::Hosted(plain_did.clone()) } _ => RepoLookup::Unhosted, }) diff --git a/knot2/crates/knot-pack/tests/h3_conformance.rs b/knot2/crates/knot-pack/tests/h3_conformance.rs --- a/knot2/crates/knot-pack/tests/h3_conformance.rs +++ b/knot2/crates/knot-pack/tests/h3_conformance.rs @@ -53,7 +53,7 @@ fn serve_dids() -> Arc { Arc::new(|target: &RepoTarget| match target { RepoTarget::Did(did) => RepoLookup::Hosted(did.clone()), - RepoTarget::OwnerRkey(_, _) => RepoLookup::Unhosted, + RepoTarget::OwnerPath(_, _) => RepoLookup::Unhosted, }) } diff --git a/knot2/crates/knot-pack/tests/handle_owner.rs b/knot2/crates/knot-pack/tests/handle_owner.rs --- a/knot2/crates/knot-pack/tests/handle_owner.rs +++ b/knot2/crates/knot-pack/tests/handle_owner.rs @@ -27,7 +27,9 @@ let owner = OwnerDid::new("did:plc:nel").unwrap(); let repo = RepoDid::new("did:plc:whelk").unwrap(); Arc::new(move |target: &RepoTarget| match target { - RepoTarget::OwnerRkey(o, n) if *o == owner && n.as_str() == "squid" => { + RepoTarget::OwnerPath(o, p) + if *o == owner && p.rkeys().any(|rkey| rkey.as_str() == "squid") => + { RepoLookup::Hosted(repo.clone()) } RepoTarget::Did(d) if *d == repo => RepoLookup::Hosted(d.clone()), diff --git a/knot2/crates/knot-pack/tests/serving.rs b/knot2/crates/knot-pack/tests/serving.rs --- a/knot2/crates/knot-pack/tests/serving.rs +++ b/knot2/crates/knot-pack/tests/serving.rs @@ -18,7 +18,7 @@ fn serve_dids() -> Arc { Arc::new(|target: &RepoTarget| match target { RepoTarget::Did(did) => RepoLookup::Hosted(did.clone()), - RepoTarget::OwnerRkey(_, _) => RepoLookup::Unhosted, + RepoTarget::OwnerPath(_, _) => RepoLookup::Unhosted, }) } diff --git a/knot2/crates/knot-pack/tests/soak.rs b/knot2/crates/knot-pack/tests/soak.rs --- a/knot2/crates/knot-pack/tests/soak.rs +++ b/knot2/crates/knot-pack/tests/soak.rs @@ -15,7 +15,7 @@ fn serve_dids() -> std::sync::Arc { std::sync::Arc::new(|target: &RepoTarget| match target { RepoTarget::Did(did) => RepoLookup::Hosted(did.clone()), - RepoTarget::OwnerRkey(_, _) => RepoLookup::Unhosted, + RepoTarget::OwnerPath(_, _) => RepoLookup::Unhosted, }) } diff --git a/knot2/crates/knot-server/src/main.rs b/knot2/crates/knot-server/src/main.rs --- a/knot2/crates/knot-server/src/main.rs +++ b/knot2/crates/knot-server/src/main.rs @@ -563,9 +563,10 @@ knot_pack::RepoTarget::Did(did) => { knot_pack::RepoLookup::from_resolved(index.owner_of(did), |_| did.clone()) } - knot_pack::RepoTarget::OwnerRkey(owner, rkey) => { - knot_pack::RepoLookup::from_resolved(index.resolve_repo(owner, rkey), |found| found) - } + knot_pack::RepoTarget::OwnerPath(owner, path) => knot_pack::RepoLookup::from_resolved( + index.resolve_clone_path(owner, path), + |found| found, + ), }) }; let receive_advertiser = knot_xrpc::receive_advertiser(Arc::clone(&xrpc_state)); diff --git a/knot2/crates/knot-sim/src/harness.rs b/knot2/crates/knot-sim/src/harness.rs --- a/knot2/crates/knot-sim/src/harness.rs +++ b/knot2/crates/knot-sim/src/harness.rs @@ -781,8 +781,8 @@ Resolved::Ready(None) => knot_pack::RepoLookup::Unhosted, Resolved::Warming => knot_pack::RepoLookup::Unavailable, }, - knot_pack::RepoTarget::OwnerRkey(owner, rkey) => { - match index.resolve_repo(owner, rkey) { + knot_pack::RepoTarget::OwnerPath(owner, path) => { + match index.resolve_clone_path(owner, path) { Resolved::Ready(Some(found)) => knot_pack::RepoLookup::Hosted(found), Resolved::Ready(None) => knot_pack::RepoLookup::Unhosted, Resolved::Warming => knot_pack::RepoLookup::Unavailable, diff --git a/knot2/crates/knot-sim/tests/h3.rs b/knot2/crates/knot-sim/tests/h3.rs --- a/knot2/crates/knot-sim/tests/h3.rs +++ b/knot2/crates/knot-sim/tests/h3.rs @@ -128,7 +128,7 @@ fn serve_dids() -> Arc { Arc::new(|target: &RepoTarget| match target { RepoTarget::Did(did) => RepoLookup::Hosted(did.clone()), - RepoTarget::OwnerRkey(_, _) => RepoLookup::Unhosted, + RepoTarget::OwnerPath(_, _) => RepoLookup::Unhosted, }) } diff --git a/knot2/crates/knot-sim/tests/lfs_roundtrip.rs b/knot2/crates/knot-sim/tests/lfs_roundtrip.rs --- a/knot2/crates/knot-sim/tests/lfs_roundtrip.rs +++ b/knot2/crates/knot-sim/tests/lfs_roundtrip.rs @@ -393,8 +393,8 @@ knot_index::Resolved::Ready(None) => knot_pack::RepoLookup::Unhosted, knot_index::Resolved::Warming => knot_pack::RepoLookup::Unavailable, }, - knot_pack::RepoTarget::OwnerRkey(owner, rkey) => { - match index.resolve_repo(owner, rkey) { + knot_pack::RepoTarget::OwnerPath(owner, path) => { + match index.resolve_clone_path(owner, path) { knot_index::Resolved::Ready(Some(found)) => { knot_pack::RepoLookup::Hosted(found) } diff --git a/knot2/crates/knot-ssh/src/exec.rs b/knot2/crates/knot-ssh/src/exec.rs --- a/knot2/crates/knot-ssh/src/exec.rs +++ b/knot2/crates/knot-ssh/src/exec.rs @@ -9,7 +9,7 @@ use knot_lfs::TransferOp; use knot_pack::{PackError, PackLimits, RepoLookup}; use knot_runtime::{Clock, HttpTransport}; -use knot_types::{AccountDid, ObjectFormat, OfferedKey, OwnerDid, RepoDid, RepoRkey}; +use knot_types::{AccountDid, ClonePath, ObjectFormat, OfferedKey, OwnerDid, RepoDid}; use russh::Channel; use russh::server::Msg; use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt}; @@ -49,13 +49,13 @@ enum RepoRef { Did(RepoDid), - OwnerRkey(OwnerDid, Vec), - HandleRkey(knot_types::Handle, Vec), + OwnerPath(OwnerDid, ClonePath), + HandlePath(knot_types::Handle, ClonePath), } enum ResolvedRef { Did(RepoDid), - OwnerRkey(OwnerDid, Vec), + OwnerPath(OwnerDid, ClonePath), } fn parse_exec(command: &[u8]) -> Option<(Service, RepoRef)> { @@ -86,14 +86,11 @@ .trim_start_matches('/'); match path.split_once('/') { Some((owner, name)) => { - let candidates: Vec = RepoRkey::clone_path_candidates(name).collect(); - if candidates.is_empty() { - return None; - } + let candidates = ClonePath::parse(name)?; match knot_types::OwnerRef::parse(owner)? { - knot_types::OwnerRef::Did(owner) => Some(RepoRef::OwnerRkey(owner, candidates)), + knot_types::OwnerRef::Did(owner) => Some(RepoRef::OwnerPath(owner, candidates)), knot_types::OwnerRef::Handle(handle) => { - Some(RepoRef::HandleRkey(handle, candidates)) + Some(RepoRef::HandlePath(handle, candidates)) } } } @@ -107,9 +104,10 @@ ) -> RepoLookup { let candidate = match repo_ref { ResolvedRef::Did(did) => RepoLookup::Hosted(did), - ResolvedRef::OwnerRkey(owner, candidates) => RepoLookup::first(candidates, |rkey| { - RepoLookup::from_resolved(state.index.resolve_repo(&owner, &rkey), |found| found) - }), + ResolvedRef::OwnerPath(owner, candidates) => RepoLookup::from_resolved( + state.index.resolve_clone_path(&owner, &candidates), + |found| found, + ), }; match candidate { RepoLookup::Hosted(did) => { @@ -151,15 +149,15 @@ }; let resolved_ref = match repo_ref { RepoRef::Did(did) => ResolvedRef::Did(did), - RepoRef::OwnerRkey(owner, candidates) => ResolvedRef::OwnerRkey(owner, candidates), - RepoRef::HandleRkey(owner_handle, candidates) => { + RepoRef::OwnerPath(owner, candidates) => ResolvedRef::OwnerPath(owner, candidates), + RepoRef::HandlePath(owner_handle, candidates) => { match state .atproto .resolve_handle_to_did(&owner_handle) .await .ok() { - Some(did) => ResolvedRef::OwnerRkey(did.into(), candidates), + Some(did) => ResolvedRef::OwnerPath(did.into(), candidates), None => { fail(channel, &state.catalog.ssh.repo_not_found.text()).await; return; @@ -949,11 +947,11 @@ fn the_repo_path_parser_separates_dids_from_handles() { assert!(matches!( parse_repo_path("did:plc:nel/squid"), - Some(RepoRef::OwnerRkey(..)) + Some(RepoRef::OwnerPath(..)) )); assert!(matches!( parse_repo_path("nel.pet/squid"), - Some(RepoRef::HandleRkey(..)) + Some(RepoRef::HandlePath(..)) )); assert!(matches!( parse_repo_path("did:plc:barnacle"), diff --git a/knot2/crates/knot-ssh/tests/ssh_push.rs b/knot2/crates/knot-ssh/tests/ssh_push.rs --- a/knot2/crates/knot-ssh/tests/ssh_push.rs +++ b/knot2/crates/knot-ssh/tests/ssh_push.rs @@ -24,6 +24,9 @@ const REPO_DID: &str = "did:plc:squid"; const REPO_NAME: &str = "anemone"; const OWNER_DID: &str = "did:plc:nel"; +const TID_REPO_DID: &str = "did:plc:limpet"; +const TID_RKEY: &str = "3mizfnpxii522"; +const TID_REPO_NAME: &str = "periwinkle.cloud"; const PDS_HOST: &str = "pds.oyster.cafe"; fn git(cwd: &Path, env: &[(&str, &str)], args: &[&str]) -> (bool, String) { @@ -140,6 +143,8 @@ list_records_body(&[&published_line]) } else if path.ends_with(REPO_DID) { did_document(&signer, REPO_DID, &pds) + } else if path.ends_with(TID_REPO_DID) { + did_document(&signer, TID_REPO_DID, &pds) } else if path.ends_with(OWNER_DID) { did_document(&signer, OWNER_DID, &pds) } else { @@ -225,9 +230,10 @@ let signer = K256Signer::generate(&SeededEntropy::new(2)); let meta = Repo::open(&meta_path).unwrap(); let store = CobStore::new(&meta); - store + let home = CobHome::from(&KnotId::new("did:web:nel.pet").unwrap()); + let registry = store .create( - &CobHome::from(&KnotId::new("did:web:nel.pet").unwrap()), + &home, &RegistryChange::Register(Registration { owner: OwnerDid::new(OWNER_DID).unwrap(), rkey: RepoRkey::new(REPO_NAME).unwrap(), @@ -237,6 +243,25 @@ }), &signer, UnixSeconds::new(1), + ) + .unwrap() + .object; + + let tid_repo_did = RepoDid::new(TID_REPO_DID).unwrap(); + layout.create(&tid_repo_did).unwrap(); + store + .update( + &home, + registry, + &RegistryChange::Register(Registration { + owner: OwnerDid::new(OWNER_DID).unwrap(), + rkey: RepoRkey::new(TID_RKEY).unwrap(), + name: RepoName::new(TID_REPO_NAME).unwrap(), + repo: tid_repo_did, + created_at: UnixSeconds::new(2), + }), + &signer, + UnixSeconds::new(2), ) .unwrap(); @@ -536,6 +561,50 @@ ); }) .await; +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 4)] +async fn a_display_name_addresses_a_repo_whose_record_key_is_a_tid() { + let fx = fixture().await; + let head = seed_work(&fx.work); + let head_oid = Oid::from_hex(&head).unwrap(); + let port = fx.server.port; + let target = RepoDid::new(TID_REPO_DID).unwrap(); + let variants = [ + format!("ssh://git@127.0.0.1:{port}/{OWNER_DID}/{TID_REPO_NAME}"), + format!("ssh://git@127.0.0.1:{port}/{OWNER_DID}/{TID_REPO_NAME}.git"), + format!("ssh://git@127.0.0.1:{port}/nel.pet/{TID_REPO_NAME}"), + format!("ssh://git@127.0.0.1:{port}/{OWNER_DID}/{TID_RKEY}"), + ]; + let fx = &fx; + let target = ⌖ + futures::stream::iter(variants) + .for_each(|url| async move { + let (ok, out) = push(&fx.work, &url, &fx.key_path, &["main"]).await; + assert!( + ok, + "a PDS-minted record key leaves the display name as the only human \ + path, so {url} must resolve and push:\n{out}" + ); + assert_eq!( + main_tip(&fx.server.layout, target), + Some(head_oid), + "{url}: pushed commit must be the named repository's main tip" + ); + }) + .await; + + let (ok, out) = push( + &fx.work, + &format!("ssh://git@127.0.0.1:{port}/{OWNER_DID}/whelk"), + &fx.key_path, + &["main"], + ) + .await; + assert!( + !ok, + "a segment matching neither a record key nor a name stays unresolvable:\n{out}" + ); } #[tokio::test(flavor = "multi_thread", worker_threads = 4)] diff --git a/knot2/crates/knot-types/src/ids.rs b/knot2/crates/knot-types/src/ids.rs --- a/knot2/crates/knot-types/src/ids.rs +++ b/knot2/crates/knot-types/src/ids.rs @@ -542,11 +542,30 @@ } } -impl RepoRkey { - pub fn clone_path_candidates(raw: &str) -> impl Iterator + '_ { - std::iter::once(raw) - .chain(raw.strip_suffix(".git")) - .filter_map(|candidate| Self::new(candidate).ok()) +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ClonePath { + rkeys: Vec, + names: Vec, +} + +impl ClonePath { + pub fn parse(raw: &str) -> Option { + let segments = || std::iter::once(raw).chain(raw.strip_suffix(".git")); + let rkeys: Vec = segments() + .filter_map(|segment| RepoRkey::new(segment).ok()) + .collect(); + let names: Vec = segments() + .filter_map(|segment| RepoName::new(segment).ok()) + .collect(); + (!rkeys.is_empty() || !names.is_empty()).then_some(Self { rkeys, names }) + } + + pub fn rkeys(&self) -> impl Iterator { + self.rkeys.iter() + } + + pub fn names(&self) -> impl Iterator { + self.names.iter() } } @@ -1020,28 +1039,61 @@ } #[test] - fn clone_path_candidates_try_the_exact_rkey_before_the_stripped_one() { - let suffixed: Vec = RepoRkey::clone_path_candidates("anemone.git").collect(); + fn clone_paths_try_the_exact_segment_before_the_stripped_one() { + let suffixed = ClonePath::parse("anemone.git").unwrap(); assert_eq!( - suffixed, + suffixed.rkeys().cloned().collect::>(), vec![ RepoRkey::new("anemone.git").unwrap(), RepoRkey::new("anemone").unwrap() ], "literal .git rkey wins over conventional suffix interpretation" ); - - let plain: Vec = RepoRkey::clone_path_candidates("anemone").collect(); - assert_eq!(plain, vec![RepoRkey::new("anemone").unwrap()]); - - let bare: Vec = RepoRkey::clone_path_candidates(".git").collect(); assert_eq!( - bare, + suffixed.names().cloned().collect::>(), + vec![ + RepoName::new("anemone.git").unwrap(), + RepoName::new("anemone").unwrap() + ] + ); + + let plain = ClonePath::parse("anemone").unwrap(); + assert_eq!( + plain.rkeys().cloned().collect::>(), + vec![RepoRkey::new("anemone").unwrap()] + ); + + let bare = ClonePath::parse(".git").unwrap(); + assert_eq!( + bare.rkeys().cloned().collect::>(), vec![RepoRkey::new(".git").unwrap()], "stripping .git from bare suffix leaves nothing valid to try" ); - assert_eq!(RepoRkey::clone_path_candidates("a/b.git").count(), 0); + assert!(ClonePath::parse("a/b.git").is_none()); + } + + #[test] + fn clone_paths_keep_segments_only_valid_as_one_of_the_two_kinds() { + let plus = ClonePath::parse("c++").unwrap(); + assert_eq!( + plus.rkeys().count(), + 0, + "a record key allows only [A-Za-z0-9._~:-]" + ); + assert_eq!( + plus.names().cloned().collect::>(), + vec![RepoName::new("c++").unwrap()], + "a repo name accepts the wider charset, so the segment resolves by name" + ); + + let long = "x".repeat(200); + let overlong = ClonePath::parse(&long).unwrap(); + assert_eq!(overlong.names().count(), 0, "a repo name is at most 100"); + assert_eq!( + overlong.rkeys().cloned().collect::>(), + vec![RepoRkey::new(&long).unwrap()] + ); } #[test] diff --git a/knot2/crates/knot-types/src/lib.rs b/knot2/crates/knot-types/src/lib.rs --- a/knot2/crates/knot-types/src/lib.rs +++ b/knot2/crates/knot-types/src/lib.rs @@ -6,9 +6,9 @@ mod ids; pub use ids::{ - AccountDid, ActorId, AppviewEndpoint, AuthorName, BranchName, ChangeId, CiLogsAddr, CobId, - Email, HttpStatus, KnotHostname, KnotId, KnotServiceUrl, LanguageBytes, LanguageName, LogsHost, - LogsPort, ObjectCount, ObjectFormat, OfferedKey, Oid, OwnerDid, OwnerRef, ParseError, + AccountDid, ActorId, AppviewEndpoint, AuthorName, BranchName, ChangeId, CiLogsAddr, ClonePath, + CobId, Email, HttpStatus, KnotHostname, KnotId, KnotServiceUrl, LanguageBytes, LanguageName, + LogsHost, LogsPort, ObjectCount, ObjectFormat, OfferedKey, Oid, OwnerDid, OwnerRef, ParseError, PushOption, PushOptions, RefName, RefTransition, RepoDid, RepoName, RepoPath, RepoRkey, ServiceDid, TagName, TypeName, UnixMicros, UnixSeconds, }; diff --git a/knot2/crates/knot-xrpc/src/lib.rs b/knot2/crates/knot-xrpc/src/lib.rs --- a/knot2/crates/knot-xrpc/src/lib.rs +++ b/knot2/crates/knot-xrpc/src/lib.rs @@ -64,8 +64,8 @@ use knot_runtime::{Clock, Entropy, HttpTransport}; use knot_secrets::SealedStore; use knot_types::{ - AccountDid, AdmissionPolicy, AppviewEndpoint, CiLogsAddr, KnotHostname, KnotId, KnotServiceUrl, - Nsid, OwnerDid, OwnerRef, RepoDid, RepoRkey, UnixSeconds, + AccountDid, AdmissionPolicy, AppviewEndpoint, CiLogsAddr, ClonePath, KnotHostname, KnotId, + KnotServiceUrl, Nsid, OwnerDid, OwnerRef, RepoDid, UnixSeconds, }; use base64::Engine; @@ -578,15 +578,15 @@ name: &RepoNameSegment, ) -> Result { let owner = resolve_owner_segment(state, owner).await?; - RepoRkey::clone_path_candidates(name.as_str()) - .find_map(|rkey| match state.index.resolve_repo(&owner, &rkey) { - Resolved::Ready(Some(did)) => Some(Ok(did)), - Resolved::Ready(None) => None, - Resolved::Warming => Some(Err(XrpcError::warming( - "registry projection is still warming, retry shortly", - ))), - }) - .unwrap_or_else(|| Err(XrpcError::not_found("repository not found"))) + let path = ClonePath::parse(name.as_str()) + .ok_or_else(|| XrpcError::not_found("repository not found"))?; + match state.index.resolve_clone_path(&owner, &path) { + Resolved::Ready(Some(did)) => Ok(did), + Resolved::Ready(None) => Err(XrpcError::not_found("repository not found")), + Resolved::Warming => Err(XrpcError::warming( + "registry projection is still warming, retry shortly", + )), + } } async fn resolve_owner_segment( diff --git a/knot2/crates/knot-index/tests/common/mod.rs b/knot2/crates/knot-index/tests/common/mod.rs --- a/knot2/crates/knot-index/tests/common/mod.rs +++ b/knot2/crates/knot-index/tests/common/mod.rs @@ -52,6 +52,22 @@ } } +pub fn named_registration( + owner_id: &str, + key: &str, + display: &str, + repo: &RepoDid, + seconds: i64, +) -> Registration { + Registration { + owner: own(owner_id), + rkey: rkey(key), + name: RepoName::new(display).unwrap(), + repo: repo.clone(), + created_at: at(seconds), + } +} + pub struct World { _dir: TempDir, pub meta_path: PathBuf, diff --git a/knot2/crates/knot-pack/tests/common/mod.rs b/knot2/crates/knot-pack/tests/common/mod.rs --- a/knot2/crates/knot-pack/tests/common/mod.rs +++ b/knot2/crates/knot-pack/tests/common/mod.rs @@ -235,7 +235,7 @@ pub fn serve_dids() -> Arc { Arc::new(|target: &RepoTarget| match target { RepoTarget::Did(did) => RepoLookup::Hosted(did.clone()), - RepoTarget::OwnerRkey(_, _) => RepoLookup::Unhosted, + RepoTarget::OwnerPath(_, _) => RepoLookup::Unhosted, }) } -- tangled.sh