gix_protocol/fetch/refmap/
init.rs

1use std::collections::HashSet;
2
3use crate::fetch;
4use crate::fetch::refmap::{Mapping, Source, SpecIndex};
5use crate::fetch::RefMap;
6use crate::transport::client::Transport;
7use bstr::{BString, ByteVec};
8use gix_features::progress::Progress;
9
10/// The error returned by [`RefMap::new()`].
11#[derive(Debug, thiserror::Error)]
12#[allow(missing_docs)]
13pub enum Error {
14    #[error("The object format {format:?} as used by the remote is unsupported")]
15    UnknownObjectFormat { format: BString },
16    #[error(transparent)]
17    MappingValidation(#[from] gix_refspec::match_group::validate::Error),
18    #[error(transparent)]
19    ListRefs(#[from] crate::ls_refs::Error),
20}
21
22/// For use in [`RefMap::new()`].
23#[derive(Debug, Clone)]
24pub struct Options {
25    /// Use a two-component prefix derived from the ref-spec's source, like `refs/heads/`  to let the server pre-filter refs
26    /// with great potential for savings in traffic and local CPU time. Defaults to `true`.
27    pub prefix_from_spec_as_filter_on_remote: bool,
28    /// A list of refspecs to use as implicit refspecs which won't be saved or otherwise be part of the remote in question.
29    ///
30    /// This is useful for handling `remote.<name>.tagOpt` for example.
31    pub extra_refspecs: Vec<gix_refspec::RefSpec>,
32}
33
34impl Default for Options {
35    fn default() -> Self {
36        Options {
37            prefix_from_spec_as_filter_on_remote: true,
38            extra_refspecs: Vec::new(),
39        }
40    }
41}
42
43impl RefMap {
44    /// Create a new instance by obtaining all references on the remote that have been filtered through our remote's
45    /// for _fetching_.
46    ///
47    /// A [context](fetch::Context) is provided to bundle what would be additional parameters,
48    /// and [options](Options) are used to further configure the call.
49    ///
50    /// * `progress` is used if `ls-refs` is invoked on the remote. Always the case when V2 is used.
51    /// * `fetch_refspecs` are all explicit refspecs to identify references on the remote that you are interested in.
52    ///    Note that these are copied to [`RefMap::refspecs`] for convenience, as `RefMap::mappings` refer to them by index.
53    #[allow(clippy::result_large_err)]
54    #[maybe_async::maybe_async]
55    pub async fn new<T>(
56        mut progress: impl Progress,
57        fetch_refspecs: &[gix_refspec::RefSpec],
58        fetch::Context {
59            handshake,
60            transport,
61            user_agent,
62            trace_packetlines,
63        }: fetch::Context<'_, T>,
64        Options {
65            prefix_from_spec_as_filter_on_remote,
66            extra_refspecs,
67        }: Options,
68    ) -> Result<Self, Error>
69    where
70        T: Transport,
71    {
72        let _span = gix_trace::coarse!("gix_protocol::fetch::RefMap::new()");
73        let null = gix_hash::ObjectId::null(gix_hash::Kind::Sha1); // OK to hardcode Sha1, it's not supposed to match, ever.
74
75        let all_refspecs = {
76            let mut s: Vec<_> = fetch_refspecs.to_vec();
77            s.extend(extra_refspecs.clone());
78            s
79        };
80        let remote_refs = match handshake.refs.take() {
81            Some(refs) => refs,
82            None => {
83                crate::ls_refs(
84                    transport,
85                    &handshake.capabilities,
86                    |_capabilities, arguments, features| {
87                        features.push(user_agent);
88                        if prefix_from_spec_as_filter_on_remote {
89                            let mut seen = HashSet::new();
90                            for spec in &all_refspecs {
91                                let spec = spec.to_ref();
92                                if seen.insert(spec.instruction()) {
93                                    let mut prefixes = Vec::with_capacity(1);
94                                    spec.expand_prefixes(&mut prefixes);
95                                    for mut prefix in prefixes {
96                                        prefix.insert_str(0, "ref-prefix ");
97                                        arguments.push(prefix);
98                                    }
99                                }
100                            }
101                        }
102                        Ok(crate::ls_refs::Action::Continue)
103                    },
104                    &mut progress,
105                    trace_packetlines,
106                )
107                .await?
108            }
109        };
110        let num_explicit_specs = fetch_refspecs.len();
111        let group = gix_refspec::MatchGroup::from_fetch_specs(all_refspecs.iter().map(gix_refspec::RefSpec::to_ref));
112        let (res, fixes) = group
113            .match_lhs(remote_refs.iter().map(|r| {
114                let (full_ref_name, target, object) = r.unpack();
115                gix_refspec::match_group::Item {
116                    full_ref_name,
117                    target: target.unwrap_or(&null),
118                    object,
119                }
120            }))
121            .validated()?;
122
123        let mappings = res.mappings;
124        let mappings = mappings
125            .into_iter()
126            .map(|m| Mapping {
127                remote: m.item_index.map_or_else(
128                    || {
129                        Source::ObjectId(match m.lhs {
130                            gix_refspec::match_group::SourceRef::ObjectId(id) => id,
131                            _ => unreachable!("no item index implies having an object id"),
132                        })
133                    },
134                    |idx| Source::Ref(remote_refs[idx].clone()),
135                ),
136                local: m.rhs.map(std::borrow::Cow::into_owned),
137                spec_index: if m.spec_index < num_explicit_specs {
138                    SpecIndex::ExplicitInRemote(m.spec_index)
139                } else {
140                    SpecIndex::Implicit(m.spec_index - num_explicit_specs)
141                },
142            })
143            .collect();
144
145        let object_hash = extract_object_format(handshake)?;
146        Ok(RefMap {
147            mappings,
148            refspecs: fetch_refspecs.to_vec(),
149            extra_refspecs,
150            fixes,
151            remote_refs,
152            object_hash,
153        })
154    }
155}
156
157/// Assume sha1 if server says nothing, otherwise configure anything beyond sha1 in the local repo configuration
158#[allow(clippy::result_large_err)]
159fn extract_object_format(outcome: &crate::handshake::Outcome) -> Result<gix_hash::Kind, Error> {
160    use bstr::ByteSlice;
161    let object_hash =
162        if let Some(object_format) = outcome.capabilities.capability("object-format").and_then(|c| c.value()) {
163            let object_format = object_format.to_str().map_err(|_| Error::UnknownObjectFormat {
164                format: object_format.into(),
165            })?;
166            match object_format {
167                "sha1" => gix_hash::Kind::Sha1,
168                unknown => return Err(Error::UnknownObjectFormat { format: unknown.into() }),
169            }
170        } else {
171            gix_hash::Kind::Sha1
172        };
173    Ok(object_hash)
174}