gix_protocol/fetch/refmap/
init.rs

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