1#[cfg(any(feature = "blocking-client", feature = "async-client"))]
2mod error {
3 use crate::handshake::refs::parse;
4
5 #[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 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 pub fn new() -> RefPrefixes {
77 RefPrefixes { prefixes: Vec::new() }
78 }
79
80 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 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 #[$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 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}