summaryrefslogtreecommitdiffstats
path: root/vendor/gix-protocol/src
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-05-04 12:41:41 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-05-04 12:41:41 +0000
commit10ee2acdd26a7f1298c6f6d6b7af9b469fe29b87 (patch)
treebdffd5d80c26cf4a7a518281a204be1ace85b4c1 /vendor/gix-protocol/src
parentReleasing progress-linux version 1.70.0+dfsg1-9~progress7.99u1. (diff)
downloadrustc-10ee2acdd26a7f1298c6f6d6b7af9b469fe29b87.tar.xz
rustc-10ee2acdd26a7f1298c6f6d6b7af9b469fe29b87.zip
Merging upstream version 1.70.0+dfsg2.
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'vendor/gix-protocol/src')
-rw-r--r--vendor/gix-protocol/src/command/mod.rs214
-rw-r--r--vendor/gix-protocol/src/command/tests.rs156
-rw-r--r--vendor/gix-protocol/src/fetch/arguments/async_io.rs55
-rw-r--r--vendor/gix-protocol/src/fetch/arguments/blocking_io.rs56
-rw-r--r--vendor/gix-protocol/src/fetch/arguments/mod.rs252
-rw-r--r--vendor/gix-protocol/src/fetch/delegate.rs313
-rw-r--r--vendor/gix-protocol/src/fetch/error.rs21
-rw-r--r--vendor/gix-protocol/src/fetch/handshake.rs27
-rw-r--r--vendor/gix-protocol/src/fetch/mod.rs20
-rw-r--r--vendor/gix-protocol/src/fetch/response/async_io.rs136
-rw-r--r--vendor/gix-protocol/src/fetch/response/blocking_io.rs135
-rw-r--r--vendor/gix-protocol/src/fetch/response/mod.rs244
-rw-r--r--vendor/gix-protocol/src/fetch/tests.rs388
-rw-r--r--vendor/gix-protocol/src/fetch_fn.rs168
-rw-r--r--vendor/gix-protocol/src/handshake/function.rs100
-rw-r--r--vendor/gix-protocol/src/handshake/mod.rs95
-rw-r--r--vendor/gix-protocol/src/handshake/refs/async_io.rs43
-rw-r--r--vendor/gix-protocol/src/handshake/refs/blocking_io.rs31
-rw-r--r--vendor/gix-protocol/src/handshake/refs/mod.rs72
-rw-r--r--vendor/gix-protocol/src/handshake/refs/shared.rs237
-rw-r--r--vendor/gix-protocol/src/handshake/refs/tests.rs223
-rw-r--r--vendor/gix-protocol/src/lib.rs64
-rw-r--r--vendor/gix-protocol/src/ls_refs.rs110
-rw-r--r--vendor/gix-protocol/src/remote_progress.rs108
-rw-r--r--vendor/gix-protocol/src/util.rs27
25 files changed, 3295 insertions, 0 deletions
diff --git a/vendor/gix-protocol/src/command/mod.rs b/vendor/gix-protocol/src/command/mod.rs
new file mode 100644
index 000000000..d560220d0
--- /dev/null
+++ b/vendor/gix-protocol/src/command/mod.rs
@@ -0,0 +1,214 @@
+//! V2 command abstraction to validate invocations and arguments, like a database of what we know about them.
+use std::borrow::Cow;
+
+use super::Command;
+
+/// A key value pair of values known at compile time.
+pub type Feature = (&'static str, Option<Cow<'static, str>>);
+
+impl Command {
+ /// Produce the name of the command as known by the server side.
+ pub fn as_str(&self) -> &'static str {
+ match self {
+ Command::LsRefs => "ls-refs",
+ Command::Fetch => "fetch",
+ }
+ }
+}
+
+#[cfg(any(test, feature = "async-client", feature = "blocking-client"))]
+mod with_io {
+ use bstr::{BString, ByteSlice};
+ use gix_transport::client::Capabilities;
+
+ use crate::{command::Feature, Command};
+
+ impl Command {
+ /// Only V2
+ fn all_argument_prefixes(&self) -> &'static [&'static str] {
+ match self {
+ Command::LsRefs => &["symrefs", "peel", "ref-prefix ", "unborn"],
+ Command::Fetch => &[
+ "want ", // hex oid
+ "have ", // hex oid
+ "done",
+ "thin-pack",
+ "no-progress",
+ "include-tag",
+ "ofs-delta",
+ // Shallow feature/capability
+ "shallow ", // hex oid
+ "deepen ", // commit depth
+ "deepen-relative",
+ "deepen-since ", // time-stamp
+ "deepen-not ", // rev
+ // filter feature/capability
+ "filter ", // filter-spec
+ // ref-in-want feature
+ "want-ref ", // ref path
+ // sideband-all feature
+ "sideband-all",
+ // packfile-uris feature
+ "packfile-uris ", // protocols
+ // wait-for-done feature
+ "wait-for-done",
+ ],
+ }
+ }
+
+ fn all_features(&self, version: gix_transport::Protocol) -> &'static [&'static str] {
+ match self {
+ Command::LsRefs => &[],
+ Command::Fetch => match version {
+ gix_transport::Protocol::V1 => &[
+ "multi_ack",
+ "thin-pack",
+ "side-band",
+ "side-band-64k",
+ "ofs-delta",
+ "shallow",
+ "deepen-since",
+ "deepen-not",
+ "deepen-relative",
+ "no-progress",
+ "include-tag",
+ "multi_ack_detailed",
+ "allow-tip-sha1-in-want",
+ "allow-reachable-sha1-in-want",
+ "no-done",
+ "filter",
+ ],
+ gix_transport::Protocol::V2 => &[
+ "shallow",
+ "filter",
+ "ref-in-want",
+ "sideband-all",
+ "packfile-uris",
+ "wait-for-done",
+ ],
+ },
+ }
+ }
+
+ /// Compute initial arguments based on the given `features`. They are typically provided by the `default_features(…)` method.
+ /// Only useful for V2
+ pub(crate) fn initial_arguments(&self, features: &[Feature]) -> Vec<BString> {
+ match self {
+ Command::Fetch => ["thin-pack", "ofs-delta"]
+ .iter()
+ .map(|s| s.as_bytes().as_bstr().to_owned())
+ .chain(
+ [
+ "sideband-all",
+ /* "packfile-uris" */ // packfile-uris must be configurable and can't just be used. Some servers advertise it and reject it later.
+ ]
+ .iter()
+ .filter(|f| features.iter().any(|(sf, _)| sf == *f))
+ .map(|f| f.as_bytes().as_bstr().to_owned()),
+ )
+ .collect(),
+ Command::LsRefs => vec![b"symrefs".as_bstr().to_owned(), b"peel".as_bstr().to_owned()],
+ }
+ }
+
+ /// Turns on all modern features for V1 and all supported features for V2, returning them as a vector of features.
+ /// Note that this is the basis for any fetch operation as these features fulfil basic requirements and reasonably up-to-date servers.
+ pub fn default_features(
+ &self,
+ version: gix_transport::Protocol,
+ server_capabilities: &Capabilities,
+ ) -> Vec<Feature> {
+ match self {
+ Command::Fetch => match version {
+ gix_transport::Protocol::V1 => {
+ let has_multi_ack_detailed = server_capabilities.contains("multi_ack_detailed");
+ let has_sideband_64k = server_capabilities.contains("side-band-64k");
+ self.all_features(version)
+ .iter()
+ .copied()
+ .filter(|feature| match *feature {
+ "side-band" if has_sideband_64k => false,
+ "multi_ack" if has_multi_ack_detailed => false,
+ "no-progress" => false,
+ feature => server_capabilities.contains(feature),
+ })
+ .map(|s| (s, None))
+ .collect()
+ }
+ gix_transport::Protocol::V2 => {
+ let supported_features: Vec<_> = server_capabilities
+ .iter()
+ .find_map(|c| {
+ if c.name() == Command::Fetch.as_str() {
+ c.values().map(|v| v.map(|f| f.to_owned()).collect())
+ } else {
+ None
+ }
+ })
+ .unwrap_or_default();
+ self.all_features(version)
+ .iter()
+ .copied()
+ .filter(|feature| supported_features.iter().any(|supported| supported == feature))
+ .map(|s| (s, None))
+ .collect()
+ }
+ },
+ Command::LsRefs => vec![],
+ }
+ }
+ /// Panics if the given arguments and features don't match what's statically known. It's considered a bug in the delegate.
+ pub(crate) fn validate_argument_prefixes_or_panic(
+ &self,
+ version: gix_transport::Protocol,
+ server: &Capabilities,
+ arguments: &[BString],
+ features: &[Feature],
+ ) {
+ let allowed = self.all_argument_prefixes();
+ for arg in arguments {
+ if allowed.iter().any(|allowed| arg.starts_with(allowed.as_bytes())) {
+ continue;
+ }
+ panic!("{}: argument {} is not known or allowed", self.as_str(), arg);
+ }
+ match version {
+ gix_transport::Protocol::V1 => {
+ for (feature, _) in features {
+ if server
+ .iter()
+ .any(|c| feature.starts_with(c.name().to_str_lossy().as_ref()))
+ {
+ continue;
+ }
+ panic!("{}: capability {} is not supported", self.as_str(), feature);
+ }
+ }
+ gix_transport::Protocol::V2 => {
+ let allowed = server
+ .iter()
+ .find_map(|c| {
+ if c.name() == self.as_str().as_bytes().as_bstr() {
+ c.values().map(|v| v.map(|f| f.to_string()).collect::<Vec<_>>())
+ } else {
+ None
+ }
+ })
+ .unwrap_or_default();
+ for (feature, _) in features {
+ if allowed.iter().any(|allowed| feature == allowed) {
+ continue;
+ }
+ match *feature {
+ "agent" => {}
+ _ => panic!("{}: V2 feature/capability {} is not supported", self.as_str(), feature),
+ }
+ }
+ }
+ }
+ }
+ }
+}
+
+#[cfg(test)]
+mod tests;
diff --git a/vendor/gix-protocol/src/command/tests.rs b/vendor/gix-protocol/src/command/tests.rs
new file mode 100644
index 000000000..12c0eb6df
--- /dev/null
+++ b/vendor/gix-protocol/src/command/tests.rs
@@ -0,0 +1,156 @@
+mod v1 {
+ fn capabilities(input: &str) -> gix_transport::client::Capabilities {
+ gix_transport::client::Capabilities::from_bytes(format!("\0{input}").as_bytes())
+ .expect("valid input capabilities")
+ .0
+ }
+
+ const GITHUB_CAPABILITIES: &str = "multi_ack thin-pack side-band ofs-delta shallow deepen-since deepen-not deepen-relative no-progress include-tag allow-tip-sha1-in-want allow-reachable-sha1-in-want no-done symref=HEAD:refs/heads/main filter agent=git/github-gdf51a71f0236";
+ mod fetch {
+ mod default_features {
+ use crate::{
+ command::tests::v1::{capabilities, GITHUB_CAPABILITIES},
+ Command,
+ };
+
+ #[test]
+ fn it_chooses_the_best_multi_ack_and_sideband() {
+ assert_eq!(
+ Command::Fetch.default_features(
+ gix_transport::Protocol::V1,
+ &capabilities("multi_ack side-band side-band-64k multi_ack_detailed")
+ ),
+ &[("side-band-64k", None), ("multi_ack_detailed", None),]
+ );
+ }
+
+ #[test]
+ fn it_chooses_all_supported_non_stacking_capabilities_and_leaves_no_progress() {
+ assert_eq!(
+ Command::Fetch.default_features(gix_transport::Protocol::V1, &capabilities(GITHUB_CAPABILITIES)),
+ &[
+ ("multi_ack", None),
+ ("thin-pack", None),
+ ("side-band", None),
+ ("ofs-delta", None),
+ ("shallow", None),
+ ("deepen-since", None),
+ ("deepen-not", None),
+ ("deepen-relative", None),
+ ("include-tag", None),
+ ("allow-tip-sha1-in-want", None),
+ ("allow-reachable-sha1-in-want", None),
+ ("no-done", None),
+ ("filter", None),
+ ],
+ "we don't enforce no-progress"
+ );
+ }
+ }
+ }
+}
+
+mod v2 {
+ use gix_transport::client::Capabilities;
+
+ fn capabilities(command: &str, input: &str) -> Capabilities {
+ Capabilities::from_lines(format!("version 2\n{command}={input}").into())
+ .expect("valid input for V2 capabilities")
+ }
+
+ mod fetch {
+ mod default_features {
+ use crate::{command::tests::v2::capabilities, Command};
+
+ #[test]
+ fn all_features() {
+ assert_eq!(
+ Command::Fetch.default_features(
+ gix_transport::Protocol::V2,
+ &capabilities("fetch", "shallow filter ref-in-want sideband-all packfile-uris")
+ ),
+ ["shallow", "filter", "ref-in-want", "sideband-all", "packfile-uris"]
+ .iter()
+ .map(|s| (*s, None))
+ .collect::<Vec<_>>()
+ )
+ }
+ }
+
+ mod initial_arguments {
+ use bstr::ByteSlice;
+
+ use crate::{command::tests::v2::capabilities, Command};
+
+ #[test]
+ fn for_all_features() {
+ assert_eq!(
+ Command::Fetch.initial_arguments(&Command::Fetch.default_features(
+ gix_transport::Protocol::V2,
+ &capabilities("fetch", "shallow filter sideband-all packfile-uris")
+ )),
+ ["thin-pack", "ofs-delta", "sideband-all"]
+ .iter()
+ .map(|s| s.as_bytes().as_bstr().to_owned())
+ .collect::<Vec<_>>(),
+ "packfile-uris isn't really supported that well and we don't support it either yet"
+ )
+ }
+ }
+ }
+
+ mod ls_refs {
+ mod default_features {
+ use crate::{command::tests::v2::capabilities, Command};
+
+ #[test]
+ fn default_as_there_are_no_features() {
+ assert_eq!(
+ Command::LsRefs.default_features(
+ gix_transport::Protocol::V2,
+ &capabilities("something-else", "does not matter as there are none")
+ ),
+ &[]
+ );
+ }
+ }
+
+ mod validate {
+ use bstr::ByteSlice;
+
+ use crate::{command::tests::v2::capabilities, Command};
+
+ #[test]
+ fn ref_prefixes_can_always_be_used() {
+ Command::LsRefs.validate_argument_prefixes_or_panic(
+ gix_transport::Protocol::V2,
+ &capabilities("something else", "do-not-matter"),
+ &[b"ref-prefix hello/".as_bstr().into()],
+ &[],
+ );
+ }
+
+ #[test]
+ #[should_panic]
+ fn unknown_argument() {
+ Command::LsRefs.validate_argument_prefixes_or_panic(
+ gix_transport::Protocol::V2,
+ &capabilities("other", "do-not-matter"),
+ &[b"definitely-nothing-we-know".as_bstr().into()],
+ &[],
+ );
+ }
+
+ #[test]
+ #[should_panic]
+ fn unknown_feature() {
+ Command::LsRefs.validate_argument_prefixes_or_panic(
+ gix_transport::Protocol::V2,
+ &capabilities("other", "do-not-matter"),
+ &[],
+ &[("some-feature-that-does-not-exist", None)],
+ );
+ }
+ }
+ }
+}
diff --git a/vendor/gix-protocol/src/fetch/arguments/async_io.rs b/vendor/gix-protocol/src/fetch/arguments/async_io.rs
new file mode 100644
index 000000000..3984ec610
--- /dev/null
+++ b/vendor/gix-protocol/src/fetch/arguments/async_io.rs
@@ -0,0 +1,55 @@
+use futures_lite::io::AsyncWriteExt;
+use gix_transport::{client, client::TransportV2Ext};
+
+use crate::{fetch::Arguments, Command};
+
+impl Arguments {
+ /// Send fetch arguments to the server, and indicate this is the end of negotiations only if `add_done_argument` is present.
+ pub async fn send<'a, T: client::Transport + 'a>(
+ &mut self,
+ transport: &'a mut T,
+ add_done_argument: bool,
+ ) -> Result<Box<dyn client::ExtendedBufRead + Unpin + 'a>, client::Error> {
+ if self.haves.is_empty() {
+ assert!(add_done_argument, "If there are no haves, is_done must be true.");
+ }
+ match self.version {
+ gix_transport::Protocol::V1 => {
+ let (on_into_read, retained_state) = self.prepare_v1(
+ transport.connection_persists_across_multiple_requests(),
+ add_done_argument,
+ )?;
+ let mut line_writer =
+ transport.request(client::WriteMode::OneLfTerminatedLinePerWriteCall, on_into_read)?;
+ let had_args = !self.args.is_empty();
+ for arg in self.args.drain(..) {
+ line_writer.write_all(&arg).await?;
+ }
+ if had_args {
+ line_writer.write_message(client::MessageKind::Flush).await?;
+ }
+ for line in self.haves.drain(..) {
+ line_writer.write_all(&line).await?;
+ }
+ if let Some(next_args) = retained_state {
+ self.args = next_args;
+ }
+ Ok(line_writer.into_read().await?)
+ }
+ gix_transport::Protocol::V2 => {
+ let retained_state = self.args.clone();
+ self.args.append(&mut self.haves);
+ if add_done_argument {
+ self.args.push("done".into());
+ }
+ transport
+ .invoke(
+ Command::Fetch.as_str(),
+ self.features.iter().filter(|(_, v)| v.is_some()).cloned(),
+ Some(std::mem::replace(&mut self.args, retained_state).into_iter()),
+ )
+ .await
+ }
+ }
+ }
+}
diff --git a/vendor/gix-protocol/src/fetch/arguments/blocking_io.rs b/vendor/gix-protocol/src/fetch/arguments/blocking_io.rs
new file mode 100644
index 000000000..b49d1a1ba
--- /dev/null
+++ b/vendor/gix-protocol/src/fetch/arguments/blocking_io.rs
@@ -0,0 +1,56 @@
+use std::io::Write;
+
+use gix_transport::{client, client::TransportV2Ext};
+
+use crate::{fetch::Arguments, Command};
+
+impl Arguments {
+ /// Send fetch arguments to the server, and indicate this is the end of negotiations only if `add_done_argument` is present.
+ pub fn send<'a, T: client::Transport + 'a>(
+ &mut self,
+ transport: &'a mut T,
+ add_done_argument: bool,
+ ) -> Result<Box<dyn client::ExtendedBufRead + Unpin + 'a>, client::Error> {
+ if self.haves.is_empty() {
+ assert!(add_done_argument, "If there are no haves, is_done must be true.");
+ }
+ match self.version {
+ gix_transport::Protocol::V1 => {
+ let (on_into_read, retained_state) = self.prepare_v1(
+ transport.connection_persists_across_multiple_requests(),
+ add_done_argument,
+ )?;
+ let mut line_writer =
+ transport.request(client::WriteMode::OneLfTerminatedLinePerWriteCall, on_into_read)?;
+ let had_args = !self.args.is_empty();
+ for arg in self.args.drain(..) {
+ line_writer.write_all(&arg)?;
+ }
+ if had_args {
+ line_writer.write_message(client::MessageKind::Flush)?;
+ }
+ for line in self.haves.drain(..) {
+ line_writer.write_all(&line)?;
+ }
+ if let Some(next_args) = retained_state {
+ self.args = next_args;
+ }
+ Ok(line_writer.into_read()?)
+ }
+ gix_transport::Protocol::V2 => {
+ let retained_state = self.args.clone();
+ self.args.append(&mut self.haves);
+ if add_done_argument {
+ self.args.push("done".into());
+ }
+ transport.invoke(
+ Command::Fetch.as_str(),
+ self.features
+ .iter()
+ .filter_map(|(k, v)| v.as_ref().map(|v| (*k, Some(v.as_ref())))),
+ Some(std::mem::replace(&mut self.args, retained_state).into_iter()),
+ )
+ }
+ }
+ }
+}
diff --git a/vendor/gix-protocol/src/fetch/arguments/mod.rs b/vendor/gix-protocol/src/fetch/arguments/mod.rs
new file mode 100644
index 000000000..39c9eee3a
--- /dev/null
+++ b/vendor/gix-protocol/src/fetch/arguments/mod.rs
@@ -0,0 +1,252 @@
+use std::fmt;
+
+use bstr::{BStr, BString, ByteSlice, ByteVec};
+
+/// The arguments passed to a server command.
+#[derive(Debug)]
+pub struct Arguments {
+ /// The active features/capabilities of the fetch invocation
+ #[cfg(any(feature = "async-client", feature = "blocking-client"))]
+ features: Vec<crate::command::Feature>,
+
+ args: Vec<BString>,
+ haves: Vec<BString>,
+
+ filter: bool,
+ shallow: bool,
+ deepen_since: bool,
+ deepen_not: bool,
+ deepen_relative: bool,
+ ref_in_want: bool,
+ supports_include_tag: bool,
+
+ features_for_first_want: Option<Vec<String>>,
+ #[cfg(any(feature = "async-client", feature = "blocking-client"))]
+ version: gix_transport::Protocol,
+}
+
+impl Arguments {
+ /// Return true if there is no argument at all.
+ ///
+ /// This can happen if callers assure that they won't add 'wants' if their 'have' is the same, i.e. if the remote has nothing
+ /// new for them.
+ pub fn is_empty(&self) -> bool {
+ self.haves.is_empty() && !self.args.iter().rev().any(|arg| arg.starts_with_str("want "))
+ }
+ /// Return true if ref filters is supported.
+ pub fn can_use_filter(&self) -> bool {
+ self.filter
+ }
+ /// Return true if shallow refs are supported.
+ ///
+ /// This is relevant for partial clones when using `--depth X`.
+ pub fn can_use_shallow(&self) -> bool {
+ self.shallow
+ }
+ /// Return true if the 'deepen' capability is supported.
+ ///
+ /// This is relevant for partial clones when using `--depth X` and retrieving additional history.
+ pub fn can_use_deepen(&self) -> bool {
+ self.shallow
+ }
+ /// Return true if the 'deepen_since' capability is supported.
+ ///
+ /// This is relevant for partial clones when using `--depth X` and retrieving additional history
+ /// based on a date beyond which all history should be present.
+ pub fn can_use_deepen_since(&self) -> bool {
+ self.deepen_since
+ }
+ /// Return true if the 'deepen_not' capability is supported.
+ ///
+ /// This is relevant for partial clones when using `--depth X`.
+ pub fn can_use_deepen_not(&self) -> bool {
+ self.deepen_not
+ }
+ /// Return true if the 'deepen_relative' capability is supported.
+ ///
+ /// This is relevant for partial clones when using `--depth X`.
+ pub fn can_use_deepen_relative(&self) -> bool {
+ self.deepen_relative
+ }
+ /// Return true if the 'ref-in-want' capability is supported.
+ ///
+ /// This can be used to bypass 'ls-refs' entirely in protocol v2.
+ pub fn can_use_ref_in_want(&self) -> bool {
+ self.ref_in_want
+ }
+ /// Return true if the 'include-tag' capability is supported.
+ pub fn can_use_include_tag(&self) -> bool {
+ self.supports_include_tag
+ }
+
+ /// Add the given `id` pointing to a commit to the 'want' list.
+ ///
+ /// As such it should be included in the server response as it's not present on the client.
+ pub fn want(&mut self, id: impl AsRef<gix_hash::oid>) {
+ match self.features_for_first_want.take() {
+ Some(features) => self.prefixed("want ", format!("{} {}", id.as_ref(), features.join(" "))),
+ None => self.prefixed("want ", id.as_ref()),
+ }
+ }
+ /// Add the given ref to the 'want-ref' list.
+ ///
+ /// The server should respond with a corresponding 'wanted-refs' section if it will include the
+ /// wanted ref in the packfile response.
+ pub fn want_ref(&mut self, ref_path: &BStr) {
+ let mut arg = BString::from("want-ref ");
+ arg.push_str(ref_path);
+ self.args.push(arg);
+ }
+ /// Add the given `id` pointing to a commit to the 'have' list.
+ ///
+ /// As such it should _not_ be included in the server response as it's already present on the client.
+ pub fn have(&mut self, id: impl AsRef<gix_hash::oid>) {
+ self.haves.push(format!("have {}", id.as_ref()).into());
+ }
+ /// Add the given `id` pointing to a commit to the 'shallow' list.
+ pub fn shallow(&mut self, id: impl AsRef<gix_hash::oid>) {
+ debug_assert!(self.shallow, "'shallow' feature required for 'shallow <id>'");
+ if self.shallow {
+ self.prefixed("shallow ", id.as_ref());
+ }
+ }
+ /// Deepen the commit history by `depth` amount of commits.
+ pub fn deepen(&mut self, depth: usize) {
+ debug_assert!(self.shallow, "'shallow' feature required for deepen");
+ if self.shallow {
+ self.prefixed("deepen ", depth);
+ }
+ }
+ /// Deepen the commit history to include all commits from now to `seconds_since_unix_epoch`.
+ pub fn deepen_since(&mut self, seconds_since_unix_epoch: usize) {
+ debug_assert!(self.deepen_since, "'deepen-since' feature required");
+ if self.deepen_since {
+ self.prefixed("deepen-since ", seconds_since_unix_epoch);
+ }
+ }
+ /// Deepen the commit history in a relative instead of absolute fashion.
+ pub fn deepen_relative(&mut self) {
+ debug_assert!(self.deepen_relative, "'deepen-relative' feature required");
+ if self.deepen_relative {
+ self.args.push("deepen-relative".into());
+ }
+ }
+ /// Do not include commits reachable by the given `ref_path` when deepening the history.
+ pub fn deepen_not(&mut self, ref_path: &BStr) {
+ debug_assert!(self.deepen_not, "'deepen-not' feature required");
+ if self.deepen_not {
+ let mut line = BString::from("deepen-not ");
+ line.extend_from_slice(ref_path);
+ self.args.push(line);
+ }
+ }
+ /// Set the given filter `spec` when listing references.
+ pub fn filter(&mut self, spec: &str) {
+ debug_assert!(self.filter, "'filter' feature required");
+ if self.filter {
+ self.prefixed("filter ", spec);
+ }
+ }
+ /// Permanently allow the server to include tags that point to commits or objects it would return.
+ ///
+ /// Needs to only be called once.
+ pub fn use_include_tag(&mut self) {
+ debug_assert!(self.supports_include_tag, "'include-tag' feature required");
+ if self.supports_include_tag {
+ self.args.push("include-tag".into());
+ }
+ }
+ fn prefixed(&mut self, prefix: &str, value: impl fmt::Display) {
+ self.args.push(format!("{prefix}{value}").into());
+ }
+ /// Create a new instance to help setting up arguments to send to the server as part of a `fetch` operation
+ /// for which `features` are the available and configured features to use.
+ #[cfg(any(feature = "async-client", feature = "blocking-client"))]
+ pub fn new(version: gix_transport::Protocol, features: Vec<crate::command::Feature>) -> Self {
+ use crate::Command;
+ let has = |name: &str| features.iter().any(|f| f.0 == name);
+ let filter = has("filter");
+ let shallow = has("shallow");
+ let ref_in_want = has("ref-in-want");
+ let mut deepen_since = shallow;
+ let mut deepen_not = shallow;
+ let mut deepen_relative = shallow;
+ let supports_include_tag;
+ let (initial_arguments, features_for_first_want) = match version {
+ gix_transport::Protocol::V1 => {
+ deepen_since = has("deepen-since");
+ deepen_not = has("deepen-not");
+ deepen_relative = has("deepen-relative");
+ supports_include_tag = has("include-tag");
+ let baked_features = features
+ .iter()
+ .map(|(n, v)| match v {
+ Some(v) => format!("{n}={v}"),
+ None => n.to_string(),
+ })
+ .collect::<Vec<_>>();
+ (Vec::new(), Some(baked_features))
+ }
+ gix_transport::Protocol::V2 => {
+ supports_include_tag = true;
+ (Command::Fetch.initial_arguments(&features), None)
+ }
+ };
+
+ Arguments {
+ features,
+ version,
+ args: initial_arguments,
+ haves: Vec::new(),
+ filter,
+ shallow,
+ supports_include_tag,
+ deepen_not,
+ deepen_relative,
+ ref_in_want,
+ deepen_since,
+ features_for_first_want,
+ }
+ }
+}
+
+#[cfg(any(feature = "blocking-client", feature = "async-client"))]
+mod shared {
+ use bstr::{BString, ByteSlice};
+ use gix_transport::{client, client::MessageKind};
+
+ use crate::fetch::Arguments;
+
+ impl Arguments {
+ pub(in crate::fetch::arguments) fn prepare_v1(
+ &mut self,
+ transport_is_stateful: bool,
+ add_done_argument: bool,
+ ) -> Result<(MessageKind, Option<Vec<BString>>), client::Error> {
+ if self.haves.is_empty() {
+ assert!(add_done_argument, "If there are no haves, is_done must be true.");
+ }
+ let on_into_read = if add_done_argument {
+ client::MessageKind::Text(&b"done"[..])
+ } else {
+ client::MessageKind::Flush
+ };
+ let retained_state = if transport_is_stateful {
+ None
+ } else {
+ Some(self.args.clone())
+ };
+
+ if let Some(first_arg_position) = self.args.iter().position(|l| l.starts_with_str("want ")) {
+ self.args.swap(first_arg_position, 0);
+ }
+ Ok((on_into_read, retained_state))
+ }
+ }
+}
+
+#[cfg(feature = "async-client")]
+mod async_io;
+
+#[cfg(feature = "blocking-client")]
+mod blocking_io;
diff --git a/vendor/gix-protocol/src/fetch/delegate.rs b/vendor/gix-protocol/src/fetch/delegate.rs
new file mode 100644
index 000000000..b0db2f833
--- /dev/null
+++ b/vendor/gix-protocol/src/fetch/delegate.rs
@@ -0,0 +1,313 @@
+use std::{
+ borrow::Cow,
+ io,
+ ops::{Deref, DerefMut},
+};
+
+use bstr::BString;
+use gix_transport::client::Capabilities;
+
+use crate::{
+ fetch::{Arguments, Response},
+ handshake::Ref,
+};
+
+/// Defines what to do next after certain [`Delegate`] operations.
+#[derive(PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone, Copy)]
+pub enum Action {
+ /// Continue the typical flow of operations in this flow.
+ Continue,
+ /// Return at the next possible opportunity without making further requests, possibly after closing the connection.
+ Cancel,
+}
+
+/// The non-IO protocol delegate is the bare minimal interface needed to fully control the [`fetch`][crate::fetch()] operation, sparing
+/// the IO parts.
+/// Async implementations must treat it as blocking and unblock it by evaluating it elsewhere.
+///
+/// See [Delegate] for the complete trait.
+pub trait DelegateBlocking {
+ /// Return extra parameters to be provided during the handshake.
+ ///
+ /// Note that this method is only called once and the result is reused during subsequent handshakes which may happen
+ /// if there is an authentication failure.
+ fn handshake_extra_parameters(&self) -> Vec<(String, Option<String>)> {
+ Vec::new()
+ }
+ /// Called before invoking 'ls-refs' on the server to allow providing it with additional `arguments` and to enable `features`.
+ /// If the server `capabilities` don't match the requirements abort with an error to abort the entire fetch operation.
+ ///
+ /// Note that some arguments are preset based on typical use, and `features` are preset to maximize options.
+ /// The `server` capabilities can be used to see which additional capabilities the server supports as per the handshake which happened prior.
+ ///
+ /// If the delegate returns [`ls_refs::Action::Skip`], no 'ls-refs` command is sent to the server.
+ ///
+ /// Note that this is called only if we are using protocol version 2.
+ fn prepare_ls_refs(
+ &mut self,
+ _server: &Capabilities,
+ _arguments: &mut Vec<BString>,
+ _features: &mut Vec<(&str, Option<Cow<'_, str>>)>,
+ ) -> std::io::Result<ls_refs::Action> {
+ Ok(ls_refs::Action::Continue)
+ }
+
+ /// Called before invoking the 'fetch' interaction with `features` pre-filled for typical use
+ /// and to maximize capabilities to allow aborting an interaction early.
+ ///
+ /// `refs` is a list of known references on the remote based on the handshake or a prior call to ls_refs.
+ /// These can be used to abort early in case the refs are already known here.
+ ///
+ /// As there will be another call allowing to post arguments conveniently in the correct format, i.e. `want hex-oid`,
+ /// there is no way to set arguments at this time.
+ ///
+ /// `version` is the actually supported version as reported by the server, which is relevant in case the server requested a downgrade.
+ /// `server` capabilities is a list of features the server supports for your information, along with enabled `features` that the server knows about.
+ fn prepare_fetch(
+ &mut self,
+ _version: gix_transport::Protocol,
+ _server: &Capabilities,
+ _features: &mut Vec<(&str, Option<Cow<'_, str>>)>,
+ _refs: &[Ref],
+ ) -> std::io::Result<Action> {
+ Ok(Action::Continue)
+ }
+
+ /// A method called repeatedly to negotiate the objects to receive in [`receive_pack(…)`][Delegate::receive_pack()].
+ ///
+ /// The first call has `previous_response` set to `None` as there was no previous response. Every call that follows `previous_response`
+ /// will be set to `Some`.
+ ///
+ /// ### If `previous_response` is `None`…
+ ///
+ /// Given a list of `arguments` to populate with wants, want-refs, shallows, filters and other contextual information to be
+ /// sent to the server. This method is called once.
+ /// Send the objects you `have` have afterwards based on the tips of your refs, in preparation to walk down their parents
+ /// with each call to `negotiate` to find the common base(s).
+ ///
+ /// Note that you should not `want` and object that you already have.
+ /// `refs` are the the tips of on the server side, effectively the latest objects _they_ have.
+ ///
+ /// Return `Action::Close` if you know that there are no `haves` on your end to allow the server to send all of its objects
+ /// as is the case during initial clones.
+ ///
+ /// ### If `previous_response` is `Some`…
+ ///
+ /// Populate `arguments` with the objects you `have` starting from the tips of _your_ refs, taking into consideration
+ /// the `previous_response` response of the server to see which objects they acknowledged to have. You have to maintain
+ /// enough state to be able to walk down from your tips on each call, if they are not in common, and keep setting `have`
+ /// for those which are in common if that helps teaching the server about our state and to acknowledge their existence on _their_ end.
+ /// This method is called until the other side signals they are ready to send a pack.
+ /// Return `Action::Close` if you want to give up before finding a common base. This can happen if the remote repository
+ /// has radically changed so there are no bases, or they are very far in the past, causing all objects to be sent.
+ fn negotiate(
+ &mut self,
+ refs: &[Ref],
+ arguments: &mut Arguments,
+ previous_response: Option<&Response>,
+ ) -> io::Result<Action>;
+}
+
+impl<T: DelegateBlocking> DelegateBlocking for Box<T> {
+ fn handshake_extra_parameters(&self) -> Vec<(String, Option<String>)> {
+ self.deref().handshake_extra_parameters()
+ }
+
+ fn prepare_ls_refs(
+ &mut self,
+ _server: &Capabilities,
+ _arguments: &mut Vec<BString>,
+ _features: &mut Vec<(&str, Option<Cow<'_, str>>)>,
+ ) -> io::Result<ls_refs::Action> {
+ self.deref_mut().prepare_ls_refs(_server, _arguments, _features)
+ }
+
+ fn prepare_fetch(
+ &mut self,
+ _version: gix_transport::Protocol,
+ _server: &Capabilities,
+ _features: &mut Vec<(&str, Option<Cow<'_, str>>)>,
+ _refs: &[Ref],
+ ) -> io::Result<Action> {
+ self.deref_mut().prepare_fetch(_version, _server, _features, _refs)
+ }
+
+ fn negotiate(
+ &mut self,
+ refs: &[Ref],
+ arguments: &mut Arguments,
+ previous_response: Option<&Response>,
+ ) -> io::Result<Action> {
+ self.deref_mut().negotiate(refs, arguments, previous_response)
+ }
+}
+
+impl<T: DelegateBlocking> DelegateBlocking for &mut T {
+ fn handshake_extra_parameters(&self) -> Vec<(String, Option<String>)> {
+ self.deref().handshake_extra_parameters()
+ }
+
+ fn prepare_ls_refs(
+ &mut self,
+ _server: &Capabilities,
+ _arguments: &mut Vec<BString>,
+ _features: &mut Vec<(&str, Option<Cow<'_, str>>)>,
+ ) -> io::Result<ls_refs::Action> {
+ self.deref_mut().prepare_ls_refs(_server, _arguments, _features)
+ }
+
+ fn prepare_fetch(
+ &mut self,
+ _version: gix_transport::Protocol,
+ _server: &Capabilities,
+ _features: &mut Vec<(&str, Option<Cow<'_, str>>)>,
+ _refs: &[Ref],
+ ) -> io::Result<Action> {
+ self.deref_mut().prepare_fetch(_version, _server, _features, _refs)
+ }
+
+ fn negotiate(
+ &mut self,
+ refs: &[Ref],
+ arguments: &mut Arguments,
+ previous_response: Option<&Response>,
+ ) -> io::Result<Action> {
+ self.deref_mut().negotiate(refs, arguments, previous_response)
+ }
+}
+
+#[cfg(feature = "blocking-client")]
+mod blocking_io {
+ use std::{
+ io::{self, BufRead},
+ ops::DerefMut,
+ };
+
+ use gix_features::progress::Progress;
+
+ use crate::{
+ fetch::{DelegateBlocking, Response},
+ handshake::Ref,
+ };
+
+ /// The protocol delegate is the bare minimal interface needed to fully control the [`fetch`][crate::fetch()] operation.
+ ///
+ /// Implementations of this trait are controlled by code with intricate knowledge about how fetching works in protocol version V1 and V2,
+ /// so you don't have to.
+ /// Everything is tucked away behind type-safety so 'nothing can go wrong'©. Runtime assertions assure invalid
+ /// features or arguments don't make it to the server in the first place.
+ /// Please note that this trait mostly corresponds to what V2 would look like, even though V1 is supported as well.
+ pub trait Delegate: DelegateBlocking {
+ /// Receive a pack provided from the given `input`.
+ ///
+ /// Use `progress` to emit your own progress messages when decoding the pack.
+ ///
+ /// `refs` of the remote side are provided for convenience, along with the parsed `previous_response` response in case you want
+ /// to check additional acks.
+ fn receive_pack(
+ &mut self,
+ input: impl io::BufRead,
+ progress: impl Progress,
+ refs: &[Ref],
+ previous_response: &Response,
+ ) -> io::Result<()>;
+ }
+
+ impl<T: Delegate> Delegate for Box<T> {
+ fn receive_pack(
+ &mut self,
+ input: impl BufRead,
+ progress: impl Progress,
+ refs: &[Ref],
+ previous_response: &Response,
+ ) -> io::Result<()> {
+ self.deref_mut().receive_pack(input, progress, refs, previous_response)
+ }
+ }
+
+ impl<T: Delegate> Delegate for &mut T {
+ fn receive_pack(
+ &mut self,
+ input: impl BufRead,
+ progress: impl Progress,
+ refs: &[Ref],
+ previous_response: &Response,
+ ) -> io::Result<()> {
+ self.deref_mut().receive_pack(input, progress, refs, previous_response)
+ }
+ }
+}
+#[cfg(feature = "blocking-client")]
+pub use blocking_io::Delegate;
+
+#[cfg(feature = "async-client")]
+mod async_io {
+ use std::{io, ops::DerefMut};
+
+ use async_trait::async_trait;
+ use futures_io::AsyncBufRead;
+ use gix_features::progress::Progress;
+
+ use crate::{
+ fetch::{DelegateBlocking, Response},
+ handshake::Ref,
+ };
+
+ /// The protocol delegate is the bare minimal interface needed to fully control the [`fetch`][crate::fetch()] operation.
+ ///
+ /// Implementations of this trait are controlled by code with intricate knowledge about how fetching works in protocol version V1 and V2,
+ /// so you don't have to.
+ /// Everything is tucked away behind type-safety so 'nothing can go wrong'©. Runtime assertions assure invalid
+ /// features or arguments don't make it to the server in the first place.
+ /// Please note that this trait mostly corresponds to what V2 would look like, even though V1 is supported as well.
+ #[async_trait(?Send)]
+ pub trait Delegate: DelegateBlocking {
+ /// Receive a pack provided from the given `input`, and the caller should consider it to be blocking as
+ /// most operations on the received pack are implemented in a blocking fashion.
+ ///
+ /// Use `progress` to emit your own progress messages when decoding the pack.
+ ///
+ /// `refs` of the remote side are provided for convenience, along with the parsed `previous_response` response in case you want
+ /// to check additional acks.
+ async fn receive_pack(
+ &mut self,
+ input: impl AsyncBufRead + Unpin + 'async_trait,
+ progress: impl Progress,
+ refs: &[Ref],
+ previous_response: &Response,
+ ) -> io::Result<()>;
+ }
+ #[async_trait(?Send)]
+ impl<T: Delegate> Delegate for Box<T> {
+ async fn receive_pack(
+ &mut self,
+ input: impl AsyncBufRead + Unpin + 'async_trait,
+ progress: impl Progress,
+ refs: &[Ref],
+ previous_response: &Response,
+ ) -> io::Result<()> {
+ self.deref_mut()
+ .receive_pack(input, progress, refs, previous_response)
+ .await
+ }
+ }
+
+ #[async_trait(?Send)]
+ impl<T: Delegate> Delegate for &mut T {
+ async fn receive_pack(
+ &mut self,
+ input: impl AsyncBufRead + Unpin + 'async_trait,
+ progress: impl Progress,
+ refs: &[Ref],
+ previous_response: &Response,
+ ) -> io::Result<()> {
+ self.deref_mut()
+ .receive_pack(input, progress, refs, previous_response)
+ .await
+ }
+ }
+}
+#[cfg(feature = "async-client")]
+pub use async_io::Delegate;
+
+use crate::ls_refs;
diff --git a/vendor/gix-protocol/src/fetch/error.rs b/vendor/gix-protocol/src/fetch/error.rs
new file mode 100644
index 000000000..5646ce4ec
--- /dev/null
+++ b/vendor/gix-protocol/src/fetch/error.rs
@@ -0,0 +1,21 @@
+use std::io;
+
+use gix_transport::client;
+
+use crate::{fetch::response, handshake, ls_refs};
+
+/// The error used in [`fetch()`][crate::fetch()].
+#[derive(Debug, thiserror::Error)]
+#[allow(missing_docs)]
+pub enum Error {
+ #[error(transparent)]
+ Handshake(#[from] handshake::Error),
+ #[error("Could not access repository or failed to read streaming pack file")]
+ Io(#[from] io::Error),
+ #[error(transparent)]
+ Transport(#[from] client::Error),
+ #[error(transparent)]
+ LsRefs(#[from] ls_refs::Error),
+ #[error(transparent)]
+ Response(#[from] response::Error),
+}
diff --git a/vendor/gix-protocol/src/fetch/handshake.rs b/vendor/gix-protocol/src/fetch/handshake.rs
new file mode 100644
index 000000000..9ffc184a8
--- /dev/null
+++ b/vendor/gix-protocol/src/fetch/handshake.rs
@@ -0,0 +1,27 @@
+use gix_features::progress::Progress;
+use gix_transport::{client, Service};
+use maybe_async::maybe_async;
+
+use crate::{
+ credentials,
+ handshake::{Error, Outcome},
+};
+
+/// Perform a handshake with the server on the other side of `transport`, with `authenticate` being used if authentication
+/// turns out to be required. `extra_parameters` are the parameters `(name, optional value)` to add to the handshake,
+/// each time it is performed in case authentication is required.
+/// `progress` is used to inform about what's currently happening.
+#[allow(clippy::result_large_err)]
+#[maybe_async]
+pub async fn upload_pack<AuthFn, T>(
+ transport: T,
+ authenticate: AuthFn,
+ extra_parameters: Vec<(String, Option<String>)>,
+ progress: &mut impl Progress,
+) -> Result<Outcome, Error>
+where
+ AuthFn: FnMut(credentials::helper::Action) -> credentials::protocol::Result,
+ T: client::Transport,
+{
+ crate::handshake(transport, Service::UploadPack, authenticate, extra_parameters, progress).await
+}
diff --git a/vendor/gix-protocol/src/fetch/mod.rs b/vendor/gix-protocol/src/fetch/mod.rs
new file mode 100644
index 000000000..0828ea733
--- /dev/null
+++ b/vendor/gix-protocol/src/fetch/mod.rs
@@ -0,0 +1,20 @@
+mod arguments;
+pub use arguments::Arguments;
+
+///
+pub mod delegate;
+#[cfg(any(feature = "async-client", feature = "blocking-client"))]
+pub use delegate::Delegate;
+pub use delegate::{Action, DelegateBlocking};
+
+mod error;
+pub use error::Error;
+///
+pub mod response;
+pub use response::Response;
+
+mod handshake;
+pub use handshake::upload_pack as handshake;
+
+#[cfg(test)]
+mod tests;
diff --git a/vendor/gix-protocol/src/fetch/response/async_io.rs b/vendor/gix-protocol/src/fetch/response/async_io.rs
new file mode 100644
index 000000000..7b00d843c
--- /dev/null
+++ b/vendor/gix-protocol/src/fetch/response/async_io.rs
@@ -0,0 +1,136 @@
+use std::io;
+
+use futures_lite::AsyncBufReadExt;
+use gix_transport::{client, Protocol};
+
+use crate::fetch::{
+ response,
+ response::{Acknowledgement, ShallowUpdate, WantedRef},
+ Response,
+};
+
+async fn parse_v2_section<T>(
+ line: &mut String,
+ reader: &mut (impl client::ExtendedBufRead + Unpin),
+ res: &mut Vec<T>,
+ parse: impl Fn(&str) -> Result<T, response::Error>,
+) -> Result<bool, response::Error> {
+ line.clear();
+ while reader.read_line(line).await? != 0 {
+ res.push(parse(line)?);
+ line.clear();
+ }
+ // End of message, or end of section?
+ Ok(if reader.stopped_at() == Some(client::MessageKind::Delimiter) {
+ // try reading more sections
+ reader.reset(Protocol::V2);
+ false
+ } else {
+ // we are done, there is no pack
+ true
+ })
+}
+
+impl Response {
+ /// Parse a response of the given `version` of the protocol from `reader`.
+ pub async fn from_line_reader(
+ version: Protocol,
+ reader: &mut (impl client::ExtendedBufRead + Unpin),
+ ) -> Result<Response, response::Error> {
+ match version {
+ Protocol::V1 => {
+ let mut line = String::new();
+ let mut acks = Vec::<Acknowledgement>::new();
+ let mut shallows = Vec::<ShallowUpdate>::new();
+ let has_pack = 'lines: loop {
+ line.clear();
+ let peeked_line = match reader.peek_data_line().await {
+ Some(Ok(Ok(line))) => String::from_utf8_lossy(line),
+ // This special case (hang/block forever) deals with a single NAK being a legitimate EOF sometimes
+ // Note that this might block forever in stateful connections as there it's not really clear
+ // if something will be following or not by just looking at the response. Instead you have to know
+ // the arguments sent to the server and count response lines based on intricate knowledge on how the
+ // server works.
+ // For now this is acceptable, as V2 can be used as a workaround, which also is the default.
+ Some(Err(err)) if err.kind() == io::ErrorKind::UnexpectedEof => break 'lines false,
+ Some(Err(err)) => return Err(err.into()),
+ Some(Ok(Err(err))) => return Err(err.into()),
+ None => {
+ // maybe we saw a shallow flush packet, let's reset and retry
+ debug_assert_eq!(
+ reader.stopped_at(),
+ Some(client::MessageKind::Flush),
+ "If this isn't a flush packet, we don't know what's going on"
+ );
+ reader.read_line(&mut line).await?;
+ reader.reset(Protocol::V1);
+ match reader.peek_data_line().await {
+ Some(Ok(Ok(line))) => String::from_utf8_lossy(line),
+ Some(Err(err)) => return Err(err.into()),
+ Some(Ok(Err(err))) => return Err(err.into()),
+ None => break 'lines false, // EOF
+ }
+ }
+ };
+
+ if Response::parse_v1_ack_or_shallow_or_assume_pack(&mut acks, &mut shallows, &peeked_line) {
+ break 'lines true;
+ }
+ assert_ne!(reader.read_line(&mut line).await?, 0, "consuming a peeked line works");
+ };
+ Ok(Response {
+ acks,
+ shallows,
+ wanted_refs: vec![],
+ has_pack,
+ })
+ }
+ Protocol::V2 => {
+ // NOTE: We only read acknowledgements and scrub to the pack file, until we have use for the other features
+ let mut line = String::new();
+ reader.reset(Protocol::V2);
+ let mut acks = Vec::<Acknowledgement>::new();
+ let mut shallows = Vec::<ShallowUpdate>::new();
+ let mut wanted_refs = Vec::<WantedRef>::new();
+ let has_pack = 'section: loop {
+ line.clear();
+ if reader.read_line(&mut line).await? == 0 {
+ return Err(response::Error::Io(io::Error::new(
+ io::ErrorKind::UnexpectedEof,
+ "Could not read message headline",
+ )));
+ };
+
+ match line.trim_end() {
+ "acknowledgments" => {
+ if parse_v2_section(&mut line, reader, &mut acks, Acknowledgement::from_line).await? {
+ break 'section false;
+ }
+ }
+ "shallow-info" => {
+ if parse_v2_section(&mut line, reader, &mut shallows, ShallowUpdate::from_line).await? {
+ break 'section false;
+ }
+ }
+ "wanted-refs" => {
+ if parse_v2_section(&mut line, reader, &mut wanted_refs, WantedRef::from_line).await? {
+ break 'section false;
+ }
+ }
+ "packfile" => {
+ // what follows is the packfile itself, which can be read with a sideband enabled reader
+ break 'section true;
+ }
+ _ => return Err(response::Error::UnknownSectionHeader { header: line }),
+ }
+ };
+ Ok(Response {
+ acks,
+ shallows,
+ wanted_refs,
+ has_pack,
+ })
+ }
+ }
+ }
+}
diff --git a/vendor/gix-protocol/src/fetch/response/blocking_io.rs b/vendor/gix-protocol/src/fetch/response/blocking_io.rs
new file mode 100644
index 000000000..ca79724e2
--- /dev/null
+++ b/vendor/gix-protocol/src/fetch/response/blocking_io.rs
@@ -0,0 +1,135 @@
+use std::io;
+
+use gix_transport::{client, Protocol};
+
+use crate::fetch::{
+ response,
+ response::{Acknowledgement, ShallowUpdate, WantedRef},
+ Response,
+};
+
+fn parse_v2_section<T>(
+ line: &mut String,
+ reader: &mut impl client::ExtendedBufRead,
+ res: &mut Vec<T>,
+ parse: impl Fn(&str) -> Result<T, response::Error>,
+) -> Result<bool, response::Error> {
+ line.clear();
+ while reader.read_line(line)? != 0 {
+ res.push(parse(line)?);
+ line.clear();
+ }
+ // End of message, or end of section?
+ Ok(if reader.stopped_at() == Some(client::MessageKind::Delimiter) {
+ // try reading more sections
+ reader.reset(Protocol::V2);
+ false
+ } else {
+ // we are done, there is no pack
+ true
+ })
+}
+
+impl Response {
+ /// Parse a response of the given `version` of the protocol from `reader`.
+ pub fn from_line_reader(
+ version: Protocol,
+ reader: &mut impl client::ExtendedBufRead,
+ ) -> Result<Response, response::Error> {
+ match version {
+ Protocol::V1 => {
+ let mut line = String::new();
+ let mut acks = Vec::<Acknowledgement>::new();
+ let mut shallows = Vec::<ShallowUpdate>::new();
+ let has_pack = 'lines: loop {
+ line.clear();
+ let peeked_line = match reader.peek_data_line() {
+ Some(Ok(Ok(line))) => String::from_utf8_lossy(line),
+ // This special case (hang/block forever) deals with a single NAK being a legitimate EOF sometimes
+ // Note that this might block forever in stateful connections as there it's not really clear
+ // if something will be following or not by just looking at the response. Instead you have to know
+ // the arguments sent to the server and count response lines based on intricate knowledge on how the
+ // server works.
+ // For now this is acceptable, as V2 can be used as a workaround, which also is the default.
+ Some(Err(err)) if err.kind() == io::ErrorKind::UnexpectedEof => break 'lines false,
+ Some(Err(err)) => return Err(err.into()),
+ Some(Ok(Err(err))) => return Err(err.into()),
+ None => {
+ // maybe we saw a shallow flush packet, let's reset and retry
+ debug_assert_eq!(
+ reader.stopped_at(),
+ Some(client::MessageKind::Flush),
+ "If this isn't a flush packet, we don't know what's going on"
+ );
+ reader.read_line(&mut line)?;
+ reader.reset(Protocol::V1);
+ match reader.peek_data_line() {
+ Some(Ok(Ok(line))) => String::from_utf8_lossy(line),
+ Some(Err(err)) => return Err(err.into()),
+ Some(Ok(Err(err))) => return Err(err.into()),
+ None => break 'lines false, // EOF
+ }
+ }
+ };
+
+ if Response::parse_v1_ack_or_shallow_or_assume_pack(&mut acks, &mut shallows, &peeked_line) {
+ break 'lines true;
+ }
+ assert_ne!(reader.read_line(&mut line)?, 0, "consuming a peeked line works");
+ };
+ Ok(Response {
+ acks,
+ shallows,
+ wanted_refs: vec![],
+ has_pack,
+ })
+ }
+ Protocol::V2 => {
+ // NOTE: We only read acknowledgements and scrub to the pack file, until we have use for the other features
+ let mut line = String::new();
+ reader.reset(Protocol::V2);
+ let mut acks = Vec::<Acknowledgement>::new();
+ let mut shallows = Vec::<ShallowUpdate>::new();
+ let mut wanted_refs = Vec::<WantedRef>::new();
+ let has_pack = 'section: loop {
+ line.clear();
+ if reader.read_line(&mut line)? == 0 {
+ return Err(response::Error::Io(io::Error::new(
+ io::ErrorKind::UnexpectedEof,
+ "Could not read message headline",
+ )));
+ };
+
+ match line.trim_end() {
+ "acknowledgments" => {
+ if parse_v2_section(&mut line, reader, &mut acks, Acknowledgement::from_line)? {
+ break 'section false;
+ }
+ }
+ "shallow-info" => {
+ if parse_v2_section(&mut line, reader, &mut shallows, ShallowUpdate::from_line)? {
+ break 'section false;
+ }
+ }
+ "wanted-refs" => {
+ if parse_v2_section(&mut line, reader, &mut wanted_refs, WantedRef::from_line)? {
+ break 'section false;
+ }
+ }
+ "packfile" => {
+ // what follows is the packfile itself, which can be read with a sideband enabled reader
+ break 'section true;
+ }
+ _ => return Err(response::Error::UnknownSectionHeader { header: line }),
+ }
+ };
+ Ok(Response {
+ acks,
+ shallows,
+ wanted_refs,
+ has_pack,
+ })
+ }
+ }
+ }
+}
diff --git a/vendor/gix-protocol/src/fetch/response/mod.rs b/vendor/gix-protocol/src/fetch/response/mod.rs
new file mode 100644
index 000000000..8c99cc872
--- /dev/null
+++ b/vendor/gix-protocol/src/fetch/response/mod.rs
@@ -0,0 +1,244 @@
+use bstr::BString;
+use gix_transport::{client, Protocol};
+
+use crate::command::Feature;
+
+/// The error returned in the [response module][crate::fetch::response].
+#[derive(Debug, thiserror::Error)]
+#[allow(missing_docs)]
+pub enum Error {
+ #[error("Failed to read from line reader")]
+ Io(#[source] std::io::Error),
+ #[error(transparent)]
+ UploadPack(#[from] gix_transport::packetline::read::Error),
+ #[error(transparent)]
+ Transport(#[from] client::Error),
+ #[error("Currently we require feature {feature:?}, which is not supported by the server")]
+ MissingServerCapability { feature: &'static str },
+ #[error("Encountered an unknown line prefix in {line:?}")]
+ UnknownLineType { line: String },
+ #[error("Unknown or unsupported header: {header:?}")]
+ UnknownSectionHeader { header: String },
+}
+
+impl From<std::io::Error> for Error {
+ fn from(err: std::io::Error) -> Self {
+ if err.kind() == std::io::ErrorKind::Other {
+ match err.into_inner() {
+ Some(err) => match err.downcast::<gix_transport::packetline::read::Error>() {
+ Ok(err) => Error::UploadPack(*err),
+ Err(err) => Error::Io(std::io::Error::new(std::io::ErrorKind::Other, err)),
+ },
+ None => Error::Io(std::io::ErrorKind::Other.into()),
+ }
+ } else {
+ Error::Io(err)
+ }
+ }
+}
+
+impl gix_transport::IsSpuriousError for Error {
+ fn is_spurious(&self) -> bool {
+ match self {
+ Error::Io(err) => err.is_spurious(),
+ Error::Transport(err) => err.is_spurious(),
+ _ => false,
+ }
+ }
+}
+
+/// An 'ACK' line received from the server.
+#[derive(PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone, Copy)]
+#[cfg_attr(feature = "serde1", derive(serde::Serialize, serde::Deserialize))]
+pub enum Acknowledgement {
+ /// The contained `id` is in common.
+ Common(gix_hash::ObjectId),
+ /// The server is ready to receive more lines.
+ Ready,
+ /// The server isn't ready yet.
+ Nak,
+}
+
+/// A shallow line received from the server.
+#[derive(PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone, Copy)]
+#[cfg_attr(feature = "serde1", derive(serde::Serialize, serde::Deserialize))]
+pub enum ShallowUpdate {
+ /// Shallow the given `id`.
+ Shallow(gix_hash::ObjectId),
+ /// Don't shallow the given `id` anymore.
+ Unshallow(gix_hash::ObjectId),
+}
+
+/// A wanted-ref line received from the server.
+#[derive(PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone)]
+#[cfg_attr(feature = "serde1", derive(serde::Serialize, serde::Deserialize))]
+pub struct WantedRef {
+ /// The object id of the wanted ref, as seen by the server.
+ pub id: gix_hash::ObjectId,
+ /// The name of the ref, as requested by the client as a `want-ref` argument.
+ pub path: BString,
+}
+
+impl ShallowUpdate {
+ /// Parse a `ShallowUpdate` from a `line` as received to the server.
+ pub fn from_line(line: &str) -> Result<ShallowUpdate, Error> {
+ match line.trim_end().split_once(' ') {
+ Some((prefix, id)) => {
+ let id = gix_hash::ObjectId::from_hex(id.as_bytes())
+ .map_err(|_| Error::UnknownLineType { line: line.to_owned() })?;
+ Ok(match prefix {
+ "shallow" => ShallowUpdate::Shallow(id),
+ "unshallow" => ShallowUpdate::Unshallow(id),
+ _ => return Err(Error::UnknownLineType { line: line.to_owned() }),
+ })
+ }
+ None => Err(Error::UnknownLineType { line: line.to_owned() }),
+ }
+ }
+}
+
+impl Acknowledgement {
+ /// Parse an `Acknowledgement` from a `line` as received to the server.
+ pub fn from_line(line: &str) -> Result<Acknowledgement, Error> {
+ let mut tokens = line.trim_end().splitn(3, ' ');
+ match (tokens.next(), tokens.next(), tokens.next()) {
+ (Some(first), id, description) => Ok(match first {
+ "ready" => Acknowledgement::Ready, // V2
+ "NAK" => Acknowledgement::Nak, // V1
+ "ACK" => {
+ let id = match id {
+ Some(id) => gix_hash::ObjectId::from_hex(id.as_bytes())
+ .map_err(|_| Error::UnknownLineType { line: line.to_owned() })?,
+ None => return Err(Error::UnknownLineType { line: line.to_owned() }),
+ };
+ if let Some(description) = description {
+ match description {
+ "common" => {}
+ "ready" => return Ok(Acknowledgement::Ready),
+ _ => return Err(Error::UnknownLineType { line: line.to_owned() }),
+ }
+ }
+ Acknowledgement::Common(id)
+ }
+ _ => return Err(Error::UnknownLineType { line: line.to_owned() }),
+ }),
+ (None, _, _) => Err(Error::UnknownLineType { line: line.to_owned() }),
+ }
+ }
+ /// Returns the hash of the acknowledged object if this instance acknowledges a common one.
+ pub fn id(&self) -> Option<&gix_hash::ObjectId> {
+ match self {
+ Acknowledgement::Common(id) => Some(id),
+ _ => None,
+ }
+ }
+}
+
+impl WantedRef {
+ /// Parse a `WantedRef` from a `line` as received from the server.
+ pub fn from_line(line: &str) -> Result<WantedRef, Error> {
+ match line.trim_end().split_once(' ') {
+ Some((id, path)) => {
+ let id = gix_hash::ObjectId::from_hex(id.as_bytes())
+ .map_err(|_| Error::UnknownLineType { line: line.to_owned() })?;
+ Ok(WantedRef { id, path: path.into() })
+ }
+ None => Err(Error::UnknownLineType { line: line.to_owned() }),
+ }
+ }
+}
+
+/// A representation of a complete fetch response
+#[derive(Debug)]
+pub struct Response {
+ acks: Vec<Acknowledgement>,
+ shallows: Vec<ShallowUpdate>,
+ wanted_refs: Vec<WantedRef>,
+ has_pack: bool,
+}
+
+impl Response {
+ /// Return true if the response has a pack which can be read next.
+ pub fn has_pack(&self) -> bool {
+ self.has_pack
+ }
+
+ /// Return an error if the given `features` don't contain the required ones (the ones this implementation needs)
+ /// for the given `version` of the protocol.
+ ///
+ /// Even though technically any set of features supported by the server could work, we only implement the ones that
+ /// make it easy to maintain all versions with a single code base that aims to be and remain maintainable.
+ pub fn check_required_features(version: Protocol, features: &[Feature]) -> Result<(), Error> {
+ match version {
+ Protocol::V1 => {
+ let has = |name: &str| features.iter().any(|f| f.0 == name);
+ // Let's focus on V2 standards, and simply not support old servers to keep our code simpler
+ if !has("multi_ack_detailed") {
+ return Err(Error::MissingServerCapability {
+ feature: "multi_ack_detailed",
+ });
+ }
+ // It's easy to NOT do sideband for us, but then again, everyone supports it.
+ // CORRECTION: If side-band is off, it would send the packfile without packet line encoding,
+ // which is nothing we ever want to deal with (despite it being more efficient). In V2, this
+ // is not even an option anymore, sidebands are always present.
+ if !has("side-band") && !has("side-band-64k") {
+ return Err(Error::MissingServerCapability {
+ feature: "side-band OR side-band-64k",
+ });
+ }
+ }
+ Protocol::V2 => {}
+ }
+ Ok(())
+ }
+
+ /// Return all acknowledgements [parsed previously][Response::from_line_reader()].
+ pub fn acknowledgements(&self) -> &[Acknowledgement] {
+ &self.acks
+ }
+
+ /// Return all shallow update lines [parsed previously][Response::from_line_reader()].
+ pub fn shallow_updates(&self) -> &[ShallowUpdate] {
+ &self.shallows
+ }
+
+ /// Return all wanted-refs [parsed previously][Response::from_line_reader()].
+ pub fn wanted_refs(&self) -> &[WantedRef] {
+ &self.wanted_refs
+ }
+}
+
+#[cfg(any(feature = "async-client", feature = "blocking-client"))]
+impl Response {
+ /// with a friendly server, we just assume that a non-ack line is a pack line
+ /// which is our hint to stop here.
+ fn parse_v1_ack_or_shallow_or_assume_pack(
+ acks: &mut Vec<Acknowledgement>,
+ shallows: &mut Vec<ShallowUpdate>,
+ peeked_line: &str,
+ ) -> bool {
+ match Acknowledgement::from_line(peeked_line) {
+ Ok(ack) => match ack.id() {
+ Some(id) => {
+ if !acks.iter().any(|a| a.id() == Some(id)) {
+ acks.push(ack);
+ }
+ }
+ None => acks.push(ack),
+ },
+ Err(_) => match ShallowUpdate::from_line(peeked_line) {
+ Ok(shallow) => {
+ shallows.push(shallow);
+ }
+ Err(_) => return true,
+ },
+ };
+ false
+ }
+}
+
+#[cfg(feature = "async-client")]
+mod async_io;
+#[cfg(feature = "blocking-client")]
+mod blocking_io;
diff --git a/vendor/gix-protocol/src/fetch/tests.rs b/vendor/gix-protocol/src/fetch/tests.rs
new file mode 100644
index 000000000..5a1902ad2
--- /dev/null
+++ b/vendor/gix-protocol/src/fetch/tests.rs
@@ -0,0 +1,388 @@
+#[cfg(any(feature = "async-client", feature = "blocking-client"))]
+mod arguments {
+ use bstr::ByteSlice;
+ use gix_transport::Protocol;
+
+ use crate::fetch;
+
+ fn arguments_v1(features: impl IntoIterator<Item = &'static str>) -> fetch::Arguments {
+ fetch::Arguments::new(Protocol::V1, features.into_iter().map(|n| (n, None)).collect())
+ }
+
+ fn arguments_v2(features: impl IntoIterator<Item = &'static str>) -> fetch::Arguments {
+ fetch::Arguments::new(Protocol::V2, features.into_iter().map(|n| (n, None)).collect())
+ }
+
+ struct Transport<T> {
+ inner: T,
+ stateful: bool,
+ }
+
+ #[cfg(feature = "blocking-client")]
+ mod impls {
+ use std::borrow::Cow;
+
+ use bstr::BStr;
+ use gix_transport::{
+ client,
+ client::{Error, MessageKind, RequestWriter, SetServiceResponse, WriteMode},
+ Protocol, Service,
+ };
+
+ use crate::fetch::tests::arguments::Transport;
+
+ impl<T: client::TransportWithoutIO> client::TransportWithoutIO for Transport<T> {
+ fn set_identity(&mut self, identity: client::Account) -> Result<(), Error> {
+ self.inner.set_identity(identity)
+ }
+
+ fn request(
+ &mut self,
+ write_mode: WriteMode,
+ on_into_read: MessageKind,
+ ) -> Result<RequestWriter<'_>, Error> {
+ self.inner.request(write_mode, on_into_read)
+ }
+
+ fn to_url(&self) -> Cow<'_, BStr> {
+ self.inner.to_url()
+ }
+
+ fn supported_protocol_versions(&self) -> &[Protocol] {
+ self.inner.supported_protocol_versions()
+ }
+
+ fn connection_persists_across_multiple_requests(&self) -> bool {
+ self.stateful
+ }
+
+ fn configure(
+ &mut self,
+ config: &dyn std::any::Any,
+ ) -> Result<(), Box<dyn std::error::Error + Send + Sync + 'static>> {
+ self.inner.configure(config)
+ }
+ }
+
+ impl<T: client::Transport> client::Transport for Transport<T> {
+ fn handshake<'a>(
+ &mut self,
+ service: Service,
+ extra_parameters: &'a [(&'a str, Option<&'a str>)],
+ ) -> Result<SetServiceResponse<'_>, Error> {
+ self.inner.handshake(service, extra_parameters)
+ }
+ }
+ }
+
+ #[cfg(feature = "async-client")]
+ mod impls {
+ use std::borrow::Cow;
+
+ use async_trait::async_trait;
+ use bstr::BStr;
+ use gix_transport::{
+ client,
+ client::{Error, MessageKind, RequestWriter, SetServiceResponse, WriteMode},
+ Protocol, Service,
+ };
+
+ use crate::fetch::tests::arguments::Transport;
+ impl<T: client::TransportWithoutIO + Send> client::TransportWithoutIO for Transport<T> {
+ fn set_identity(&mut self, identity: client::Account) -> Result<(), Error> {
+ self.inner.set_identity(identity)
+ }
+
+ fn request(
+ &mut self,
+ write_mode: WriteMode,
+ on_into_read: MessageKind,
+ ) -> Result<RequestWriter<'_>, Error> {
+ self.inner.request(write_mode, on_into_read)
+ }
+
+ fn to_url(&self) -> Cow<'_, BStr> {
+ self.inner.to_url()
+ }
+
+ fn supported_protocol_versions(&self) -> &[Protocol] {
+ self.inner.supported_protocol_versions()
+ }
+
+ fn connection_persists_across_multiple_requests(&self) -> bool {
+ self.stateful
+ }
+
+ fn configure(
+ &mut self,
+ config: &dyn std::any::Any,
+ ) -> Result<(), Box<dyn std::error::Error + Send + Sync + 'static>> {
+ self.inner.configure(config)
+ }
+ }
+
+ #[async_trait(?Send)]
+ impl<T: client::Transport + Send> client::Transport for Transport<T> {
+ async fn handshake<'a>(
+ &mut self,
+ service: Service,
+ extra_parameters: &'a [(&'a str, Option<&'a str>)],
+ ) -> Result<SetServiceResponse<'_>, Error> {
+ self.inner.handshake(service, extra_parameters).await
+ }
+ }
+ }
+
+ fn transport(
+ out: &mut Vec<u8>,
+ stateful: bool,
+ ) -> Transport<gix_transport::client::git::Connection<&'static [u8], &mut Vec<u8>>> {
+ Transport {
+ inner: gix_transport::client::git::Connection::new(
+ &[],
+ out,
+ Protocol::V1, // does not matter
+ b"does/not/matter".as_bstr().to_owned(),
+ None::<(&str, _)>,
+ gix_transport::client::git::ConnectMode::Process, // avoid header to be sent
+ ),
+ stateful,
+ }
+ }
+
+ fn id(hex: &str) -> gix_hash::ObjectId {
+ gix_hash::ObjectId::from_hex(hex.as_bytes()).expect("expect valid hex id")
+ }
+
+ mod v1 {
+ use bstr::ByteSlice;
+
+ use crate::fetch::tests::arguments::{arguments_v1, id, transport};
+
+ #[maybe_async::test(feature = "blocking-client", async(feature = "async-client", async_std::test))]
+ async fn include_tag() {
+ let mut out = Vec::new();
+ let mut t = transport(&mut out, true);
+ let mut arguments = arguments_v1(["include-tag", "feature-b"].iter().cloned());
+ assert!(arguments.can_use_include_tag());
+
+ arguments.use_include_tag();
+ arguments.want(id("ff333369de1221f9bfbbe03a3a13e9a09bc1ffff"));
+ arguments.send(&mut t, true).await.expect("sending to buffer to work");
+ assert_eq!(
+ out.as_bstr(),
+ b"0048want ff333369de1221f9bfbbe03a3a13e9a09bc1ffff include-tag feature-b
+0010include-tag
+00000009done
+"
+ .as_bstr()
+ );
+ }
+
+ #[maybe_async::test(feature = "blocking-client", async(feature = "async-client", async_std::test))]
+ async fn haves_and_wants_for_clone() {
+ let mut out = Vec::new();
+ let mut t = transport(&mut out, true);
+ let mut arguments = arguments_v1(["feature-a", "feature-b"].iter().cloned());
+ assert!(
+ !arguments.can_use_include_tag(),
+ "needs to be enabled by features in V1"
+ );
+
+ arguments.want(id("7b333369de1221f9bfbbe03a3a13e9a09bc1c907"));
+ arguments.want(id("ff333369de1221f9bfbbe03a3a13e9a09bc1ffff"));
+ arguments.send(&mut t, true).await.expect("sending to buffer to work");
+ assert_eq!(
+ out.as_bstr(),
+ b"0046want 7b333369de1221f9bfbbe03a3a13e9a09bc1c907 feature-a feature-b
+0032want ff333369de1221f9bfbbe03a3a13e9a09bc1ffff
+00000009done
+"
+ .as_bstr()
+ );
+ }
+
+ #[maybe_async::test(feature = "blocking-client", async(feature = "async-client", async_std::test))]
+ async fn haves_and_wants_for_fetch_stateless() {
+ let mut out = Vec::new();
+ let mut t = transport(&mut out, false);
+ let mut arguments = arguments_v1(["feature-a", "shallow", "deepen-since", "deepen-not"].iter().copied());
+
+ arguments.deepen(1);
+ arguments.shallow(id("7b333369de1221f9bfbbe03a3a13e9a09bc1c9ff"));
+ arguments.want(id("7b333369de1221f9bfbbe03a3a13e9a09bc1c907"));
+ arguments.deepen_since(12345);
+ arguments.deepen_not("refs/heads/main".into());
+ arguments.have(id("0000000000000000000000000000000000000000"));
+ arguments.send(&mut t, false).await.expect("sending to buffer to work");
+
+ arguments.have(id("1111111111111111111111111111111111111111"));
+ arguments.send(&mut t, true).await.expect("sending to buffer to work");
+ assert_eq!(
+ out.as_bstr(),
+ b"005cwant 7b333369de1221f9bfbbe03a3a13e9a09bc1c907 feature-a shallow deepen-since deepen-not
+0035shallow 7b333369de1221f9bfbbe03a3a13e9a09bc1c9ff
+000ddeepen 1
+0017deepen-since 12345
+001fdeepen-not refs/heads/main
+00000032have 0000000000000000000000000000000000000000
+0000005cwant 7b333369de1221f9bfbbe03a3a13e9a09bc1c907 feature-a shallow deepen-since deepen-not
+0035shallow 7b333369de1221f9bfbbe03a3a13e9a09bc1c9ff
+000ddeepen 1
+0017deepen-since 12345
+001fdeepen-not refs/heads/main
+00000032have 1111111111111111111111111111111111111111
+0009done
+"
+ .as_bstr()
+ );
+ }
+
+ #[maybe_async::test(feature = "blocking-client", async(feature = "async-client", async_std::test))]
+ async fn haves_and_wants_for_fetch_stateful() {
+ let mut out = Vec::new();
+ let mut t = transport(&mut out, true);
+ let mut arguments = arguments_v1(["feature-a", "shallow"].iter().copied());
+
+ arguments.deepen(1);
+ arguments.want(id("7b333369de1221f9bfbbe03a3a13e9a09bc1c907"));
+ arguments.have(id("0000000000000000000000000000000000000000"));
+ arguments.send(&mut t, false).await.expect("sending to buffer to work");
+
+ arguments.have(id("1111111111111111111111111111111111111111"));
+ arguments.send(&mut t, true).await.expect("sending to buffer to work");
+ assert_eq!(
+ out.as_bstr(),
+ b"0044want 7b333369de1221f9bfbbe03a3a13e9a09bc1c907 feature-a shallow
+000ddeepen 1
+00000032have 0000000000000000000000000000000000000000
+00000032have 1111111111111111111111111111111111111111
+0009done
+"
+ .as_bstr()
+ );
+ }
+ }
+
+ mod v2 {
+ use bstr::ByteSlice;
+
+ use crate::fetch::tests::arguments::{arguments_v2, id, transport};
+
+ #[maybe_async::test(feature = "blocking-client", async(feature = "async-client", async_std::test))]
+ async fn include_tag() {
+ let mut out = Vec::new();
+ let mut t = transport(&mut out, true);
+ let mut arguments = arguments_v2(["does not matter for us here"].iter().copied());
+ assert!(arguments.can_use_include_tag(), "always on in V2");
+ arguments.use_include_tag();
+
+ arguments.want(id("ff333369de1221f9bfbbe03a3a13e9a09bc1ffff"));
+ arguments.send(&mut t, true).await.expect("sending to buffer to work");
+ assert_eq!(
+ out.as_bstr(),
+ b"0012command=fetch
+0001000ethin-pack
+000eofs-delta
+0010include-tag
+0032want ff333369de1221f9bfbbe03a3a13e9a09bc1ffff
+0009done
+0000"
+ .as_bstr(),
+ "we filter features/capabilities without value as these apparently shouldn't be listed (remote dies otherwise)"
+ );
+ }
+
+ #[maybe_async::test(feature = "blocking-client", async(feature = "async-client", async_std::test))]
+ async fn haves_and_wants_for_clone_stateful() {
+ let mut out = Vec::new();
+ let mut t = transport(&mut out, true);
+ let mut arguments = arguments_v2(["feature-a", "shallow"].iter().copied());
+
+ arguments.deepen(1);
+ arguments.deepen_relative();
+ arguments.want(id("7b333369de1221f9bfbbe03a3a13e9a09bc1c907"));
+ arguments.want(id("ff333369de1221f9bfbbe03a3a13e9a09bc1ffff"));
+ arguments.send(&mut t, true).await.expect("sending to buffer to work");
+ assert_eq!(
+ out.as_bstr(),
+ b"0012command=fetch
+0001000ethin-pack
+000eofs-delta
+000ddeepen 1
+0014deepen-relative
+0032want 7b333369de1221f9bfbbe03a3a13e9a09bc1c907
+0032want ff333369de1221f9bfbbe03a3a13e9a09bc1ffff
+0009done
+0000"
+ .as_bstr(),
+ "we filter features/capabilities without value as these apparently shouldn't be listed (remote dies otherwise)"
+ );
+ }
+
+ #[maybe_async::test(feature = "blocking-client", async(feature = "async-client", async_std::test))]
+ async fn haves_and_wants_for_fetch_stateless_and_stateful() {
+ for is_stateful in &[false, true] {
+ let mut out = Vec::new();
+ let mut t = transport(&mut out, *is_stateful);
+ let mut arguments = arguments_v2(Some("shallow"));
+
+ arguments.deepen(1);
+ arguments.deepen_since(12345);
+ arguments.shallow(id("7b333369de1221f9bfbbe03a3a13e9a09bc1c9ff"));
+ arguments.want(id("7b333369de1221f9bfbbe03a3a13e9a09bc1c907"));
+ arguments.deepen_not("refs/heads/main".into());
+ arguments.have(id("0000000000000000000000000000000000000000"));
+ arguments.send(&mut t, false).await.expect("sending to buffer to work");
+
+ arguments.have(id("1111111111111111111111111111111111111111"));
+ arguments.send(&mut t, true).await.expect("sending to buffer to work");
+ assert_eq!(
+ out.as_bstr(),
+ b"0012command=fetch
+0001000ethin-pack
+000eofs-delta
+000ddeepen 1
+0017deepen-since 12345
+0035shallow 7b333369de1221f9bfbbe03a3a13e9a09bc1c9ff
+0032want 7b333369de1221f9bfbbe03a3a13e9a09bc1c907
+001fdeepen-not refs/heads/main
+0032have 0000000000000000000000000000000000000000
+00000012command=fetch
+0001000ethin-pack
+000eofs-delta
+000ddeepen 1
+0017deepen-since 12345
+0035shallow 7b333369de1221f9bfbbe03a3a13e9a09bc1c9ff
+0032want 7b333369de1221f9bfbbe03a3a13e9a09bc1c907
+001fdeepen-not refs/heads/main
+0032have 1111111111111111111111111111111111111111
+0009done
+0000"
+ .as_bstr(),
+ "V2 is stateless by default, so it repeats all but 'haves' in each request"
+ );
+ }
+ }
+
+ #[maybe_async::test(feature = "blocking-client", async(feature = "async-client", async_std::test))]
+ async fn ref_in_want() {
+ let mut out = Vec::new();
+ let mut t = transport(&mut out, false);
+ let mut arguments = arguments_v2(["ref-in-want"].iter().copied());
+
+ arguments.want_ref(b"refs/heads/main".as_bstr());
+ arguments.send(&mut t, true).await.expect("sending to buffer to work");
+ assert_eq!(
+ out.as_bstr(),
+ b"0012command=fetch
+0001000ethin-pack
+000eofs-delta
+001dwant-ref refs/heads/main
+0009done
+0000"
+ .as_bstr()
+ )
+ }
+ }
+}
diff --git a/vendor/gix-protocol/src/fetch_fn.rs b/vendor/gix-protocol/src/fetch_fn.rs
new file mode 100644
index 000000000..5b2d214ae
--- /dev/null
+++ b/vendor/gix-protocol/src/fetch_fn.rs
@@ -0,0 +1,168 @@
+use std::borrow::Cow;
+
+use gix_features::progress::Progress;
+use gix_transport::client;
+use maybe_async::maybe_async;
+
+use crate::{
+ credentials,
+ fetch::{Action, Arguments, Delegate, Error, Response},
+ indicate_end_of_interaction, Command,
+};
+
+/// A way to indicate how to treat the connection underlying the transport, potentially allowing to reuse it.
+pub enum FetchConnection {
+ /// Use this variant if server should be informed that the operation is completed and no further commands will be issued
+ /// at the end of the fetch operation or after deciding that no fetch operation should happen after references were listed.
+ ///
+ /// When indicating the end-of-fetch, this flag is only relevant in protocol V2.
+ /// Generally it only applies when using persistent transports.
+ ///
+ /// In most explicit client side failure modes the end-of-operation' notification will be sent to the server automatically.
+ TerminateOnSuccessfulCompletion,
+
+ /// Indicate that persistent transport connections can be reused by _not_ sending an 'end-of-operation' notification to the server.
+ /// This is useful if multiple `fetch(…)` calls are used in succession.
+ ///
+ /// Note that this has no effect in case of non-persistent connections, like the ones over HTTP.
+ ///
+ /// As an optimization, callers can use `AllowReuse` here as the server will also know the client is done
+ /// if the connection is closed.
+ AllowReuse,
+}
+
+impl Default for FetchConnection {
+ fn default() -> Self {
+ FetchConnection::TerminateOnSuccessfulCompletion
+ }
+}
+
+/// Perform a 'fetch' operation with the server using `transport`, with `delegate` handling all server interactions.
+/// **Note** that `delegate` has blocking operations and thus this entire call should be on an executor which can handle
+/// that. This could be the current thread blocking, or another thread.
+///
+/// * `authenticate(operation_to_perform)` is used to receive credentials for the connection and potentially store it
+/// if the server indicates 'permission denied'. Note that not all transport support authentication or authorization.
+/// * `progress` is used to emit progress messages.
+/// * `name` is the name of the git client to present as `agent`, like `"my-app (v2.0)"`".
+///
+/// _Note_ that depending on the `delegate`, the actual action performed can be `ls-refs`, `clone` or `fetch`.
+#[allow(clippy::result_large_err)]
+#[maybe_async]
+pub async fn fetch<F, D, T, P>(
+ mut transport: T,
+ mut delegate: D,
+ authenticate: F,
+ mut progress: P,
+ fetch_mode: FetchConnection,
+ agent: impl Into<String>,
+) -> Result<(), Error>
+where
+ F: FnMut(credentials::helper::Action) -> credentials::protocol::Result,
+ D: Delegate,
+ T: client::Transport,
+ P: Progress,
+ P::SubProgress: 'static,
+{
+ let crate::handshake::Outcome {
+ server_protocol_version: protocol_version,
+ refs,
+ capabilities,
+ } = crate::fetch::handshake(
+ &mut transport,
+ authenticate,
+ delegate.handshake_extra_parameters(),
+ &mut progress,
+ )
+ .await?;
+
+ let agent = crate::agent(agent);
+ let refs = match refs {
+ Some(refs) => refs,
+ None => {
+ crate::ls_refs(
+ &mut transport,
+ &capabilities,
+ |a, b, c| {
+ let res = delegate.prepare_ls_refs(a, b, c);
+ c.push(("agent", Some(Cow::Owned(agent.clone()))));
+ res
+ },
+ &mut progress,
+ )
+ .await?
+ }
+ };
+
+ let fetch = Command::Fetch;
+ let mut fetch_features = fetch.default_features(protocol_version, &capabilities);
+ match delegate.prepare_fetch(protocol_version, &capabilities, &mut fetch_features, &refs) {
+ Ok(Action::Cancel) => {
+ return if matches!(protocol_version, gix_transport::Protocol::V1)
+ || matches!(fetch_mode, FetchConnection::TerminateOnSuccessfulCompletion)
+ {
+ indicate_end_of_interaction(transport).await.map_err(Into::into)
+ } else {
+ Ok(())
+ };
+ }
+ Ok(Action::Continue) => {
+ fetch.validate_argument_prefixes_or_panic(protocol_version, &capabilities, &[], &fetch_features);
+ }
+ Err(err) => {
+ indicate_end_of_interaction(transport).await?;
+ return Err(err.into());
+ }
+ }
+
+ Response::check_required_features(protocol_version, &fetch_features)?;
+ let sideband_all = fetch_features.iter().any(|(n, _)| *n == "sideband-all");
+ fetch_features.push(("agent", Some(Cow::Owned(agent))));
+ let mut arguments = Arguments::new(protocol_version, fetch_features);
+ let mut previous_response = None::<Response>;
+ let mut round = 1;
+ 'negotiation: loop {
+ progress.step();
+ progress.set_name(format!("negotiate (round {round})"));
+ round += 1;
+ let action = delegate.negotiate(&refs, &mut arguments, previous_response.as_ref())?;
+ let mut reader = arguments.send(&mut transport, action == Action::Cancel).await?;
+ if sideband_all {
+ setup_remote_progress(&mut progress, &mut reader);
+ }
+ let response = Response::from_line_reader(protocol_version, &mut reader).await?;
+ previous_response = if response.has_pack() {
+ progress.step();
+ progress.set_name("receiving pack");
+ if !sideband_all {
+ setup_remote_progress(&mut progress, &mut reader);
+ }
+ delegate.receive_pack(reader, progress, &refs, &response).await?;
+ break 'negotiation;
+ } else {
+ match action {
+ Action::Cancel => break 'negotiation,
+ Action::Continue => Some(response),
+ }
+ }
+ }
+ if matches!(protocol_version, gix_transport::Protocol::V2)
+ && matches!(fetch_mode, FetchConnection::TerminateOnSuccessfulCompletion)
+ {
+ indicate_end_of_interaction(transport).await?;
+ }
+ Ok(())
+}
+
+fn setup_remote_progress<P>(progress: &mut P, reader: &mut Box<dyn gix_transport::client::ExtendedBufRead + Unpin + '_>)
+where
+ P: Progress,
+ P::SubProgress: 'static,
+{
+ reader.set_progress_handler(Some(Box::new({
+ let mut remote_progress = progress.add_child("remote");
+ move |is_err: bool, data: &[u8]| {
+ crate::RemoteProgress::translate_to_progress(is_err, data, &mut remote_progress)
+ }
+ }) as gix_transport::client::HandleProgress));
+}
diff --git a/vendor/gix-protocol/src/handshake/function.rs b/vendor/gix-protocol/src/handshake/function.rs
new file mode 100644
index 000000000..c56824cca
--- /dev/null
+++ b/vendor/gix-protocol/src/handshake/function.rs
@@ -0,0 +1,100 @@
+use gix_features::{progress, progress::Progress};
+use gix_transport::{client, client::SetServiceResponse, Service};
+use maybe_async::maybe_async;
+
+use super::{Error, Outcome};
+use crate::{credentials, handshake::refs};
+
+/// Perform a handshake with the server on the other side of `transport`, with `authenticate` being used if authentication
+/// turns out to be required. `extra_parameters` are the parameters `(name, optional value)` to add to the handshake,
+/// each time it is performed in case authentication is required.
+/// `progress` is used to inform about what's currently happening.
+#[allow(clippy::result_large_err)]
+#[maybe_async]
+pub async fn handshake<AuthFn, T>(
+ mut transport: T,
+ service: Service,
+ mut authenticate: AuthFn,
+ extra_parameters: Vec<(String, Option<String>)>,
+ progress: &mut impl Progress,
+) -> Result<Outcome, Error>
+where
+ AuthFn: FnMut(credentials::helper::Action) -> credentials::protocol::Result,
+ T: client::Transport,
+{
+ let (server_protocol_version, refs, capabilities) = {
+ progress.init(None, progress::steps());
+ progress.set_name("handshake");
+ progress.step();
+
+ let extra_parameters: Vec<_> = extra_parameters
+ .iter()
+ .map(|(k, v)| (k.as_str(), v.as_ref().map(|s| s.as_str())))
+ .collect();
+ let supported_versions: Vec<_> = transport.supported_protocol_versions().into();
+
+ let result = transport.handshake(service, &extra_parameters).await;
+ let SetServiceResponse {
+ actual_protocol,
+ capabilities,
+ refs,
+ } = match result {
+ Ok(v) => Ok(v),
+ Err(client::Error::Io(ref err)) if err.kind() == std::io::ErrorKind::PermissionDenied => {
+ drop(result); // needed to workaround this: https://github.com/rust-lang/rust/issues/76149
+ let url = transport.to_url().into_owned();
+ progress.set_name("authentication");
+ let credentials::protocol::Outcome { identity, next } =
+ authenticate(credentials::helper::Action::get_for_url(url.clone()))?
+ .expect("FILL provides an identity or errors");
+ transport.set_identity(identity)?;
+ progress.step();
+ progress.set_name("handshake (authenticated)");
+ match transport.handshake(service, &extra_parameters).await {
+ Ok(v) => {
+ authenticate(next.store())?;
+ Ok(v)
+ }
+ // Still no permission? Reject the credentials.
+ Err(client::Error::Io(err)) if err.kind() == std::io::ErrorKind::PermissionDenied => {
+ authenticate(next.erase())?;
+ return Err(Error::InvalidCredentials { url, source: err });
+ }
+ // Otherwise, do nothing, as we don't know if it actually got to try the credentials.
+ // If they were previously stored, they remain. In the worst case, the user has to enter them again
+ // next time they try.
+ Err(err) => Err(err),
+ }
+ }
+ Err(err) => Err(err),
+ }?;
+
+ if !supported_versions.is_empty() && !supported_versions.contains(&actual_protocol) {
+ return Err(Error::TransportProtocolPolicyViolation {
+ actual_version: actual_protocol,
+ });
+ }
+
+ let parsed_refs = match refs {
+ Some(mut refs) => {
+ assert_eq!(
+ actual_protocol,
+ gix_transport::Protocol::V1,
+ "Only V1 auto-responds with refs"
+ );
+ Some(
+ refs::from_v1_refs_received_as_part_of_handshake_and_capabilities(&mut refs, capabilities.iter())
+ .await?,
+ )
+ }
+ None => None,
+ };
+ (actual_protocol, parsed_refs, capabilities)
+ }; // this scope is needed, see https://github.com/rust-lang/rust/issues/76149
+
+ Ok(Outcome {
+ server_protocol_version,
+ refs,
+ capabilities,
+ })
+}
diff --git a/vendor/gix-protocol/src/handshake/mod.rs b/vendor/gix-protocol/src/handshake/mod.rs
new file mode 100644
index 000000000..4e0741012
--- /dev/null
+++ b/vendor/gix-protocol/src/handshake/mod.rs
@@ -0,0 +1,95 @@
+use bstr::BString;
+use gix_transport::client::Capabilities;
+
+/// A git reference, commonly referred to as 'ref', as returned by a git server before sending a pack.
+#[derive(PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone)]
+#[cfg_attr(feature = "serde1", derive(serde::Serialize, serde::Deserialize))]
+pub enum Ref {
+ /// A ref pointing to a `tag` object, which in turns points to an `object`, usually a commit
+ Peeled {
+ /// The name at which the ref is located, like `refs/tags/1.0`.
+ full_ref_name: BString,
+ /// The hash of the tag the ref points to.
+ tag: gix_hash::ObjectId,
+ /// The hash of the object the `tag` points to.
+ object: gix_hash::ObjectId,
+ },
+ /// A ref pointing to a commit object
+ Direct {
+ /// The name at which the ref is located, like `refs/heads/main` or `refs/tags/v1.0` for lightweight tags.
+ full_ref_name: BString,
+ /// The hash of the object the ref points to.
+ object: gix_hash::ObjectId,
+ },
+ /// A symbolic ref pointing to `target` ref, which in turn points to an `object`
+ Symbolic {
+ /// The name at which the symbolic ref is located, like `HEAD`.
+ full_ref_name: BString,
+ /// The path of the ref the symbolic ref points to, like `refs/heads/main`.
+ ///
+ /// See issue [#205] for details
+ ///
+ /// [#205]: https://github.com/Byron/gitoxide/issues/205
+ target: BString,
+ /// The hash of the object the `target` ref points to.
+ object: gix_hash::ObjectId,
+ },
+ /// A ref is unborn on the remote and just points to the initial, unborn branch, as is the case in a newly initialized repository
+ /// or dangling symbolic refs.
+ Unborn {
+ /// The name at which the ref is located, typically `HEAD`.
+ full_ref_name: BString,
+ /// The path of the ref the symbolic ref points to, like `refs/heads/main`, even though the `target` does not yet exist.
+ target: BString,
+ },
+}
+
+/// The result of the [`handshake()`][super::handshake()] function.
+#[derive(Default, Debug, Clone)]
+#[cfg_attr(feature = "serde1", derive(serde::Serialize, serde::Deserialize))]
+pub struct Outcome {
+ /// The protocol version the server responded with. It might have downgraded the desired version.
+ pub server_protocol_version: gix_transport::Protocol,
+ /// The references reported as part of the Protocol::V1 handshake, or `None` otherwise as V2 requires a separate request.
+ pub refs: Option<Vec<Ref>>,
+ /// The server capabilities.
+ pub capabilities: Capabilities,
+}
+
+mod error {
+ use bstr::BString;
+ use gix_transport::client;
+
+ use crate::{credentials, handshake::refs};
+
+ /// The error returned by [`handshake()`][crate::fetch::handshake()].
+ #[derive(Debug, thiserror::Error)]
+ #[allow(missing_docs)]
+ pub enum Error {
+ #[error("Failed to obtain credentials")]
+ Credentials(#[from] credentials::protocol::Error),
+ #[error("Credentials provided for \"{url}\" were not accepted by the remote")]
+ InvalidCredentials { url: BString, source: std::io::Error },
+ #[error(transparent)]
+ Transport(#[from] client::Error),
+ #[error("The transport didn't accept the advertised server version {actual_version:?} and closed the connection client side")]
+ TransportProtocolPolicyViolation { actual_version: gix_transport::Protocol },
+ #[error(transparent)]
+ ParseRefs(#[from] refs::parse::Error),
+ }
+
+ impl gix_transport::IsSpuriousError for Error {
+ fn is_spurious(&self) -> bool {
+ match self {
+ Error::Transport(err) => err.is_spurious(),
+ _ => false,
+ }
+ }
+ }
+}
+pub use error::Error;
+
+pub(crate) mod function;
+
+///
+pub mod refs;
diff --git a/vendor/gix-protocol/src/handshake/refs/async_io.rs b/vendor/gix-protocol/src/handshake/refs/async_io.rs
new file mode 100644
index 000000000..19ea543c7
--- /dev/null
+++ b/vendor/gix-protocol/src/handshake/refs/async_io.rs
@@ -0,0 +1,43 @@
+use crate::handshake::{refs, refs::parse::Error, Ref};
+
+/// Parse refs from the given input line by line. Protocol V2 is required for this to succeed.
+pub async fn from_v2_refs(in_refs: &mut dyn gix_transport::client::ReadlineBufRead) -> Result<Vec<Ref>, Error> {
+ let mut out_refs = Vec::new();
+ while let Some(line) = in_refs
+ .readline()
+ .await
+ .transpose()?
+ .transpose()?
+ .and_then(|l| l.as_bstr())
+ {
+ out_refs.push(refs::shared::parse_v2(line)?);
+ }
+ Ok(out_refs)
+}
+
+/// Parse refs from the return stream of the handshake as well as the server capabilities, also received as part of the
+/// handshake.
+/// Together they form a complete set of refs.
+///
+/// # Note
+///
+/// Symbolic refs are shoe-horned into server capabilities whereas refs (without symbolic ones) are sent automatically as
+/// part of the handshake. Both symbolic and peeled refs need to be combined to fit into the [`Ref`] type provided here.
+pub async fn from_v1_refs_received_as_part_of_handshake_and_capabilities<'a>(
+ in_refs: &mut dyn gix_transport::client::ReadlineBufRead,
+ capabilities: impl Iterator<Item = gix_transport::client::capabilities::Capability<'a>>,
+) -> Result<Vec<Ref>, refs::parse::Error> {
+ let mut out_refs = refs::shared::from_capabilities(capabilities)?;
+ let number_of_possible_symbolic_refs_for_lookup = out_refs.len();
+
+ while let Some(line) = in_refs
+ .readline()
+ .await
+ .transpose()?
+ .transpose()?
+ .and_then(|l| l.as_bstr())
+ {
+ refs::shared::parse_v1(number_of_possible_symbolic_refs_for_lookup, &mut out_refs, line)?;
+ }
+ Ok(out_refs.into_iter().map(Into::into).collect())
+}
diff --git a/vendor/gix-protocol/src/handshake/refs/blocking_io.rs b/vendor/gix-protocol/src/handshake/refs/blocking_io.rs
new file mode 100644
index 000000000..7ad695b77
--- /dev/null
+++ b/vendor/gix-protocol/src/handshake/refs/blocking_io.rs
@@ -0,0 +1,31 @@
+use crate::handshake::{refs, refs::parse::Error, Ref};
+
+/// Parse refs from the given input line by line. Protocol V2 is required for this to succeed.
+pub fn from_v2_refs(in_refs: &mut dyn gix_transport::client::ReadlineBufRead) -> Result<Vec<Ref>, Error> {
+ let mut out_refs = Vec::new();
+ while let Some(line) = in_refs.readline().transpose()?.transpose()?.and_then(|l| l.as_bstr()) {
+ out_refs.push(refs::shared::parse_v2(line)?);
+ }
+ Ok(out_refs)
+}
+
+/// Parse refs from the return stream of the handshake as well as the server capabilities, also received as part of the
+/// handshake.
+/// Together they form a complete set of refs.
+///
+/// # Note
+///
+/// Symbolic refs are shoe-horned into server capabilities whereas refs (without symbolic ones) are sent automatically as
+/// part of the handshake. Both symbolic and peeled refs need to be combined to fit into the [`Ref`] type provided here.
+pub fn from_v1_refs_received_as_part_of_handshake_and_capabilities<'a>(
+ in_refs: &mut dyn gix_transport::client::ReadlineBufRead,
+ capabilities: impl Iterator<Item = gix_transport::client::capabilities::Capability<'a>>,
+) -> Result<Vec<Ref>, Error> {
+ let mut out_refs = refs::shared::from_capabilities(capabilities)?;
+ let number_of_possible_symbolic_refs_for_lookup = out_refs.len();
+
+ while let Some(line) = in_refs.readline().transpose()?.transpose()?.and_then(|l| l.as_bstr()) {
+ refs::shared::parse_v1(number_of_possible_symbolic_refs_for_lookup, &mut out_refs, line)?;
+ }
+ Ok(out_refs.into_iter().map(Into::into).collect())
+}
diff --git a/vendor/gix-protocol/src/handshake/refs/mod.rs b/vendor/gix-protocol/src/handshake/refs/mod.rs
new file mode 100644
index 000000000..889842e4c
--- /dev/null
+++ b/vendor/gix-protocol/src/handshake/refs/mod.rs
@@ -0,0 +1,72 @@
+use bstr::BStr;
+
+use super::Ref;
+
+///
+pub mod parse {
+ use bstr::BString;
+
+ /// The error returned when parsing References/refs from the server response.
+ #[derive(Debug, thiserror::Error)]
+ #[allow(missing_docs)]
+ pub enum Error {
+ #[error(transparent)]
+ Io(#[from] std::io::Error),
+ #[error(transparent)]
+ DecodePacketline(#[from] gix_transport::packetline::decode::Error),
+ #[error(transparent)]
+ Id(#[from] gix_hash::decode::Error),
+ #[error("{symref:?} could not be parsed. A symref is expected to look like <NAME>:<target>.")]
+ MalformedSymref { symref: BString },
+ #[error("{0:?} could not be parsed. A V1 ref line should be '<hex-hash> <path>'.")]
+ MalformedV1RefLine(BString),
+ #[error(
+ "{0:?} could not be parsed. A V2 ref line should be '<hex-hash> <path>[ (peeled|symref-target):<value>'."
+ )]
+ MalformedV2RefLine(BString),
+ #[error("The ref attribute {attribute:?} is unknown. Found in line {line:?}")]
+ UnknownAttribute { attribute: BString, line: BString },
+ #[error("{message}")]
+ InvariantViolation { message: &'static str },
+ }
+}
+
+impl Ref {
+ /// Provide shared fields referring to the ref itself, namely `(name, target, [peeled])`.
+ /// In case of peeled refs, the tag object itself is returned as it is what the ref directly refers to, and target of the tag is returned
+ /// as `peeled`.
+ /// If `unborn`, the first object id will be the null oid.
+ pub fn unpack(&self) -> (&BStr, Option<&gix_hash::oid>, Option<&gix_hash::oid>) {
+ match self {
+ Ref::Direct { full_ref_name, object }
+ | Ref::Symbolic {
+ full_ref_name, object, ..
+ } => (full_ref_name.as_ref(), Some(object), None),
+ Ref::Peeled {
+ full_ref_name,
+ tag: object,
+ object: peeled,
+ } => (full_ref_name.as_ref(), Some(object), Some(peeled)),
+ Ref::Unborn {
+ full_ref_name,
+ target: _,
+ } => (full_ref_name.as_ref(), None, None),
+ }
+ }
+}
+
+#[cfg(any(feature = "blocking-client", feature = "async-client"))]
+pub(crate) mod shared;
+
+#[cfg(feature = "async-client")]
+mod async_io;
+#[cfg(feature = "async-client")]
+pub use async_io::{from_v1_refs_received_as_part_of_handshake_and_capabilities, from_v2_refs};
+
+#[cfg(feature = "blocking-client")]
+mod blocking_io;
+#[cfg(feature = "blocking-client")]
+pub use blocking_io::{from_v1_refs_received_as_part_of_handshake_and_capabilities, from_v2_refs};
+
+#[cfg(test)]
+mod tests;
diff --git a/vendor/gix-protocol/src/handshake/refs/shared.rs b/vendor/gix-protocol/src/handshake/refs/shared.rs
new file mode 100644
index 000000000..1d0dfc256
--- /dev/null
+++ b/vendor/gix-protocol/src/handshake/refs/shared.rs
@@ -0,0 +1,237 @@
+use bstr::{BStr, BString, ByteSlice};
+
+use crate::handshake::{refs::parse::Error, Ref};
+
+impl From<InternalRef> for Ref {
+ fn from(v: InternalRef) -> Self {
+ match v {
+ InternalRef::Symbolic {
+ path,
+ target: Some(target),
+ object,
+ } => Ref::Symbolic {
+ full_ref_name: path,
+ target,
+ object,
+ },
+ InternalRef::Symbolic {
+ path,
+ target: None,
+ object,
+ } => Ref::Direct {
+ full_ref_name: path,
+ object,
+ },
+ InternalRef::Peeled { path, tag, object } => Ref::Peeled {
+ full_ref_name: path,
+ tag,
+ object,
+ },
+ InternalRef::Direct { path, object } => Ref::Direct {
+ full_ref_name: path,
+ object,
+ },
+ InternalRef::SymbolicForLookup { .. } => {
+ unreachable!("this case should have been removed during processing")
+ }
+ }
+ }
+}
+
+#[cfg_attr(test, derive(PartialEq, Eq, Debug, Clone))]
+pub(crate) enum InternalRef {
+ /// A ref pointing to a `tag` object, which in turns points to an `object`, usually a commit
+ Peeled {
+ path: BString,
+ tag: gix_hash::ObjectId,
+ object: gix_hash::ObjectId,
+ },
+ /// A ref pointing to a commit object
+ Direct { path: BString, object: gix_hash::ObjectId },
+ /// A symbolic ref pointing to `target` ref, which in turn points to an `object`
+ Symbolic {
+ path: BString,
+ /// It is `None` if the target is unreachable as it points to another namespace than the one is currently set
+ /// on the server (i.e. based on the repository at hand or the user performing the operation).
+ ///
+ /// The latter is more of an edge case, please [this issue][#205] for details.
+ target: Option<BString>,
+ object: gix_hash::ObjectId,
+ },
+ /// extracted from V1 capabilities, which contain some important symbolic refs along with their targets
+ /// These don't contain the Id
+ SymbolicForLookup { path: BString, target: Option<BString> },
+}
+
+impl InternalRef {
+ fn unpack_direct(self) -> Option<(BString, gix_hash::ObjectId)> {
+ match self {
+ InternalRef::Direct { path, object } => Some((path, object)),
+ _ => None,
+ }
+ }
+ fn lookup_symbol_has_path(&self, predicate_path: &BStr) -> bool {
+ matches!(self, InternalRef::SymbolicForLookup { path, .. } if path == predicate_path)
+ }
+}
+
+pub(crate) fn from_capabilities<'a>(
+ capabilities: impl Iterator<Item = gix_transport::client::capabilities::Capability<'a>>,
+) -> Result<Vec<InternalRef>, Error> {
+ let mut out_refs = Vec::new();
+ let symref_values = capabilities.filter_map(|c| {
+ if c.name() == b"symref".as_bstr() {
+ c.value().map(ToOwned::to_owned)
+ } else {
+ None
+ }
+ });
+ for symref in symref_values {
+ let (left, right) = symref.split_at(symref.find_byte(b':').ok_or_else(|| Error::MalformedSymref {
+ symref: symref.to_owned(),
+ })?);
+ if left.is_empty() || right.is_empty() {
+ return Err(Error::MalformedSymref {
+ symref: symref.to_owned(),
+ });
+ }
+ out_refs.push(InternalRef::SymbolicForLookup {
+ path: left.into(),
+ target: match &right[1..] {
+ b"(null)" => None,
+ name => Some(name.into()),
+ },
+ })
+ }
+ Ok(out_refs)
+}
+
+pub(in crate::handshake::refs) fn parse_v1(
+ num_initial_out_refs: usize,
+ out_refs: &mut Vec<InternalRef>,
+ line: &BStr,
+) -> Result<(), Error> {
+ let trimmed = line.trim_end();
+ let (hex_hash, path) = trimmed.split_at(
+ trimmed
+ .find(b" ")
+ .ok_or_else(|| Error::MalformedV1RefLine(trimmed.to_owned().into()))?,
+ );
+ let path = &path[1..];
+ if path.is_empty() {
+ return Err(Error::MalformedV1RefLine(trimmed.to_owned().into()));
+ }
+ match path.strip_suffix(b"^{}") {
+ Some(stripped) => {
+ let (previous_path, tag) =
+ out_refs
+ .pop()
+ .and_then(InternalRef::unpack_direct)
+ .ok_or(Error::InvariantViolation {
+ message: "Expecting peeled refs to be preceded by direct refs",
+ })?;
+ if previous_path != stripped {
+ return Err(Error::InvariantViolation {
+ message: "Expecting peeled refs to have the same base path as the previous, unpeeled one",
+ });
+ }
+ out_refs.push(InternalRef::Peeled {
+ path: previous_path,
+ tag,
+ object: gix_hash::ObjectId::from_hex(hex_hash.as_bytes())?,
+ });
+ }
+ None => {
+ let object = gix_hash::ObjectId::from_hex(hex_hash.as_bytes())?;
+ match out_refs
+ .iter()
+ .take(num_initial_out_refs)
+ .position(|r| r.lookup_symbol_has_path(path.into()))
+ {
+ Some(position) => match out_refs.swap_remove(position) {
+ InternalRef::SymbolicForLookup { path: _, target } => out_refs.push(InternalRef::Symbolic {
+ path: path.into(),
+ object,
+ target,
+ }),
+ _ => unreachable!("Bug in lookup_symbol_has_path - must return lookup symbols"),
+ },
+ None => out_refs.push(InternalRef::Direct {
+ object,
+ path: path.into(),
+ }),
+ };
+ }
+ }
+ Ok(())
+}
+
+pub(in crate::handshake::refs) fn parse_v2(line: &BStr) -> Result<Ref, Error> {
+ let trimmed = line.trim_end();
+ let mut tokens = trimmed.splitn(3, |b| *b == b' ');
+ match (tokens.next(), tokens.next()) {
+ (Some(hex_hash), Some(path)) => {
+ let id = if hex_hash == b"unborn" {
+ None
+ } else {
+ Some(gix_hash::ObjectId::from_hex(hex_hash.as_bytes())?)
+ };
+ if path.is_empty() {
+ return Err(Error::MalformedV2RefLine(trimmed.to_owned().into()));
+ }
+ Ok(if let Some(attribute) = tokens.next() {
+ let mut tokens = attribute.splitn(2, |b| *b == b':');
+ match (tokens.next(), tokens.next()) {
+ (Some(attribute), Some(value)) => {
+ if value.is_empty() {
+ return Err(Error::MalformedV2RefLine(trimmed.to_owned().into()));
+ }
+ match attribute {
+ b"peeled" => Ref::Peeled {
+ full_ref_name: path.into(),
+ object: gix_hash::ObjectId::from_hex(value.as_bytes())?,
+ tag: id.ok_or(Error::InvariantViolation {
+ message: "got 'unborn' as tag target",
+ })?,
+ },
+ b"symref-target" => match value {
+ b"(null)" => Ref::Direct {
+ full_ref_name: path.into(),
+ object: id.ok_or(Error::InvariantViolation {
+ message: "got 'unborn' while (null) was a symref target",
+ })?,
+ },
+ name => match id {
+ Some(id) => Ref::Symbolic {
+ full_ref_name: path.into(),
+ object: id,
+ target: name.into(),
+ },
+ None => Ref::Unborn {
+ full_ref_name: path.into(),
+ target: name.into(),
+ },
+ },
+ },
+ _ => {
+ return Err(Error::UnknownAttribute {
+ attribute: attribute.to_owned().into(),
+ line: trimmed.to_owned().into(),
+ })
+ }
+ }
+ }
+ _ => return Err(Error::MalformedV2RefLine(trimmed.to_owned().into())),
+ }
+ } else {
+ Ref::Direct {
+ object: id.ok_or(Error::InvariantViolation {
+ message: "got 'unborn' as object name of direct reference",
+ })?,
+ full_ref_name: path.into(),
+ }
+ })
+ }
+ _ => Err(Error::MalformedV2RefLine(trimmed.to_owned().into())),
+ }
+}
diff --git a/vendor/gix-protocol/src/handshake/refs/tests.rs b/vendor/gix-protocol/src/handshake/refs/tests.rs
new file mode 100644
index 000000000..a7c9171a5
--- /dev/null
+++ b/vendor/gix-protocol/src/handshake/refs/tests.rs
@@ -0,0 +1,223 @@
+use gix_transport::{client, client::Capabilities};
+
+/// Convert a hexadecimal hash into its corresponding `ObjectId` or _panic_.
+fn oid(hex: &str) -> gix_hash::ObjectId {
+ gix_hash::ObjectId::from_hex(hex.as_bytes()).expect("40 bytes hex")
+}
+
+use crate::handshake::{refs, refs::shared::InternalRef, Ref};
+
+#[maybe_async::test(feature = "blocking-client", async(feature = "async-client", async_std::test))]
+async fn extract_references_from_v2_refs() {
+ let input = &mut Fixture(
+ "808e50d724f604f69ab93c6da2919c014667bedb HEAD symref-target:refs/heads/main
+808e50d724f604f69ab93c6da2919c014667bedb MISSING_NAMESPACE_TARGET symref-target:(null)
+unborn HEAD symref-target:refs/heads/main
+unborn refs/heads/symbolic symref-target:refs/heads/target
+808e50d724f604f69ab93c6da2919c014667bedb refs/heads/main
+7fe1b98b39423b71e14217aa299a03b7c937d656 refs/tags/foo peeled:808e50d724f604f69ab93c6da2919c014667bedb
+7fe1b98b39423b71e14217aa299a03b7c937d6ff refs/tags/blaz
+"
+ .as_bytes(),
+ );
+
+ let out = refs::from_v2_refs(input).await.expect("no failure on valid input");
+
+ assert_eq!(
+ out,
+ vec![
+ Ref::Symbolic {
+ full_ref_name: "HEAD".into(),
+ target: "refs/heads/main".into(),
+ object: oid("808e50d724f604f69ab93c6da2919c014667bedb")
+ },
+ Ref::Direct {
+ full_ref_name: "MISSING_NAMESPACE_TARGET".into(),
+ object: oid("808e50d724f604f69ab93c6da2919c014667bedb")
+ },
+ Ref::Unborn {
+ full_ref_name: "HEAD".into(),
+ target: "refs/heads/main".into(),
+ },
+ Ref::Unborn {
+ full_ref_name: "refs/heads/symbolic".into(),
+ target: "refs/heads/target".into(),
+ },
+ Ref::Direct {
+ full_ref_name: "refs/heads/main".into(),
+ object: oid("808e50d724f604f69ab93c6da2919c014667bedb")
+ },
+ Ref::Peeled {
+ full_ref_name: "refs/tags/foo".into(),
+ tag: oid("7fe1b98b39423b71e14217aa299a03b7c937d656"),
+ object: oid("808e50d724f604f69ab93c6da2919c014667bedb")
+ },
+ Ref::Direct {
+ full_ref_name: "refs/tags/blaz".into(),
+ object: oid("7fe1b98b39423b71e14217aa299a03b7c937d6ff")
+ },
+ ]
+ )
+}
+
+#[maybe_async::test(feature = "blocking-client", async(feature = "async-client", async_std::test))]
+async fn extract_references_from_v1_refs() {
+ let input = &mut Fixture(
+ "73a6868963993a3328e7d8fe94e5a6ac5078a944 HEAD
+21c9b7500cb144b3169a6537961ec2b9e865be81 MISSING_NAMESPACE_TARGET
+73a6868963993a3328e7d8fe94e5a6ac5078a944 refs/heads/main
+8e472f9ccc7d745927426cbb2d9d077de545aa4e refs/pull/13/head
+dce0ea858eef7ff61ad345cc5cdac62203fb3c10 refs/tags/gix-commitgraph-v0.0.0
+21c9b7500cb144b3169a6537961ec2b9e865be81 refs/tags/gix-commitgraph-v0.0.0^{}"
+ .as_bytes(),
+ );
+ let out = refs::from_v1_refs_received_as_part_of_handshake_and_capabilities(
+ input,
+ Capabilities::from_bytes(b"\0symref=HEAD:refs/heads/main symref=MISSING_NAMESPACE_TARGET:(null)")
+ .expect("valid capabilities")
+ .0
+ .iter(),
+ )
+ .await
+ .expect("no failure from valid input");
+ assert_eq!(
+ out,
+ vec![
+ Ref::Symbolic {
+ full_ref_name: "HEAD".into(),
+ target: "refs/heads/main".into(),
+ object: oid("73a6868963993a3328e7d8fe94e5a6ac5078a944")
+ },
+ Ref::Direct {
+ full_ref_name: "MISSING_NAMESPACE_TARGET".into(),
+ object: oid("21c9b7500cb144b3169a6537961ec2b9e865be81")
+ },
+ Ref::Direct {
+ full_ref_name: "refs/heads/main".into(),
+ object: oid("73a6868963993a3328e7d8fe94e5a6ac5078a944")
+ },
+ Ref::Direct {
+ full_ref_name: "refs/pull/13/head".into(),
+ object: oid("8e472f9ccc7d745927426cbb2d9d077de545aa4e")
+ },
+ Ref::Peeled {
+ full_ref_name: "refs/tags/gix-commitgraph-v0.0.0".into(),
+ tag: oid("dce0ea858eef7ff61ad345cc5cdac62203fb3c10"),
+ object: oid("21c9b7500cb144b3169a6537961ec2b9e865be81")
+ },
+ ]
+ )
+}
+
+#[test]
+fn extract_symbolic_references_from_capabilities() -> Result<(), client::Error> {
+ let caps = client::Capabilities::from_bytes(
+ b"\0unrelated symref=HEAD:refs/heads/main symref=ANOTHER:refs/heads/foo symref=MISSING_NAMESPACE_TARGET:(null) agent=git/2.28.0",
+ )?
+ .0;
+ let out = refs::shared::from_capabilities(caps.iter()).expect("a working example");
+
+ assert_eq!(
+ out,
+ vec![
+ InternalRef::SymbolicForLookup {
+ path: "HEAD".into(),
+ target: Some("refs/heads/main".into())
+ },
+ InternalRef::SymbolicForLookup {
+ path: "ANOTHER".into(),
+ target: Some("refs/heads/foo".into())
+ },
+ InternalRef::SymbolicForLookup {
+ path: "MISSING_NAMESPACE_TARGET".into(),
+ target: None
+ }
+ ]
+ );
+ Ok(())
+}
+
+#[cfg(any(feature = "async-client", feature = "blocking-client"))]
+struct Fixture<'a>(&'a [u8]);
+
+#[cfg(feature = "blocking-client")]
+impl<'a> std::io::Read for Fixture<'a> {
+ fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
+ self.0.read(buf)
+ }
+}
+
+#[cfg(feature = "blocking-client")]
+impl<'a> std::io::BufRead for Fixture<'a> {
+ fn fill_buf(&mut self) -> std::io::Result<&[u8]> {
+ self.0.fill_buf()
+ }
+
+ fn consume(&mut self, amt: usize) {
+ self.0.consume(amt)
+ }
+}
+
+#[cfg(feature = "blocking-client")]
+impl<'a> gix_transport::client::ReadlineBufRead for Fixture<'a> {
+ fn readline(
+ &mut self,
+ ) -> Option<std::io::Result<Result<gix_packetline::PacketLineRef<'_>, gix_packetline::decode::Error>>> {
+ use bstr::{BStr, ByteSlice};
+ let bytes: &BStr = self.0.into();
+ let mut lines = bytes.lines();
+ let res = lines.next()?;
+ self.0 = lines.as_bytes();
+ Some(Ok(Ok(gix_packetline::PacketLineRef::Data(res))))
+ }
+}
+
+#[cfg(feature = "async-client")]
+impl<'a> Fixture<'a> {
+ fn project_inner(self: std::pin::Pin<&mut Self>) -> std::pin::Pin<&mut &'a [u8]> {
+ #[allow(unsafe_code)]
+ unsafe {
+ std::pin::Pin::new(&mut self.get_unchecked_mut().0)
+ }
+ }
+}
+
+#[cfg(feature = "async-client")]
+impl<'a> futures_io::AsyncRead for Fixture<'a> {
+ fn poll_read(
+ self: std::pin::Pin<&mut Self>,
+ cx: &mut std::task::Context<'_>,
+ buf: &mut [u8],
+ ) -> std::task::Poll<std::io::Result<usize>> {
+ self.project_inner().poll_read(cx, buf)
+ }
+}
+
+#[cfg(feature = "async-client")]
+impl<'a> futures_io::AsyncBufRead for Fixture<'a> {
+ fn poll_fill_buf(
+ self: std::pin::Pin<&mut Self>,
+ cx: &mut std::task::Context<'_>,
+ ) -> std::task::Poll<std::io::Result<&[u8]>> {
+ self.project_inner().poll_fill_buf(cx)
+ }
+
+ fn consume(self: std::pin::Pin<&mut Self>, amt: usize) {
+ self.project_inner().consume(amt)
+ }
+}
+
+#[cfg(feature = "async-client")]
+#[async_trait::async_trait(?Send)]
+impl<'a> gix_transport::client::ReadlineBufRead for Fixture<'a> {
+ async fn readline(
+ &mut self,
+ ) -> Option<std::io::Result<Result<gix_packetline::PacketLineRef<'_>, gix_packetline::decode::Error>>> {
+ use bstr::{BStr, ByteSlice};
+ let bytes: &BStr = self.0.into();
+ let mut lines = bytes.lines();
+ let res = lines.next()?;
+ self.0 = lines.as_bytes();
+ Some(Ok(Ok(gix_packetline::PacketLineRef::Data(res))))
+ }
+}
diff --git a/vendor/gix-protocol/src/lib.rs b/vendor/gix-protocol/src/lib.rs
new file mode 100644
index 000000000..7f7355711
--- /dev/null
+++ b/vendor/gix-protocol/src/lib.rs
@@ -0,0 +1,64 @@
+//! An abstraction over [fetching][fetch()] a pack from the server.
+//!
+//! This implementation hides the transport layer, statefulness and the protocol version to the [fetch delegate][fetch::Delegate],
+//! the actual client implementation.
+//! ## 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, unsafe_code)]
+
+/// A selector for V2 commands to invoke on the server for purpose of pre-invocation validation.
+#[derive(PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone, Copy)]
+pub enum Command {
+ /// List references.
+ LsRefs,
+ /// Fetch a pack.
+ Fetch,
+}
+pub mod command;
+
+#[cfg(feature = "async-trait")]
+pub use async_trait;
+#[cfg(feature = "futures-io")]
+pub use futures_io;
+#[cfg(feature = "futures-lite")]
+pub use futures_lite;
+pub use gix_credentials as credentials;
+/// A convenience export allowing users of gix-protocol to use the transport layer without their own cargo dependency.
+pub use gix_transport as transport;
+pub use maybe_async;
+
+///
+#[cfg(any(feature = "blocking-client", feature = "async-client"))]
+pub mod fetch;
+
+#[cfg(any(feature = "blocking-client", feature = "async-client"))]
+mod fetch_fn;
+#[cfg(any(feature = "blocking-client", feature = "async-client"))]
+pub use fetch_fn::{fetch, FetchConnection};
+
+mod remote_progress;
+pub use remote_progress::RemoteProgress;
+
+#[cfg(all(feature = "blocking-client", feature = "async-client"))]
+compile_error!("Cannot set both 'blocking-client' and 'async-client' features as they are mutually exclusive");
+
+///
+#[cfg(any(feature = "blocking-client", feature = "async-client"))]
+pub mod handshake;
+#[cfg(any(feature = "blocking-client", feature = "async-client"))]
+pub use handshake::function::handshake;
+
+///
+#[cfg(any(feature = "blocking-client", feature = "async-client"))]
+pub mod ls_refs;
+#[cfg(any(feature = "blocking-client", feature = "async-client"))]
+pub use ls_refs::function::ls_refs;
+
+mod util;
+pub use util::agent;
+#[cfg(any(feature = "blocking-client", feature = "async-client"))]
+pub use util::indicate_end_of_interaction;
diff --git a/vendor/gix-protocol/src/ls_refs.rs b/vendor/gix-protocol/src/ls_refs.rs
new file mode 100644
index 000000000..a31588894
--- /dev/null
+++ b/vendor/gix-protocol/src/ls_refs.rs
@@ -0,0 +1,110 @@
+mod error {
+ use crate::handshake::refs::parse;
+
+ /// The error returned by [ls_refs()][crate::ls_refs()].
+ #[derive(Debug, thiserror::Error)]
+ #[allow(missing_docs)]
+ pub enum Error {
+ #[error(transparent)]
+ Io(#[from] std::io::Error),
+ #[error(transparent)]
+ Transport(#[from] gix_transport::client::Error),
+ #[error(transparent)]
+ Parse(#[from] parse::Error),
+ }
+
+ impl gix_transport::IsSpuriousError for Error {
+ fn is_spurious(&self) -> bool {
+ match self {
+ Error::Io(err) => err.is_spurious(),
+ Error::Transport(err) => err.is_spurious(),
+ _ => false,
+ }
+ }
+ }
+}
+pub use error::Error;
+
+/// What to do after preparing ls-refs in [ls_refs()][crate::ls_refs()].
+#[derive(PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone)]
+pub enum Action {
+ /// Continue by sending a 'ls-refs' command.
+ Continue,
+ /// Skip 'ls-refs' entirely.
+ ///
+ /// This is useful if the `ref-in-want` capability is taken advantage of. When fetching, one must must then send
+ /// `want-ref`s during the negotiation phase.
+ Skip,
+}
+
+pub(crate) mod function {
+ use std::borrow::Cow;
+
+ use bstr::BString;
+ use gix_features::progress::Progress;
+ use gix_transport::client::{Capabilities, Transport, TransportV2Ext};
+ use maybe_async::maybe_async;
+
+ use super::{Action, Error};
+ use crate::{
+ handshake::{refs::from_v2_refs, Ref},
+ indicate_end_of_interaction, Command,
+ };
+
+ /// Invoke an ls-refs V2 command on `transport`, which requires a prior handshake that yielded
+ /// server `capabilities`. `prepare_ls_refs(capabilities, arguments, features)` can be used to alter the _ls-refs_. `progress` is used to provide feedback.
+ /// Note that `prepare_ls_refs()` is expected to add the `(agent, Some(name))` to the list of `features`.
+ #[maybe_async]
+ pub async fn ls_refs(
+ mut transport: impl Transport,
+ capabilities: &Capabilities,
+ prepare_ls_refs: impl FnOnce(
+ &Capabilities,
+ &mut Vec<BString>,
+ &mut Vec<(&str, Option<Cow<'static, str>>)>,
+ ) -> std::io::Result<Action>,
+ progress: &mut impl Progress,
+ ) -> Result<Vec<Ref>, Error> {
+ let ls_refs = Command::LsRefs;
+ let mut ls_features = ls_refs.default_features(gix_transport::Protocol::V2, capabilities);
+ let mut ls_args = ls_refs.initial_arguments(&ls_features);
+ if capabilities
+ .capability("ls-refs")
+ .and_then(|cap| cap.supports("unborn"))
+ .unwrap_or_default()
+ {
+ ls_args.push("unborn".into());
+ }
+ let refs = match prepare_ls_refs(capabilities, &mut ls_args, &mut ls_features) {
+ Ok(Action::Skip) => Vec::new(),
+ Ok(Action::Continue) => {
+ ls_refs.validate_argument_prefixes_or_panic(
+ gix_transport::Protocol::V2,
+ capabilities,
+ &ls_args,
+ &ls_features,
+ );
+
+ progress.step();
+ progress.set_name("list refs");
+ let mut remote_refs = transport
+ .invoke(
+ ls_refs.as_str(),
+ ls_features.into_iter(),
+ if ls_args.is_empty() {
+ None
+ } else {
+ Some(ls_args.into_iter())
+ },
+ )
+ .await?;
+ from_v2_refs(&mut remote_refs).await?
+ }
+ Err(err) => {
+ indicate_end_of_interaction(transport).await?;
+ return Err(err.into());
+ }
+ };
+ Ok(refs)
+ }
+}
diff --git a/vendor/gix-protocol/src/remote_progress.rs b/vendor/gix-protocol/src/remote_progress.rs
new file mode 100644
index 000000000..50d0eed17
--- /dev/null
+++ b/vendor/gix-protocol/src/remote_progress.rs
@@ -0,0 +1,108 @@
+use std::convert::TryFrom;
+
+use bstr::ByteSlice;
+use nom::{
+ bytes::complete::{tag, take_till, take_till1},
+ combinator::{map_res, opt},
+ sequence::{preceded, terminated},
+};
+
+/// The information usually found in remote progress messages as sent by a git server during
+/// fetch, clone and push operations.
+#[derive(PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone, Copy)]
+#[cfg_attr(feature = "serde1", derive(serde::Serialize, serde::Deserialize))]
+pub struct RemoteProgress<'a> {
+ #[cfg_attr(feature = "serde1", serde(borrow))]
+ /// The name of the action, like "clone".
+ pub action: &'a bstr::BStr,
+ /// The percentage to indicate progress, between 0 and 100.
+ pub percent: Option<u32>,
+ /// The amount of items already processed.
+ pub step: Option<usize>,
+ /// The maximum expected amount of items. `step` / `max` * 100 = `percent`.
+ pub max: Option<usize>,
+}
+
+impl<'a> RemoteProgress<'a> {
+ /// Parse the progress from a typical git progress `line` as sent by the remote.
+ pub fn from_bytes(line: &[u8]) -> Option<RemoteProgress<'_>> {
+ parse_progress(line).ok().and_then(|(_, r)| {
+ if r.percent.is_none() && r.step.is_none() && r.max.is_none() {
+ None
+ } else {
+ Some(r)
+ }
+ })
+ }
+
+ /// Parse `text`, which is interpreted as error if `is_error` is true, as [`RemoteProgress`] and call the respective
+ /// methods on the given `progress` instance.
+ pub fn translate_to_progress(is_error: bool, text: &[u8], progress: &mut impl gix_features::progress::Progress) {
+ fn progress_name(current: Option<String>, action: &[u8]) -> String {
+ match current {
+ Some(current) => format!(
+ "{}: {}",
+ current.split_once(':').map_or(&*current, |x| x.0),
+ action.as_bstr()
+ ),
+ None => action.as_bstr().to_string(),
+ }
+ }
+ if is_error {
+ // ignore keep-alive packages sent with 'sideband-all'
+ if !text.is_empty() {
+ progress.fail(progress_name(None, text));
+ }
+ } else {
+ match RemoteProgress::from_bytes(text) {
+ Some(RemoteProgress {
+ action,
+ percent: _,
+ step,
+ max,
+ }) => {
+ progress.set_name(progress_name(progress.name(), action));
+ progress.init(max, gix_features::progress::count("objects"));
+ if let Some(step) = step {
+ progress.set(step);
+ }
+ }
+ None => progress.set_name(progress_name(progress.name(), text)),
+ };
+ }
+ }
+}
+
+fn parse_number(i: &[u8]) -> nom::IResult<&[u8], usize> {
+ map_res(take_till(|c: u8| !c.is_ascii_digit()), btoi::btoi)(i)
+}
+
+fn next_optional_percentage(i: &[u8]) -> nom::IResult<&[u8], Option<u32>> {
+ opt(terminated(
+ preceded(
+ take_till(|c: u8| c.is_ascii_digit()),
+ map_res(parse_number, u32::try_from),
+ ),
+ tag(b"%"),
+ ))(i)
+}
+
+fn next_optional_number(i: &[u8]) -> nom::IResult<&[u8], Option<usize>> {
+ opt(preceded(take_till(|c: u8| c.is_ascii_digit()), parse_number))(i)
+}
+
+fn parse_progress(line: &[u8]) -> nom::IResult<&[u8], RemoteProgress<'_>> {
+ let (i, action) = take_till1(|c| c == b':')(line)?;
+ let (i, percent) = next_optional_percentage(i)?;
+ let (i, step) = next_optional_number(i)?;
+ let (i, max) = next_optional_number(i)?;
+ Ok((
+ i,
+ RemoteProgress {
+ action: action.into(),
+ percent,
+ step,
+ max,
+ },
+ ))
+}
diff --git a/vendor/gix-protocol/src/util.rs b/vendor/gix-protocol/src/util.rs
new file mode 100644
index 000000000..a790aebd5
--- /dev/null
+++ b/vendor/gix-protocol/src/util.rs
@@ -0,0 +1,27 @@
+/// The name of the `git` client in a format suitable for presentation to a `git` server, using `name` as user-defined portion of the value.
+pub fn agent(name: impl Into<String>) -> String {
+ let mut name = name.into();
+ if !name.starts_with("git/") {
+ name.insert_str(0, "git/");
+ }
+ name
+}
+
+/// Send a message to indicate the remote side that there is nothing more to expect from us, indicating a graceful shutdown.
+#[cfg(any(feature = "blocking-client", feature = "async-client"))]
+#[maybe_async::maybe_async]
+pub async fn indicate_end_of_interaction(
+ mut transport: impl gix_transport::client::Transport,
+) -> Result<(), gix_transport::client::Error> {
+ // An empty request marks the (early) end of the interaction. Only relevant in stateful transports though.
+ if transport.connection_persists_across_multiple_requests() {
+ transport
+ .request(
+ gix_transport::client::WriteMode::Binary,
+ gix_transport::client::MessageKind::Flush,
+ )?
+ .into_read()
+ .await?;
+ }
+ Ok(())
+}