#[cfg(any(feature = "blocking-client", feature = "async-client"))] mod error { use crate::handshake::refs::parse; /// The error returned by invoking a [`super::function::LsRefsCommand`]. #[derive(Debug, thiserror::Error)] #[expect(missing_docs)] pub enum Error { #[error(transparent)] Io(#[from] std::io::Error), #[error(transparent)] Transport(#[from] gix_transport::client::Error), #[error(transparent)] Parse(#[from] parse::Error), #[error(transparent)] ArgumentValidation(#[from] crate::command::validate_argument_prefixes::Error), } impl gix_transport::IsSpuriousError for Error { fn is_spurious(&self) -> bool { match self { Error::Io(err) => err.is_spurious(), Error::Transport(err) => err.is_spurious(), _ => false, } } } } #[cfg(any(feature = "blocking-client", feature = "async-client"))] pub use error::Error; #[cfg(any(feature = "blocking-client", feature = "async-client"))] pub use self::function::RefPrefixes; #[cfg(any(feature = "blocking-client", feature = "async-client"))] pub(crate) mod function { use std::collections::HashSet; use bstr::{BString, ByteVec}; use gix_features::progress::Progress; use gix_transport::client::Capabilities; use super::Error; #[cfg(feature = "async-client")] use crate::transport::client::async_io::TransportV2Ext as _; #[cfg(feature = "blocking-client")] use crate::transport::client::blocking_io::TransportV2Ext as _; use crate::{Command, handshake::Ref}; /// [`RefPrefixes`] are the set of prefixes that are sent to the server for /// filtering purposes. /// /// These are communicated by sending zero or more `ref-prefix` values, and /// are documented in [gitprotocol-v2.adoc#ls-refs]. /// /// These prefixes can be constructed from a set of [`RefSpec`]'s using /// [`RefPrefixes::from_refspecs`]. /// /// Alternatively, they can be constructed using [`RefPrefixes::new`] and /// using [`RefPrefixes::extend`] to add new prefixes. /// /// [`RefSpec`]: gix_refspec::RefSpec /// [gitprotocol-v2.adoc#ls-refs]: https://github.com/git/git/blob/master/Documentation/gitprotocol-v2.adoc#ls-refs pub struct RefPrefixes { prefixes: Vec, } impl Default for RefPrefixes { fn default() -> Self { Self::new() } } impl RefPrefixes { /// Create an empty set of [`RefPrefixes`]. pub fn new() -> RefPrefixes { RefPrefixes { prefixes: Vec::new() } } /// Convert a series of [`RefSpec`]'s into a set of [`RefPrefixes`]. /// /// It attempts to expand each [`RefSpec`] into prefix references, e.g. /// `refs/heads/`, `refs/remotes/`, `refs/namespaces/foo/`, etc. /// /// Inputs that aren't fully qualified refs, like `HEAD` or `main`, are /// expanded in the same DWIM-style way that Git uses for `ref-prefix` /// generation, yielding prefixes like `HEAD`, `refs/heads/main`, and /// other rev-parse candidates. /// /// [`RefSpec`]: gix_refspec::RefSpec pub fn from_refspecs<'a>(refspecs: impl IntoIterator) -> Self { let mut seen = HashSet::new(); let mut prefixes = Self::new(); for spec in refspecs.into_iter() { let spec = spec.to_ref(); if seen.insert(spec.instruction()) { let mut out = Vec::with_capacity(1); spec.expand_prefixes(&mut out); prefixes.extend(out); } } prefixes } fn into_args(self) -> impl Iterator { self.prefixes.into_iter().map(|mut prefix| { prefix.insert_str(0, "ref-prefix "); prefix }) } } impl Extend for RefPrefixes { fn extend>(&mut self, iter: T) { for prefix in iter { if !self.prefixes.iter().any(|existing| existing == &prefix) { self.prefixes.push(prefix); } } } } /// A command to list references from a remote Git repository. /// /// Its invocation uses the same implementation with either blocking or asynchronous I/O. pub struct LsRefsCommand<'a> { pub(crate) capabilities: &'a Capabilities, features: Vec, arguments: Vec, } macro_rules! invoke { ($name:ident, $bisync:path, $transport:path, $from_v2_refs:path, $mode:literal) => { /// Invoke a ls-refs V2 command on `transport`. /// /// `progress` is used to provide feedback. /// If `trace` is `true`, all packetlines received or sent will be passed to the facilities of the `gix-trace` crate. #[$bisync] pub async fn $name( self, mut transport: impl $transport, progress: &mut impl Progress, trace: bool, ) -> Result, Error> { let _span = gix_features::trace::detail!("gix_protocol::LsRefsCommand::invoke()", mode = $mode); Command::LsRefs.validate_argument_prefixes( gix_transport::Protocol::V2, self.capabilities, &self.arguments, &self.features, )?; progress.step(); progress.set_name("list refs".into()); let mut remote_refs = transport .invoke( Command::LsRefs.as_str(), self.features.into_iter(), if self.arguments.is_empty() { None } else { Some(self.arguments.into_iter()) }, trace, ) .await?; Ok($from_v2_refs(&mut remote_refs).await?) } }; } impl<'a> LsRefsCommand<'a> { /// Build a command to list refs from the given server `capabilities`, /// using `agent` information to identify ourselves. /// /// Use [`crate::ls_refs::RefPrefixes::from_refspecs()`] to construct `ref_prefixes` /// from refspecs, or [`crate::ls_refs::RefPrefixes::new()`] to build them manually. pub fn new( ref_prefixes: Option, capabilities: &'a Capabilities, agent: crate::command::Feature, ) -> Self { let ls_refs = Command::LsRefs; let mut features = ls_refs.default_features(gix_transport::Protocol::V2, capabilities); features.push(agent); let mut arguments = ls_refs.initial_v2_arguments(&features); if capabilities .capability("ls-refs") .and_then(|cap| cap.supports("unborn")) .unwrap_or_default() { arguments.push("unborn".into()); } if let Some(prefixes) = ref_prefixes { arguments.extend(prefixes.into_args()); } Self { capabilities, features, arguments, } } #[cfg(feature = "async-client")] invoke!( invoke_async, ::bisync::asynchronous::bisync, crate::transport::client::async_io::Transport, crate::handshake::refs::async_io::from_v2_refs, "async" ); #[cfg(feature = "blocking-client")] invoke!( invoke_blocking, ::bisync::synchronous::bisync, crate::transport::client::blocking_io::Transport, crate::handshake::refs::blocking_io::from_v2_refs, "blocking" ); } #[cfg(test)] mod ref_prefixes { use bstr::{BString, ByteSlice}; use super::RefPrefixes; #[test] fn extend_preserves_first_seen_order_and_deduplicates_prefixes() { let mut prefixes = RefPrefixes::new(); prefixes.extend( [ "refs/tags", "HEAD", "main", "refs/heads/main", "refs/tags", "HEAD", "refs/heads/feature", "refs/heads/main", ] .into_iter() .map(|prefix| prefix.as_bytes().as_bstr().to_owned()), ); assert_eq!( prefixes.into_args().collect::>(), [ "ref-prefix refs/tags", "ref-prefix HEAD", "ref-prefix main", "ref-prefix refs/heads/main", "ref-prefix refs/heads/feature" ] .into_iter() .map(BString::from) .collect::>() ); } #[test] fn from_refspecs_keeps_exact_refs_and_dwim_expansions() { let specs = [ gix_refspec::parse("HEAD".into(), gix_refspec::parse::Operation::Fetch) .expect("valid") .to_owned(), gix_refspec::parse("dwim".into(), gix_refspec::parse::Operation::Fetch) .expect("valid") .to_owned(), gix_refspec::parse( "refs/tags/prefix*:refs/tags/prefix*".into(), gix_refspec::parse::Operation::Fetch, ) .expect("valid") .to_owned(), gix_refspec::parse("refs/heads/main".into(), gix_refspec::parse::Operation::Fetch) .expect("valid") .to_owned(), ]; let prefixes = RefPrefixes::from_refspecs(&specs); assert_eq!( prefixes.into_args().collect::>(), [ "ref-prefix HEAD", "ref-prefix dwim", "ref-prefix refs/dwim", "ref-prefix refs/tags/dwim", "ref-prefix refs/heads/dwim", "ref-prefix refs/remotes/dwim", "ref-prefix refs/remotes/dwim/HEAD", "ref-prefix refs/tags/prefix", "ref-prefix refs/heads/main", ] .into_iter() .map(BString::from) .collect::>() ); } } }