From 10ee2acdd26a7f1298c6f6d6b7af9b469fe29b87 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Sat, 4 May 2024 14:41:41 +0200 Subject: Merging upstream version 1.70.0+dfsg2. Signed-off-by: Daniel Baumann --- vendor/gix-protocol/src/command/mod.rs | 214 ++++++++++++ vendor/gix-protocol/src/command/tests.rs | 156 +++++++++ .../gix-protocol/src/fetch/arguments/async_io.rs | 55 +++ .../src/fetch/arguments/blocking_io.rs | 56 +++ vendor/gix-protocol/src/fetch/arguments/mod.rs | 252 +++++++++++++ vendor/gix-protocol/src/fetch/delegate.rs | 313 +++++++++++++++++ vendor/gix-protocol/src/fetch/error.rs | 21 ++ vendor/gix-protocol/src/fetch/handshake.rs | 27 ++ vendor/gix-protocol/src/fetch/mod.rs | 20 ++ vendor/gix-protocol/src/fetch/response/async_io.rs | 136 ++++++++ .../gix-protocol/src/fetch/response/blocking_io.rs | 135 +++++++ vendor/gix-protocol/src/fetch/response/mod.rs | 244 +++++++++++++ vendor/gix-protocol/src/fetch/tests.rs | 388 +++++++++++++++++++++ vendor/gix-protocol/src/fetch_fn.rs | 168 +++++++++ vendor/gix-protocol/src/handshake/function.rs | 100 ++++++ vendor/gix-protocol/src/handshake/mod.rs | 95 +++++ vendor/gix-protocol/src/handshake/refs/async_io.rs | 43 +++ .../gix-protocol/src/handshake/refs/blocking_io.rs | 31 ++ vendor/gix-protocol/src/handshake/refs/mod.rs | 72 ++++ vendor/gix-protocol/src/handshake/refs/shared.rs | 237 +++++++++++++ vendor/gix-protocol/src/handshake/refs/tests.rs | 223 ++++++++++++ vendor/gix-protocol/src/lib.rs | 64 ++++ vendor/gix-protocol/src/ls_refs.rs | 110 ++++++ vendor/gix-protocol/src/remote_progress.rs | 108 ++++++ vendor/gix-protocol/src/util.rs | 27 ++ 25 files changed, 3295 insertions(+) create mode 100644 vendor/gix-protocol/src/command/mod.rs create mode 100644 vendor/gix-protocol/src/command/tests.rs create mode 100644 vendor/gix-protocol/src/fetch/arguments/async_io.rs create mode 100644 vendor/gix-protocol/src/fetch/arguments/blocking_io.rs create mode 100644 vendor/gix-protocol/src/fetch/arguments/mod.rs create mode 100644 vendor/gix-protocol/src/fetch/delegate.rs create mode 100644 vendor/gix-protocol/src/fetch/error.rs create mode 100644 vendor/gix-protocol/src/fetch/handshake.rs create mode 100644 vendor/gix-protocol/src/fetch/mod.rs create mode 100644 vendor/gix-protocol/src/fetch/response/async_io.rs create mode 100644 vendor/gix-protocol/src/fetch/response/blocking_io.rs create mode 100644 vendor/gix-protocol/src/fetch/response/mod.rs create mode 100644 vendor/gix-protocol/src/fetch/tests.rs create mode 100644 vendor/gix-protocol/src/fetch_fn.rs create mode 100644 vendor/gix-protocol/src/handshake/function.rs create mode 100644 vendor/gix-protocol/src/handshake/mod.rs create mode 100644 vendor/gix-protocol/src/handshake/refs/async_io.rs create mode 100644 vendor/gix-protocol/src/handshake/refs/blocking_io.rs create mode 100644 vendor/gix-protocol/src/handshake/refs/mod.rs create mode 100644 vendor/gix-protocol/src/handshake/refs/shared.rs create mode 100644 vendor/gix-protocol/src/handshake/refs/tests.rs create mode 100644 vendor/gix-protocol/src/lib.rs create mode 100644 vendor/gix-protocol/src/ls_refs.rs create mode 100644 vendor/gix-protocol/src/remote_progress.rs create mode 100644 vendor/gix-protocol/src/util.rs (limited to 'vendor/gix-protocol/src') 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>); + +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 { + 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 { + 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::>()) + } 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::>() + ) + } + } + + 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::>(), + "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, 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, 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, + + args: Vec, + haves: Vec, + + 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>, + #[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) { + 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) { + 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) { + debug_assert!(self.shallow, "'shallow' feature required for 'shallow '"); + 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) -> 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::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>), 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)> { + 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, + _features: &mut Vec<(&str, Option>)>, + ) -> std::io::Result { + 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>)>, + _refs: &[Ref], + ) -> std::io::Result { + 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; +} + +impl DelegateBlocking for Box { + fn handshake_extra_parameters(&self) -> Vec<(String, Option)> { + self.deref().handshake_extra_parameters() + } + + fn prepare_ls_refs( + &mut self, + _server: &Capabilities, + _arguments: &mut Vec, + _features: &mut Vec<(&str, Option>)>, + ) -> io::Result { + 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>)>, + _refs: &[Ref], + ) -> io::Result { + self.deref_mut().prepare_fetch(_version, _server, _features, _refs) + } + + fn negotiate( + &mut self, + refs: &[Ref], + arguments: &mut Arguments, + previous_response: Option<&Response>, + ) -> io::Result { + self.deref_mut().negotiate(refs, arguments, previous_response) + } +} + +impl DelegateBlocking for &mut T { + fn handshake_extra_parameters(&self) -> Vec<(String, Option)> { + self.deref().handshake_extra_parameters() + } + + fn prepare_ls_refs( + &mut self, + _server: &Capabilities, + _arguments: &mut Vec, + _features: &mut Vec<(&str, Option>)>, + ) -> io::Result { + 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>)>, + _refs: &[Ref], + ) -> io::Result { + self.deref_mut().prepare_fetch(_version, _server, _features, _refs) + } + + fn negotiate( + &mut self, + refs: &[Ref], + arguments: &mut Arguments, + previous_response: Option<&Response>, + ) -> io::Result { + 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 Delegate for Box { + 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 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 Delegate for Box { + 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 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( + transport: T, + authenticate: AuthFn, + extra_parameters: Vec<(String, Option)>, + progress: &mut impl Progress, +) -> Result +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( + line: &mut String, + reader: &mut (impl client::ExtendedBufRead + Unpin), + res: &mut Vec, + parse: impl Fn(&str) -> Result, +) -> Result { + 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 { + match version { + Protocol::V1 => { + let mut line = String::new(); + let mut acks = Vec::::new(); + let mut shallows = Vec::::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::::new(); + let mut shallows = Vec::::new(); + let mut wanted_refs = Vec::::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( + line: &mut String, + reader: &mut impl client::ExtendedBufRead, + res: &mut Vec, + parse: impl Fn(&str) -> Result, +) -> Result { + 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 { + match version { + Protocol::V1 => { + let mut line = String::new(); + let mut acks = Vec::::new(); + let mut shallows = Vec::::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::::new(); + let mut shallows = Vec::::new(); + let mut wanted_refs = Vec::::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 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::() { + 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 { + 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 { + 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 { + 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, + shallows: Vec, + wanted_refs: Vec, + 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, + shallows: &mut Vec, + 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) -> fetch::Arguments { + fetch::Arguments::new(Protocol::V1, features.into_iter().map(|n| (n, None)).collect()) + } + + fn arguments_v2(features: impl IntoIterator) -> fetch::Arguments { + fetch::Arguments::new(Protocol::V2, features.into_iter().map(|n| (n, None)).collect()) + } + + struct Transport { + 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 client::TransportWithoutIO for Transport { + 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, 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> { + self.inner.configure(config) + } + } + + impl client::Transport for Transport { + fn handshake<'a>( + &mut self, + service: Service, + extra_parameters: &'a [(&'a str, Option<&'a str>)], + ) -> Result, 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 client::TransportWithoutIO for Transport { + 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, 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> { + self.inner.configure(config) + } + } + + #[async_trait(?Send)] + impl client::Transport for Transport { + async fn handshake<'a>( + &mut self, + service: Service, + extra_parameters: &'a [(&'a str, Option<&'a str>)], + ) -> Result, Error> { + self.inner.handshake(service, extra_parameters).await + } + } + } + + fn transport( + out: &mut Vec, + stateful: bool, + ) -> Transport>> { + 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( + mut transport: T, + mut delegate: D, + authenticate: F, + mut progress: P, + fetch_mode: FetchConnection, + agent: impl Into, +) -> 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::; + 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

(progress: &mut P, reader: &mut Box) +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( + mut transport: T, + service: Service, + mut authenticate: AuthFn, + extra_parameters: Vec<(String, Option)>, + progress: &mut impl Progress, +) -> Result +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>, + /// 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, 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>, +) -> Result, 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, 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>, +) -> Result, 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 :.")] + MalformedSymref { symref: BString }, + #[error("{0:?} could not be parsed. A V1 ref line should be ' '.")] + MalformedV1RefLine(BString), + #[error( + "{0:?} could not be parsed. A V2 ref line should be ' [ (peeled|symref-target):'." + )] + 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 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, + 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 }, +} + +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>, +) -> Result, 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, + 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 { + 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 { + 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, 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> { + 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> { + 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, 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, + &mut Vec<(&str, Option>)>, + ) -> std::io::Result, + progress: &mut impl Progress, + ) -> Result, 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, + /// The amount of items already processed. + pub step: Option, + /// The maximum expected amount of items. `step` / `max` * 100 = `percent`. + pub max: Option, +} + +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> { + 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, 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> { + 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> { + 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 { + 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(()) +} -- cgit v1.2.3