fetch: introduce domain type RefPrefix

Better represent the domain by introducing the `RefPrefix` type.
It captures the different `ref-prefix` values that can be used in the
`ls-refs` step of the protocol.

These are turned into their equivalent `ref-prefix` values using
`RefPrefix::into_bstring`.

This better connects the `ProtocolStage::ls_refs` method with the
`ls_refs::run` through the use of the `RefPrefix` type, rather than
any set of `BString` values.
This commit is contained in:
Fintan Halpenny 2026-02-16 13:37:13 +00:00
parent 8070f98a5b
commit 391571178e
4 changed files with 68 additions and 22 deletions

View File

@ -89,6 +89,42 @@ pub mod error {
} }
} }
/// A `ref-prefix` used in the `ls-refs` step of the fetch protocol.
///
/// Since the Radicle protocol only wants to filter by very specific references,
/// this type captures the possible reference prefixes.
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
pub(crate) enum RefPrefix {
/// Represents `refs/rad/id`.
RadId,
/// Represents `"refs/namespaces/<namespace>/refs/rad/id"`.
NamespacedRadId { namespace: PublicKey },
/// Represents `"refs/namespaces/<namespace>/refs/rad/sigrefs"`.
NamespacedRadSigrefs { namespace: PublicKey },
/// Represents `"refs/namespaces"`
AllNamespaces,
}
impl RefPrefix {
/// Convert the [`RefPrefix`] into its equivalent [`BString`].
///
/// See the [`RefPrefix`] variants for their [`BString`] values.
pub fn into_bstring(self) -> BString {
match self {
RefPrefix::RadId => refs::REFS_RAD_ID.as_bstr().into(),
RefPrefix::NamespacedRadId { namespace } => {
radicle::git::refs::storage::id(&namespace).as_bstr().into()
}
RefPrefix::NamespacedRadSigrefs { namespace } => {
radicle::git::refs::storage::sigrefs(&namespace)
.as_bstr()
.into()
}
RefPrefix::AllNamespaces => "refs/namespaces".into(),
}
}
}
/// A [`ProtocolStage`] describes a single roundtrip with the Radicle /// A [`ProtocolStage`] describes a single roundtrip with the Radicle
/// node that is serving the data. /// node that is serving the data.
/// ///
@ -107,7 +143,7 @@ pub mod error {
/// refdb (in-memory and production). /// refdb (in-memory and production).
pub(crate) trait ProtocolStage { pub(crate) trait ProtocolStage {
/// If and how to perform `ls-refs`. /// If and how to perform `ls-refs`.
fn ls_refs(&self) -> Option<NonEmpty<BString>>; fn ls_refs(&self) -> Option<NonEmpty<RefPrefix>>;
/// Filter a remote-advertised [`Ref`]. /// Filter a remote-advertised [`Ref`].
/// ///
@ -163,8 +199,8 @@ pub struct CanonicalId {
} }
impl ProtocolStage for CanonicalId { impl ProtocolStage for CanonicalId {
fn ls_refs(&self) -> Option<NonEmpty<BString>> { fn ls_refs(&self) -> Option<NonEmpty<RefPrefix>> {
Some(NonEmpty::new(refs::REFS_RAD_ID.as_bstr().into())) Some(NonEmpty::new(RefPrefix::RadId))
} }
fn ref_filter(&self, r: Ref) -> Option<ReceivedRef> { fn ref_filter(&self, r: Ref) -> Option<ReceivedRef> {
@ -250,17 +286,17 @@ pub struct SpecialRefs {
} }
impl ProtocolStage for SpecialRefs { impl ProtocolStage for SpecialRefs {
fn ls_refs(&self) -> Option<NonEmpty<BString>> { fn ls_refs(&self) -> Option<NonEmpty<RefPrefix>> {
match &self.followed { match &self.followed {
policy::Allowed::All => Some(NonEmpty::new("refs/namespaces".into())), policy::Allowed::All => Some(NonEmpty::new(RefPrefix::AllNamespaces)),
policy::Allowed::Followed { remotes } => NonEmpty::collect( policy::Allowed::Followed { remotes } => NonEmpty::collect(
remotes remotes
.iter() .iter()
.chain(self.delegates.iter()) .chain(self.delegates.iter())
.flat_map(|remote| { .flat_map(|remote| {
[ [
BString::from(radicle::git::refs::storage::id(remote).to_string()), RefPrefix::NamespacedRadSigrefs { namespace: *remote },
BString::from(radicle::git::refs::storage::sigrefs(remote).to_string()), RefPrefix::NamespacedRadId { namespace: *remote },
] ]
}), }),
), ),
@ -331,12 +367,16 @@ pub struct SigrefsAt {
} }
impl ProtocolStage for SigrefsAt { impl ProtocolStage for SigrefsAt {
fn ls_refs(&self) -> Option<NonEmpty<BString>> { fn ls_refs(&self) -> Option<NonEmpty<RefPrefix>> {
// N.b. the `Oid`s are known but the `rad/sigrefs` are still // N.b. the `Oid`s are known but the `rad/sigrefs` are still
// asked for to mark them for updating the fetch state. // asked for to mark them for updating the fetch state.
NonEmpty::collect(self.refs_at.iter().map(|refs_at| { NonEmpty::collect(
BString::from(radicle::git::refs::storage::sigrefs(&refs_at.remote).to_string()) self.refs_at
})) .iter()
.map(|refs_at| RefPrefix::NamespacedRadSigrefs {
namespace: refs_at.remote,
}),
)
} }
// We only asked for `rad/sigrefs` so we should only get // We only asked for `rad/sigrefs` so we should only get
@ -421,7 +461,7 @@ pub struct DataRefs {
impl ProtocolStage for DataRefs { impl ProtocolStage for DataRefs {
// We don't need to ask for refs since we have all reference names // We don't need to ask for refs since we have all reference names
// and `Oid`s in `rad/sigrefs`. // and `Oid`s in `rad/sigrefs`.
fn ls_refs(&self) -> Option<NonEmpty<BString>> { fn ls_refs(&self) -> Option<NonEmpty<RefPrefix>> {
None None
} }

View File

@ -225,7 +225,7 @@ impl FetchState {
let refs = match step.ls_refs() { let refs = match step.ls_refs() {
Some(refs) => handle Some(refs) => handle
.transport .transport
.ls_refs(refs.into(), handshake)? .ls_refs(refs, handshake)?
.into_iter() .into_iter()
.filter_map(|r| step.ref_filter(r)) .filter_map(|r| step.ref_filter(r))
.collect::<Vec<_>>(), .collect::<Vec<_>>(),

View File

@ -20,6 +20,7 @@ use thiserror::Error;
use crate::git::packfile::Keepfile; use crate::git::packfile::Keepfile;
use crate::git::repository; use crate::git::repository;
use crate::stage::RefPrefix;
/// Open a reader and writer stream to pass to the ls-refs and fetch /// Open a reader and writer stream to pass to the ls-refs and fetch
/// processes for communicating during their respective protocols. /// processes for communicating during their respective protocols.
@ -104,11 +105,10 @@ where
/// Perform ls-refs with the server side. /// Perform ls-refs with the server side.
pub(crate) fn ls_refs( pub(crate) fn ls_refs(
&mut self, &mut self,
mut prefixes: Vec<BString>, prefixes: impl IntoIterator<Item = RefPrefix>,
handshake: &handshake::Outcome, handshake: &handshake::Outcome,
) -> Result<Vec<handshake::Ref>, Error> { ) -> Result<Vec<handshake::Ref>, Error> {
prefixes.sort(); let prefixes = prefixes.into_iter().collect::<BTreeSet<_>>();
prefixes.dedup();
let (read, write) = self.stream.open(); let (read, write) = self.stream.open();
Ok(ls_refs::run( Ok(ls_refs::run(
ls_refs::Config { ls_refs::Config {

View File

@ -1,4 +1,5 @@
use std::borrow::Cow; use std::borrow::Cow;
use std::collections::BTreeSet;
use std::io; use std::io;
use gix_features::progress::Progress; use gix_features::progress::Progress;
@ -7,6 +8,8 @@ use gix_protocol::ls_refs;
use gix_protocol::transport::Protocol; use gix_protocol::transport::Protocol;
use gix_transport::bstr::{BString, ByteVec}; use gix_transport::bstr::{BString, ByteVec};
use crate::stage::RefPrefix;
use super::{agent_name, Connection}; use super::{agent_name, Connection};
/// Configuration for running an ls-refs process. /// Configuration for running an ls-refs process.
@ -17,7 +20,7 @@ pub struct Config {
#[allow(dead_code)] #[allow(dead_code)]
pub repo: BString, pub repo: BString,
/// Ref prefixes for filtering the output of the ls-refs process. /// Ref prefixes for filtering the output of the ls-refs process.
pub prefixes: Vec<BString>, pub prefixes: BTreeSet<RefPrefix>,
} }
/// Run the ls-refs process using the provided `config`. /// Run the ls-refs process using the provided `config`.
@ -51,11 +54,17 @@ where
))); )));
} }
let prefixes = config
.prefixes
.into_iter()
.map(|prefix| prefix.into_bstring())
.collect::<BTreeSet<_>>();
let refs = gix_protocol::ls_refs( let refs = gix_protocol::ls_refs(
&mut conn, &mut conn,
capabilities, capabilities,
|_caps, args, features| { |_caps, args, features| {
for prefix in &config.prefixes { for prefix in &prefixes {
let mut arg = BString::from("ref-prefix "); let mut arg = BString::from("ref-prefix ");
arg.push_str(prefix); arg.push_str(prefix);
args.push(arg) args.push(arg)
@ -74,10 +83,7 @@ where
.into_iter() .into_iter()
.filter(|r| { .filter(|r| {
let (refname, _, _) = r.unpack(); let (refname, _, _) = r.unpack();
config prefixes.iter().any(|prefix| refname.starts_with(prefix))
.prefixes
.iter()
.any(|prefix| refname.starts_with(prefix))
}) })
.collect(); .collect();