summaryrefslogtreecommitdiffstats
path: root/vendor/gix-transport/src
diff options
context:
space:
mode:
Diffstat (limited to 'vendor/gix-transport/src')
-rw-r--r--vendor/gix-transport/src/client/async_io/bufread_ext.rs126
-rw-r--r--vendor/gix-transport/src/client/async_io/connect.rs47
-rw-r--r--vendor/gix-transport/src/client/async_io/mod.rs13
-rw-r--r--vendor/gix-transport/src/client/async_io/request.rs103
-rw-r--r--vendor/gix-transport/src/client/async_io/traits.rs113
-rw-r--r--vendor/gix-transport/src/client/blocking_io/bufread_ext.rs114
-rw-r--r--vendor/gix-transport/src/client/blocking_io/connect.rs68
-rw-r--r--vendor/gix-transport/src/client/blocking_io/file.rs288
-rw-r--r--vendor/gix-transport/src/client/blocking_io/http/curl/mod.rs169
-rw-r--r--vendor/gix-transport/src/client/blocking_io/http/curl/remote.rs392
-rw-r--r--vendor/gix-transport/src/client/blocking_io/http/mod.rs512
-rw-r--r--vendor/gix-transport/src/client/blocking_io/http/redirect.rs66
-rw-r--r--vendor/gix-transport/src/client/blocking_io/http/reqwest/mod.rs28
-rw-r--r--vendor/gix-transport/src/client/blocking_io/http/reqwest/remote.rs263
-rw-r--r--vendor/gix-transport/src/client/blocking_io/http/traits.rs133
-rw-r--r--vendor/gix-transport/src/client/blocking_io/mod.rs20
-rw-r--r--vendor/gix-transport/src/client/blocking_io/request.rs79
-rw-r--r--vendor/gix-transport/src/client/blocking_io/ssh/mod.rs131
-rw-r--r--vendor/gix-transport/src/client/blocking_io/ssh/program_kind.rs127
-rw-r--r--vendor/gix-transport/src/client/blocking_io/ssh/tests.rs285
-rw-r--r--vendor/gix-transport/src/client/blocking_io/traits.rs101
-rw-r--r--vendor/gix-transport/src/client/capabilities.rs307
-rw-r--r--vendor/gix-transport/src/client/git/async_io.rs151
-rw-r--r--vendor/gix-transport/src/client/git/blocking_io.rs199
-rw-r--r--vendor/gix-transport/src/client/git/mod.rs177
-rw-r--r--vendor/gix-transport/src/client/mod.rs36
-rw-r--r--vendor/gix-transport/src/client/non_io_types.rs159
-rw-r--r--vendor/gix-transport/src/client/traits.rs115
-rw-r--r--vendor/gix-transport/src/lib.rs99
29 files changed, 4421 insertions, 0 deletions
diff --git a/vendor/gix-transport/src/client/async_io/bufread_ext.rs b/vendor/gix-transport/src/client/async_io/bufread_ext.rs
new file mode 100644
index 000000000..abe206b8a
--- /dev/null
+++ b/vendor/gix-transport/src/client/async_io/bufread_ext.rs
@@ -0,0 +1,126 @@
+use std::{
+ io,
+ ops::{Deref, DerefMut},
+};
+
+use async_trait::async_trait;
+use futures_io::{AsyncBufRead, AsyncRead};
+use gix_packetline::PacketLineRef;
+
+use crate::{
+ client::{Error, MessageKind},
+ Protocol,
+};
+
+/// A function `f(is_error, text)` receiving progress or error information.
+/// As it is not a future itself, it must not block. If IO is performed within the function, be sure to spawn
+/// it onto an executor.
+pub type HandleProgress = Box<dyn FnMut(bool, &[u8])>;
+
+/// This trait exists to get a version of a `gix_packetline::Provider` without type parameters,
+/// but leave support for reading lines directly without forcing them through `String`.
+///
+/// For the sake of usability, it also implements [`std::io::BufRead`] making it trivial to
+/// read pack files while keeping open the option to read individual lines with low overhead.
+#[async_trait(?Send)]
+pub trait ReadlineBufRead: AsyncBufRead {
+ /// Read a packet line into the internal buffer and return it.
+ ///
+ /// Returns `None` if the end of iteration is reached because of one of the following:
+ ///
+ /// * natural EOF
+ /// * ERR packet line encountered
+ /// * A `delimiter` packet line encountered
+ async fn readline(
+ &mut self,
+ ) -> Option<io::Result<Result<gix_packetline::PacketLineRef<'_>, gix_packetline::decode::Error>>>;
+}
+
+/// Provide even more access to the underlying packet reader.
+#[async_trait(?Send)]
+pub trait ExtendedBufRead: ReadlineBufRead {
+ /// Set the handler to which progress will be delivered.
+ ///
+ /// Note that this is only possible if packet lines are sent in side band mode.
+ fn set_progress_handler(&mut self, handle_progress: Option<HandleProgress>);
+ /// Peek the next data packet line. Maybe None if the next line is a packet we stop at, queryable using
+ /// [`stopped_at()`][ExtendedBufRead::stopped_at()].
+ async fn peek_data_line(&mut self) -> Option<io::Result<Result<&[u8], Error>>>;
+ /// Resets the reader to allow reading past a previous stop, and sets delimiters according to the
+ /// given protocol.
+ fn reset(&mut self, version: Protocol);
+ /// Return the kind of message at which the reader stopped.
+ fn stopped_at(&self) -> Option<MessageKind>;
+}
+
+#[async_trait(?Send)]
+impl<'a, T: ReadlineBufRead + ?Sized + 'a + Unpin> ReadlineBufRead for Box<T> {
+ async fn readline(&mut self) -> Option<io::Result<Result<PacketLineRef<'_>, gix_packetline::decode::Error>>> {
+ self.deref_mut().readline().await
+ }
+}
+
+#[async_trait(?Send)]
+impl<'a, T: ExtendedBufRead + ?Sized + 'a + Unpin> ExtendedBufRead for Box<T> {
+ fn set_progress_handler(&mut self, handle_progress: Option<HandleProgress>) {
+ self.deref_mut().set_progress_handler(handle_progress)
+ }
+
+ async fn peek_data_line(&mut self) -> Option<io::Result<Result<&[u8], Error>>> {
+ self.deref_mut().peek_data_line().await
+ }
+
+ fn reset(&mut self, version: Protocol) {
+ self.deref_mut().reset(version)
+ }
+
+ fn stopped_at(&self) -> Option<MessageKind> {
+ self.deref().stopped_at()
+ }
+}
+
+#[async_trait(?Send)]
+impl<T: AsyncRead + Unpin> ReadlineBufRead for gix_packetline::read::WithSidebands<'_, T, for<'b> fn(bool, &'b [u8])> {
+ async fn readline(&mut self) -> Option<io::Result<Result<PacketLineRef<'_>, gix_packetline::decode::Error>>> {
+ self.read_data_line().await
+ }
+}
+
+#[async_trait(?Send)]
+impl<'a, T: AsyncRead + Unpin> ReadlineBufRead for gix_packetline::read::WithSidebands<'a, T, HandleProgress> {
+ async fn readline(&mut self) -> Option<io::Result<Result<PacketLineRef<'_>, gix_packetline::decode::Error>>> {
+ self.read_data_line().await
+ }
+}
+
+#[async_trait(?Send)]
+impl<'a, T: AsyncRead + Unpin> ExtendedBufRead for gix_packetline::read::WithSidebands<'a, T, HandleProgress> {
+ fn set_progress_handler(&mut self, handle_progress: Option<HandleProgress>) {
+ self.set_progress_handler(handle_progress)
+ }
+ async fn peek_data_line(&mut self) -> Option<io::Result<Result<&[u8], Error>>> {
+ match self.peek_data_line().await {
+ Some(Ok(Ok(line))) => Some(Ok(Ok(line))),
+ Some(Ok(Err(err))) => Some(Ok(Err(err.into()))),
+ Some(Err(err)) => Some(Err(err)),
+ None => None,
+ }
+ }
+ fn reset(&mut self, version: Protocol) {
+ match version {
+ Protocol::V1 => self.reset_with(&[gix_packetline::PacketLineRef::Flush]),
+ Protocol::V2 => self.reset_with(&[
+ gix_packetline::PacketLineRef::Delimiter,
+ gix_packetline::PacketLineRef::Flush,
+ ]),
+ }
+ }
+ fn stopped_at(&self) -> Option<MessageKind> {
+ self.stopped_at().map(|l| match l {
+ gix_packetline::PacketLineRef::Flush => MessageKind::Flush,
+ gix_packetline::PacketLineRef::Delimiter => MessageKind::Delimiter,
+ gix_packetline::PacketLineRef::ResponseEnd => MessageKind::ResponseEnd,
+ gix_packetline::PacketLineRef::Data(_) => unreachable!("data cannot be a delimiter"),
+ })
+ }
+}
diff --git a/vendor/gix-transport/src/client/async_io/connect.rs b/vendor/gix-transport/src/client/async_io/connect.rs
new file mode 100644
index 000000000..fe2a5808e
--- /dev/null
+++ b/vendor/gix-transport/src/client/async_io/connect.rs
@@ -0,0 +1,47 @@
+pub use crate::client::non_io_types::connect::{Error, Options};
+
+#[cfg(any(feature = "async-std"))]
+pub(crate) mod function {
+ use std::convert::TryInto;
+
+ use crate::client::{git, non_io_types::connect::Error};
+
+ /// A general purpose connector connecting to a repository identified by the given `url`.
+ ///
+ /// This includes connections to
+ /// [git daemons][crate::client::git::connect()] only at the moment.
+ ///
+ /// Use `options` to further control specifics of the transport resulting from the connection.
+ pub async fn connect<Url, E>(
+ url: Url,
+ options: super::Options,
+ ) -> Result<Box<dyn crate::client::Transport + Send>, Error>
+ where
+ Url: TryInto<gix_url::Url, Error = E>,
+ gix_url::parse::Error: From<E>,
+ {
+ let mut url = url.try_into().map_err(gix_url::parse::Error::from)?;
+ Ok(match url.scheme {
+ gix_url::Scheme::Git => {
+ if url.user().is_some() {
+ return Err(Error::UnsupportedUrlTokens {
+ url: url.to_bstring(),
+ scheme: url.scheme,
+ });
+ }
+ let path = std::mem::take(&mut url.path);
+ Box::new(
+ git::Connection::new_tcp(
+ url.host().expect("host is present in url"),
+ url.port,
+ path,
+ options.version,
+ )
+ .await
+ .map_err(|e| Box::new(e) as Box<dyn std::error::Error + Send + Sync>)?,
+ )
+ }
+ scheme => return Err(Error::UnsupportedScheme(scheme)),
+ })
+ }
+}
diff --git a/vendor/gix-transport/src/client/async_io/mod.rs b/vendor/gix-transport/src/client/async_io/mod.rs
new file mode 100644
index 000000000..6cb1a500e
--- /dev/null
+++ b/vendor/gix-transport/src/client/async_io/mod.rs
@@ -0,0 +1,13 @@
+mod bufread_ext;
+pub use bufread_ext::{ExtendedBufRead, HandleProgress, ReadlineBufRead};
+
+mod request;
+pub use request::RequestWriter;
+
+mod traits;
+pub use traits::{SetServiceResponse, Transport, TransportV2Ext};
+
+///
+pub mod connect;
+#[cfg(any(feature = "async-std"))]
+pub use connect::function::connect;
diff --git a/vendor/gix-transport/src/client/async_io/request.rs b/vendor/gix-transport/src/client/async_io/request.rs
new file mode 100644
index 000000000..1121db356
--- /dev/null
+++ b/vendor/gix-transport/src/client/async_io/request.rs
@@ -0,0 +1,103 @@
+use std::{
+ io,
+ pin::Pin,
+ task::{Context, Poll},
+};
+
+use futures_io::AsyncWrite;
+use pin_project_lite::pin_project;
+
+use crate::client::{ExtendedBufRead, MessageKind, WriteMode};
+
+pin_project! {
+ /// A [`Write`][io::Write] implementation optimized for writing packet lines.
+ /// A type implementing `Write` for packet lines, which when done can be transformed into a `Read` for
+ /// obtaining the response.
+ pub struct RequestWriter<'a> {
+ on_into_read: MessageKind,
+ #[pin]
+ writer: gix_packetline::Writer<Box<dyn AsyncWrite + Unpin + 'a>>,
+ reader: Box<dyn ExtendedBufRead + Unpin + 'a>,
+ }
+}
+impl<'a> futures_io::AsyncWrite for RequestWriter<'a> {
+ fn poll_write(self: Pin<&mut Self>, cx: &mut Context<'_>, buf: &[u8]) -> Poll<io::Result<usize>> {
+ self.project().writer.poll_write(cx, buf)
+ }
+
+ fn poll_flush(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
+ self.project().writer.poll_flush(cx)
+ }
+
+ fn poll_close(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
+ self.project().writer.poll_close(cx)
+ }
+}
+
+/// methods with bonds to IO
+impl<'a> RequestWriter<'a> {
+ /// Create a new instance from a `writer` (commonly a socket), a `reader` into which to transform once the
+ /// writes are finished, along with configuration for the `write_mode` and information about which message to write
+ /// when this instance is converted [into a `reader`][RequestWriter::into_read()] to read the request's response.
+ pub fn new_from_bufread<W: AsyncWrite + Unpin + 'a>(
+ writer: W,
+ reader: Box<dyn ExtendedBufRead + Unpin + 'a>,
+ write_mode: WriteMode,
+ on_into_read: MessageKind,
+ ) -> Self {
+ let mut writer = gix_packetline::Writer::new(Box::new(writer) as Box<dyn AsyncWrite + Unpin>);
+ match write_mode {
+ WriteMode::Binary => writer.enable_binary_mode(),
+ WriteMode::OneLfTerminatedLinePerWriteCall => writer.enable_text_mode(),
+ }
+ RequestWriter {
+ on_into_read,
+ writer,
+ reader,
+ }
+ }
+
+ /// Write the given message as packet line.
+ pub async fn write_message(&mut self, message: MessageKind) -> io::Result<()> {
+ match message {
+ MessageKind::Flush => {
+ gix_packetline::PacketLineRef::Flush
+ .write_to(self.writer.inner_mut())
+ .await
+ }
+ MessageKind::Delimiter => {
+ gix_packetline::PacketLineRef::Delimiter
+ .write_to(self.writer.inner_mut())
+ .await
+ }
+ MessageKind::ResponseEnd => {
+ gix_packetline::PacketLineRef::ResponseEnd
+ .write_to(self.writer.inner_mut())
+ .await
+ }
+ MessageKind::Text(t) => gix_packetline::TextRef::from(t).write_to(self.writer.inner_mut()).await,
+ }
+ .map(|_| ())
+ }
+ /// Discard the ability to write and turn this instance into the reader for obtaining the other side's response.
+ ///
+ /// Doing so will also write the message type this instance was initialized with.
+ pub async fn into_read(mut self) -> std::io::Result<Box<dyn ExtendedBufRead + Unpin + 'a>> {
+ self.write_message(self.on_into_read).await?;
+ Ok(self.reader)
+ }
+
+ /// Dissolve this instance into its write and read handles without any message-writing side-effect as in [RequestWriter::into_read()].
+ ///
+ /// Furthermore, the writer will not encode everything it writes as packetlines, but write everything verbatim into the
+ /// underlying channel.
+ ///
+ /// # Note
+ ///
+ /// It's of utmost importance to drop the request writer before reading the response as these might be inter-dependent, depending on
+ /// the underlying transport mechanism. Failure to do so may result in a deadlock depending on how the write and read mechanism
+ /// is implemented.
+ pub fn into_parts(self) -> (Box<dyn AsyncWrite + Unpin + 'a>, Box<dyn ExtendedBufRead + Unpin + 'a>) {
+ (self.writer.into_inner(), self.reader)
+ }
+}
diff --git a/vendor/gix-transport/src/client/async_io/traits.rs b/vendor/gix-transport/src/client/async_io/traits.rs
new file mode 100644
index 000000000..ea73f5e09
--- /dev/null
+++ b/vendor/gix-transport/src/client/async_io/traits.rs
@@ -0,0 +1,113 @@
+use std::ops::DerefMut;
+
+use async_trait::async_trait;
+use bstr::BString;
+use futures_lite::io::AsyncWriteExt;
+
+use crate::{
+ client::{Capabilities, Error, ExtendedBufRead, MessageKind, TransportWithoutIO, WriteMode},
+ Protocol, Service,
+};
+
+/// The response of the [`handshake()`][Transport::handshake()] method.
+pub struct SetServiceResponse<'a> {
+ /// The protocol the service can provide. May be different from the requested one
+ pub actual_protocol: Protocol,
+ /// The capabilities parsed from the server response.
+ pub capabilities: Capabilities,
+ /// In protocol version one, this is set to a list of refs and their peeled counterparts.
+ pub refs: Option<Box<dyn crate::client::ReadlineBufRead + Unpin + 'a>>,
+}
+
+/// All methods provided here must be called in the correct order according to the [communication protocol][Protocol]
+/// used to connect to them.
+/// It does, however, know just enough to be able to provide a higher-level interface than would otherwise be possible.
+/// Thus the consumer of this trait will not have to deal with packet lines at all.
+/// **Note that** whenever a `Read` trait or `Write` trait is produced, it must be exhausted.
+#[async_trait(?Send)]
+pub trait Transport: TransportWithoutIO {
+ /// Initiate connection to the given service and send the given `extra_parameters` along with it.
+ ///
+ /// `extra_parameters` are interpreted as `key=value` pairs if the second parameter is `Some` or as `key`
+ /// if it is None.
+ ///
+ /// Returns the service capabilities according according to the actual [Protocol] it supports,
+ /// and possibly a list of refs to be obtained.
+ /// This means that asking for an unsupported protocol might result in a protocol downgrade to the given one
+ /// if [TransportWithoutIO::supported_protocol_versions()] includes it or is empty.
+ /// Exhaust the returned [BufReader][SetServiceResponse::refs] for a list of references in case of protocol V1
+ /// before making another request.
+ async fn handshake<'a>(
+ &mut self,
+ service: Service,
+ extra_parameters: &'a [(&'a str, Option<&'a str>)],
+ ) -> Result<SetServiceResponse<'_>, Error>;
+}
+
+// Would be nice if the box implementation could auto-forward to all implemented traits.
+#[async_trait(?Send)]
+impl<T: Transport + ?Sized> Transport for Box<T> {
+ async fn handshake<'a>(
+ &mut self,
+ service: Service,
+ extra_parameters: &'a [(&'a str, Option<&'a str>)],
+ ) -> Result<SetServiceResponse<'_>, Error> {
+ self.deref_mut().handshake(service, extra_parameters).await
+ }
+}
+
+// Would be nice if the box implementation could auto-forward to all implemented traits.
+#[async_trait(?Send)]
+impl<T: Transport + ?Sized> Transport for &mut T {
+ async fn handshake<'a>(
+ &mut self,
+ service: Service,
+ extra_parameters: &'a [(&'a str, Option<&'a str>)],
+ ) -> Result<SetServiceResponse<'_>, Error> {
+ self.deref_mut().handshake(service, extra_parameters).await
+ }
+}
+
+/// An extension trait to add more methods to everything implementing [`Transport`].
+#[async_trait(?Send)]
+pub trait TransportV2Ext {
+ /// Invoke a protocol V2 style `command` with given `capabilities` and optional command specific `arguments`.
+ /// The `capabilities` were communicated during the handshake.
+ /// _Note:_ panics if [handshake][Transport::handshake()] wasn't performed beforehand.
+ async fn invoke<'a>(
+ &mut self,
+ command: &str,
+ capabilities: impl Iterator<Item = (&'a str, Option<impl AsRef<str>>)> + 'a,
+ arguments: Option<impl Iterator<Item = bstr::BString> + 'a>,
+ ) -> Result<Box<dyn ExtendedBufRead + Unpin + '_>, Error>;
+}
+
+#[async_trait(?Send)]
+impl<T: Transport> TransportV2Ext for T {
+ async fn invoke<'a>(
+ &mut self,
+ command: &str,
+ capabilities: impl Iterator<Item = (&'a str, Option<impl AsRef<str>>)> + 'a,
+ arguments: Option<impl Iterator<Item = BString> + 'a>,
+ ) -> Result<Box<dyn ExtendedBufRead + Unpin + '_>, Error> {
+ let mut writer = self.request(WriteMode::OneLfTerminatedLinePerWriteCall, MessageKind::Flush)?;
+ writer.write_all(format!("command={}", command).as_bytes()).await?;
+ for (name, value) in capabilities {
+ match value {
+ Some(value) => {
+ writer
+ .write_all(format!("{}={}", name, value.as_ref()).as_bytes())
+ .await
+ }
+ None => writer.write_all(name.as_bytes()).await,
+ }?;
+ }
+ if let Some(arguments) = arguments {
+ writer.write_message(MessageKind::Delimiter).await?;
+ for argument in arguments {
+ writer.write_all(argument.as_ref()).await?;
+ }
+ }
+ Ok(writer.into_read().await?)
+ }
+}
diff --git a/vendor/gix-transport/src/client/blocking_io/bufread_ext.rs b/vendor/gix-transport/src/client/blocking_io/bufread_ext.rs
new file mode 100644
index 000000000..5842ddd3d
--- /dev/null
+++ b/vendor/gix-transport/src/client/blocking_io/bufread_ext.rs
@@ -0,0 +1,114 @@
+use std::{
+ io,
+ ops::{Deref, DerefMut},
+};
+
+use gix_packetline::PacketLineRef;
+
+use crate::{
+ client::{Error, MessageKind},
+ Protocol,
+};
+/// A function `f(is_error, text)` receiving progress or error information.
+pub type HandleProgress = Box<dyn FnMut(bool, &[u8])>;
+
+/// This trait exists to get a version of a `gix_packetline::Provider` without type parameters,
+/// but leave support for reading lines directly without forcing them through `String`.
+///
+/// For the sake of usability, it also implements [`std::io::BufRead`] making it trivial to
+/// read pack files while keeping open the option to read individual lines with low overhead.
+pub trait ReadlineBufRead: io::BufRead {
+ /// Read a packet line into the internal buffer and return it.
+ ///
+ /// Returns `None` if the end of iteration is reached because of one of the following:
+ ///
+ /// * natural EOF
+ /// * ERR packet line encountered
+ /// * A `delimiter` packet line encountered
+ fn readline(
+ &mut self,
+ ) -> Option<io::Result<Result<gix_packetline::PacketLineRef<'_>, gix_packetline::decode::Error>>>;
+}
+
+/// Provide even more access to the underlying packet reader.
+pub trait ExtendedBufRead: ReadlineBufRead {
+ /// Set the handler to which progress will be delivered.
+ ///
+ /// Note that this is only possible if packet lines are sent in side band mode.
+ fn set_progress_handler(&mut self, handle_progress: Option<HandleProgress>);
+ /// Peek the next data packet line. Maybe None if the next line is a packet we stop at, queryable using
+ /// [`stopped_at()`][ExtendedBufRead::stopped_at()].
+ fn peek_data_line(&mut self) -> Option<io::Result<Result<&[u8], Error>>>;
+ /// Resets the reader to allow reading past a previous stop, and sets delimiters according to the
+ /// given protocol.
+ fn reset(&mut self, version: Protocol);
+ /// Return the kind of message at which the reader stopped.
+ fn stopped_at(&self) -> Option<MessageKind>;
+}
+
+impl<'a, T: ReadlineBufRead + ?Sized + 'a> ReadlineBufRead for Box<T> {
+ fn readline(&mut self) -> Option<io::Result<Result<PacketLineRef<'_>, gix_packetline::decode::Error>>> {
+ ReadlineBufRead::readline(self.deref_mut())
+ }
+}
+
+impl<'a, T: ExtendedBufRead + ?Sized + 'a> ExtendedBufRead for Box<T> {
+ fn set_progress_handler(&mut self, handle_progress: Option<HandleProgress>) {
+ self.deref_mut().set_progress_handler(handle_progress)
+ }
+
+ fn peek_data_line(&mut self) -> Option<io::Result<Result<&[u8], Error>>> {
+ self.deref_mut().peek_data_line()
+ }
+
+ fn reset(&mut self, version: Protocol) {
+ self.deref_mut().reset(version)
+ }
+
+ fn stopped_at(&self) -> Option<MessageKind> {
+ self.deref().stopped_at()
+ }
+}
+
+impl<T: io::Read> ReadlineBufRead for gix_packetline::read::WithSidebands<'_, T, fn(bool, &[u8])> {
+ fn readline(&mut self) -> Option<io::Result<Result<PacketLineRef<'_>, gix_packetline::decode::Error>>> {
+ self.read_data_line()
+ }
+}
+
+impl<'a, T: io::Read> ReadlineBufRead for gix_packetline::read::WithSidebands<'a, T, HandleProgress> {
+ fn readline(&mut self) -> Option<io::Result<Result<PacketLineRef<'_>, gix_packetline::decode::Error>>> {
+ self.read_data_line()
+ }
+}
+
+impl<'a, T: io::Read> ExtendedBufRead for gix_packetline::read::WithSidebands<'a, T, HandleProgress> {
+ fn set_progress_handler(&mut self, handle_progress: Option<HandleProgress>) {
+ self.set_progress_handler(handle_progress)
+ }
+ fn peek_data_line(&mut self) -> Option<io::Result<Result<&[u8], Error>>> {
+ match self.peek_data_line() {
+ Some(Ok(Ok(line))) => Some(Ok(Ok(line))),
+ Some(Ok(Err(err))) => Some(Ok(Err(err.into()))),
+ Some(Err(err)) => Some(Err(err)),
+ None => None,
+ }
+ }
+ fn reset(&mut self, version: Protocol) {
+ match version {
+ Protocol::V1 => self.reset_with(&[gix_packetline::PacketLineRef::Flush]),
+ Protocol::V2 => self.reset_with(&[
+ gix_packetline::PacketLineRef::Delimiter,
+ gix_packetline::PacketLineRef::Flush,
+ ]),
+ }
+ }
+ fn stopped_at(&self) -> Option<MessageKind> {
+ self.stopped_at().map(|l| match l {
+ gix_packetline::PacketLineRef::Flush => MessageKind::Flush,
+ gix_packetline::PacketLineRef::Delimiter => MessageKind::Delimiter,
+ gix_packetline::PacketLineRef::ResponseEnd => MessageKind::ResponseEnd,
+ gix_packetline::PacketLineRef::Data(_) => unreachable!("data cannot be a delimiter"),
+ })
+ }
+}
diff --git a/vendor/gix-transport/src/client/blocking_io/connect.rs b/vendor/gix-transport/src/client/blocking_io/connect.rs
new file mode 100644
index 000000000..de3334f18
--- /dev/null
+++ b/vendor/gix-transport/src/client/blocking_io/connect.rs
@@ -0,0 +1,68 @@
+pub use crate::client::non_io_types::connect::{Error, Options};
+
+pub(crate) mod function {
+ use std::convert::TryInto;
+
+ use crate::client::{non_io_types::connect::Error, Transport};
+
+ /// A general purpose connector connecting to a repository identified by the given `url`.
+ ///
+ /// This includes connections to
+ /// [local repositories][crate::client::file::connect()],
+ /// [repositories over ssh][crate::client::ssh::connect()],
+ /// [git daemons][crate::client::git::connect()],
+ /// and if compiled in connections to [git repositories over https][crate::client::http::connect()].
+ ///
+ /// Use `options` to further control specifics of the transport resulting from the connection.
+ pub fn connect<Url, E>(url: Url, options: super::Options) -> Result<Box<dyn Transport + Send>, Error>
+ where
+ Url: TryInto<gix_url::Url, Error = E>,
+ gix_url::parse::Error: From<E>,
+ {
+ let mut url = url.try_into().map_err(gix_url::parse::Error::from)?;
+ Ok(match url.scheme {
+ gix_url::Scheme::Ext(_) => return Err(Error::UnsupportedScheme(url.scheme)),
+ gix_url::Scheme::File => {
+ if url.user().is_some() || url.host().is_some() || url.port.is_some() {
+ return Err(Error::UnsupportedUrlTokens {
+ url: url.to_bstring(),
+ scheme: url.scheme,
+ });
+ }
+ Box::new(
+ crate::client::blocking_io::file::connect(url.path, options.version)
+ .map_err(|e| Box::new(e) as Box<dyn std::error::Error + Send + Sync>)?,
+ )
+ }
+ gix_url::Scheme::Ssh => Box::new({
+ crate::client::blocking_io::ssh::connect(url, options.version, options.ssh)
+ .map_err(|e| Box::new(e) as Box<dyn std::error::Error + Send + Sync>)?
+ }),
+ gix_url::Scheme::Git => {
+ if url.user().is_some() {
+ return Err(Error::UnsupportedUrlTokens {
+ url: url.to_bstring(),
+ scheme: url.scheme,
+ });
+ }
+ Box::new({
+ let path = std::mem::take(&mut url.path);
+ crate::client::git::connect(
+ url.host().expect("host is present in url"),
+ path,
+ options.version,
+ url.port,
+ )
+ .map_err(|e| Box::new(e) as Box<dyn std::error::Error + Send + Sync>)?
+ })
+ }
+ #[cfg(not(any(feature = "http-client-curl", feature = "http-client-reqwest")))]
+ gix_url::Scheme::Https | gix_url::Scheme::Http => return Err(Error::CompiledWithoutHttp(url.scheme)),
+ #[cfg(any(feature = "http-client-curl", feature = "http-client-reqwest"))]
+ gix_url::Scheme::Https | gix_url::Scheme::Http => Box::new(crate::client::http::connect(
+ &url.to_bstring().to_string(),
+ options.version,
+ )),
+ })
+ }
+}
diff --git a/vendor/gix-transport/src/client/blocking_io/file.rs b/vendor/gix-transport/src/client/blocking_io/file.rs
new file mode 100644
index 000000000..d80fe55df
--- /dev/null
+++ b/vendor/gix-transport/src/client/blocking_io/file.rs
@@ -0,0 +1,288 @@
+use std::{
+ any::Any,
+ borrow::Cow,
+ error::Error,
+ ffi::{OsStr, OsString},
+ io::Write,
+ process::{self, Stdio},
+};
+
+use bstr::{io::BufReadExt, BStr, BString, ByteSlice};
+
+use crate::{
+ client::{self, git, ssh, MessageKind, RequestWriter, SetServiceResponse, WriteMode},
+ Protocol, Service,
+};
+
+// from https://github.com/git/git/blob/20de7e7e4f4e9ae52e6cc7cfaa6469f186ddb0fa/environment.c#L115:L115
+const ENV_VARS_TO_REMOVE: &[&str] = &[
+ "GIT_ALTERNATE_OBJECT_DIRECTORIES",
+ "GIT_CONFIG",
+ "GIT_CONFIG_PARAMETERS",
+ "GIT_OBJECT_DIRECTORY",
+ "GIT_DIR",
+ "GIT_WORK_TREE",
+ "GIT_IMPLICIT_WORK_TREE",
+ "GIT_GRAFT_FILE",
+ "GIT_INDEX_FILE",
+ "GIT_NO_REPLACE_OBJECTS",
+ "GIT_REPLACE_REF_BASE",
+ "GIT_PREFIX",
+ "GIT_INTERNAL_SUPER_PREFIX",
+ "GIT_SHALLOW_FILE",
+ "GIT_COMMON_DIR",
+ "GIT_CONFIG_COUNT",
+];
+
+/// A utility to spawn a helper process to actually transmit data, possibly over `ssh`.
+///
+/// It can only be instantiated using the local [`connect()`] or [ssh connect][crate::client::ssh::connect()].
+pub struct SpawnProcessOnDemand {
+ desired_version: Protocol,
+ url: gix_url::Url,
+ path: BString,
+ ssh_cmd: Option<(OsString, ssh::ProgramKind)>,
+ /// The environment variables to set in the invoked command.
+ envs: Vec<(&'static str, String)>,
+ ssh_disallow_shell: bool,
+ connection: Option<git::Connection<Box<dyn std::io::Read + Send>, process::ChildStdin>>,
+ child: Option<process::Child>,
+}
+
+impl SpawnProcessOnDemand {
+ pub(crate) fn new_ssh(
+ url: gix_url::Url,
+ program: impl Into<OsString>,
+ path: BString,
+ ssh_kind: ssh::ProgramKind,
+ ssh_disallow_shell: bool,
+ version: Protocol,
+ ) -> SpawnProcessOnDemand {
+ SpawnProcessOnDemand {
+ url,
+ path,
+ ssh_cmd: Some((program.into(), ssh_kind)),
+ envs: Default::default(),
+ ssh_disallow_shell,
+ child: None,
+ connection: None,
+ desired_version: version,
+ }
+ }
+ fn new_local(path: BString, version: Protocol) -> SpawnProcessOnDemand {
+ SpawnProcessOnDemand {
+ url: gix_url::Url::from_parts_as_alternative_form(gix_url::Scheme::File, None, None, None, path.clone())
+ .expect("valid url"),
+ path,
+ ssh_cmd: None,
+ envs: (version != Protocol::V1)
+ .then(|| vec![("GIT_PROTOCOL", format!("version={}", version as usize))])
+ .unwrap_or_default(),
+ ssh_disallow_shell: false,
+ child: None,
+ connection: None,
+ desired_version: version,
+ }
+ }
+}
+
+impl client::TransportWithoutIO for SpawnProcessOnDemand {
+ fn set_identity(&mut self, identity: gix_sec::identity::Account) -> Result<(), client::Error> {
+ if self.url.scheme == gix_url::Scheme::Ssh {
+ self.url
+ .set_user((!identity.username.is_empty()).then_some(identity.username));
+ Ok(())
+ } else {
+ Err(client::Error::AuthenticationUnsupported)
+ }
+ }
+
+ fn request(
+ &mut self,
+ write_mode: WriteMode,
+ on_into_read: MessageKind,
+ ) -> Result<RequestWriter<'_>, client::Error> {
+ self.connection
+ .as_mut()
+ .expect("handshake() to have been called first")
+ .request(write_mode, on_into_read)
+ }
+
+ fn to_url(&self) -> Cow<'_, BStr> {
+ Cow::Owned(self.url.to_bstring())
+ }
+
+ fn connection_persists_across_multiple_requests(&self) -> bool {
+ true
+ }
+
+ fn configure(&mut self, _config: &dyn Any) -> Result<(), Box<dyn Error + Send + Sync + 'static>> {
+ Ok(())
+ }
+}
+
+struct ReadStdoutFailOnError {
+ recv: std::sync::mpsc::Receiver<std::io::Error>,
+ read: std::process::ChildStdout,
+}
+
+fn supervise_stderr(
+ ssh_kind: ssh::ProgramKind,
+ stderr: std::process::ChildStderr,
+ stdout: std::process::ChildStdout,
+) -> ReadStdoutFailOnError {
+ impl ReadStdoutFailOnError {
+ fn swap_err_if_present_in_stderr(&self, wanted: usize, res: std::io::Result<usize>) -> std::io::Result<usize> {
+ match self.recv.try_recv().ok() {
+ Some(err) => Err(err),
+ None => match res {
+ Ok(n) if n == wanted => Ok(n),
+ Ok(n) => {
+ // TODO: fix this
+ // When parsing refs this seems to happen legitimately
+ // (even though we read packet lines only and should always know exactly how much to read)
+ // Maybe this still happens in `read_exact()` as sometimes we just don't get enough bytes
+ // despite knowing how many.
+ // To prevent deadlock, we have to set a timeout which slows down legitimate parts of the protocol.
+ // This code was specifically written to make the `cargo` test-suite pass, and we can reduce
+ // the timeouts even more once there is a native ssh transport that is used by `cargo`, it will
+ // be able to handle these properly.
+ // Alternatively, one could implement something like `read2` to avoid blocking on stderr entirely.
+ self.recv
+ .recv_timeout(std::time::Duration::from_millis(5))
+ .ok()
+ .map(Err)
+ .unwrap_or(Ok(n))
+ }
+ Err(err) => Err(self.recv.recv().ok().unwrap_or(err)),
+ },
+ }
+ }
+ }
+ impl std::io::Read for ReadStdoutFailOnError {
+ fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
+ let res = self.read.read(buf);
+ self.swap_err_if_present_in_stderr(buf.len(), res)
+ }
+ }
+
+ let (send, recv) = std::sync::mpsc::sync_channel(1);
+ std::thread::Builder::new()
+ .name("supervise ssh stderr".into())
+ .stack_size(128 * 1024)
+ .spawn(move || -> std::io::Result<()> {
+ let mut process_stderr = std::io::stderr();
+ for line in std::io::BufReader::new(stderr).byte_lines() {
+ let line = line?;
+ match ssh_kind.line_to_err(line.into()) {
+ Ok(err) => {
+ send.send(err).ok();
+ }
+ Err(line) => {
+ process_stderr.write_all(&line).ok();
+ writeln!(&process_stderr).ok();
+ }
+ }
+ }
+ Ok(())
+ })
+ .expect("named threads with small stack work on all platforms");
+ ReadStdoutFailOnError { read: stdout, recv }
+}
+
+impl client::Transport for SpawnProcessOnDemand {
+ fn handshake<'a>(
+ &mut self,
+ service: Service,
+ extra_parameters: &'a [(&'a str, Option<&'a str>)],
+ ) -> Result<SetServiceResponse<'_>, client::Error> {
+ let (mut cmd, ssh_kind, cmd_name) = match &self.ssh_cmd {
+ Some((command, kind)) => (
+ kind.prepare_invocation(command, &self.url, self.desired_version, self.ssh_disallow_shell)
+ .map_err(client::Error::SshInvocation)?
+ .stderr(Stdio::piped()),
+ Some(*kind),
+ Cow::Owned(command.to_owned()),
+ ),
+ None => (
+ gix_command::prepare(service.as_str()).stderr(Stdio::null()),
+ None,
+ Cow::Borrowed(OsStr::new(service.as_str())),
+ ),
+ };
+ cmd.stdin = Stdio::piped();
+ cmd.stdout = Stdio::piped();
+ let repo_path = if self.ssh_cmd.is_some() {
+ cmd.args.push(service.as_str().into());
+ gix_quote::single(self.path.as_ref()).to_os_str_lossy().into_owned()
+ } else {
+ self.path.to_os_str_lossy().into_owned()
+ };
+ cmd.args.push(repo_path);
+
+ let mut cmd = std::process::Command::from(cmd);
+ for env_to_remove in ENV_VARS_TO_REMOVE {
+ cmd.env_remove(env_to_remove);
+ }
+ cmd.envs(std::mem::take(&mut self.envs));
+
+ let mut child = cmd.spawn().map_err(|err| client::Error::InvokeProgram {
+ source: err,
+ command: cmd_name.into_owned(),
+ })?;
+ let stdout: Box<dyn std::io::Read + Send> = match ssh_kind {
+ Some(ssh_kind) => Box::new(supervise_stderr(
+ ssh_kind,
+ child.stderr.take().expect("configured beforehand"),
+ child.stdout.take().expect("configured"),
+ )),
+ None => Box::new(child.stdout.take().expect("stdout configured")),
+ };
+ self.connection = Some(git::Connection::new_for_spawned_process(
+ stdout,
+ child.stdin.take().expect("stdin configured"),
+ self.desired_version,
+ self.path.clone(),
+ ));
+ self.child = Some(child);
+ self.connection
+ .as_mut()
+ .expect("connection to be there right after setting it")
+ .handshake(service, extra_parameters)
+ }
+}
+
+/// Connect to a locally readable repository at `path` using the given `desired_version`.
+///
+/// This will spawn a `git` process locally.
+pub fn connect(
+ path: impl Into<BString>,
+ desired_version: Protocol,
+) -> Result<SpawnProcessOnDemand, std::convert::Infallible> {
+ Ok(SpawnProcessOnDemand::new_local(path.into(), desired_version))
+}
+
+#[cfg(test)]
+mod tests {
+ mod ssh {
+ mod connect {
+ use crate::{client::blocking_io::ssh::connect, Protocol};
+
+ #[test]
+ fn path() {
+ for (url, expected) in [
+ ("ssh://host.xy/~/repo", "~/repo"),
+ ("ssh://host.xy/~username/repo", "~username/repo"),
+ ("user@host.xy:/username/repo", "/username/repo"),
+ ("user@host.xy:username/repo", "username/repo"),
+ ("user@host.xy:../username/repo", "../username/repo"),
+ ("user@host.xy:~/repo", "~/repo"),
+ ] {
+ let url = gix_url::parse((*url).into()).expect("valid url");
+ let cmd = connect(url, Protocol::V1, Default::default()).expect("parse success");
+ assert_eq!(cmd.path, expected, "the path will be substituted by the remote shell");
+ }
+ }
+ }
+ }
+}
diff --git a/vendor/gix-transport/src/client/blocking_io/http/curl/mod.rs b/vendor/gix-transport/src/client/blocking_io/http/curl/mod.rs
new file mode 100644
index 000000000..ad980dd20
--- /dev/null
+++ b/vendor/gix-transport/src/client/blocking_io/http/curl/mod.rs
@@ -0,0 +1,169 @@
+use std::{
+ sync::mpsc::{Receiver, SyncSender},
+ thread,
+};
+
+use gix_features::io;
+
+use crate::client::{blocking_io::http, http::traits::PostBodyDataKind};
+
+mod remote;
+
+/// Options to configure the `curl` HTTP handler.
+#[derive(Default)]
+pub struct Options {
+ /// If `true` and runtime configuration is possible for `curl` backends, certificates revocation will be checked.
+ ///
+ /// This only works on windows apparently. Ignored if `None`.
+ pub schannel_check_revoke: Option<bool>,
+}
+
+/// The error returned by the 'remote' helper, a purely internal construct to perform http requests.
+///
+/// It can be used for downcasting errors, which are boxed to hide the actual implementation.
+#[derive(Debug, thiserror::Error)]
+#[allow(missing_docs)]
+pub enum Error {
+ #[error(transparent)]
+ Curl(#[from] curl::Error),
+ #[error(transparent)]
+ Redirect(#[from] http::redirect::Error),
+ #[error("Could not finish reading all data to post to the remote")]
+ ReadPostBody(#[from] std::io::Error),
+ #[error(transparent)]
+ Authenticate(#[from] gix_credentials::protocol::Error),
+}
+
+impl crate::IsSpuriousError for Error {
+ fn is_spurious(&self) -> bool {
+ match self {
+ Error::Curl(err) => curl_is_spurious(err),
+ _ => false,
+ }
+ }
+}
+
+pub(crate) fn curl_is_spurious(err: &curl::Error) -> bool {
+ err.is_couldnt_connect()
+ || err.is_couldnt_resolve_proxy()
+ || err.is_couldnt_resolve_host()
+ || err.is_operation_timedout()
+ || err.is_recv_error()
+ || err.is_send_error()
+ || err.is_http2_error()
+ || err.is_http2_stream_error()
+ || err.is_ssl_connect_error()
+ || err.is_partial_file()
+}
+
+/// A utility to abstract interactions with curl handles.
+pub struct Curl {
+ req: SyncSender<remote::Request>,
+ res: Receiver<remote::Response>,
+ handle: Option<thread::JoinHandle<Result<(), Error>>>,
+ config: http::Options,
+}
+
+impl Curl {
+ fn restore_thread_after_failure(&mut self) -> http::Error {
+ let err_that_brought_thread_down = self
+ .handle
+ .take()
+ .expect("thread handle present")
+ .join()
+ .expect("handler thread should never panic")
+ .expect_err("something should have gone wrong with curl (we join on error only)");
+ let (handle, req, res) = remote::new();
+ self.handle = Some(handle);
+ self.req = req;
+ self.res = res;
+ err_that_brought_thread_down.into()
+ }
+
+ fn make_request(
+ &mut self,
+ url: &str,
+ base_url: &str,
+ headers: impl IntoIterator<Item = impl AsRef<str>>,
+ upload_body_kind: Option<PostBodyDataKind>,
+ ) -> Result<http::PostResponse<io::pipe::Reader, io::pipe::Reader, io::pipe::Writer>, http::Error> {
+ let mut list = curl::easy::List::new();
+ for header in headers {
+ list.append(header.as_ref())?;
+ }
+ if self
+ .req
+ .send(remote::Request {
+ url: url.to_owned(),
+ base_url: base_url.to_owned(),
+ headers: list,
+ upload_body_kind,
+ config: self.config.clone(),
+ })
+ .is_err()
+ {
+ return Err(self.restore_thread_after_failure());
+ }
+ let remote::Response {
+ headers,
+ body,
+ upload_body,
+ } = match self.res.recv() {
+ Ok(res) => res,
+ Err(_) => return Err(self.restore_thread_after_failure()),
+ };
+ Ok(http::PostResponse {
+ post_body: upload_body,
+ headers,
+ body,
+ })
+ }
+}
+
+impl Default for Curl {
+ fn default() -> Self {
+ let (handle, req, res) = remote::new();
+ Curl {
+ handle: Some(handle),
+ req,
+ res,
+ config: http::Options::default(),
+ }
+ }
+}
+
+#[allow(clippy::type_complexity)]
+impl http::Http for Curl {
+ type Headers = io::pipe::Reader;
+ type ResponseBody = io::pipe::Reader;
+ type PostBody = io::pipe::Writer;
+
+ fn get(
+ &mut self,
+ url: &str,
+ base_url: &str,
+ headers: impl IntoIterator<Item = impl AsRef<str>>,
+ ) -> Result<http::GetResponse<Self::Headers, Self::ResponseBody>, http::Error> {
+ self.make_request(url, base_url, headers, None).map(Into::into)
+ }
+
+ fn post(
+ &mut self,
+ url: &str,
+ base_url: &str,
+ headers: impl IntoIterator<Item = impl AsRef<str>>,
+ body: PostBodyDataKind,
+ ) -> Result<http::PostResponse<Self::Headers, Self::ResponseBody, Self::PostBody>, http::Error> {
+ self.make_request(url, base_url, headers, Some(body))
+ }
+
+ fn configure(
+ &mut self,
+ config: &dyn std::any::Any,
+ ) -> Result<(), Box<dyn std::error::Error + Send + Sync + 'static>> {
+ if let Some(config) = config.downcast_ref::<http::Options>() {
+ self.config = config.clone();
+ }
+ Ok(())
+ }
+}
diff --git a/vendor/gix-transport/src/client/blocking_io/http/curl/remote.rs b/vendor/gix-transport/src/client/blocking_io/http/curl/remote.rs
new file mode 100644
index 000000000..8f5867698
--- /dev/null
+++ b/vendor/gix-transport/src/client/blocking_io/http/curl/remote.rs
@@ -0,0 +1,392 @@
+use std::{
+ io,
+ io::{Read, Write},
+ sync::mpsc::{sync_channel, Receiver, SyncSender, TrySendError},
+ thread,
+ time::Duration,
+};
+
+use curl::easy::{Auth, Easy2};
+use gix_features::io::pipe;
+
+use crate::client::{
+ blocking_io::http::{self, curl::Error, redirect},
+ http::{
+ curl::curl_is_spurious,
+ options::{FollowRedirects, HttpVersion, ProxyAuthMethod, SslVersion},
+ traits::PostBodyDataKind,
+ },
+};
+
+enum StreamOrBuffer {
+ Stream(pipe::Reader),
+ Buffer(std::io::Cursor<Vec<u8>>),
+}
+
+#[derive(Default)]
+struct Handler {
+ send_header: Option<pipe::Writer>,
+ send_data: Option<pipe::Writer>,
+ receive_body: Option<StreamOrBuffer>,
+ checked_status: bool,
+ last_status: usize,
+ follow: FollowRedirects,
+}
+
+impl Handler {
+ fn reset(&mut self) {
+ self.checked_status = false;
+ self.last_status = 0;
+ self.follow = FollowRedirects::default();
+ }
+ fn parse_status_inner(data: &[u8]) -> Result<usize, Box<dyn std::error::Error + Send + Sync>> {
+ let code = data
+ .split(|b| *b == b' ')
+ .nth(1)
+ .ok_or("Expected HTTP/<VERSION> STATUS")?;
+ let code = std::str::from_utf8(code)?;
+ code.parse().map_err(Into::into)
+ }
+ fn parse_status(data: &[u8], follow: FollowRedirects) -> Option<(usize, Box<dyn std::error::Error + Send + Sync>)> {
+ let valid_end = match follow {
+ FollowRedirects::Initial | FollowRedirects::All => 308,
+ FollowRedirects::None => 299,
+ };
+ match Self::parse_status_inner(data) {
+ Ok(status) if !(200..=valid_end).contains(&status) => {
+ Some((status, format!("Received HTTP status {status}").into()))
+ }
+ Ok(_) => None,
+ Err(err) => Some((500, err)),
+ }
+ }
+}
+
+impl curl::easy::Handler for Handler {
+ fn write(&mut self, data: &[u8]) -> Result<usize, curl::easy::WriteError> {
+ drop(self.send_header.take()); // signal header readers to stop trying
+ match self.send_data.as_mut() {
+ Some(writer) => writer.write_all(data).map(|_| data.len()).or(Ok(0)),
+ None => Ok(0), // nothing more to receive, reader is done
+ }
+ }
+ fn read(&mut self, data: &mut [u8]) -> Result<usize, curl::easy::ReadError> {
+ match self.receive_body.as_mut() {
+ Some(StreamOrBuffer::Stream(reader)) => reader.read(data).map_err(|_err| curl::easy::ReadError::Abort),
+ Some(StreamOrBuffer::Buffer(cursor)) => cursor.read(data).map_err(|_err| curl::easy::ReadError::Abort),
+ None => Ok(0), // nothing more to read/writer depleted
+ }
+ }
+
+ fn header(&mut self, data: &[u8]) -> bool {
+ if let Some(writer) = self.send_header.as_mut() {
+ if self.checked_status {
+ writer.write_all(data).ok();
+ } else {
+ self.checked_status = true;
+ self.last_status = 200;
+ if let Some((status, err)) = Handler::parse_status(data, self.follow) {
+ self.last_status = status;
+ writer
+ .channel
+ .send(Err(io::Error::new(
+ if status == 401 {
+ io::ErrorKind::PermissionDenied
+ } else if (500..600).contains(&status) {
+ io::ErrorKind::ConnectionAborted
+ } else {
+ io::ErrorKind::Other
+ },
+ err,
+ )))
+ .ok();
+ }
+ }
+ };
+ true
+ }
+}
+
+pub struct Request {
+ pub url: String,
+ pub base_url: String,
+ pub headers: curl::easy::List,
+ pub upload_body_kind: Option<PostBodyDataKind>,
+ pub config: http::Options,
+}
+
+pub struct Response {
+ pub headers: pipe::Reader,
+ pub body: pipe::Reader,
+ pub upload_body: pipe::Writer,
+}
+
+pub fn new() -> (
+ thread::JoinHandle<Result<(), Error>>,
+ SyncSender<Request>,
+ Receiver<Response>,
+) {
+ let (req_send, req_recv) = sync_channel(0);
+ let (res_send, res_recv) = sync_channel(0);
+ let handle = std::thread::spawn(move || -> Result<(), Error> {
+ let mut handle = Easy2::new(Handler::default());
+ // We don't wait for the possibility for pipelining to become clear, and curl tries to reuse connections by default anyway.
+ handle.pipewait(false)?;
+ handle.tcp_keepalive(true)?;
+
+ let mut follow = None;
+ let mut redirected_base_url = None::<String>;
+
+ for Request {
+ url,
+ base_url,
+ mut headers,
+ upload_body_kind,
+ config:
+ http::Options {
+ extra_headers,
+ follow_redirects,
+ low_speed_limit_bytes_per_second,
+ low_speed_time_seconds,
+ connect_timeout,
+ proxy,
+ no_proxy,
+ proxy_auth_method,
+ user_agent,
+ proxy_authenticate,
+ verbose,
+ ssl_ca_info,
+ ssl_version,
+ http_version,
+ backend,
+ },
+ } in req_recv
+ {
+ let effective_url = redirect::swap_tails(redirected_base_url.as_deref(), &base_url, url.clone());
+ handle.url(&effective_url)?;
+
+ handle.post(upload_body_kind.is_some())?;
+ for header in extra_headers {
+ headers.append(&header)?;
+ }
+ // needed to avoid sending Expect: 100-continue, which adds another response and only CURL wants that
+ headers.append("Expect:")?;
+ handle.verbose(verbose)?;
+
+ if let Some(ca_info) = ssl_ca_info {
+ handle.cainfo(ca_info)?;
+ }
+
+ if let Some(ref mut curl_options) = backend.as_ref().and_then(|backend| backend.lock().ok()) {
+ if let Some(opts) = curl_options.downcast_mut::<super::Options>() {
+ if let Some(enabled) = opts.schannel_check_revoke {
+ handle.ssl_options(curl::easy::SslOpt::new().no_revoke(!enabled))?;
+ }
+ }
+ }
+
+ if let Some(ssl_version) = ssl_version {
+ let (min, max) = ssl_version.min_max();
+ if min == max {
+ handle.ssl_version(to_curl_ssl_version(min))?;
+ } else {
+ handle.ssl_min_max_version(to_curl_ssl_version(min), to_curl_ssl_version(max))?;
+ }
+ }
+
+ if let Some(http_version) = http_version {
+ let version = match http_version {
+ HttpVersion::V1_1 => curl::easy::HttpVersion::V11,
+ HttpVersion::V2 => curl::easy::HttpVersion::V2,
+ };
+ // Failing to set the version isn't critical, and may indeed fail depending on the version
+ // of libcurl we are built against.
+ // Furthermore, `git` itself doesn't actually check for errors when configuring curl at all,
+ // treating all or most flags as non-critical.
+ handle.http_version(version).ok();
+ }
+
+ let mut proxy_auth_action = None;
+ if let Some(proxy) = proxy {
+ handle.proxy(&proxy)?;
+ let proxy_type = if proxy.starts_with("socks5h") {
+ curl::easy::ProxyType::Socks5Hostname
+ } else if proxy.starts_with("socks5") {
+ curl::easy::ProxyType::Socks5
+ } else if proxy.starts_with("socks4a") {
+ curl::easy::ProxyType::Socks4a
+ } else if proxy.starts_with("socks") {
+ curl::easy::ProxyType::Socks4
+ } else {
+ curl::easy::ProxyType::Http
+ };
+ handle.proxy_type(proxy_type)?;
+
+ if let Some((obtain_creds_action, authenticate)) = proxy_authenticate {
+ let creds = authenticate.lock().expect("no panics in other threads")(obtain_creds_action)?
+ .expect("action to fetch credentials");
+ handle.proxy_username(&creds.identity.username)?;
+ handle.proxy_password(&creds.identity.password)?;
+ proxy_auth_action = Some((creds.next, authenticate));
+ }
+ }
+ if let Some(no_proxy) = no_proxy {
+ handle.noproxy(&no_proxy)?;
+ }
+ if let Some(user_agent) = user_agent {
+ handle.useragent(&user_agent)?;
+ }
+ handle.transfer_encoding(false)?;
+ if let Some(timeout) = connect_timeout {
+ handle.connect_timeout(timeout)?;
+ }
+ {
+ let mut auth = Auth::new();
+ match proxy_auth_method {
+ ProxyAuthMethod::AnyAuth => auth
+ .basic(true)
+ .digest(true)
+ .digest_ie(true)
+ .gssnegotiate(true)
+ .ntlm(true)
+ .aws_sigv4(true),
+ ProxyAuthMethod::Basic => auth.basic(true),
+ ProxyAuthMethod::Digest => auth.digest(true),
+ ProxyAuthMethod::Negotiate => auth.digest_ie(true),
+ ProxyAuthMethod::Ntlm => auth.ntlm(true),
+ };
+ handle.proxy_auth(&auth)?;
+ }
+ handle.tcp_keepalive(true)?;
+
+ if low_speed_time_seconds > 0 && low_speed_limit_bytes_per_second > 0 {
+ handle.low_speed_limit(low_speed_limit_bytes_per_second)?;
+ handle.low_speed_time(Duration::from_secs(low_speed_time_seconds))?;
+ }
+ let (receive_data, receive_headers, send_body, mut receive_body) = {
+ let handler = handle.get_mut();
+ let (send, receive_data) = pipe::unidirectional(1);
+ handler.send_data = Some(send);
+ let (send, receive_headers) = pipe::unidirectional(1);
+ handler.send_header = Some(send);
+ let (send_body, receive_body) = pipe::unidirectional(None);
+ (receive_data, receive_headers, send_body, receive_body)
+ };
+
+ let follow = follow.get_or_insert(follow_redirects);
+ handle.get_mut().follow = *follow;
+ handle.follow_location(matches!(*follow, FollowRedirects::Initial | FollowRedirects::All))?;
+
+ if *follow == FollowRedirects::Initial {
+ *follow = FollowRedirects::None;
+ }
+
+ if res_send
+ .send(Response {
+ headers: receive_headers,
+ body: receive_data,
+ upload_body: send_body,
+ })
+ .is_err()
+ {
+ break;
+ }
+
+ handle.get_mut().receive_body = Some(match upload_body_kind {
+ Some(PostBodyDataKind::Unbounded) | None => StreamOrBuffer::Stream(receive_body),
+ Some(PostBodyDataKind::BoundedAndFitsIntoMemory) => {
+ let mut buf = Vec::<u8>::with_capacity(512);
+ receive_body.read_to_end(&mut buf)?;
+ handle.post_field_size(buf.len() as u64)?;
+ drop(receive_body);
+ StreamOrBuffer::Buffer(std::io::Cursor::new(buf))
+ }
+ });
+ handle.http_headers(headers)?;
+
+ if let Err(err) = handle.perform() {
+ let handler = handle.get_mut();
+ handler.reset();
+
+ if let Some((action, authenticate)) = proxy_auth_action {
+ authenticate.lock().expect("no panics in other threads")(action.erase()).ok();
+ }
+ let err = Err(io::Error::new(
+ if curl_is_spurious(&err) {
+ std::io::ErrorKind::ConnectionReset
+ } else {
+ std::io::ErrorKind::Other
+ },
+ err,
+ ));
+ handler.receive_body.take();
+ match (handler.send_header.take(), handler.send_data.take()) {
+ (Some(header), mut data) => {
+ if let Err(TrySendError::Disconnected(err)) | Err(TrySendError::Full(err)) =
+ header.channel.try_send(err)
+ {
+ if let Some(body) = data.take() {
+ body.channel.try_send(err).ok();
+ }
+ }
+ }
+ (None, Some(body)) => {
+ body.channel.try_send(err).ok();
+ }
+ (None, None) => {}
+ };
+ } else {
+ let handler = handle.get_mut();
+ if let Some((action, authenticate)) = proxy_auth_action {
+ authenticate.lock().expect("no panics in other threads")(if handler.last_status == 200 {
+ action.store()
+ } else {
+ action.erase()
+ })?;
+ }
+ handler.reset();
+ handler.receive_body.take();
+ handler.send_header.take();
+ handler.send_data.take();
+ let actual_url = handle
+ .effective_url()?
+ .expect("effective url is present and valid UTF-8");
+ if actual_url != effective_url {
+ redirected_base_url = redirect::base_url(actual_url, &base_url, url)?.into();
+ }
+ }
+ }
+ Ok(())
+ });
+ (handle, req_send, res_recv)
+}
+
+fn to_curl_ssl_version(vers: SslVersion) -> curl::easy::SslVersion {
+ use curl::easy::SslVersion::*;
+ match vers {
+ SslVersion::Default => Default,
+ SslVersion::TlsV1 => Tlsv1,
+ SslVersion::SslV2 => Sslv2,
+ SslVersion::SslV3 => Sslv3,
+ SslVersion::TlsV1_0 => Tlsv10,
+ SslVersion::TlsV1_1 => Tlsv11,
+ SslVersion::TlsV1_2 => Tlsv12,
+ SslVersion::TlsV1_3 => Tlsv13,
+ }
+}
+
+impl From<Error> for http::Error {
+ fn from(err: Error) -> Self {
+ http::Error::Detail {
+ description: err.to_string(),
+ }
+ }
+}
+
+impl From<curl::Error> for http::Error {
+ fn from(err: curl::Error) -> Self {
+ http::Error::Detail {
+ description: err.to_string(),
+ }
+ }
+}
diff --git a/vendor/gix-transport/src/client/blocking_io/http/mod.rs b/vendor/gix-transport/src/client/blocking_io/http/mod.rs
new file mode 100644
index 000000000..d88d6bf26
--- /dev/null
+++ b/vendor/gix-transport/src/client/blocking_io/http/mod.rs
@@ -0,0 +1,512 @@
+use std::{
+ any::Any,
+ borrow::Cow,
+ io::{BufRead, Read},
+ path::PathBuf,
+ sync::{Arc, Mutex},
+};
+
+use base64::Engine;
+use bstr::BStr;
+use gix_packetline::PacketLineRef;
+pub use traits::{Error, GetResponse, Http, PostBodyDataKind, PostResponse};
+
+use crate::{
+ client::{
+ self,
+ blocking_io::bufread_ext::ReadlineBufRead,
+ capabilities,
+ http::options::{HttpVersion, SslVersionRangeInclusive},
+ Capabilities, ExtendedBufRead, HandleProgress, MessageKind, RequestWriter,
+ },
+ Protocol, Service,
+};
+
+#[cfg(all(feature = "http-client-reqwest", feature = "http-client-curl"))]
+compile_error!("Cannot set both 'http-client-reqwest' and 'http-client-curl' features as they are mutually exclusive");
+
+#[cfg(feature = "http-client-curl")]
+///
+pub mod curl;
+
+/// The experimental `reqwest` backend.
+///
+/// It doesn't support any of the shared http options yet, but can be seen as example on how to integrate blocking `http` backends.
+/// There is also nothing that would prevent it from becoming a fully-featured HTTP backend except for demand and time.
+#[cfg(feature = "http-client-reqwest")]
+pub mod reqwest;
+
+mod traits;
+
+///
+pub mod options {
+ /// A function to authenticate a URL.
+ pub type AuthenticateFn =
+ dyn FnMut(gix_credentials::helper::Action) -> gix_credentials::protocol::Result + Send + Sync;
+
+ /// Possible settings for the `http.followRedirects` configuration option.
+ #[derive(Debug, Copy, Clone, PartialEq, Eq)]
+ pub enum FollowRedirects {
+ /// Follow only the first redirect request, most suitable for typical git requests.
+ Initial,
+ /// Follow all redirect requests from the server unconditionally
+ All,
+ /// Follow no redirect request.
+ None,
+ }
+
+ impl Default for FollowRedirects {
+ fn default() -> Self {
+ FollowRedirects::Initial
+ }
+ }
+
+ /// The way to configure a proxy for authentication if a username is present in the configured proxy.
+ #[derive(Debug, Copy, Clone, PartialEq, Eq)]
+ pub enum ProxyAuthMethod {
+ /// Automatically pick a suitable authentication method.
+ AnyAuth,
+ ///HTTP basic authentication.
+ Basic,
+ /// Http digest authentication to prevent a password to be passed in clear text.
+ Digest,
+ /// GSS negotiate authentication.
+ Negotiate,
+ /// NTLM authentication
+ Ntlm,
+ }
+
+ impl Default for ProxyAuthMethod {
+ fn default() -> Self {
+ ProxyAuthMethod::AnyAuth
+ }
+ }
+
+ /// Available SSL version numbers.
+ #[derive(Debug, Copy, Clone, PartialEq, Eq, Ord, PartialOrd)]
+ #[allow(missing_docs)]
+ pub enum SslVersion {
+ /// The implementation default, which is unknown to this layer of abstraction.
+ Default,
+ TlsV1,
+ SslV2,
+ SslV3,
+ TlsV1_0,
+ TlsV1_1,
+ TlsV1_2,
+ TlsV1_3,
+ }
+
+ /// Available HTTP version numbers.
+ #[derive(Debug, Copy, Clone, PartialEq, Eq, Ord, PartialOrd)]
+ #[allow(missing_docs)]
+ pub enum HttpVersion {
+ /// Equivalent to HTTP/1.1
+ V1_1,
+ /// Equivalent to HTTP/2
+ V2,
+ }
+
+ /// The desired range of acceptable SSL versions, or the single version to allow if both are set to the same value.
+ #[derive(Debug, Copy, Clone, PartialEq, Eq)]
+ pub struct SslVersionRangeInclusive {
+ /// The smallest allowed ssl version to use.
+ pub min: SslVersion,
+ /// The highest allowed ssl version to use.
+ pub max: SslVersion,
+ }
+
+ impl SslVersionRangeInclusive {
+ /// Return `min` and `max` fields in the right order so `min` is smaller or equal to `max`.
+ pub fn min_max(&self) -> (SslVersion, SslVersion) {
+ if self.min > self.max {
+ (self.max, self.min)
+ } else {
+ (self.min, self.max)
+ }
+ }
+ }
+}
+
+/// Options to configure http requests.
+// TODO: testing most of these fields requires a lot of effort, unless special flags to introspect ongoing requests are added.
+#[derive(Default, Clone)]
+pub struct Options {
+ /// Headers to be added to every request.
+ /// They are applied unconditionally and are expected to be valid as they occour in an HTTP request, like `header: value`, without newlines.
+ ///
+ /// Refers to `http.extraHeader` multi-var.
+ pub extra_headers: Vec<String>,
+ /// How to handle redirects.
+ ///
+ /// Refers to `http.followRedirects`.
+ pub follow_redirects: options::FollowRedirects,
+ /// Used in conjunction with `low_speed_time_seconds`, any non-0 value signals the amount of bytes per second at least to avoid
+ /// aborting the connection.
+ ///
+ /// Refers to `http.lowSpeedLimit`.
+ pub low_speed_limit_bytes_per_second: u32,
+ /// Used in conjunction with `low_speed_bytes_per_second`, any non-0 value signals the amount seconds the minimal amount
+ /// of bytes per second isn't reached.
+ ///
+ /// Refers to `http.lowSpeedTime`.
+ pub low_speed_time_seconds: u64,
+ /// A curl-style proxy declaration of the form `[protocol://][user[:password]@]proxyhost[:port]`.
+ ///
+ /// Note that an empty string means the proxy is disabled entirely.
+ /// Refers to `http.proxy`.
+ pub proxy: Option<String>,
+ /// The comma-separated list of hosts to not send through the `proxy`, or `*` to entirely disable all proxying.
+ pub no_proxy: Option<String>,
+ /// The way to authenticate against the proxy if the `proxy` field contains a username.
+ ///
+ /// Refers to `http.proxyAuthMethod`.
+ pub proxy_auth_method: options::ProxyAuthMethod,
+ /// If authentication is needed for the proxy as its URL contains a username, this method must be set to provide a password
+ /// for it before making the request, and to store it if the connection succeeds.
+ pub proxy_authenticate: Option<(
+ gix_credentials::helper::Action,
+ Arc<std::sync::Mutex<options::AuthenticateFn>>,
+ )>,
+ /// The `HTTP` `USER_AGENT` string presented to an `HTTP` server, notably not the user agent present to the `git` server.
+ ///
+ /// If not overridden, it defaults to the user agent provided by `curl`, which is a deviation from how `git` handles this.
+ /// Thus it's expected from the callers to set it to their application, or use higher-level crates which make it easy to do this
+ /// more correctly.
+ ///
+ /// Using the correct user-agent might affect how the server treats the request.
+ ///
+ /// Refers to `http.userAgent`.
+ pub user_agent: Option<String>,
+ /// The amount of time we wait until aborting a connection attempt.
+ ///
+ /// If `None`, this typically defaults to 2 minutes to 5 minutes.
+ /// Refers to `gitoxide.http.connectTimeout`.
+ pub connect_timeout: Option<std::time::Duration>,
+ /// If enabled, emit additional information about connections and possibly the data received or written.
+ pub verbose: bool,
+ /// If set, use this path to point to a file with CA certificates to verify peers.
+ pub ssl_ca_info: Option<PathBuf>,
+ /// The SSL version or version range to use, or `None` to let the TLS backend determine which versions are acceptable.
+ pub ssl_version: Option<SslVersionRangeInclusive>,
+ /// The HTTP version to enforce. If unset, it is implementation defined.
+ pub http_version: Option<HttpVersion>,
+ /// Backend specific options, if available.
+ pub backend: Option<Arc<Mutex<dyn Any + Send + Sync + 'static>>>,
+}
+
+/// The actual http client implementation, using curl
+#[cfg(feature = "http-client-curl")]
+pub type Impl = curl::Curl;
+/// The actual http client implementation, using reqwest
+#[cfg(feature = "http-client-reqwest")]
+pub type Impl = reqwest::Remote;
+
+/// A transport for supporting arbitrary http clients by abstracting interactions with them into the [Http] trait.
+pub struct Transport<H: Http> {
+ url: String,
+ user_agent_header: &'static str,
+ desired_version: Protocol,
+ actual_version: Protocol,
+ http: H,
+ service: Option<Service>,
+ line_provider: Option<gix_packetline::StreamingPeekableIter<H::ResponseBody>>,
+ identity: Option<gix_sec::identity::Account>,
+}
+
+impl<H: Http> Transport<H> {
+ /// Create a new instance with `http` as implementation to communicate to `url` using the given `desired_version`.
+ /// Note that we will always fallback to other versions as supported by the server.
+ pub fn new_http(http: H, url: &str, desired_version: Protocol) -> Self {
+ Transport {
+ url: url.to_owned(),
+ user_agent_header: concat!("User-Agent: git/oxide-", env!("CARGO_PKG_VERSION")),
+ desired_version,
+ actual_version: Default::default(),
+ service: None,
+ http,
+ line_provider: None,
+ identity: None,
+ }
+ }
+}
+
+#[cfg(any(feature = "http-client-curl", feature = "http-client-reqwest"))]
+impl Transport<Impl> {
+ /// Create a new instance to communicate to `url` using the given `desired_version` of the `git` protocol.
+ ///
+ /// Note that the actual implementation depends on feature toggles.
+ pub fn new(url: &str, desired_version: Protocol) -> Self {
+ Self::new_http(Impl::default(), url, desired_version)
+ }
+}
+
+impl<H: Http> Transport<H> {
+ fn check_content_type(service: Service, kind: &str, headers: <H as Http>::Headers) -> Result<(), client::Error> {
+ let wanted_content_type = format!("application/x-{}-{}", service.as_str(), kind);
+ if !headers.lines().collect::<Result<Vec<_>, _>>()?.iter().any(|l| {
+ let mut tokens = l.split(':');
+ tokens.next().zip(tokens.next()).map_or(false, |(name, value)| {
+ name.eq_ignore_ascii_case("content-type") && value.trim() == wanted_content_type
+ })
+ }) {
+ return Err(client::Error::Http(Error::Detail {
+ description: format!(
+ "Didn't find '{wanted_content_type}' header to indicate 'smart' protocol, and 'dumb' protocol is not supported."
+ ),
+ }));
+ }
+ Ok(())
+ }
+
+ #[allow(clippy::unnecessary_wraps, unknown_lints)]
+ fn add_basic_auth_if_present(&self, headers: &mut Vec<Cow<'_, str>>) -> Result<(), client::Error> {
+ if let Some(gix_sec::identity::Account { username, password }) = &self.identity {
+ #[cfg(not(debug_assertions))]
+ if self.url.starts_with("http://") {
+ return Err(client::Error::AuthenticationRefused(
+ "Will not send credentials in clear text over http",
+ ));
+ }
+ headers.push(Cow::Owned(format!(
+ "Authorization: Basic {}",
+ base64::engine::general_purpose::STANDARD.encode(format!("{username}:{password}"))
+ )))
+ }
+ Ok(())
+ }
+}
+
+fn append_url(base: &str, suffix: &str) -> String {
+ let mut buf = base.to_owned();
+ if base.as_bytes().last() != Some(&b'/') {
+ buf.push('/');
+ }
+ buf.push_str(suffix);
+ buf
+}
+
+impl<H: Http> client::TransportWithoutIO for Transport<H> {
+ fn set_identity(&mut self, identity: gix_sec::identity::Account) -> Result<(), client::Error> {
+ self.identity = Some(identity);
+ Ok(())
+ }
+
+ fn request(
+ &mut self,
+ write_mode: client::WriteMode,
+ on_into_read: MessageKind,
+ ) -> Result<RequestWriter<'_>, client::Error> {
+ let service = self.service.expect("handshake() must have been called first");
+ let url = append_url(&self.url, service.as_str());
+ let static_headers = &[
+ Cow::Borrowed(self.user_agent_header),
+ Cow::Owned(format!("Content-Type: application/x-{}-request", service.as_str())),
+ format!("Accept: application/x-{}-result", service.as_str()).into(),
+ ];
+ let mut dynamic_headers = Vec::new();
+ self.add_basic_auth_if_present(&mut dynamic_headers)?;
+ if self.actual_version != Protocol::V1 {
+ dynamic_headers.push(Cow::Owned(format!(
+ "Git-Protocol: version={}",
+ self.actual_version as usize
+ )));
+ }
+
+ let PostResponse {
+ headers,
+ body,
+ post_body,
+ } = self.http.post(
+ &url,
+ &self.url,
+ static_headers.iter().chain(&dynamic_headers),
+ write_mode.into(),
+ )?;
+ let line_provider = self
+ .line_provider
+ .as_mut()
+ .expect("handshake to have been called first");
+ line_provider.replace(body);
+ Ok(RequestWriter::new_from_bufread(
+ post_body,
+ Box::new(HeadersThenBody::<H, _> {
+ service,
+ headers: Some(headers),
+ body: line_provider.as_read_without_sidebands(),
+ }),
+ write_mode,
+ on_into_read,
+ ))
+ }
+
+ fn to_url(&self) -> Cow<'_, BStr> {
+ Cow::Borrowed(self.url.as_str().into())
+ }
+
+ fn connection_persists_across_multiple_requests(&self) -> bool {
+ false
+ }
+
+ fn configure(&mut self, config: &dyn Any) -> Result<(), Box<dyn std::error::Error + Send + Sync + 'static>> {
+ self.http.configure(config)
+ }
+}
+
+impl<H: Http> client::Transport for Transport<H> {
+ fn handshake<'a>(
+ &mut self,
+ service: Service,
+ extra_parameters: &'a [(&'a str, Option<&'a str>)],
+ ) -> Result<client::SetServiceResponse<'_>, client::Error> {
+ let url = append_url(self.url.as_ref(), &format!("info/refs?service={}", service.as_str()));
+ let static_headers = [Cow::Borrowed(self.user_agent_header)];
+ let mut dynamic_headers = Vec::<Cow<'_, str>>::new();
+ if self.desired_version != Protocol::V1 || !extra_parameters.is_empty() {
+ let mut parameters = if self.desired_version != Protocol::V1 {
+ let mut p = format!("version={}", self.desired_version as usize);
+ if !extra_parameters.is_empty() {
+ p.push(':');
+ }
+ p
+ } else {
+ String::new()
+ };
+ parameters.push_str(
+ &extra_parameters
+ .iter()
+ .map(|(key, value)| match value {
+ Some(value) => format!("{key}={value}"),
+ None => key.to_string(),
+ })
+ .collect::<Vec<_>>()
+ .join(":"),
+ );
+ dynamic_headers.push(format!("Git-Protocol: {parameters}").into());
+ }
+ self.add_basic_auth_if_present(&mut dynamic_headers)?;
+ let GetResponse { headers, body } =
+ self.http
+ .get(url.as_ref(), &self.url, static_headers.iter().chain(&dynamic_headers))?;
+ <Transport<H>>::check_content_type(service, "advertisement", headers)?;
+
+ let line_reader = self
+ .line_provider
+ .get_or_insert_with(|| gix_packetline::StreamingPeekableIter::new(body, &[PacketLineRef::Flush]));
+
+ // the service announcement is only sent sometimes depending on the exact server/protocol version/used protocol (http?)
+ // eat the announcement when its there to avoid errors later (and check that the correct service was announced).
+ // Ignore the announcement otherwise.
+ let line_ = line_reader
+ .peek_line()
+ .ok_or(client::Error::ExpectedLine("capabilities, version or service"))???;
+ let line = line_.as_text().ok_or(client::Error::ExpectedLine("text"))?;
+
+ if let Some(announced_service) = line.as_bstr().strip_prefix(b"# service=") {
+ if announced_service != service.as_str().as_bytes() {
+ return Err(client::Error::Http(Error::Detail {
+ description: format!(
+ "Expected to see service {:?}, but got {:?}",
+ service.as_str(),
+ announced_service
+ ),
+ }));
+ }
+
+ line_reader.as_read().read_to_end(&mut Vec::new())?;
+ }
+
+ let capabilities::recv::Outcome {
+ capabilities,
+ refs,
+ protocol: actual_protocol,
+ } = Capabilities::from_lines_with_version_detection(line_reader)?;
+ self.actual_version = actual_protocol;
+ self.service = Some(service);
+ Ok(client::SetServiceResponse {
+ actual_protocol,
+ capabilities,
+ refs,
+ })
+ }
+}
+
+struct HeadersThenBody<H: Http, B: Unpin> {
+ service: Service,
+ headers: Option<H::Headers>,
+ body: B,
+}
+
+impl<H: Http, B: Unpin> HeadersThenBody<H, B> {
+ fn handle_headers(&mut self) -> std::io::Result<()> {
+ if let Some(headers) = self.headers.take() {
+ <Transport<H>>::check_content_type(self.service, "result", headers)
+ .map_err(|err| std::io::Error::new(std::io::ErrorKind::Other, err))?
+ }
+ Ok(())
+ }
+}
+
+impl<H: Http, B: Read + Unpin> Read for HeadersThenBody<H, B> {
+ fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
+ self.handle_headers()?;
+ self.body.read(buf)
+ }
+}
+
+impl<H: Http, B: BufRead + Unpin> BufRead for HeadersThenBody<H, B> {
+ fn fill_buf(&mut self) -> std::io::Result<&[u8]> {
+ self.handle_headers()?;
+ self.body.fill_buf()
+ }
+
+ fn consume(&mut self, amt: usize) {
+ self.body.consume(amt)
+ }
+}
+
+impl<H: Http, B: ReadlineBufRead + Unpin> ReadlineBufRead for HeadersThenBody<H, B> {
+ fn readline(&mut self) -> Option<std::io::Result<Result<PacketLineRef<'_>, gix_packetline::decode::Error>>> {
+ if let Err(err) = self.handle_headers() {
+ return Some(Err(err));
+ }
+ self.body.readline()
+ }
+}
+
+impl<H: Http, B: ExtendedBufRead + Unpin> ExtendedBufRead for HeadersThenBody<H, B> {
+ fn set_progress_handler(&mut self, handle_progress: Option<HandleProgress>) {
+ self.body.set_progress_handler(handle_progress)
+ }
+
+ fn peek_data_line(&mut self) -> Option<std::io::Result<Result<&[u8], client::Error>>> {
+ if let Err(err) = self.handle_headers() {
+ return Some(Err(err));
+ }
+ self.body.peek_data_line()
+ }
+
+ fn reset(&mut self, version: Protocol) {
+ self.body.reset(version)
+ }
+
+ fn stopped_at(&self) -> Option<MessageKind> {
+ self.body.stopped_at()
+ }
+}
+
+/// Connect to the given `url` via HTTP/S using the `desired_version` of the `git` protocol, with `http` as implementation.
+#[cfg(all(feature = "http-client", not(feature = "http-client-curl")))]
+pub fn connect_http<H: Http>(http: H, url: &str, desired_version: Protocol) -> Transport<H> {
+ Transport::new_http(http, url, desired_version)
+}
+
+/// Connect to the given `url` via HTTP/S using the `desired_version` of the `git` protocol.
+#[cfg(any(feature = "http-client-curl", feature = "http-client-reqwest"))]
+pub fn connect(url: &str, desired_version: Protocol) -> Transport<Impl> {
+ Transport::new(url, desired_version)
+}
+
+///
+#[cfg(feature = "http-client-curl")]
+pub mod redirect;
diff --git a/vendor/gix-transport/src/client/blocking_io/http/redirect.rs b/vendor/gix-transport/src/client/blocking_io/http/redirect.rs
new file mode 100644
index 000000000..f429961e0
--- /dev/null
+++ b/vendor/gix-transport/src/client/blocking_io/http/redirect.rs
@@ -0,0 +1,66 @@
+/// The error provided when redirection went beyond what we deem acceptable.
+#[derive(Debug, thiserror::Error)]
+#[error("Redirect url {redirect_url:?} could not be reconciled with original url {expected_url} as they don't share the same suffix")]
+pub struct Error {
+ redirect_url: String,
+ expected_url: String,
+}
+
+pub(crate) fn base_url(redirect_url: &str, base_url: &str, url: String) -> Result<String, Error> {
+ let tail = url
+ .strip_prefix(base_url)
+ .expect("BUG: caller assures `base_url` is subset of `url`");
+ redirect_url
+ .strip_suffix(tail)
+ .ok_or_else(|| Error {
+ redirect_url: redirect_url.into(),
+ expected_url: url,
+ })
+ .map(ToOwned::to_owned)
+}
+
+pub(crate) fn swap_tails(effective_base_url: Option<&str>, base_url: &str, mut url: String) -> String {
+ match effective_base_url {
+ Some(effective_base) => {
+ url.replace_range(..base_url.len(), effective_base);
+ url
+ }
+ None => url,
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn base_url_complete() {
+ assert_eq!(
+ base_url(
+ "https://redirected.org/b/info/refs?hi",
+ "https://original/a",
+ "https://original/a/info/refs?hi".into()
+ )
+ .unwrap(),
+ "https://redirected.org/b"
+ );
+ }
+
+ #[test]
+ fn swap_tails_complete() {
+ assert_eq!(
+ swap_tails(None, "not interesting", "used".into()),
+ "used",
+ "without effective base url, it passes url, no redirect happened yet"
+ );
+ assert_eq!(
+ swap_tails(
+ Some("https://redirected.org/b"),
+ "https://original/a",
+ "https://original/a/info/refs?something".into()
+ ),
+ "https://redirected.org/b/info/refs?something",
+ "the tail stays the same if redirection happened"
+ )
+ }
+}
diff --git a/vendor/gix-transport/src/client/blocking_io/http/reqwest/mod.rs b/vendor/gix-transport/src/client/blocking_io/http/reqwest/mod.rs
new file mode 100644
index 000000000..7c68b166e
--- /dev/null
+++ b/vendor/gix-transport/src/client/blocking_io/http/reqwest/mod.rs
@@ -0,0 +1,28 @@
+/// An implementation for HTTP requests via `reqwest`.
+pub struct Remote {
+ /// A worker thread which performs the actual request.
+ handle: Option<std::thread::JoinHandle<Result<(), remote::Error>>>,
+ /// A channel to send requests (work) to the worker thread.
+ request: std::sync::mpsc::SyncSender<remote::Request>,
+ /// A channel to receive the result of the prior request.
+ response: std::sync::mpsc::Receiver<remote::Response>,
+ /// A mechanism for configuring the remote.
+ config: crate::client::http::Options,
+}
+
+/// A function to configure a single request prior to sending it, support most complex configuration beyond what's possible with
+/// basic `git` http configuration.
+pub type ConfigureRequestFn = dyn FnMut(&mut reqwest::blocking::Request) -> Result<(), Box<dyn std::error::Error + Send + Sync + 'static>>
+ + Send
+ + Sync
+ + 'static;
+
+/// Options to configure the reqwest HTTP handler.
+#[derive(Default)]
+pub struct Options {
+ /// A function to configure the request that is about to be made.
+ pub configure_request: Option<Box<ConfigureRequestFn>>,
+}
+
+///
+pub mod remote;
diff --git a/vendor/gix-transport/src/client/blocking_io/http/reqwest/remote.rs b/vendor/gix-transport/src/client/blocking_io/http/reqwest/remote.rs
new file mode 100644
index 000000000..724528ab9
--- /dev/null
+++ b/vendor/gix-transport/src/client/blocking_io/http/reqwest/remote.rs
@@ -0,0 +1,263 @@
+use std::{
+ any::Any,
+ convert::TryFrom,
+ io::{Read, Write},
+ str::FromStr,
+};
+
+use gix_features::io::pipe;
+
+use crate::client::{
+ http,
+ http::{reqwest::Remote, traits::PostBodyDataKind},
+};
+
+/// The error returned by the 'remote' helper, a purely internal construct to perform http requests.
+#[derive(Debug, thiserror::Error)]
+#[allow(missing_docs)]
+pub enum Error {
+ #[error(transparent)]
+ Reqwest(#[from] reqwest::Error),
+ #[error("Could not finish reading all data to post to the remote")]
+ ReadPostBody(#[from] std::io::Error),
+ #[error("Request configuration failed")]
+ ConfigureRequest(#[from] Box<dyn std::error::Error + Send + Sync + 'static>),
+}
+
+impl crate::IsSpuriousError for Error {
+ fn is_spurious(&self) -> bool {
+ match self {
+ Error::Reqwest(err) => {
+ err.is_timeout() || err.is_connect() || err.status().map_or(false, |status| status.is_server_error())
+ }
+ _ => false,
+ }
+ }
+}
+
+impl Default for Remote {
+ fn default() -> Self {
+ let (req_send, req_recv) = std::sync::mpsc::sync_channel(0);
+ let (res_send, res_recv) = std::sync::mpsc::sync_channel(0);
+ let handle = std::thread::spawn(move || -> Result<(), Error> {
+ // We may error while configuring, which is expected as part of the internal protocol. The error will be
+ // received and the sender of the request might restart us.
+ let client = reqwest::blocking::ClientBuilder::new()
+ .connect_timeout(std::time::Duration::from_secs(20))
+ .http1_title_case_headers()
+ .build()?;
+ for Request {
+ url,
+ headers,
+ upload_body_kind,
+ config,
+ } in req_recv
+ {
+ let mut req_builder = if upload_body_kind.is_some() {
+ client.post(url)
+ } else {
+ client.get(url)
+ }
+ .headers(headers);
+ let (post_body_tx, mut post_body_rx) = pipe::unidirectional(0);
+ let (mut response_body_tx, response_body_rx) = pipe::unidirectional(0);
+ let (mut headers_tx, headers_rx) = pipe::unidirectional(0);
+ if res_send
+ .send(Response {
+ headers: headers_rx,
+ body: response_body_rx,
+ upload_body: post_body_tx,
+ })
+ .is_err()
+ {
+ // This means our internal protocol is violated as the one who sent the request isn't listening anymore.
+ // Shut down as something is off.
+ break;
+ }
+ req_builder = match upload_body_kind {
+ Some(PostBodyDataKind::BoundedAndFitsIntoMemory) => {
+ let mut buf = Vec::<u8>::with_capacity(512);
+ post_body_rx.read_to_end(&mut buf)?;
+ req_builder.body(buf)
+ }
+ Some(PostBodyDataKind::Unbounded) => req_builder.body(reqwest::blocking::Body::new(post_body_rx)),
+ None => req_builder,
+ };
+ let mut req = req_builder.build()?;
+ if let Some(ref mut request_options) = config.backend.as_ref().and_then(|backend| backend.lock().ok()) {
+ if let Some(options) = request_options.downcast_mut::<super::Options>() {
+ if let Some(configure_request) = &mut options.configure_request {
+ configure_request(&mut req)?;
+ }
+ }
+ }
+ let mut res = match client.execute(req).and_then(|res| res.error_for_status()) {
+ Ok(res) => res,
+ Err(err) => {
+ let (kind, err) = match err.status() {
+ Some(status) => {
+ let kind = if status == reqwest::StatusCode::UNAUTHORIZED {
+ std::io::ErrorKind::PermissionDenied
+ } else if status.is_server_error() {
+ std::io::ErrorKind::ConnectionAborted
+ } else {
+ std::io::ErrorKind::Other
+ };
+ (kind, format!("Received HTTP status {}", status.as_str()))
+ }
+ None => (std::io::ErrorKind::Other, err.to_string()),
+ };
+ let err = Err(std::io::Error::new(kind, err));
+ headers_tx.channel.send(err).ok();
+ continue;
+ }
+ };
+
+ let send_headers = {
+ let headers = res.headers();
+ move || -> std::io::Result<()> {
+ for (name, value) in headers {
+ headers_tx.write_all(name.as_str().as_bytes())?;
+ headers_tx.write_all(b":")?;
+ headers_tx.write_all(value.as_bytes())?;
+ headers_tx.write_all(b"\n")?;
+ }
+ // Make sure this is an FnOnce closure to signal the remote reader we are done.
+ drop(headers_tx);
+ Ok(())
+ }
+ };
+
+ // We don't have to care if anybody is receiving the header, as a matter of fact we cannot fail sending them.
+ // Thus an error means the receiver failed somehow, but might also have decided not to read headers at all. Fine with us.
+ send_headers().ok();
+
+ // reading the response body is streaming and may fail for many reasons. If so, we send the error over the response
+ // body channel and that's all we can do.
+ if let Err(err) = std::io::copy(&mut res, &mut response_body_tx) {
+ response_body_tx.channel.send(Err(err)).ok();
+ }
+ }
+ Ok(())
+ });
+
+ Remote {
+ handle: Some(handle),
+ request: req_send,
+ response: res_recv,
+ config: http::Options::default(),
+ }
+ }
+}
+
+/// utilities
+impl Remote {
+ fn make_request(
+ &mut self,
+ url: &str,
+ _base_url: &str,
+ headers: impl IntoIterator<Item = impl AsRef<str>>,
+ upload_body_kind: Option<PostBodyDataKind>,
+ ) -> Result<http::PostResponse<pipe::Reader, pipe::Reader, pipe::Writer>, http::Error> {
+ let mut header_map = reqwest::header::HeaderMap::new();
+ for header_line in headers {
+ let header_line = header_line.as_ref();
+ let colon_pos = header_line
+ .find(':')
+ .expect("header line must contain a colon to separate key and value");
+ let header_name = &header_line[..colon_pos];
+ let value = &header_line[colon_pos + 1..];
+
+ match reqwest::header::HeaderName::from_str(header_name)
+ .ok()
+ .zip(reqwest::header::HeaderValue::try_from(value.trim()).ok())
+ {
+ Some((key, val)) => header_map.insert(key, val),
+ None => continue,
+ };
+ }
+ self.request
+ .send(Request {
+ url: url.to_owned(),
+ headers: header_map,
+ upload_body_kind,
+ config: self.config.clone(),
+ })
+ .expect("the remote cannot be down at this point");
+
+ let Response {
+ headers,
+ body,
+ upload_body,
+ } = match self.response.recv() {
+ Ok(res) => res,
+ Err(_) => {
+ let err = self
+ .handle
+ .take()
+ .expect("always present")
+ .join()
+ .expect("no panic")
+ .expect_err("no receiver means thread is down with init error");
+ *self = Self::default();
+ return Err(http::Error::InitHttpClient { source: Box::new(err) });
+ }
+ };
+
+ Ok(http::PostResponse {
+ post_body: upload_body,
+ headers,
+ body,
+ })
+ }
+}
+
+impl http::Http for Remote {
+ type Headers = pipe::Reader;
+ type ResponseBody = pipe::Reader;
+ type PostBody = pipe::Writer;
+
+ fn get(
+ &mut self,
+ url: &str,
+ base_url: &str,
+ headers: impl IntoIterator<Item = impl AsRef<str>>,
+ ) -> Result<http::GetResponse<Self::Headers, Self::ResponseBody>, http::Error> {
+ self.make_request(url, base_url, headers, None).map(Into::into)
+ }
+
+ fn post(
+ &mut self,
+ url: &str,
+ base_url: &str,
+ headers: impl IntoIterator<Item = impl AsRef<str>>,
+ post_body_kind: PostBodyDataKind,
+ ) -> Result<http::PostResponse<Self::Headers, Self::ResponseBody, Self::PostBody>, http::Error> {
+ self.make_request(url, base_url, headers, Some(post_body_kind))
+ }
+
+ fn configure(&mut self, config: &dyn Any) -> Result<(), Box<dyn std::error::Error + Send + Sync + 'static>> {
+ if let Some(config) = config.downcast_ref::<http::Options>() {
+ self.config = config.clone();
+ }
+ Ok(())
+ }
+}
+
+pub(crate) struct Request {
+ pub url: String,
+ pub headers: reqwest::header::HeaderMap,
+ pub upload_body_kind: Option<PostBodyDataKind>,
+ pub config: http::Options,
+}
+
+/// A link to a thread who provides data for the contained readers.
+/// The expected order is:
+/// - write `upload_body`
+/// - read `headers` to end
+/// - read `body` to hend
+pub(crate) struct Response {
+ pub headers: pipe::Reader,
+ pub body: pipe::Reader,
+ pub upload_body: pipe::Writer,
+}
diff --git a/vendor/gix-transport/src/client/blocking_io/http/traits.rs b/vendor/gix-transport/src/client/blocking_io/http/traits.rs
new file mode 100644
index 000000000..5b163f892
--- /dev/null
+++ b/vendor/gix-transport/src/client/blocking_io/http/traits.rs
@@ -0,0 +1,133 @@
+use crate::client::WriteMode;
+
+/// The error used by the [Http] trait.
+#[derive(Debug, thiserror::Error)]
+#[allow(missing_docs)]
+pub enum Error {
+ #[error("Could initialize the http client")]
+ InitHttpClient {
+ source: Box<dyn std::error::Error + Send + Sync + 'static>,
+ },
+ #[error("{description}")]
+ Detail { description: String },
+ #[error("An IO error occurred while uploading the body of a POST request")]
+ PostBody(#[from] std::io::Error),
+}
+
+impl crate::IsSpuriousError for Error {
+ fn is_spurious(&self) -> bool {
+ match self {
+ Error::PostBody(err) => err.is_spurious(),
+ #[cfg(any(feature = "http-client-reqwest", feature = "http-client-curl"))]
+ Error::InitHttpClient { source } => {
+ #[cfg(feature = "http-client-curl")]
+ if let Some(err) = source.downcast_ref::<crate::client::http::curl::Error>() {
+ return err.is_spurious();
+ };
+ #[cfg(feature = "http-client-reqwest")]
+ if let Some(err) = source.downcast_ref::<crate::client::http::reqwest::remote::Error>() {
+ return err.is_spurious();
+ };
+ false
+ }
+ _ => false,
+ }
+ }
+}
+
+/// The return value of [Http::get()].
+pub struct GetResponse<H, B> {
+ /// The response headers.
+ pub headers: H,
+ /// The response body.
+ pub body: B,
+}
+
+/// The return value of [Http::post()].
+pub struct PostResponse<H, B, PB> {
+ /// The body to post to the server as part of the request.
+ ///
+ /// **Note**: Implementations should drop the handle to avoid deadlocks.
+ pub post_body: PB,
+ /// The headers of the post response.
+ pub headers: H,
+ /// The body of the post response.
+ pub body: B,
+}
+
+/// Whether or not the post body is expected to fit into memory or not.
+#[derive(PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone, Copy)]
+pub enum PostBodyDataKind {
+ /// We know how much data we are sending and think it will fit into memory. This allows to collect it into a buffer
+ /// and send it with `Content-Length: <body-len>`.
+ BoundedAndFitsIntoMemory,
+ /// We don't know how much data we will send and assume it won't fit into memory. This enables streaming mode.
+ Unbounded,
+}
+
+impl From<WriteMode> for PostBodyDataKind {
+ fn from(m: WriteMode) -> Self {
+ match m {
+ WriteMode::Binary => PostBodyDataKind::Unbounded,
+ WriteMode::OneLfTerminatedLinePerWriteCall => PostBodyDataKind::BoundedAndFitsIntoMemory,
+ }
+ }
+}
+
+impl<A, B, C> From<PostResponse<A, B, C>> for GetResponse<A, B> {
+ fn from(v: PostResponse<A, B, C>) -> Self {
+ GetResponse {
+ headers: v.headers,
+ body: v.body,
+ }
+ }
+}
+
+/// A trait to abstract the HTTP operations needed to power all git interactions: read via GET and write via POST.
+/// Note that 401 must be turned into `std::io::Error(PermissionDenied)`, and other non-success http stati must be transformed
+/// into `std::io::Error(Other)`
+#[allow(clippy::type_complexity)]
+pub trait Http {
+ /// A type providing headers line by line.
+ type Headers: std::io::BufRead + Unpin;
+ /// A type providing the response.
+ type ResponseBody: std::io::BufRead;
+ /// A type allowing to write the content to post.
+ type PostBody: std::io::Write;
+
+ /// Initiate a `GET` request to `url` provided the given `headers`, where `base_url` is so that `base_url + tail == url`.
+ ///
+ /// The `base_url` helps to validate redirects and to swap it with the effective base after a redirect.
+ ///
+ /// The `headers` are provided verbatim and include both the key as well as the value.
+ fn get(
+ &mut self,
+ url: &str,
+ base_url: &str,
+ headers: impl IntoIterator<Item = impl AsRef<str>>,
+ ) -> Result<GetResponse<Self::Headers, Self::ResponseBody>, Error>;
+
+ /// Initiate a `POST` request to `url` providing with the given `headers`, where `base_url` is so that `base_url + tail == url`.
+ ///
+ /// The `base_url` helps to validate redirects and to swap it with the effective base after a redirect.
+ ///
+ /// The `headers` are provided verbatim and include both the key as well as the value.
+ /// Note that the [`PostResponse`] contains the [`post_body`][PostResponse::post_body] field which implements [`std::io::Write`]
+ /// and is expected to receive the body to post to the server. **It must be dropped** before reading the response
+ /// to prevent deadlocks.
+ fn post(
+ &mut self,
+ url: &str,
+ base_url: &str,
+ headers: impl IntoIterator<Item = impl AsRef<str>>,
+ body: PostBodyDataKind,
+ ) -> Result<PostResponse<Self::Headers, Self::ResponseBody, Self::PostBody>, Error>;
+
+ /// Pass `config` which can deserialize in the implementation's configuration, as documented separately.
+ ///
+ /// The caller must know how that `config` data looks like for the intended implementation.
+ fn configure(
+ &mut self,
+ config: &dyn std::any::Any,
+ ) -> Result<(), Box<dyn std::error::Error + Send + Sync + 'static>>;
+}
diff --git a/vendor/gix-transport/src/client/blocking_io/mod.rs b/vendor/gix-transport/src/client/blocking_io/mod.rs
new file mode 100644
index 000000000..dfb3752af
--- /dev/null
+++ b/vendor/gix-transport/src/client/blocking_io/mod.rs
@@ -0,0 +1,20 @@
+///
+pub mod connect;
+
+///
+pub mod file;
+///
+#[cfg(feature = "http-client")]
+pub mod http;
+
+mod bufread_ext;
+pub use bufread_ext::{ExtendedBufRead, HandleProgress, ReadlineBufRead};
+
+mod request;
+pub use request::RequestWriter;
+
+///
+pub mod ssh;
+
+mod traits;
+pub use traits::{SetServiceResponse, Transport, TransportV2Ext};
diff --git a/vendor/gix-transport/src/client/blocking_io/request.rs b/vendor/gix-transport/src/client/blocking_io/request.rs
new file mode 100644
index 000000000..ae818eff3
--- /dev/null
+++ b/vendor/gix-transport/src/client/blocking_io/request.rs
@@ -0,0 +1,79 @@
+use std::io;
+
+use crate::client::{ExtendedBufRead, MessageKind, WriteMode};
+
+/// A [`Write`][io::Write] implementation optimized for writing packet lines.
+/// A type implementing `Write` for packet lines, which when done can be transformed into a `Read` for
+/// obtaining the response.
+pub struct RequestWriter<'a> {
+ on_into_read: MessageKind,
+ writer: gix_packetline::Writer<Box<dyn io::Write + 'a>>,
+ reader: Box<dyn ExtendedBufRead + Unpin + 'a>,
+}
+
+impl<'a> io::Write for RequestWriter<'a> {
+ fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
+ self.writer.write(buf)
+ }
+
+ fn flush(&mut self) -> io::Result<()> {
+ self.writer.flush()
+ }
+}
+
+/// methods with bonds to IO
+impl<'a> RequestWriter<'a> {
+ /// Create a new instance from a `writer` (commonly a socket), a `reader` into which to transform once the
+ /// writes are finished, along with configuration for the `write_mode` and information about which message to write
+ /// when this instance is converted into a `reader` to read the request's response.
+ pub fn new_from_bufread<W: io::Write + 'a>(
+ writer: W,
+ reader: Box<dyn ExtendedBufRead + Unpin + 'a>,
+ write_mode: WriteMode,
+ on_into_read: MessageKind,
+ ) -> Self {
+ let mut writer = gix_packetline::Writer::new(Box::new(writer) as Box<dyn io::Write>);
+ match write_mode {
+ WriteMode::Binary => writer.enable_binary_mode(),
+ WriteMode::OneLfTerminatedLinePerWriteCall => writer.enable_text_mode(),
+ }
+ RequestWriter {
+ on_into_read,
+ writer,
+ reader,
+ }
+ }
+
+ /// Write the given message as packet line.
+ pub fn write_message(&mut self, message: MessageKind) -> io::Result<()> {
+ match message {
+ MessageKind::Flush => gix_packetline::PacketLineRef::Flush.write_to(self.writer.inner_mut()),
+ MessageKind::Delimiter => gix_packetline::PacketLineRef::Delimiter.write_to(self.writer.inner_mut()),
+ MessageKind::ResponseEnd => gix_packetline::PacketLineRef::ResponseEnd.write_to(self.writer.inner_mut()),
+ MessageKind::Text(t) => gix_packetline::TextRef::from(t).write_to(self.writer.inner_mut()),
+ }
+ .map(|_| ())
+ }
+
+ /// Discard the ability to write and turn this instance into the reader for obtaining the other side's response.
+ ///
+ /// Doing so will also write the message type this instance was initialized with.
+ pub fn into_read(mut self) -> std::io::Result<Box<dyn ExtendedBufRead + Unpin + 'a>> {
+ self.write_message(self.on_into_read)?;
+ Ok(self.reader)
+ }
+
+ /// Dissolve this instance into its write and read handles without any message-writing side-effect as in [RequestWriter::into_read()].
+ ///
+ /// Furthermore, the writer will not encode everything it writes as packetlines, but write everything verbatim into the
+ /// underlying channel.
+ ///
+ /// # Note
+ ///
+ /// It's of utmost importance to drop the request writer before reading the response as these might be inter-dependent, depending on
+ /// the underlying transport mechanism. Failure to do so may result in a deadlock depending on how the write and read mechanism
+ /// is implemented.
+ pub fn into_parts(self) -> (Box<dyn io::Write + 'a>, Box<dyn ExtendedBufRead + Unpin + 'a>) {
+ (self.writer.into_inner(), self.reader)
+ }
+}
diff --git a/vendor/gix-transport/src/client/blocking_io/ssh/mod.rs b/vendor/gix-transport/src/client/blocking_io/ssh/mod.rs
new file mode 100644
index 000000000..63e4dc817
--- /dev/null
+++ b/vendor/gix-transport/src/client/blocking_io/ssh/mod.rs
@@ -0,0 +1,131 @@
+use std::process::Stdio;
+
+use crate::{client::blocking_io, Protocol};
+
+/// The error used in [`connect()`].
+#[derive(Debug, thiserror::Error)]
+#[allow(missing_docs)]
+pub enum Error {
+ #[error("The scheme in \"{}\" is not usable for an ssh connection", .0.to_bstring())]
+ UnsupportedScheme(gix_url::Url),
+}
+
+impl crate::IsSpuriousError for Error {}
+
+/// The kind of SSH programs we have built-in support for.
+///
+/// Various different programs exists with different capabilities, and we have a few built in.
+#[derive(Debug, Copy, Clone, Eq, PartialEq)]
+pub enum ProgramKind {
+ /// The standard linux ssh program
+ Ssh,
+ /// The `(plink|putty).exe` binaries, typically only on windows.
+ Plink,
+ /// The `putty.exe` binary, typically only on windows.
+ Putty,
+ /// The `tortoiseplink.exe` binary, only on windows.
+ TortoisePlink,
+ /// A minimal ssh client that supports on options.
+ Simple,
+}
+
+mod program_kind;
+
+///
+pub mod invocation {
+ use std::ffi::OsString;
+
+ /// The error returned when producing ssh invocation arguments based on a selected invocation kind.
+ #[derive(Debug, thiserror::Error)]
+ #[error("The 'Simple' ssh variant doesn't support {function}")]
+ pub struct Error {
+ /// The simple command that should have been invoked.
+ pub command: OsString,
+ /// The function that was unsupported
+ pub function: &'static str,
+ }
+}
+
+///
+pub mod connect {
+ use std::ffi::{OsStr, OsString};
+
+ use crate::client::ssh::ProgramKind;
+
+ /// The options for use when [connecting][super::connect()] via the `ssh` protocol.
+ #[derive(Debug, Clone, Default)]
+ pub struct Options {
+ /// The program or script to use.
+ /// If unset, it defaults to `ssh` or `ssh.exe`, or the program implied by `kind` if that one is set.
+ pub command: Option<OsString>,
+ /// If `true`, a shell must not be used to execute `command`.
+ /// This defaults to `false`, and a shell can then be used if `command` seems to require it, but won't be
+ /// used unnecessarily.
+ pub disallow_shell: bool,
+ /// The ssh variant further identifying `program`. This determines which arguments will be used
+ /// when invoking the program.
+ /// If unset, the `program` basename determines the variant, or an invocation of the `command` itself.
+ pub kind: Option<ProgramKind>,
+ }
+
+ impl Options {
+ /// Return the configured ssh command, defaulting to `ssh` if neither the `command` nor the `kind` fields are set.
+ pub fn ssh_command(&self) -> &OsStr {
+ self.command
+ .as_deref()
+ .or_else(|| self.kind.and_then(|kind| kind.exe()))
+ .unwrap_or_else(|| OsStr::new("ssh"))
+ }
+ }
+}
+
+/// Connect to `host` using the ssh program to obtain data from the repository at `path` on the remote.
+///
+/// The optional `user` identifies the user's account to which to connect, while `port` allows to specify non-standard
+/// ssh ports.
+///
+/// The `desired_version` is the preferred protocol version when establishing the connection, but note that it can be
+/// downgraded by servers not supporting it.
+pub fn connect(
+ url: gix_url::Url,
+ desired_version: Protocol,
+ options: connect::Options,
+) -> Result<blocking_io::file::SpawnProcessOnDemand, Error> {
+ if url.scheme != gix_url::Scheme::Ssh || url.host().is_none() {
+ return Err(Error::UnsupportedScheme(url));
+ }
+ let ssh_cmd = options.ssh_command();
+ let mut kind = options.kind.unwrap_or_else(|| ProgramKind::from(ssh_cmd));
+ if options.kind.is_none() && kind == ProgramKind::Simple {
+ kind = if std::process::Command::from(
+ gix_command::prepare(ssh_cmd)
+ .stderr(Stdio::null())
+ .stdout(Stdio::null())
+ .stdin(Stdio::null())
+ .with_shell()
+ .arg("-G")
+ .arg(url.host().expect("always set for ssh urls")),
+ )
+ .status()
+ .ok()
+ .map_or(false, |status| status.success())
+ {
+ ProgramKind::Ssh
+ } else {
+ ProgramKind::Simple
+ };
+ }
+
+ let path = gix_url::expand_path::for_shell(url.path.clone());
+ Ok(blocking_io::file::SpawnProcessOnDemand::new_ssh(
+ url,
+ ssh_cmd,
+ path,
+ kind,
+ options.disallow_shell,
+ desired_version,
+ ))
+}
+
+#[cfg(test)]
+mod tests;
diff --git a/vendor/gix-transport/src/client/blocking_io/ssh/program_kind.rs b/vendor/gix-transport/src/client/blocking_io/ssh/program_kind.rs
new file mode 100644
index 000000000..5e9d14a82
--- /dev/null
+++ b/vendor/gix-transport/src/client/blocking_io/ssh/program_kind.rs
@@ -0,0 +1,127 @@
+use std::{ffi::OsStr, io::ErrorKind};
+
+use bstr::{BString, ByteSlice, ByteVec};
+
+use crate::{
+ client::{ssh, ssh::ProgramKind},
+ Protocol,
+};
+
+impl ProgramKind {
+ /// Provide the name of the executable that belongs to this kind, or `None` if the kind is `Simple`.
+ pub fn exe(&self) -> Option<&'static OsStr> {
+ Some(OsStr::new(match self {
+ ProgramKind::Ssh => "ssh",
+ ProgramKind::Plink => "plink",
+ ProgramKind::Putty => "putty",
+ ProgramKind::TortoisePlink => "tortoiseplink.exe",
+ ProgramKind::Simple => return None,
+ }))
+ }
+
+ /// Prepare all information needed to invoke the ssh command
+ pub(crate) fn prepare_invocation(
+ &self,
+ ssh_cmd: &OsStr,
+ url: &gix_url::Url,
+ desired_version: Protocol,
+ disallow_shell: bool,
+ ) -> Result<gix_command::Prepare, ssh::invocation::Error> {
+ let mut prepare = gix_command::prepare(ssh_cmd).with_shell();
+ if disallow_shell {
+ prepare.use_shell = false;
+ }
+ let host = url.host().expect("present in ssh urls");
+ match self {
+ ProgramKind::Ssh => {
+ if desired_version != Protocol::V1 {
+ prepare = prepare
+ .args(["-o", "SendEnv=GIT_PROTOCOL"])
+ .env("GIT_PROTOCOL", format!("version={}", desired_version as usize))
+ }
+ if let Some(port) = url.port {
+ prepare = prepare.arg(format!("-p{port}"));
+ }
+ }
+ ProgramKind::Plink | ProgramKind::Putty | ProgramKind::TortoisePlink => {
+ if *self == ProgramKind::TortoisePlink {
+ prepare = prepare.arg("-batch");
+ }
+ if let Some(port) = url.port {
+ prepare = prepare.arg("-P");
+ prepare = prepare.arg(port.to_string());
+ }
+ }
+ ProgramKind::Simple => {
+ if url.port.is_some() {
+ return Err(ssh::invocation::Error {
+ command: ssh_cmd.into(),
+ function: "setting the port",
+ });
+ }
+ }
+ };
+ let host_as_ssh_arg = match url.user() {
+ Some(user) => format!("{user}@{host}"),
+ None => host.into(),
+ };
+
+ // Try to force ssh to yield english messages (for parsing later)
+ Ok(prepare.arg(host_as_ssh_arg).env("LANG", "C").env("LC_ALL", "C"))
+ }
+
+ /// Note that the caller has to assure that the ssh program is launched in English by setting the locale.
+ pub(crate) fn line_to_err(&self, line: BString) -> Result<std::io::Error, BString> {
+ let kind = match self {
+ ProgramKind::Ssh | ProgramKind::Simple => {
+ if line.contains_str(b"Permission denied") || line.contains_str(b"permission denied") {
+ Some(ErrorKind::PermissionDenied)
+ } else if line.contains_str(b"resolve hostname") {
+ Some(ErrorKind::ConnectionRefused)
+ } else if line.contains_str(b"connect to host")
+ || line.contains_str("Connection to ")
+ || line.contains_str("Connection closed by ")
+ {
+ // TODO: turn this into HostUnreachable when stable, or NetworkUnreachable in 'no route' example.
+ // It's important that it WON'T be considered spurious, but is considered a permanent failure.
+ Some(ErrorKind::NotFound)
+ } else {
+ None
+ }
+ }
+ ProgramKind::Plink | ProgramKind::Putty | ProgramKind::TortoisePlink => {
+ if line.contains_str(b"publickey") {
+ Some(ErrorKind::PermissionDenied)
+ } else {
+ None
+ }
+ }
+ };
+ match kind {
+ Some(kind) => Ok(std::io::Error::new(kind, Vec::from(line).into_string_lossy())),
+ None => Err(line),
+ }
+ }
+}
+
+impl<'a> From<&'a OsStr> for ProgramKind {
+ fn from(v: &'a OsStr) -> Self {
+ let p = std::path::Path::new(v);
+ match p.file_stem().and_then(|s| s.to_str()) {
+ None => ProgramKind::Simple,
+ Some(stem) => {
+ if stem.eq_ignore_ascii_case("ssh") {
+ ProgramKind::Ssh
+ } else if stem.eq_ignore_ascii_case("plink") {
+ ProgramKind::Plink
+ } else if stem.eq_ignore_ascii_case("putty") {
+ ProgramKind::Putty
+ } else if stem.eq_ignore_ascii_case("tortoiseplink") {
+ ProgramKind::TortoisePlink
+ } else {
+ ProgramKind::Simple
+ }
+ }
+ }
+ }
+}
diff --git a/vendor/gix-transport/src/client/blocking_io/ssh/tests.rs b/vendor/gix-transport/src/client/blocking_io/ssh/tests.rs
new file mode 100644
index 000000000..27c661bd8
--- /dev/null
+++ b/vendor/gix-transport/src/client/blocking_io/ssh/tests.rs
@@ -0,0 +1,285 @@
+mod options {
+ mod ssh_command {
+ use crate::client::ssh::{connect::Options, ProgramKind};
+
+ #[test]
+ fn no_field_means_ssh() {
+ assert_eq!(Options::default().ssh_command(), "ssh");
+ }
+
+ #[test]
+ fn command_field_determines_ssh_command() {
+ assert_eq!(
+ Options {
+ command: Some("field-value".into()),
+ ..Default::default()
+ }
+ .ssh_command(),
+ "field-value"
+ );
+ assert_eq!(
+ Options {
+ command: Some("field-value".into()),
+ kind: Some(ProgramKind::TortoisePlink),
+ ..Default::default()
+ }
+ .ssh_command(),
+ "field-value"
+ );
+ }
+
+ #[test]
+ fn kind_serves_as_fallback() {
+ assert_eq!(
+ Options {
+ kind: Some(ProgramKind::TortoisePlink),
+ ..Default::default()
+ }
+ .ssh_command(),
+ "tortoiseplink.exe"
+ );
+ }
+ }
+}
+
+mod program_kind {
+ mod from_os_str {
+ use std::ffi::OsStr;
+
+ use crate::client::ssh::ProgramKind;
+
+ #[test]
+ fn known_variants_are_derived_from_basename() {
+ for name_or_path in [
+ "ssh",
+ "ssh.exe",
+ "SSH",
+ "SSH.exe",
+ "/bin/ssh",
+ "/bin/SSH",
+ #[cfg(windows)]
+ "c:\\bin\\ssh.exe",
+ ] {
+ assert_eq!(
+ ProgramKind::from(OsStr::new(name_or_path)),
+ ProgramKind::Ssh,
+ "{name_or_path:?} could not be identified correctly"
+ );
+ }
+ assert_eq!(
+ ProgramKind::from(OsStr::new("TortoisePlink.exe")),
+ ProgramKind::TortoisePlink
+ );
+ assert_eq!(ProgramKind::from(OsStr::new("putty")), ProgramKind::Putty);
+ assert_eq!(
+ ProgramKind::from(OsStr::new("../relative/Plink.exe")),
+ ProgramKind::Plink
+ );
+ }
+
+ #[test]
+ fn unknown_variants_fallback_to_simple() {
+ assert_eq!(
+ ProgramKind::from(OsStr::new("something-unknown-that-does-not-exist-for-sure-foobar")),
+ ProgramKind::Simple,
+ "in theory, we could fail right here but we don't and leave non-existing programs to fail during handshake"
+ );
+ }
+
+ #[test]
+ fn ssh_disguised_within_a_script_cannot_be_detected_due_to_invocation_with_dash_g() {
+ assert_eq!(
+ ProgramKind::from(OsStr::new("ssh -VVV")),
+ ProgramKind::Simple,
+ "we don't execute the command here but assume simple, even though we could determine it's ssh if we would do what git does here"
+ );
+ }
+ }
+
+ mod prepare_invocation {
+ use std::ffi::OsStr;
+
+ use crate::{
+ client::{ssh, ssh::ProgramKind},
+ Protocol,
+ };
+
+ #[test]
+ fn ssh() {
+ for (url, protocol, expected) in [
+ ("ssh://user@host:42/p", Protocol::V1, &["ssh", "-p42", "user@host"][..]),
+ ("ssh://user@host/p", Protocol::V1, &["ssh", "user@host"][..]),
+ ("ssh://host/p", Protocol::V1, &["ssh", "host"][..]),
+ (
+ "ssh://user@host:42/p",
+ Protocol::V2,
+ &["ssh", "-o", "SendEnv=GIT_PROTOCOL", "-p42", "user@host"][..],
+ ),
+ (
+ "ssh://user@host/p",
+ Protocol::V2,
+ &["ssh", "-o", "SendEnv=GIT_PROTOCOL", "user@host"][..],
+ ),
+ (
+ "ssh://host/p",
+ Protocol::V2,
+ &["ssh", "-o", "SendEnv=GIT_PROTOCOL", "host"][..],
+ ),
+ ] {
+ assert_eq!(call_args(ProgramKind::Ssh, url, protocol), quoted(expected));
+ }
+ }
+
+ #[test]
+ fn tortoise_plink_has_batch_command() {
+ assert_eq!(
+ call_args(ProgramKind::TortoisePlink, "ssh://user@host:42/p", Protocol::V2),
+ quoted(&["tortoiseplink.exe", "-batch", "-P", "42", "user@host"])
+ );
+ }
+
+ #[test]
+ fn port_for_all() {
+ for kind in [ProgramKind::TortoisePlink, ProgramKind::Plink, ProgramKind::Putty] {
+ assert!(call_args(kind, "ssh://user@host:43/p", Protocol::V2).ends_with(r#""-P" "43" "user@host""#));
+ }
+ }
+
+ #[test]
+ fn simple_cannot_handle_any_arguments() {
+ match try_call(ProgramKind::Simple, "ssh://user@host:42/p", Protocol::V2) {
+ Err(ssh::invocation::Error { .. }) => {}
+ _ => panic!("BUG: unexpected outcome"),
+ }
+ assert_eq!(
+ call_args(ProgramKind::Simple, "ssh://user@host/p", Protocol::V2),
+ quoted(&["simple", "user@host"]),
+ "simple can only do simple invocations"
+ );
+ }
+
+ #[test]
+ fn ssh_env_v2() {
+ let prepare = call(ProgramKind::Ssh, "ssh://host/p", Protocol::V2);
+ assert_eq!(
+ prepare.env,
+ &[
+ ("GIT_PROTOCOL".into(), "version=2".into()),
+ ("LANG".into(), "C".into()),
+ ("LC_ALL".into(), "C".into())
+ ]
+ );
+ assert!(!prepare.use_shell);
+ }
+
+ #[test]
+ fn disallow_shell_is_honored() -> Result {
+ let url = gix_url::parse("ssh://host/path".into()).expect("valid url");
+
+ let disallow_shell = false;
+ let prepare =
+ ProgramKind::Ssh.prepare_invocation(OsStr::new("echo hi"), &url, Protocol::V1, disallow_shell)?;
+ assert!(prepare.use_shell, "shells are used when needed");
+
+ let disallow_shell = true;
+ let prepare =
+ ProgramKind::Ssh.prepare_invocation(OsStr::new("echo hi"), &url, Protocol::V1, disallow_shell)?;
+ assert!(
+ !prepare.use_shell,
+ "but we can enforce it not to be used as well for historical reasons"
+ );
+ Ok(())
+ }
+
+ fn quoted(input: &[&str]) -> String {
+ input.iter().map(|s| format!("\"{s}\"")).collect::<Vec<_>>().join(" ")
+ }
+ fn try_call(
+ kind: ProgramKind,
+ url: &str,
+ version: Protocol,
+ ) -> std::result::Result<gix_command::Prepare, ssh::invocation::Error> {
+ let ssh_cmd = kind.exe().unwrap_or_else(|| OsStr::new("simple"));
+ let url = gix_url::parse(url.into()).expect("valid url");
+ kind.prepare_invocation(ssh_cmd, &url, version, false)
+ }
+ fn call(kind: ProgramKind, url: &str, version: Protocol) -> gix_command::Prepare {
+ try_call(kind, url, version).expect("no error")
+ }
+ fn call_args(kind: ProgramKind, url: &str, version: Protocol) -> String {
+ format!("{:?}", std::process::Command::from(call(kind, url, version)))
+ }
+
+ type Result = std::result::Result<(), ssh::invocation::Error>;
+ }
+
+ mod line_to_err {
+ use std::io::ErrorKind;
+
+ use crate::client::ssh::ProgramKind;
+
+ #[test]
+ fn all() {
+ for (kind, line, expected) in [
+ (
+ ProgramKind::Ssh,
+ "byron@github.com: Permission denied (publickey).",
+ ErrorKind::PermissionDenied,
+ ),
+ (
+ ProgramKind::Ssh,
+ "ssh: Could not resolve hostname hostfoobar: nodename nor servname provided, or not known",
+ ErrorKind::ConnectionRefused,
+ ),
+ (
+ ProgramKind::Ssh,
+ "ssh: connect to host example.org port 22: No route to host",
+ ErrorKind::NotFound,
+ ),
+ // connection closed by remote on windows
+ (
+ ProgramKind::Ssh,
+ "banner exchange: Connection to 127.0.0.1 port 61024: Software caused connection abort",
+ ErrorKind::NotFound,
+ ),
+ // connection closed by remote on unix
+ (
+ ProgramKind::Ssh,
+ "Connection closed by 127.0.0.1 port 8888", //
+ ErrorKind::NotFound,
+ ),
+ // this kind is basically unknown but we try our best, and simple equals ssh
+ (
+ ProgramKind::Simple,
+ "something permission denied something",
+ ErrorKind::PermissionDenied,
+ ),
+ (
+ ProgramKind::Simple,
+ "something resolve hostname hostfoobar: nodename nor servname something",
+ ErrorKind::ConnectionRefused,
+ ),
+ (
+ ProgramKind::Simple,
+ "something connect to host something",
+ ErrorKind::NotFound,
+ ),
+ ] {
+ assert_eq!(kind.line_to_err(line.into()).map(|err| err.kind()), Ok(expected));
+ }
+ }
+
+ #[test]
+ fn tortoiseplink_putty_plink() {
+ for kind in [ProgramKind::TortoisePlink, ProgramKind::Plink, ProgramKind::Putty] {
+ assert_eq!(
+ kind
+ .line_to_err("publickey".into())
+ .map(|err| err.kind()),
+ Ok(std::io::ErrorKind::PermissionDenied),
+ "this program pops up error messages in a window, no way to extract information from it. Maybe there is other ways to use it, 'publickey' they mention all"
+ );
+ }
+ }
+ }
+}
diff --git a/vendor/gix-transport/src/client/blocking_io/traits.rs b/vendor/gix-transport/src/client/blocking_io/traits.rs
new file mode 100644
index 000000000..7b2eaebbe
--- /dev/null
+++ b/vendor/gix-transport/src/client/blocking_io/traits.rs
@@ -0,0 +1,101 @@
+use std::{io::Write, ops::DerefMut};
+
+use bstr::BString;
+
+use crate::{
+ client::{Capabilities, Error, ExtendedBufRead, MessageKind, TransportWithoutIO, WriteMode},
+ Protocol, Service,
+};
+
+/// The response of the [`handshake()`][Transport::handshake()] method.
+pub struct SetServiceResponse<'a> {
+ /// The protocol the service can provide. May be different from the requested one
+ pub actual_protocol: Protocol,
+ /// The capabilities parsed from the server response.
+ pub capabilities: Capabilities,
+ /// In protocol version one, this is set to a list of refs and their peeled counterparts.
+ pub refs: Option<Box<dyn crate::client::ReadlineBufRead + 'a>>,
+}
+
+/// All methods provided here must be called in the correct order according to the [communication protocol][Protocol]
+/// used to connect to them.
+/// It does, however, know just enough to be able to provide a higher-level interface than would otherwise be possible.
+/// Thus the consumer of this trait will not have to deal with packet lines at all.
+/// **Note that** whenever a `Read` trait or `Write` trait is produced, it must be exhausted.
+pub trait Transport: TransportWithoutIO {
+ /// Initiate connection to the given service and send the given `extra_parameters` along with it.
+ ///
+ /// `extra_parameters` are interpreted as `key=value` pairs if the second parameter is `Some` or as `key`
+ /// if it is None.
+ ///
+ /// Returns the service capabilities according according to the actual [Protocol] it supports,
+ /// and possibly a list of refs to be obtained.
+ /// This means that asking for an unsupported protocol might result in a protocol downgrade to the given one
+ /// if [TransportWithoutIO::supported_protocol_versions()] includes it.
+ /// Exhaust the returned [BufReader][SetServiceResponse::refs] for a list of references in case of protocol V1
+ /// before making another request.
+ fn handshake<'a>(
+ &mut self,
+ service: Service,
+ extra_parameters: &'a [(&'a str, Option<&'a str>)],
+ ) -> Result<SetServiceResponse<'_>, Error>;
+}
+
+// Would be nice if the box implementation could auto-forward to all implemented traits.
+impl<T: Transport + ?Sized> Transport for Box<T> {
+ fn handshake<'a>(
+ &mut self,
+ service: Service,
+ extra_parameters: &'a [(&'a str, Option<&'a str>)],
+ ) -> Result<SetServiceResponse<'_>, Error> {
+ self.deref_mut().handshake(service, extra_parameters)
+ }
+}
+
+impl<T: Transport + ?Sized> Transport for &mut T {
+ fn handshake<'a>(
+ &mut self,
+ service: Service,
+ extra_parameters: &'a [(&'a str, Option<&'a str>)],
+ ) -> Result<SetServiceResponse<'_>, Error> {
+ self.deref_mut().handshake(service, extra_parameters)
+ }
+}
+
+/// An extension trait to add more methods to everything implementing [`Transport`].
+pub trait TransportV2Ext {
+ /// Invoke a protocol V2 style `command` with given `capabilities` and optional command specific `arguments`.
+ /// The `capabilities` were communicated during the handshake.
+ /// _Note:_ panics if [handshake][Transport::handshake()] wasn't performed beforehand.
+ fn invoke<'a>(
+ &mut self,
+ command: &str,
+ capabilities: impl Iterator<Item = (&'a str, Option<impl AsRef<str>>)> + 'a,
+ arguments: Option<impl Iterator<Item = bstr::BString>>,
+ ) -> Result<Box<dyn ExtendedBufRead + Unpin + '_>, Error>;
+}
+
+impl<T: Transport> TransportV2Ext for T {
+ fn invoke<'a>(
+ &mut self,
+ command: &str,
+ capabilities: impl Iterator<Item = (&'a str, Option<impl AsRef<str>>)> + 'a,
+ arguments: Option<impl Iterator<Item = BString>>,
+ ) -> Result<Box<dyn ExtendedBufRead + Unpin + '_>, Error> {
+ let mut writer = self.request(WriteMode::OneLfTerminatedLinePerWriteCall, MessageKind::Flush)?;
+ writer.write_all(format!("command={command}").as_bytes())?;
+ for (name, value) in capabilities {
+ match value {
+ Some(value) => writer.write_all(format!("{name}={}", value.as_ref()).as_bytes()),
+ None => writer.write_all(name.as_bytes()),
+ }?;
+ }
+ if let Some(arguments) = arguments {
+ writer.write_message(MessageKind::Delimiter)?;
+ for argument in arguments {
+ writer.write_all(argument.as_ref())?;
+ }
+ }
+ Ok(writer.into_read()?)
+ }
+}
diff --git a/vendor/gix-transport/src/client/capabilities.rs b/vendor/gix-transport/src/client/capabilities.rs
new file mode 100644
index 000000000..4c10dc100
--- /dev/null
+++ b/vendor/gix-transport/src/client/capabilities.rs
@@ -0,0 +1,307 @@
+use bstr::{BStr, BString, ByteSlice};
+
+#[cfg(any(feature = "blocking-client", feature = "async-client"))]
+use crate::client;
+use crate::Protocol;
+
+/// The error used in [`Capabilities::from_bytes()`] and [`Capabilities::from_lines()`].
+#[derive(Debug, thiserror::Error)]
+#[allow(missing_docs)]
+pub enum Error {
+ #[error("Capabilities were missing entirely as there was no 0 byte")]
+ MissingDelimitingNullByte,
+ #[error("there was not a single capability behind the delimiter")]
+ NoCapabilities,
+ #[error("a version line was expected, but none was retrieved")]
+ MissingVersionLine,
+ #[error("expected 'version X', got {0:?}")]
+ MalformattedVersionLine(BString),
+ #[error("Got unsupported version {actual:?}, expected {}", *desired as u8)]
+ UnsupportedVersion { desired: Protocol, actual: BString },
+ #[error("An IO error occurred while reading V2 lines")]
+ Io(#[from] std::io::Error),
+}
+
+/// A structure to represent multiple [capabilities][Capability] or features supported by the server.
+#[derive(Debug, Clone, Default)]
+#[cfg_attr(feature = "serde1", derive(serde::Serialize, serde::Deserialize))]
+pub struct Capabilities {
+ data: BString,
+ value_sep: u8,
+}
+
+/// The name of a single capability.
+pub struct Capability<'a>(&'a BStr);
+
+impl<'a> Capability<'a> {
+ /// Returns the name of the capability.
+ ///
+ /// Most capabilities only consist of a name, making them appear like a feature toggle.
+ pub fn name(&self) -> &'a BStr {
+ self.0
+ .splitn(2, |b| *b == b'=')
+ .next()
+ .expect("there is always a single item")
+ .as_bstr()
+ }
+ /// Returns the value associated with the capability.
+ ///
+ /// Note that the caller must know whether a single or multiple values are expected, in which
+ /// case [`values()`][Capability::values()] should be called.
+ pub fn value(&self) -> Option<&'a BStr> {
+ self.0.splitn(2, |b| *b == b'=').nth(1).map(|s| s.as_bstr())
+ }
+ /// Returns the values of a capability if its [`value()`][Capability::value()] is space separated.
+ pub fn values(&self) -> Option<impl Iterator<Item = &'a BStr>> {
+ self.value().map(|v| v.split(|b| *b == b' ').map(|s| s.as_bstr()))
+ }
+ /// Returns true if its space-separated [`value()`][Capability::value()] contains the given `want`ed capability.
+ pub fn supports(&self, want: impl Into<&'a BStr>) -> Option<bool> {
+ let want = want.into();
+ self.values().map(|mut iter| iter.any(|v| v == want))
+ }
+}
+
+impl Capabilities {
+ /// Parse capabilities from the given `bytes`.
+ ///
+ /// Useful in case they are encoded within a `ref` behind a null byte.
+ pub fn from_bytes(bytes: &[u8]) -> Result<(Capabilities, usize), Error> {
+ let delimiter_pos = bytes.find_byte(0).ok_or(Error::MissingDelimitingNullByte)?;
+ if delimiter_pos + 1 == bytes.len() {
+ return Err(Error::NoCapabilities);
+ }
+ let capabilities = &bytes[delimiter_pos + 1..];
+ Ok((
+ Capabilities {
+ data: capabilities.as_bstr().to_owned(),
+ value_sep: b' ',
+ },
+ delimiter_pos,
+ ))
+ }
+
+ /// Parse capabilities from the given a `lines_buf` which is expected to be all newline separated lines
+ /// from the server.
+ ///
+ /// Useful for parsing capabilities from a data sent from a server, and to avoid having to deal with
+ /// blocking and async traits for as long as possible. There is no value in parsing a few bytes
+ /// in a non-blocking fashion.
+ pub fn from_lines(lines_buf: BString) -> Result<Capabilities, Error> {
+ let mut lines = <_ as bstr::ByteSlice>::lines(lines_buf.as_slice().trim());
+ let version_line = lines.next().ok_or(Error::MissingVersionLine)?;
+ let (name, value) = version_line.split_at(
+ version_line
+ .find(b" ")
+ .ok_or_else(|| Error::MalformattedVersionLine(version_line.to_owned().into()))?,
+ );
+ if name != b"version" {
+ return Err(Error::MalformattedVersionLine(version_line.to_owned().into()));
+ }
+ if value != b" 2" {
+ return Err(Error::UnsupportedVersion {
+ desired: Protocol::V2,
+ actual: value.to_owned().into(),
+ });
+ }
+ Ok(Capabilities {
+ value_sep: b'\n',
+ data: lines.as_bytes().into(),
+ })
+ }
+
+ /// Returns true of the given `feature` is mentioned in this list of capabilities.
+ pub fn contains(&self, feature: &str) -> bool {
+ self.capability(feature).is_some()
+ }
+
+ /// Returns the capability with `name`.
+ pub fn capability(&self, name: &str) -> Option<Capability<'_>> {
+ self.iter().find(|c| c.name() == name.as_bytes().as_bstr())
+ }
+
+ /// Returns an iterator over all capabilities.
+ pub fn iter(&self) -> impl Iterator<Item = Capability<'_>> {
+ self.data
+ .split(move |b| *b == self.value_sep)
+ .map(|c| Capability(c.as_bstr()))
+ }
+}
+
+/// internal use
+#[cfg(any(feature = "blocking-client", feature = "async-client"))]
+impl Capabilities {
+ fn extract_protocol(capabilities_or_version: gix_packetline::TextRef<'_>) -> Result<Protocol, client::Error> {
+ let line = capabilities_or_version.as_bstr();
+ let version = if line.starts_with_str("version ") {
+ if line.len() != "version X".len() {
+ return Err(client::Error::UnsupportedProtocolVersion(line.as_bstr().into()));
+ }
+ match line {
+ line if line.ends_with_str("1") => Protocol::V1,
+ line if line.ends_with_str("2") => Protocol::V2,
+ _ => return Err(client::Error::UnsupportedProtocolVersion(line.as_bstr().into())),
+ }
+ } else {
+ Protocol::V1
+ };
+ Ok(version)
+ }
+}
+
+#[cfg(feature = "blocking-client")]
+///
+pub mod recv {
+ use std::io;
+
+ use bstr::ByteVec;
+
+ use crate::{client, client::Capabilities, Protocol};
+
+ /// Success outcome of [`Capabilities::from_lines_with_version_detection`].
+ pub struct Outcome<'a> {
+ /// The [`Capabilities`] the remote advertised.
+ pub capabilities: Capabilities,
+ /// The remote refs as a [`io::BufRead`].
+ ///
+ /// This is `Some` only when protocol v1 is used. The [`io::BufRead`] must be exhausted by
+ /// the caller.
+ pub refs: Option<Box<dyn crate::client::ReadlineBufRead + 'a>>,
+ /// The [`Protocol`] the remote advertised.
+ pub protocol: Protocol,
+ }
+
+ impl Capabilities {
+ /// Read the capabilities and version advertisement from the given packetline reader.
+ ///
+ /// If [`Protocol::V1`] was requested, or the remote decided to downgrade, the remote refs
+ /// advertisement will also be included in the [`Outcome`].
+ pub fn from_lines_with_version_detection<T: io::Read>(
+ rd: &mut gix_packetline::StreamingPeekableIter<T>,
+ ) -> Result<Outcome<'_>, client::Error> {
+ // NOTE that this is vitally important - it is turned on and stays on for all following requests so
+ // we automatically abort if the server sends an ERR line anywhere.
+ // We are sure this can't clash with binary data when sent due to the way the PACK
+ // format looks like, thus there is no binary blob that could ever look like an ERR line by accident.
+ rd.fail_on_err_lines(true);
+
+ let line = rd
+ .peek_line()
+ .ok_or(client::Error::ExpectedLine("capabilities or version"))???;
+ let line = line.as_text().ok_or(client::Error::ExpectedLine("text"))?;
+
+ let version = Capabilities::extract_protocol(line)?;
+ match version {
+ Protocol::V1 => {
+ let (capabilities, delimiter_position) = Capabilities::from_bytes(line.0)?;
+ rd.peek_buffer_replace_and_truncate(delimiter_position, b'\n');
+ Ok(Outcome {
+ capabilities,
+ refs: Some(Box::new(rd.as_read())),
+ protocol: Protocol::V1,
+ })
+ }
+ Protocol::V2 => Ok(Outcome {
+ capabilities: {
+ let mut rd = rd.as_read();
+ let mut buf = Vec::new();
+ while let Some(line) = rd.read_data_line() {
+ let line = line??;
+ match line.as_bstr() {
+ Some(line) => {
+ buf.push_str(line);
+ if buf.last() != Some(&b'\n') {
+ buf.push(b'\n');
+ }
+ }
+ None => break,
+ }
+ }
+ Capabilities::from_lines(buf.into())?
+ },
+ refs: None,
+ protocol: Protocol::V2,
+ }),
+ }
+ }
+ }
+}
+
+#[cfg(feature = "async-client")]
+#[allow(missing_docs)]
+///
+pub mod recv {
+ use bstr::ByteVec;
+ use futures_io::AsyncRead;
+
+ use crate::{client, client::Capabilities, Protocol};
+
+ /// Success outcome of [`Capabilities::from_lines_with_version_detection`].
+ pub struct Outcome<'a> {
+ /// The [`Capabilities`] the remote advertised.
+ pub capabilities: Capabilities,
+ /// The remote refs as an [`AsyncBufRead`].
+ ///
+ /// This is `Some` only when protocol v1 is used. The [`AsyncBufRead`] must be exhausted by
+ /// the caller.
+ pub refs: Option<Box<dyn crate::client::ReadlineBufRead + Unpin + 'a>>,
+ /// The [`Protocol`] the remote advertised.
+ pub protocol: Protocol,
+ }
+
+ impl Capabilities {
+ /// Read the capabilities and version advertisement from the given packetline reader.
+ ///
+ /// If [`Protocol::V1`] was requested, or the remote decided to downgrade, the remote refs
+ /// advertisement will also be included in the [`Outcome`].
+ pub async fn from_lines_with_version_detection<T: AsyncRead + Unpin>(
+ rd: &mut gix_packetline::StreamingPeekableIter<T>,
+ ) -> Result<Outcome<'_>, client::Error> {
+ // NOTE that this is vitally important - it is turned on and stays on for all following requests so
+ // we automatically abort if the server sends an ERR line anywhere.
+ // We are sure this can't clash with binary data when sent due to the way the PACK
+ // format looks like, thus there is no binary blob that could ever look like an ERR line by accident.
+ rd.fail_on_err_lines(true);
+
+ let line = rd
+ .peek_line()
+ .await
+ .ok_or(client::Error::ExpectedLine("capabilities or version"))???;
+ let line = line.as_text().ok_or(client::Error::ExpectedLine("text"))?;
+
+ let version = Capabilities::extract_protocol(line)?;
+ match version {
+ Protocol::V1 => {
+ let (capabilities, delimiter_position) = Capabilities::from_bytes(line.0)?;
+ rd.peek_buffer_replace_and_truncate(delimiter_position, b'\n');
+ Ok(Outcome {
+ capabilities,
+ refs: Some(Box::new(rd.as_read())),
+ protocol: Protocol::V1,
+ })
+ }
+ Protocol::V2 => Ok(Outcome {
+ capabilities: {
+ let mut rd = rd.as_read();
+ let mut buf = Vec::new();
+ while let Some(line) = rd.read_data_line().await {
+ let line = line??;
+ match line.as_bstr() {
+ Some(line) => {
+ buf.push_str(line);
+ if buf.last() != Some(&b'\n') {
+ buf.push(b'\n');
+ }
+ }
+ None => break,
+ }
+ }
+ Capabilities::from_lines(buf.into())?
+ },
+ refs: None,
+ protocol: Protocol::V2,
+ }),
+ }
+ }
+ }
+}
diff --git a/vendor/gix-transport/src/client/git/async_io.rs b/vendor/gix-transport/src/client/git/async_io.rs
new file mode 100644
index 000000000..00bea41a3
--- /dev/null
+++ b/vendor/gix-transport/src/client/git/async_io.rs
@@ -0,0 +1,151 @@
+use std::{borrow::Cow, error::Error};
+
+use async_trait::async_trait;
+use bstr::{BStr, BString, ByteVec};
+use futures_io::{AsyncRead, AsyncWrite};
+use futures_lite::AsyncWriteExt;
+use gix_packetline::PacketLineRef;
+
+use crate::{
+ client::{self, capabilities, git, Capabilities, SetServiceResponse},
+ Protocol, Service,
+};
+
+impl<R, W> client::TransportWithoutIO for git::Connection<R, W>
+where
+ R: AsyncRead + Unpin,
+ W: AsyncWrite + Unpin,
+{
+ fn request(
+ &mut self,
+ write_mode: client::WriteMode,
+ on_into_read: client::MessageKind,
+ ) -> Result<client::RequestWriter<'_>, client::Error> {
+ Ok(client::RequestWriter::new_from_bufread(
+ &mut self.writer,
+ Box::new(self.line_provider.as_read_without_sidebands()),
+ write_mode,
+ on_into_read,
+ ))
+ }
+ fn to_url(&self) -> Cow<'_, BStr> {
+ self.custom_url.as_ref().map_or_else(
+ || {
+ let mut possibly_lossy_url = self.path.clone();
+ possibly_lossy_url.insert_str(0, "file://");
+ Cow::Owned(possibly_lossy_url)
+ },
+ |url| Cow::Borrowed(url.as_ref()),
+ )
+ }
+
+ fn connection_persists_across_multiple_requests(&self) -> bool {
+ true
+ }
+
+ fn configure(&mut self, _config: &dyn std::any::Any) -> Result<(), Box<dyn Error + Send + Sync + 'static>> {
+ Ok(())
+ }
+}
+
+#[async_trait(?Send)]
+impl<R, W> client::Transport for git::Connection<R, W>
+where
+ R: AsyncRead + Unpin,
+ W: AsyncWrite + Unpin,
+{
+ async fn handshake<'a>(
+ &mut self,
+ service: Service,
+ extra_parameters: &'a [(&'a str, Option<&'a str>)],
+ ) -> Result<SetServiceResponse<'_>, client::Error> {
+ if self.mode == git::ConnectMode::Daemon {
+ let mut line_writer = gix_packetline::Writer::new(&mut self.writer).binary_mode();
+ line_writer
+ .write_all(&git::message::connect(
+ service,
+ self.desired_version,
+ &self.path,
+ self.virtual_host.as_ref(),
+ extra_parameters,
+ ))
+ .await?;
+ line_writer.flush().await?;
+ }
+
+ let capabilities::recv::Outcome {
+ capabilities,
+ refs,
+ protocol: actual_protocol,
+ } = Capabilities::from_lines_with_version_detection(&mut self.line_provider).await?;
+ Ok(SetServiceResponse {
+ actual_protocol,
+ capabilities,
+ refs,
+ })
+ }
+}
+
+impl<R, W> git::Connection<R, W>
+where
+ R: AsyncRead + Unpin,
+ W: AsyncWrite + Unpin,
+{
+ /// Create a connection from the given `read` and `write`, asking for `desired_version` as preferred protocol
+ /// and the transfer of the repository at `repository_path`.
+ ///
+ /// `virtual_host` along with a port to which to connect to, while `mode` determines the kind of endpoint to connect to.
+ pub fn new(
+ read: R,
+ write: W,
+ desired_version: Protocol,
+ repository_path: impl Into<BString>,
+ virtual_host: Option<(impl Into<String>, Option<u16>)>,
+ mode: git::ConnectMode,
+ ) -> Self {
+ git::Connection {
+ writer: write,
+ line_provider: gix_packetline::StreamingPeekableIter::new(read, &[PacketLineRef::Flush]),
+ path: repository_path.into(),
+ virtual_host: virtual_host.map(|(h, p)| (h.into(), p)),
+ desired_version,
+ custom_url: None,
+ mode,
+ }
+ }
+}
+
+#[cfg(feature = "async-std")]
+mod async_net {
+ use std::time::Duration;
+
+ use async_std::net::TcpStream;
+
+ use crate::client::{git, Error};
+
+ impl git::Connection<TcpStream, TcpStream> {
+ /// Create a new TCP connection using the `git` protocol of `desired_version`, and make a connection to `host`
+ /// at `port` for accessing the repository at `path` on the server side.
+ pub async fn new_tcp(
+ host: &str,
+ port: Option<u16>,
+ path: bstr::BString,
+ desired_version: crate::Protocol,
+ ) -> Result<git::Connection<TcpStream, TcpStream>, Error> {
+ let read = async_std::io::timeout(
+ Duration::from_secs(5),
+ TcpStream::connect(&(host, port.unwrap_or(9418))),
+ )
+ .await?;
+ let write = read.clone();
+ Ok(git::Connection::new(
+ read,
+ write,
+ desired_version,
+ path,
+ None::<(String, _)>,
+ git::ConnectMode::Daemon,
+ ))
+ }
+ }
+}
diff --git a/vendor/gix-transport/src/client/git/blocking_io.rs b/vendor/gix-transport/src/client/git/blocking_io.rs
new file mode 100644
index 000000000..8087b8aea
--- /dev/null
+++ b/vendor/gix-transport/src/client/git/blocking_io.rs
@@ -0,0 +1,199 @@
+use std::{any::Any, borrow::Cow, error::Error, io::Write};
+
+use bstr::{BStr, BString, ByteVec};
+use gix_packetline::PacketLineRef;
+
+use crate::{
+ client::{self, capabilities, git, Capabilities, SetServiceResponse},
+ Protocol, Service,
+};
+
+impl<R, W> client::TransportWithoutIO for git::Connection<R, W>
+where
+ R: std::io::Read,
+ W: std::io::Write,
+{
+ fn request(
+ &mut self,
+ write_mode: client::WriteMode,
+ on_into_read: client::MessageKind,
+ ) -> Result<client::RequestWriter<'_>, client::Error> {
+ Ok(client::RequestWriter::new_from_bufread(
+ &mut self.writer,
+ Box::new(self.line_provider.as_read_without_sidebands()),
+ write_mode,
+ on_into_read,
+ ))
+ }
+
+ fn to_url(&self) -> Cow<'_, BStr> {
+ self.custom_url.as_ref().map_or_else(
+ || {
+ let mut possibly_lossy_url = self.path.clone();
+ possibly_lossy_url.insert_str(0, "file://");
+ Cow::Owned(possibly_lossy_url)
+ },
+ |url| Cow::Borrowed(url.as_ref()),
+ )
+ }
+
+ fn connection_persists_across_multiple_requests(&self) -> bool {
+ true
+ }
+
+ fn configure(&mut self, _config: &dyn Any) -> Result<(), Box<dyn Error + Send + Sync + 'static>> {
+ Ok(())
+ }
+}
+
+impl<R, W> client::Transport for git::Connection<R, W>
+where
+ R: std::io::Read,
+ W: std::io::Write,
+{
+ fn handshake<'a>(
+ &mut self,
+ service: Service,
+ extra_parameters: &'a [(&'a str, Option<&'a str>)],
+ ) -> Result<SetServiceResponse<'_>, client::Error> {
+ if self.mode == git::ConnectMode::Daemon {
+ let mut line_writer = gix_packetline::Writer::new(&mut self.writer).binary_mode();
+ line_writer.write_all(&git::message::connect(
+ service,
+ self.desired_version,
+ &self.path,
+ self.virtual_host.as_ref(),
+ extra_parameters,
+ ))?;
+ line_writer.flush()?;
+ }
+
+ let capabilities::recv::Outcome {
+ capabilities,
+ refs,
+ protocol: actual_protocol,
+ } = Capabilities::from_lines_with_version_detection(&mut self.line_provider)?;
+ Ok(SetServiceResponse {
+ actual_protocol,
+ capabilities,
+ refs,
+ })
+ }
+}
+
+impl<R, W> git::Connection<R, W>
+where
+ R: std::io::Read,
+ W: std::io::Write,
+{
+ /// Create a connection from the given `read` and `write`, asking for `desired_version` as preferred protocol
+ /// and the transfer of the repository at `repository_path`.
+ ///
+ /// `virtual_host` along with a port to which to connect to, while `mode` determines the kind of endpoint to connect to.
+ pub fn new(
+ read: R,
+ write: W,
+ desired_version: Protocol,
+ repository_path: impl Into<BString>,
+ virtual_host: Option<(impl Into<String>, Option<u16>)>,
+ mode: git::ConnectMode,
+ ) -> Self {
+ git::Connection {
+ writer: write,
+ line_provider: gix_packetline::StreamingPeekableIter::new(read, &[PacketLineRef::Flush]),
+ path: repository_path.into(),
+ virtual_host: virtual_host.map(|(h, p)| (h.into(), p)),
+ desired_version,
+ custom_url: None,
+ mode,
+ }
+ }
+ pub(crate) fn new_for_spawned_process(
+ reader: R,
+ writer: W,
+ desired_version: Protocol,
+ repository_path: impl Into<BString>,
+ ) -> Self {
+ Self::new(
+ reader,
+ writer,
+ desired_version,
+ repository_path,
+ None::<(&str, _)>,
+ git::ConnectMode::Process,
+ )
+ }
+}
+
+///
+pub mod connect {
+ use std::net::{TcpStream, ToSocketAddrs};
+
+ use bstr::BString;
+
+ use crate::client::git;
+ /// The error used in [`connect()`].
+ #[derive(Debug, thiserror::Error)]
+ #[allow(missing_docs)]
+ pub enum Error {
+ #[error("An IO error occurred when connecting to the server")]
+ Io(#[from] std::io::Error),
+ #[error("Could not parse {host:?} as virtual host with format <host>[:port]")]
+ VirtualHostInvalid { host: String },
+ }
+
+ impl crate::IsSpuriousError for Error {
+ fn is_spurious(&self) -> bool {
+ match self {
+ Error::Io(err) => err.is_spurious(),
+ _ => false,
+ }
+ }
+ }
+
+ fn parse_host(input: String) -> Result<(String, Option<u16>), Error> {
+ let mut tokens = input.splitn(2, ':');
+ Ok(match (tokens.next(), tokens.next()) {
+ (Some(host), None) => (host.to_owned(), None),
+ (Some(host), Some(port)) => (
+ host.to_owned(),
+ Some(port.parse().map_err(|_| Error::VirtualHostInvalid { host: input })?),
+ ),
+ _ => unreachable!("we expect at least one token, the original string"),
+ })
+ }
+
+ /// Connect to a git daemon running on `host` and optionally `port` and a repository at `path`.
+ ///
+ /// Use `desired_version` to specify a preferred protocol to use, knowing that it can be downgraded by a server not supporting it.
+ pub fn connect(
+ host: &str,
+ path: BString,
+ desired_version: crate::Protocol,
+ port: Option<u16>,
+ ) -> Result<git::Connection<TcpStream, TcpStream>, Error> {
+ let read = TcpStream::connect_timeout(
+ &(host, port.unwrap_or(9418))
+ .to_socket_addrs()?
+ .next()
+ .expect("after successful resolution there is an IP address"),
+ std::time::Duration::from_secs(5),
+ )?;
+ let write = read.try_clone()?;
+ let vhost = std::env::var("GIT_OVERRIDE_VIRTUAL_HOST")
+ .ok()
+ .map(parse_host)
+ .transpose()?
+ .unwrap_or_else(|| (host.to_owned(), port));
+ Ok(git::Connection::new(
+ read,
+ write,
+ desired_version,
+ path,
+ Some(vhost),
+ git::ConnectMode::Daemon,
+ ))
+ }
+}
+
+pub use connect::connect;
diff --git a/vendor/gix-transport/src/client/git/mod.rs b/vendor/gix-transport/src/client/git/mod.rs
new file mode 100644
index 000000000..2b950b44a
--- /dev/null
+++ b/vendor/gix-transport/src/client/git/mod.rs
@@ -0,0 +1,177 @@
+use bstr::BString;
+
+use crate::Protocol;
+
+/// The way to connect to a process speaking the `git` protocol.
+#[derive(PartialEq, Eq, Clone, Copy)]
+pub enum ConnectMode {
+ /// A git daemon.
+ Daemon,
+ /// A spawned `git` process to upload a pack to the client.
+ Process,
+}
+
+/// A TCP connection to either a `git` daemon or a spawned `git` process.
+///
+/// When connecting to a daemon, additional context information is sent with the first line of the handshake. Otherwise that
+/// context is passed using command line arguments to a [spawned `git` process][crate::client::file::SpawnProcessOnDemand].
+pub struct Connection<R, W> {
+ pub(in crate::client) writer: W,
+ pub(in crate::client) line_provider: gix_packetline::StreamingPeekableIter<R>,
+ pub(in crate::client) path: BString,
+ pub(in crate::client) virtual_host: Option<(String, Option<u16>)>,
+ pub(in crate::client) desired_version: Protocol,
+ custom_url: Option<BString>,
+ pub(in crate::client) mode: ConnectMode,
+}
+
+impl<R, W> Connection<R, W> {
+ /// Return the inner reader and writer
+ pub fn into_inner(self) -> (R, W) {
+ (self.line_provider.into_inner(), self.writer)
+ }
+
+ /// Optionally set the URL to be returned when asked for it if `Some` or calculate a default for `None`.
+ ///
+ /// The URL is required as parameter for authentication helpers which are called in transports
+ /// that support authentication. Even though plain git transports don't support that, this
+ /// may well be the case in custom transports.
+ pub fn custom_url(mut self, url: Option<BString>) -> Self {
+ self.custom_url = url;
+ self
+ }
+}
+
+mod message {
+ use bstr::{BString, ByteVec};
+
+ use crate::{Protocol, Service};
+
+ pub fn connect(
+ service: Service,
+ desired_version: Protocol,
+ path: &[u8],
+ virtual_host: Option<&(String, Option<u16>)>,
+ extra_parameters: &[(&str, Option<&str>)],
+ ) -> BString {
+ let mut out = bstr::BString::from(service.as_str());
+ out.push(b' ');
+ let path = gix_url::expand_path::for_shell(path.into());
+ out.extend_from_slice(&path);
+ out.push(0);
+ if let Some((host, port)) = virtual_host {
+ out.push_str("host=");
+ out.extend_from_slice(host.as_bytes());
+ if let Some(port) = port {
+ out.push_byte(b':');
+ out.push_str(&format!("{port}"));
+ }
+ out.push(0);
+ }
+ // We only send the version when needed, as otherwise a V2 server who is asked for V1 will respond with 'version 1'
+ // as extra lines in the reply, which we don't want to handle. Especially since an old server will not respond with that
+ // line (is what I assume, at least), so it's an optional part in the response to understand and handle. There is no value
+ // in that, so let's help V2 servers to respond in a way that assumes V1.
+ let extra_params_need_null_prefix = if desired_version != Protocol::V1 {
+ out.push(0);
+ out.push_str(format!("version={}", desired_version as usize));
+ out.push(0);
+ false
+ } else {
+ true
+ };
+
+ if !extra_parameters.is_empty() {
+ if extra_params_need_null_prefix {
+ out.push(0);
+ }
+ for (key, value) in extra_parameters {
+ match value {
+ Some(value) => out.push_str(format!("{key}={value}")),
+ None => out.push_str(key),
+ }
+ out.push(0);
+ }
+ }
+ out
+ }
+ #[cfg(test)]
+ mod tests {
+ use crate::{client::git, Protocol, Service};
+
+ #[test]
+ fn version_1_without_host_and_version() {
+ assert_eq!(
+ git::message::connect(Service::UploadPack, Protocol::V1, b"hello/world", None, &[]),
+ "git-upload-pack hello/world\0"
+ )
+ }
+ #[test]
+ fn version_2_without_host_and_version() {
+ assert_eq!(
+ git::message::connect(Service::UploadPack, Protocol::V2, b"hello\\world", None, &[]),
+ "git-upload-pack hello\\world\0\0version=2\0"
+ )
+ }
+ #[test]
+ fn version_2_without_host_and_version_and_exta_parameters() {
+ assert_eq!(
+ git::message::connect(
+ Service::UploadPack,
+ Protocol::V2,
+ b"/path/project.git",
+ None,
+ &[("key", Some("value")), ("value-only", None)]
+ ),
+ "git-upload-pack /path/project.git\0\0version=2\0key=value\0value-only\0"
+ )
+ }
+ #[test]
+ fn with_host_without_port() {
+ assert_eq!(
+ git::message::connect(
+ Service::UploadPack,
+ Protocol::V1,
+ b"hello\\world",
+ Some(&("host".into(), None)),
+ &[]
+ ),
+ "git-upload-pack hello\\world\0host=host\0"
+ )
+ }
+ #[test]
+ fn with_host_without_port_and_extra_parameters() {
+ assert_eq!(
+ git::message::connect(
+ Service::UploadPack,
+ Protocol::V1,
+ b"hello\\world",
+ Some(&("host".into(), None)),
+ &[("key", Some("value")), ("value-only", None)]
+ ),
+ "git-upload-pack hello\\world\0host=host\0\0key=value\0value-only\0"
+ )
+ }
+ #[test]
+ fn with_host_with_port() {
+ assert_eq!(
+ git::message::connect(
+ Service::UploadPack,
+ Protocol::V1,
+ b"hello\\world",
+ Some(&("host".into(), Some(404))),
+ &[]
+ ),
+ "git-upload-pack hello\\world\0host=host:404\0"
+ )
+ }
+ }
+}
+
+#[cfg(feature = "async-client")]
+mod async_io;
+
+#[cfg(feature = "blocking-client")]
+mod blocking_io;
+#[cfg(feature = "blocking-client")]
+pub use blocking_io::connect;
diff --git a/vendor/gix-transport/src/client/mod.rs b/vendor/gix-transport/src/client/mod.rs
new file mode 100644
index 000000000..b5296543e
--- /dev/null
+++ b/vendor/gix-transport/src/client/mod.rs
@@ -0,0 +1,36 @@
+#[cfg(feature = "async-client")]
+mod async_io;
+#[cfg(feature = "async-client")]
+pub use async_io::{
+ connect, ExtendedBufRead, HandleProgress, ReadlineBufRead, RequestWriter, SetServiceResponse, Transport,
+ TransportV2Ext,
+};
+
+mod traits;
+pub use traits::TransportWithoutIO;
+
+#[cfg(feature = "blocking-client")]
+mod blocking_io;
+#[cfg(feature = "http-client")]
+pub use blocking_io::http;
+#[cfg(feature = "blocking-client")]
+pub use blocking_io::{
+ connect, file, ssh, ExtendedBufRead, HandleProgress, ReadlineBufRead, RequestWriter, SetServiceResponse, Transport,
+ TransportV2Ext,
+};
+#[cfg(feature = "blocking-client")]
+#[doc(inline)]
+pub use connect::function::connect;
+
+///
+pub mod capabilities;
+#[doc(inline)]
+pub use capabilities::Capabilities;
+
+mod non_io_types;
+pub use gix_sec::identity::Account;
+pub use non_io_types::{Error, MessageKind, WriteMode};
+
+///
+#[cfg(any(feature = "blocking-client", feature = "async-client"))]
+pub mod git;
diff --git a/vendor/gix-transport/src/client/non_io_types.rs b/vendor/gix-transport/src/client/non_io_types.rs
new file mode 100644
index 000000000..a207f7f0b
--- /dev/null
+++ b/vendor/gix-transport/src/client/non_io_types.rs
@@ -0,0 +1,159 @@
+/// Configure how the [`RequestWriter`][crate::client::RequestWriter] behaves when writing bytes.
+#[derive(PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone, Copy)]
+#[cfg_attr(feature = "serde1", derive(serde::Serialize, serde::Deserialize))]
+pub enum WriteMode {
+ /// Each [write()][std::io::Write::write()] call writes the bytes verbatim as one or more packet lines.
+ ///
+ /// This mode also indicates to the transport that it should try to stream data as it is unbounded. This mode is typically used
+ /// for sending packs whose exact size is not necessarily known in advance.
+ Binary,
+ /// Each [write()][std::io::Write::write()] call assumes text in the input, assures a trailing newline and writes it as single packet line.
+ ///
+ /// This mode also indicates that the lines written fit into memory, hence the transport may chose to not stream it but to buffer it
+ /// instead. This is relevant for some transports, like the one for HTTP.
+ OneLfTerminatedLinePerWriteCall,
+}
+
+impl Default for WriteMode {
+ fn default() -> Self {
+ WriteMode::OneLfTerminatedLinePerWriteCall
+ }
+}
+
+/// The kind of packet line to write when transforming a [`RequestWriter`][crate::client::RequestWriter] into an
+/// [`ExtendedBufRead`][crate::client::ExtendedBufRead].
+#[derive(PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone, Copy)]
+#[cfg_attr(feature = "serde1", derive(serde::Serialize, serde::Deserialize))]
+pub enum MessageKind {
+ /// A `flush` packet.
+ Flush,
+ /// A V2 delimiter.
+ Delimiter,
+ /// The end of a response.
+ ResponseEnd,
+ /// The given text.
+ Text(&'static [u8]),
+}
+
+#[cfg(any(feature = "blocking-client", feature = "async-client"))]
+pub(crate) mod connect {
+ /// Options for connecting to a remote.
+ #[derive(Debug, Default, Clone)]
+ pub struct Options {
+ /// Use `version` to set the desired protocol version to use when connecting, but note that the server may downgrade it.
+ pub version: crate::Protocol,
+ #[cfg(feature = "blocking-client")]
+ /// Options to use if the scheme of the URL is `ssh`.
+ pub ssh: crate::client::ssh::connect::Options,
+ }
+
+ /// The error used in [`connect()`][crate::connect()].
+ #[derive(Debug, thiserror::Error)]
+ #[allow(missing_docs)]
+ pub enum Error {
+ #[error(transparent)]
+ Url(#[from] gix_url::parse::Error),
+ #[error("The git repository path could not be converted to UTF8")]
+ PathConversion(#[from] bstr::Utf8Error),
+ #[error("connection failed")]
+ Connection(#[from] Box<dyn std::error::Error + Send + Sync>),
+ #[error("The url {url:?} contains information that would not be used by the {scheme} protocol")]
+ UnsupportedUrlTokens {
+ url: bstr::BString,
+ scheme: gix_url::Scheme,
+ },
+ #[error("The '{0}' protocol is currently unsupported")]
+ UnsupportedScheme(gix_url::Scheme),
+ #[cfg(not(any(feature = "http-client-curl", feature = "http-client-reqwest")))]
+ #[error(
+ "'{0}' is not compiled in. Compile with the 'http-client-curl' or 'http-client-reqwest' cargo feature"
+ )]
+ CompiledWithoutHttp(gix_url::Scheme),
+ }
+
+ // TODO: maybe fix this workaround: want `IsSpuriousError` in `Connection(…)`
+ impl crate::IsSpuriousError for Error {
+ fn is_spurious(&self) -> bool {
+ match self {
+ Error::Connection(err) => {
+ #[cfg(feature = "blocking-client")]
+ if let Some(err) = err.downcast_ref::<crate::client::git::connect::Error>() {
+ return err.is_spurious();
+ };
+ if let Some(err) = err.downcast_ref::<crate::client::Error>() {
+ return err.is_spurious();
+ }
+ false
+ }
+ _ => false,
+ }
+ }
+ }
+}
+
+mod error {
+ use std::ffi::OsString;
+
+ use bstr::BString;
+
+ use crate::client::capabilities;
+ #[cfg(feature = "http-client")]
+ use crate::client::http;
+ #[cfg(feature = "blocking-client")]
+ use crate::client::ssh;
+
+ #[cfg(feature = "http-client")]
+ type HttpError = http::Error;
+ #[cfg(feature = "blocking-client")]
+ type SshInvocationError = ssh::invocation::Error;
+ #[cfg(not(feature = "http-client"))]
+ type HttpError = std::convert::Infallible;
+ #[cfg(not(feature = "blocking-client"))]
+ type SshInvocationError = std::convert::Infallible;
+
+ /// The error used in most methods of the [`client`][crate::client] module
+ #[derive(thiserror::Error, Debug)]
+ #[allow(missing_docs)]
+ pub enum Error {
+ #[error("An IO error occurred when talking to the server")]
+ Io(#[from] std::io::Error),
+ #[error("Capabilities could not be parsed")]
+ Capabilities {
+ #[from]
+ err: capabilities::Error,
+ },
+ #[error("A packet line could not be decoded")]
+ LineDecode {
+ #[from]
+ err: gix_packetline::decode::Error,
+ },
+ #[error("A {0} line was expected, but there was none")]
+ ExpectedLine(&'static str),
+ #[error("Expected a data line, but got a delimiter")]
+ ExpectedDataLine,
+ #[error("The transport layer does not support authentication")]
+ AuthenticationUnsupported,
+ #[error("The transport layer refuses to use a given identity: {0}")]
+ AuthenticationRefused(&'static str),
+ #[error("The protocol version indicated by {:?} is unsupported", {0})]
+ UnsupportedProtocolVersion(BString),
+ #[error("Failed to invoke program {command:?}")]
+ InvokeProgram { source: std::io::Error, command: OsString },
+ #[error(transparent)]
+ Http(#[from] HttpError),
+ #[error(transparent)]
+ SshInvocation(SshInvocationError),
+ }
+
+ impl crate::IsSpuriousError for Error {
+ fn is_spurious(&self) -> bool {
+ match self {
+ Error::Io(err) => err.is_spurious(),
+ Error::Http(err) => err.is_spurious(),
+ _ => false,
+ }
+ }
+ }
+}
+
+pub use error::Error;
diff --git a/vendor/gix-transport/src/client/traits.rs b/vendor/gix-transport/src/client/traits.rs
new file mode 100644
index 000000000..408935391
--- /dev/null
+++ b/vendor/gix-transport/src/client/traits.rs
@@ -0,0 +1,115 @@
+use std::{
+ any::Any,
+ borrow::Cow,
+ ops::{Deref, DerefMut},
+};
+
+use bstr::BStr;
+
+#[cfg(any(feature = "blocking-client", feature = "async-client"))]
+use crate::client::{MessageKind, RequestWriter, WriteMode};
+use crate::{client::Error, Protocol};
+
+/// This trait represents all transport related functions that don't require any input/output to be done which helps
+/// implementation to share more code across blocking and async programs.
+pub trait TransportWithoutIO {
+ /// If the handshake or subsequent reads failed with [std::io::ErrorKind::PermissionDenied], use this method to
+ /// inform the transport layer about the identity to use for subsequent calls.
+ /// If authentication continues to fail even with an identity set, consider communicating this to the provider
+ /// of the identity in order to mark it as invalid. Otherwise the user might have difficulty updating obsolete
+ /// credentials.
+ /// Please note that most transport layers are unauthenticated and thus return [an error][Error::AuthenticationUnsupported] here.
+ fn set_identity(&mut self, _identity: gix_sec::identity::Account) -> Result<(), Error> {
+ Err(Error::AuthenticationUnsupported)
+ }
+ /// Get a writer for sending data and obtaining the response. It can be configured in various ways
+ /// to support the task at hand.
+ /// `write_mode` determines how calls to the `write(…)` method are interpreted, and `on_into_read` determines
+ /// which message to write when the writer is turned into the response reader using [`into_read()`][RequestWriter::into_read()].
+ #[cfg(any(feature = "blocking-client", feature = "async-client"))]
+ fn request(&mut self, write_mode: WriteMode, on_into_read: MessageKind) -> Result<RequestWriter<'_>, Error>;
+
+ /// Returns the canonical URL pointing to the destination of this transport.
+ fn to_url(&self) -> Cow<'_, BStr>;
+
+ /// If the actually advertised server version is contained in the returned slice or it is empty, continue as normal,
+ /// assume the server's protocol version is desired or acceptable.
+ ///
+ /// Otherwise, abort the fetch operation with an error to avoid continuing any interaction with the transport.
+ ///
+ /// In V1 this means a potentially large list of advertised refs won't be read, instead the connection is ignored
+ /// leaving the server with a client who potentially unexpectedly terminated the connection.
+ ///
+ /// Note that `transport.close()` is not called explicitly.
+ ///
+ /// Custom transports can override this to prevent any use of older protocol versions.
+ fn supported_protocol_versions(&self) -> &[Protocol] {
+ &[]
+ }
+
+ /// Returns true if the transport provides persistent connections across multiple requests, or false otherwise.
+ /// Not being persistent implies that certain information has to be resent on each 'turn'
+ /// of the fetch negotiation or that the end of interaction (i.e. no further request will be made) has to be indicated
+ /// to the server for most graceful termination of the connection.
+ fn connection_persists_across_multiple_requests(&self) -> bool;
+
+ /// Pass `config` can be cast and interpreted by the implementation, as documented separately.
+ ///
+ /// The caller must know how that `config` data looks like for the intended implementation.
+ fn configure(&mut self, config: &dyn Any) -> Result<(), Box<dyn std::error::Error + Send + Sync + 'static>>;
+}
+
+// Would be nice if the box implementation could auto-forward to all implemented traits.
+impl<T: TransportWithoutIO + ?Sized> TransportWithoutIO for Box<T> {
+ fn set_identity(&mut self, identity: gix_sec::identity::Account) -> Result<(), Error> {
+ self.deref_mut().set_identity(identity)
+ }
+
+ #[cfg(any(feature = "blocking-client", feature = "async-client"))]
+ fn request(&mut self, write_mode: WriteMode, on_into_read: MessageKind) -> Result<RequestWriter<'_>, Error> {
+ self.deref_mut().request(write_mode, on_into_read)
+ }
+
+ fn to_url(&self) -> Cow<'_, BStr> {
+ self.deref().to_url()
+ }
+
+ fn supported_protocol_versions(&self) -> &[Protocol] {
+ self.deref().supported_protocol_versions()
+ }
+
+ fn connection_persists_across_multiple_requests(&self) -> bool {
+ self.deref().connection_persists_across_multiple_requests()
+ }
+
+ fn configure(&mut self, config: &dyn Any) -> Result<(), Box<dyn std::error::Error + Send + Sync + 'static>> {
+ self.deref_mut().configure(config)
+ }
+}
+
+impl<T: TransportWithoutIO + ?Sized> TransportWithoutIO for &mut T {
+ fn set_identity(&mut self, identity: gix_sec::identity::Account) -> Result<(), Error> {
+ self.deref_mut().set_identity(identity)
+ }
+
+ #[cfg(any(feature = "blocking-client", feature = "async-client"))]
+ fn request(&mut self, write_mode: WriteMode, on_into_read: MessageKind) -> Result<RequestWriter<'_>, Error> {
+ self.deref_mut().request(write_mode, on_into_read)
+ }
+
+ fn to_url(&self) -> Cow<'_, BStr> {
+ self.deref().to_url()
+ }
+
+ fn supported_protocol_versions(&self) -> &[Protocol] {
+ self.deref().supported_protocol_versions()
+ }
+
+ fn connection_persists_across_multiple_requests(&self) -> bool {
+ self.deref().connection_persists_across_multiple_requests()
+ }
+
+ fn configure(&mut self, config: &dyn Any) -> Result<(), Box<dyn std::error::Error + Send + Sync + 'static>> {
+ self.deref_mut().configure(config)
+ }
+}
diff --git a/vendor/gix-transport/src/lib.rs b/vendor/gix-transport/src/lib.rs
new file mode 100644
index 000000000..ca02b3b59
--- /dev/null
+++ b/vendor/gix-transport/src/lib.rs
@@ -0,0 +1,99 @@
+//! An implementation of the `git` transport layer, abstracting over all of its [versions][Protocol], providing
+//! [`connect()`] to establish a connection given a repository URL.
+//!
+//! All git transports are supported, including `ssh`, `git`, `http` and `https`, as well as local repository paths.
+//! ## Feature Flags
+#![cfg_attr(
+ feature = "document-features",
+ cfg_attr(doc, doc = ::document_features::document_features!())
+)]
+#![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))]
+#![deny(missing_docs, rust_2018_idioms)]
+#![forbid(unsafe_code)]
+
+#[cfg(feature = "async-trait")]
+pub use async_trait;
+pub use bstr;
+#[cfg(feature = "futures-io")]
+pub use futures_io;
+pub use gix_packetline as packetline;
+
+/// The version of the way client and server communicate.
+#[derive(PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone, Copy)]
+#[cfg_attr(feature = "serde1", derive(serde::Serialize, serde::Deserialize))]
+#[allow(missing_docs)]
+pub enum Protocol {
+ /// Version 1 was the first one conceived, is stateful, and our implementation was seen to cause deadlocks. Prefer V2
+ V1 = 1,
+ /// A command-based and stateless protocol with clear semantics, and the one to use assuming the server isn't very old.
+ /// This is the default.
+ V2 = 2,
+}
+
+impl Default for Protocol {
+ fn default() -> Self {
+ Protocol::V2
+ }
+}
+
+/// The kind of service to invoke on the client or the server side.
+#[derive(PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone, Copy)]
+#[cfg_attr(feature = "serde1", derive(serde::Serialize, serde::Deserialize))]
+pub enum Service {
+ /// The service sending packs from a server to the client. Used for fetching pack data.
+ UploadPack,
+ /// The service receiving packs produced by the client, who sends a pack to the server.
+ ReceivePack,
+}
+
+impl Service {
+ /// Render this instance as string recognized by the git transport layer.
+ pub fn as_str(&self) -> &'static str {
+ match self {
+ Service::ReceivePack => "git-receive-pack",
+ Service::UploadPack => "git-upload-pack",
+ }
+ }
+}
+
+mod traits {
+ use std::convert::Infallible;
+
+ /// An error which can tell whether it's worth retrying to maybe succeed next time.
+ pub trait IsSpuriousError: std::error::Error {
+ /// Return `true` if retrying might result in a different outcome due to IO working out differently.
+ fn is_spurious(&self) -> bool {
+ false
+ }
+ }
+
+ impl IsSpuriousError for Infallible {}
+
+ impl IsSpuriousError for std::io::Error {
+ fn is_spurious(&self) -> bool {
+ // TODO: also include the new special Kinds (currently unstable)
+ use std::io::ErrorKind::*;
+ match self.kind() {
+ Unsupported | WriteZero | InvalidInput | InvalidData | WouldBlock | AlreadyExists
+ | AddrNotAvailable | NotConnected | Other | PermissionDenied | NotFound => false,
+ Interrupted | UnexpectedEof | OutOfMemory | TimedOut | BrokenPipe | AddrInUse | ConnectionAborted
+ | ConnectionReset | ConnectionRefused => true,
+ _ => false,
+ }
+ }
+ }
+}
+pub use traits::IsSpuriousError;
+
+///
+pub mod client;
+
+#[doc(inline)]
+#[cfg(any(
+ feature = "blocking-client",
+ all(feature = "async-client", any(feature = "async-std"))
+))]
+pub use client::connect;
+
+#[cfg(all(feature = "async-client", feature = "blocking-client"))]
+compile_error!("Cannot set both 'blocking-client' and 'async-client' features as they are mutually exclusive");