gix_protocol/fetch/refmap/
init.rs

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