1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
use git_protocol::transport::client::Transport;

use crate::{remote::Connection, Progress, Remote};

mod error {
    use crate::{bstr::BString, remote};

    /// The error returned by [connect()][crate::Remote::connect()].
    #[derive(Debug, thiserror::Error)]
    #[allow(missing_docs)]
    pub enum Error {
        #[error(transparent)]
        SchemePermission(#[from] remote::url::scheme_permission::init::Error),
        #[error("Protocol {scheme:?} of url {url:?} is denied per configuration")]
        ProtocolDenied { url: BString, scheme: git_url::Scheme },
        #[error(transparent)]
        Connect(#[from] git_protocol::transport::client::connect::Error),
        #[error("The {} url was missing - don't know where to establish a connection to", direction.as_str())]
        MissingUrl { direction: remote::Direction },
        #[error("Protocol named {given:?} is not a valid protocol. Choose between 1 and 2")]
        UnknownProtocol { given: BString },
        #[error("Could not verify that file:// url is a valid git directory before attempting to use it")]
        FileUrl(#[from] git_discover::is_git::Error),
    }
}
pub use error::Error;

/// Establishing connections to remote hosts (without performing a git-handshake).
impl<'repo> Remote<'repo> {
    /// Create a new connection using `transport` to communicate, with `progress` to indicate changes.
    ///
    /// Note that this method expects the `transport` to be created by the user, which would involve the [`url()`][Self::url()].
    /// It's meant to be used when async operation is needed with runtimes of the user's choice.
    pub fn to_connection_with_transport<T, P>(&self, transport: T, progress: P) -> Connection<'_, 'repo, T, P>
    where
        T: Transport,
        P: Progress,
    {
        Connection {
            remote: self,
            authenticate: None,
            transport,
            progress,
        }
    }

    /// Connect to the url suitable for `direction` and return a handle through which operations can be performed.
    ///
    /// Note that the `protocol.version` configuration key affects the transport protocol used to connect,
    /// with `2` being the default.
    ///
    /// The transport used for connection can be configured via `transport_mut().configure()` assuming the actually
    /// used transport is well known. If that's not the case, the transport can be created by hand and passed to
    /// [to_connection_with_transport()][Self::to_connection_with_transport()].
    #[cfg(any(feature = "blocking-network-client", feature = "async-network-client-async-std"))]
    #[git_protocol::maybe_async::maybe_async]
    pub async fn connect<P>(
        &self,
        direction: crate::remote::Direction,
        progress: P,
    ) -> Result<Connection<'_, 'repo, Box<dyn Transport + Send>, P>, Error>
    where
        P: Progress,
    {
        let (url, version) = self.sanitized_url_and_version(direction)?;
        let transport = git_protocol::transport::connect(url, version).await?;
        Ok(self.to_connection_with_transport(transport, progress))
    }

    /// Produce the sanitized URL and protocol version to use as obtained by querying the repository configuration.
    ///
    /// This can be useful when using custom transports to allow additional configuration.
    pub fn sanitized_url_and_version(
        &self,
        direction: crate::remote::Direction,
    ) -> Result<(git_url::Url, git_protocol::transport::Protocol), Error> {
        fn sanitize(mut url: git_url::Url) -> Result<git_url::Url, Error> {
            if url.scheme == git_url::Scheme::File {
                let mut dir = git_path::from_bstr(url.path.as_ref());
                let kind = git_discover::is_git(dir.as_ref()).or_else(|_| {
                    dir.to_mut().push(git_discover::DOT_GIT_DIR);
                    git_discover::is_git(dir.as_ref())
                })?;
                let (git_dir, _work_dir) = git_discover::repository::Path::from_dot_git_dir(dir.into_owned(), kind)
                    .into_repository_and_work_tree_directories();
                url.path = git_path::into_bstr(git_dir).into_owned();
            }
            Ok(url)
        }

        use git_protocol::transport::Protocol;
        let version = self
            .repo
            .config
            .resolved
            .integer("protocol", None, "version")
            .unwrap_or(Ok(2))
            .map_err(|err| Error::UnknownProtocol { given: err.input })
            .and_then(|num| {
                Ok(match num {
                    1 => Protocol::V1,
                    2 => Protocol::V2,
                    num => {
                        return Err(Error::UnknownProtocol {
                            given: num.to_string().into(),
                        })
                    }
                })
            })?;

        let url = self.url(direction).ok_or(Error::MissingUrl { direction })?.to_owned();
        if !self.repo.config.url_scheme()?.allow(&url.scheme) {
            return Err(Error::ProtocolDenied {
                url: url.to_bstring(),
                scheme: url.scheme,
            });
        }
        Ok((sanitize(url)?, version))
    }
}