git-repository 0.33.0

Abstractions for git repositories
Documentation
use std::collections::HashSet;

use git_features::progress::Progress;
use git_protocol::transport::client::Transport;

use crate::{
    bstr,
    bstr::{BString, ByteVec},
    remote::{connection::HandshakeWithRefs, fetch, fetch::SpecIndex, Connection, Direction},
};

/// The error returned by [`Connection::ref_map()`].
#[derive(Debug, thiserror::Error)]
#[allow(missing_docs)]
pub enum Error {
    #[error("Failed to configure the transport before connecting to {url:?}")]
    GatherTransportConfig {
        url: BString,
        source: crate::config::transport::Error,
    },
    #[error("Failed to configure the transport layer")]
    ConfigureTransport(#[from] Box<dyn std::error::Error + Send + Sync + 'static>),
    #[error(transparent)]
    Handshake(#[from] git_protocol::handshake::Error),
    #[error("The object format {format:?} as used by the remote is unsupported")]
    UnknownObjectFormat { format: BString },
    #[error(transparent)]
    ListRefs(#[from] git_protocol::ls_refs::Error),
    #[error(transparent)]
    Transport(#[from] git_protocol::transport::client::Error),
    #[error(transparent)]
    ConfigureCredentials(#[from] crate::config::credential_helpers::Error),
    #[error(transparent)]
    MappingValidation(#[from] git_refspec::match_group::validate::Error),
}

impl git_protocol::transport::IsSpuriousError for Error {
    fn is_spurious(&self) -> bool {
        match self {
            Error::Transport(err) => err.is_spurious(),
            Error::ListRefs(err) => err.is_spurious(),
            Error::Handshake(err) => err.is_spurious(),
            _ => false,
        }
    }
}

/// For use in [`Connection::ref_map()`].
#[derive(Debug, Clone)]
pub struct Options {
    /// Use a two-component prefix derived from the ref-spec's source, like `refs/heads/`  to let the server pre-filter refs
    /// with great potential for savings in traffic and local CPU time. Defaults to `true`.
    pub prefix_from_spec_as_filter_on_remote: bool,
    /// Parameters in the form of `(name, optional value)` to add to the handshake.
    ///
    /// This is useful in case of custom servers.
    pub handshake_parameters: Vec<(String, Option<String>)>,
    /// A list of refspecs to use as implicit refspecs which won't be saved or otherwise be part of the remote in question.
    ///
    /// This is useful for handling `remote.<name>.tagOpt` for example.
    pub extra_refspecs: Vec<git_refspec::RefSpec>,
}

impl Default for Options {
    fn default() -> Self {
        Options {
            prefix_from_spec_as_filter_on_remote: true,
            handshake_parameters: Vec::new(),
            extra_refspecs: Vec::new(),
        }
    }
}

impl<'remote, 'repo, T, P> Connection<'remote, 'repo, T, P>
where
    T: Transport,
    P: Progress,
{
    /// List all references on the remote that have been filtered through our remote's [`refspecs`][crate::Remote::refspecs()]
    /// for _fetching_.
    ///
    /// This comes in the form of all matching tips on the remote and the object they point to, along with
    /// with the local tracking branch of these tips (if available).
    ///
    /// Note that this doesn't fetch the objects mentioned in the tips nor does it make any change to underlying repository.
    ///
    /// # Consumption
    ///
    /// Due to management of the transport, it's cleanest to only use it for a single interaction. Thus it's consumed along with
    /// the connection.
    ///
    /// ### Configuration
    ///
    /// - `gitoxide.userAgent` is read to obtain the application user agent for git servers and for HTTP servers as well.
    #[allow(clippy::result_large_err)]
    #[git_protocol::maybe_async::maybe_async]
    pub async fn ref_map(mut self, options: Options) -> Result<fetch::RefMap, Error> {
        let res = self.ref_map_inner(options).await;
        git_protocol::indicate_end_of_interaction(&mut self.transport)
            .await
            .ok();
        res
    }

    #[allow(clippy::result_large_err)]
    #[git_protocol::maybe_async::maybe_async]
    pub(crate) async fn ref_map_inner(
        &mut self,
        Options {
            prefix_from_spec_as_filter_on_remote,
            handshake_parameters,
            mut extra_refspecs,
        }: Options,
    ) -> Result<fetch::RefMap, Error> {
        let null = git_hash::ObjectId::null(git_hash::Kind::Sha1); // OK to hardcode Sha1, it's not supposed to match, ever.

        if let Some(tag_spec) = self.remote.fetch_tags.to_refspec().map(|spec| spec.to_owned()) {
            if !extra_refspecs.contains(&tag_spec) {
                extra_refspecs.push(tag_spec);
            }
        };
        let specs = {
            let mut s = self.remote.fetch_specs.clone();
            s.extend(extra_refspecs.clone());
            s
        };
        let remote = self
            .fetch_refs(prefix_from_spec_as_filter_on_remote, handshake_parameters, &specs)
            .await?;
        let num_explicit_specs = self.remote.fetch_specs.len();
        let group = git_refspec::MatchGroup::from_fetch_specs(specs.iter().map(|s| s.to_ref()));
        let (res, fixes) = group
            .match_remotes(remote.refs.iter().map(|r| {
                let (full_ref_name, target, object) = r.unpack();
                git_refspec::match_group::Item {
                    full_ref_name,
                    target: target.unwrap_or(&null),
                    object,
                }
            }))
            .validated()?;
        let mappings = res.mappings;
        let mappings = mappings
            .into_iter()
            .map(|m| fetch::Mapping {
                remote: m
                    .item_index
                    .map(|idx| fetch::Source::Ref(remote.refs[idx].clone()))
                    .unwrap_or_else(|| {
                        fetch::Source::ObjectId(match m.lhs {
                            git_refspec::match_group::SourceRef::ObjectId(id) => id,
                            _ => unreachable!("no item index implies having an object id"),
                        })
                    }),
                local: m.rhs.map(|c| c.into_owned()),
                spec_index: if m.spec_index < num_explicit_specs {
                    SpecIndex::ExplicitInRemote(m.spec_index)
                } else {
                    SpecIndex::Implicit(m.spec_index - num_explicit_specs)
                },
            })
            .collect();

        let object_hash = extract_object_format(self.remote.repo, &remote.outcome)?;
        Ok(fetch::RefMap {
            mappings,
            extra_refspecs,
            fixes,
            remote_refs: remote.refs,
            handshake: remote.outcome,
            object_hash,
        })
    }

    #[allow(clippy::result_large_err)]
    #[git_protocol::maybe_async::maybe_async]
    async fn fetch_refs(
        &mut self,
        filter_by_prefix: bool,
        extra_parameters: Vec<(String, Option<String>)>,
        refspecs: &[git_refspec::RefSpec],
    ) -> Result<HandshakeWithRefs, Error> {
        let mut credentials_storage;
        let url = self.transport.to_url();
        let authenticate = match self.authenticate.as_mut() {
            Some(f) => f,
            None => {
                let url = self
                    .remote
                    .url(Direction::Fetch)
                    .map(ToOwned::to_owned)
                    .unwrap_or_else(|| git_url::parse(url.as_ref()).expect("valid URL to be provided by transport"));
                credentials_storage = self.configured_credentials(url)?;
                &mut credentials_storage
            }
        };

        if self.transport_options.is_none() {
            self.transport_options = self
                .remote
                .repo
                .transport_options(url.as_ref(), self.remote.name().map(|n| n.as_bstr()))
                .map_err(|err| Error::GatherTransportConfig {
                    source: err,
                    url: url.into_owned(),
                })?;
        }
        if let Some(config) = self.transport_options.as_ref() {
            self.transport.configure(&**config)?;
        }
        let mut outcome =
            git_protocol::fetch::handshake(&mut self.transport, authenticate, extra_parameters, &mut self.progress)
                .await?;
        let refs = match outcome.refs.take() {
            Some(refs) => refs,
            None => {
                let agent_feature = self.remote.repo.config.user_agent_tuple();
                git_protocol::ls_refs(
                    &mut self.transport,
                    &outcome.capabilities,
                    move |_capabilities, arguments, features| {
                        features.push(agent_feature);
                        if filter_by_prefix {
                            let mut seen = HashSet::new();
                            for spec in refspecs {
                                let spec = spec.to_ref();
                                if seen.insert(spec.instruction()) {
                                    let mut prefixes = Vec::with_capacity(1);
                                    spec.expand_prefixes(&mut prefixes);
                                    for mut prefix in prefixes {
                                        prefix.insert_str(0, "ref-prefix ");
                                        arguments.push(prefix);
                                    }
                                }
                            }
                        }
                        Ok(git_protocol::ls_refs::Action::Continue)
                    },
                    &mut self.progress,
                )
                .await?
            }
        };
        Ok(HandshakeWithRefs { outcome, refs })
    }
}

/// Assume sha1 if server says nothing, otherwise configure anything beyond sha1 in the local repo configuration
#[allow(clippy::result_large_err)]
fn extract_object_format(
    _repo: &crate::Repository,
    outcome: &git_protocol::handshake::Outcome,
) -> Result<git_hash::Kind, Error> {
    use bstr::ByteSlice;
    let object_hash =
        if let Some(object_format) = outcome.capabilities.capability("object-format").and_then(|c| c.value()) {
            let object_format = object_format.to_str().map_err(|_| Error::UnknownObjectFormat {
                format: object_format.into(),
            })?;
            match object_format {
                "sha1" => git_hash::Kind::Sha1,
                unknown => return Err(Error::UnknownObjectFormat { format: unknown.into() }),
            }
        } else {
            git_hash::Kind::Sha1
        };
    Ok(object_hash)
}