Skip to main content

gix_protocol/
ls_refs.rs

1#[cfg(any(feature = "blocking-client", feature = "async-client"))]
2mod error {
3    use crate::handshake::refs::parse;
4
5    /// The error returned by invoking a [`super::function::LsRefsCommand`].
6    #[derive(Debug, thiserror::Error)]
7    #[allow(missing_docs)]
8    pub enum Error {
9        #[error(transparent)]
10        Io(#[from] std::io::Error),
11        #[error(transparent)]
12        Transport(#[from] gix_transport::client::Error),
13        #[error(transparent)]
14        Parse(#[from] parse::Error),
15        #[error(transparent)]
16        ArgumentValidation(#[from] crate::command::validate_argument_prefixes::Error),
17    }
18
19    impl gix_transport::IsSpuriousError for Error {
20        fn is_spurious(&self) -> bool {
21            match self {
22                Error::Io(err) => err.is_spurious(),
23                Error::Transport(err) => err.is_spurious(),
24                _ => false,
25            }
26        }
27    }
28}
29#[cfg(any(feature = "blocking-client", feature = "async-client"))]
30pub use error::Error;
31
32#[cfg(any(feature = "blocking-client", feature = "async-client"))]
33pub use self::function::RefPrefixes;
34
35#[cfg(any(feature = "blocking-client", feature = "async-client"))]
36pub(crate) mod function {
37    use std::{borrow::Cow, collections::HashSet};
38
39    use bstr::{BString, ByteVec};
40    use gix_features::progress::Progress;
41    use gix_transport::client::Capabilities;
42
43    use super::Error;
44    #[cfg(feature = "async-client")]
45    use crate::transport::client::async_io::{self, TransportV2Ext as _};
46    #[cfg(feature = "blocking-client")]
47    use crate::transport::client::blocking_io::{self, TransportV2Ext as _};
48    use crate::{
49        handshake::{refs::from_v2_refs, Ref},
50        Command,
51    };
52
53    /// [`RefPrefixes`] are the set of prefixes that are sent to the server for
54    /// filtering purposes.
55    ///
56    /// These are communicated by sending zero or more `ref-prefix` values, and
57    /// are documented in [gitprotocol-v2.adoc#ls-refs].
58    ///
59    /// These prefixes can be constructed from a set of [`RefSpec`]'s using
60    /// [`RefPrefixes::from_refspecs`].
61    ///
62    /// Alternatively, they can be constructed using [`RefPrefixes::new`] and
63    /// using [`RefPrefixes::extend`] to add new prefixes.
64    ///
65    /// [`RefSpec`]: gix_refspec::RefSpec
66    /// [gitprotocol-v2.adoc#ls-refs]: https://github.com/git/git/blob/master/Documentation/gitprotocol-v2.adoc#ls-refs
67    pub struct RefPrefixes {
68        prefixes: Vec<BString>,
69    }
70
71    impl Default for RefPrefixes {
72        fn default() -> Self {
73            Self::new()
74        }
75    }
76
77    impl RefPrefixes {
78        /// Create an empty set of [`RefPrefixes`].
79        pub fn new() -> RefPrefixes {
80            RefPrefixes { prefixes: Vec::new() }
81        }
82
83        /// Convert a series of [`RefSpec`]'s into a set of [`RefPrefixes`].
84        ///
85        /// It attempts to expand each [`RefSpec`] into prefix references, e.g.
86        /// `refs/heads/`, `refs/remotes/`, `refs/namespaces/foo/`, etc.
87        ///
88        /// Inputs that aren't fully qualified refs, like `HEAD` or `main`, are
89        /// expanded in the same DWIM-style way that Git uses for `ref-prefix`
90        /// generation, yielding prefixes like `HEAD`, `refs/heads/main`, and
91        /// other rev-parse candidates.
92        ///
93        /// [`RefSpec`]: gix_refspec::RefSpec
94        pub fn from_refspecs<'a>(refspecs: impl IntoIterator<Item = &'a gix_refspec::RefSpec>) -> Self {
95            let mut seen = HashSet::new();
96            let mut prefixes = Self::new();
97            for spec in refspecs.into_iter() {
98                let spec = spec.to_ref();
99                if seen.insert(spec.instruction()) {
100                    let mut out = Vec::with_capacity(1);
101                    spec.expand_prefixes(&mut out);
102                    prefixes.extend(out);
103                }
104            }
105            prefixes
106        }
107
108        fn into_args(self) -> impl Iterator<Item = BString> {
109            self.prefixes.into_iter().map(|mut prefix| {
110                prefix.insert_str(0, "ref-prefix ");
111                prefix
112            })
113        }
114    }
115
116    impl Extend<BString> for RefPrefixes {
117        fn extend<T: IntoIterator<Item = BString>>(&mut self, iter: T) {
118            for prefix in iter {
119                if !self.prefixes.iter().any(|existing| existing == &prefix) {
120                    self.prefixes.push(prefix);
121                }
122            }
123        }
124    }
125
126    /// A command to list references from a remote Git repository.
127    ///
128    /// It acts as a utility to separate the invocation into the shared blocking portion,
129    /// and the one that performs IO either blocking or `async`.
130    pub struct LsRefsCommand<'a> {
131        pub(crate) capabilities: &'a Capabilities,
132        features: Vec<(&'static str, Option<Cow<'static, str>>)>,
133        arguments: Vec<BString>,
134    }
135
136    impl<'a> LsRefsCommand<'a> {
137        /// Build a command to list refs from the given server `capabilities`,
138        /// using `agent` information to identify ourselves.
139        ///
140        /// Use [`crate::ls_refs::RefPrefixes::from_refspecs()`] to construct `ref_prefixes`
141        /// from refspecs, or [`crate::ls_refs::RefPrefixes::new()`] to build them manually.
142        pub fn new(
143            ref_prefixes: Option<RefPrefixes>,
144            capabilities: &'a Capabilities,
145            agent: (&'static str, Option<Cow<'static, str>>),
146        ) -> Self {
147            let ls_refs = Command::LsRefs;
148            let mut features = ls_refs.default_features(gix_transport::Protocol::V2, capabilities);
149            features.push(agent);
150            let mut arguments = ls_refs.initial_v2_arguments(&features);
151            if capabilities
152                .capability("ls-refs")
153                .and_then(|cap| cap.supports("unborn"))
154                .unwrap_or_default()
155            {
156                arguments.push("unborn".into());
157            }
158
159            if let Some(prefixes) = ref_prefixes {
160                arguments.extend(prefixes.into_args());
161            }
162
163            Self {
164                capabilities,
165                features,
166                arguments,
167            }
168        }
169
170        /// Invoke a ls-refs V2 command on `transport`.
171        ///
172        /// `progress` is used to provide feedback.
173        /// If `trace` is `true`, all packetlines received or sent will be passed to the facilities of the `gix-trace` crate.
174        #[cfg(feature = "async-client")]
175        pub async fn invoke_async(
176            self,
177            mut transport: impl async_io::Transport,
178            progress: &mut impl Progress,
179            trace: bool,
180        ) -> Result<Vec<Ref>, Error> {
181            let _span = gix_features::trace::detail!("gix_protocol::LsRefsCommand::invoke_async()");
182            Command::LsRefs.validate_argument_prefixes(
183                gix_transport::Protocol::V2,
184                self.capabilities,
185                &self.arguments,
186                &self.features,
187            )?;
188
189            progress.step();
190            progress.set_name("list refs".into());
191            let mut remote_refs = transport
192                .invoke(
193                    Command::LsRefs.as_str(),
194                    self.features.into_iter(),
195                    if self.arguments.is_empty() {
196                        None
197                    } else {
198                        Some(self.arguments.into_iter())
199                    },
200                    trace,
201                )
202                .await?;
203            Ok(from_v2_refs(&mut remote_refs).await?)
204        }
205
206        /// Invoke a ls-refs V2 command on `transport`.
207        ///
208        /// `progress` is used to provide feedback.
209        /// If `trace` is `true`, all packetlines received or sent will be passed to the facilities of the `gix-trace` crate.
210        #[cfg(feature = "blocking-client")]
211        pub fn invoke_blocking(
212            self,
213            mut transport: impl blocking_io::Transport,
214            progress: &mut impl Progress,
215            trace: bool,
216        ) -> Result<Vec<Ref>, Error> {
217            let _span = gix_features::trace::detail!("gix_protocol::LsRefsCommand::invoke_blocking()");
218            Command::LsRefs.validate_argument_prefixes(
219                gix_transport::Protocol::V2,
220                self.capabilities,
221                &self.arguments,
222                &self.features,
223            )?;
224
225            progress.step();
226            progress.set_name("list refs".into());
227            let mut remote_refs = transport.invoke(
228                Command::LsRefs.as_str(),
229                self.features.into_iter(),
230                if self.arguments.is_empty() {
231                    None
232                } else {
233                    Some(self.arguments.into_iter())
234                },
235                trace,
236            )?;
237            Ok(from_v2_refs(&mut remote_refs)?)
238        }
239    }
240
241    #[cfg(test)]
242    mod ref_prefixes {
243        use bstr::{BString, ByteSlice};
244
245        use super::RefPrefixes;
246
247        #[test]
248        fn extend_preserves_first_seen_order_and_deduplicates_prefixes() {
249            let mut prefixes = RefPrefixes::new();
250            prefixes.extend(
251                [
252                    "refs/tags",
253                    "HEAD",
254                    "main",
255                    "refs/heads/main",
256                    "refs/tags",
257                    "HEAD",
258                    "refs/heads/feature",
259                    "refs/heads/main",
260                ]
261                .into_iter()
262                .map(|prefix| prefix.as_bytes().as_bstr().to_owned()),
263            );
264
265            assert_eq!(
266                prefixes.into_args().collect::<Vec<_>>(),
267                [
268                    "ref-prefix refs/tags",
269                    "ref-prefix HEAD",
270                    "ref-prefix main",
271                    "ref-prefix refs/heads/main",
272                    "ref-prefix refs/heads/feature"
273                ]
274                .into_iter()
275                .map(BString::from)
276                .collect::<Vec<_>>()
277            );
278        }
279
280        #[test]
281        fn from_refspecs_keeps_exact_refs_and_dwim_expansions() {
282            let specs = [
283                gix_refspec::parse("HEAD".into(), gix_refspec::parse::Operation::Fetch)
284                    .expect("valid")
285                    .to_owned(),
286                gix_refspec::parse("dwim".into(), gix_refspec::parse::Operation::Fetch)
287                    .expect("valid")
288                    .to_owned(),
289                gix_refspec::parse(
290                    "refs/tags/prefix*:refs/tags/prefix*".into(),
291                    gix_refspec::parse::Operation::Fetch,
292                )
293                .expect("valid")
294                .to_owned(),
295                gix_refspec::parse("refs/heads/main".into(), gix_refspec::parse::Operation::Fetch)
296                    .expect("valid")
297                    .to_owned(),
298            ];
299
300            let prefixes = RefPrefixes::from_refspecs(&specs);
301
302            assert_eq!(
303                prefixes.into_args().collect::<Vec<_>>(),
304                [
305                    "ref-prefix HEAD",
306                    "ref-prefix dwim",
307                    "ref-prefix refs/dwim",
308                    "ref-prefix refs/tags/dwim",
309                    "ref-prefix refs/heads/dwim",
310                    "ref-prefix refs/remotes/dwim",
311                    "ref-prefix refs/remotes/dwim/HEAD",
312                    "ref-prefix refs/tags/prefix",
313                    "ref-prefix refs/heads/main",
314                ]
315                .into_iter()
316                .map(BString::from)
317                .collect::<Vec<_>>()
318            );
319        }
320    }
321}