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