gix-protocol-0.45.1/.cargo_vcs_info.json0000644000000001520000000000100135470ustar { "git": { "sha1": "4f98e94e0e8b79ed2899b35bef40f3c30b3025b0" }, "path_in_vcs": "gix-protocol" }gix-protocol-0.45.1/Cargo.toml0000644000000047320000000000100115550ustar # THIS FILE IS AUTOMATICALLY GENERATED BY CARGO # # When uploading crates to the registry Cargo will automatically # "normalize" Cargo.toml files for maximal compatibility # with all versions of Cargo and also rewrite `path` dependencies # to registry (e.g., crates.io) dependencies. # # If you are reading this file be aware that the original Cargo.toml # will likely look very different (and much more reasonable). # See Cargo.toml.orig for the original contents. [package] edition = "2021" rust-version = "1.65" name = "gix-protocol" version = "0.45.1" authors = ["Sebastian Thiel "] build = false include = [ "src/**/*", "LICENSE-*", "!**/tests/**/*", ] autobins = false autoexamples = false autotests = false autobenches = false description = "A crate of the gitoxide project for implementing git protocols" readme = false license = "MIT OR Apache-2.0" repository = "https://github.com/Byron/gitoxide" [package.metadata.docs.rs] features = [ "blocking-client", "document-features", "serde", ] [lib] name = "gix_protocol" path = "src/lib.rs" doctest = false [dependencies.async-trait] version = "0.1.51" optional = true [dependencies.bstr] version = "1.3.0" features = [ "std", "unicode", ] default-features = false [dependencies.document-features] version = "0.2.0" optional = true [dependencies.futures-io] version = "0.3.16" optional = true [dependencies.futures-lite] version = "2.1.0" features = ["std"] optional = true default-features = false [dependencies.gix-credentials] version = "^0.24.2" [dependencies.gix-date] version = "^0.8.6" [dependencies.gix-features] version = "^0.38.2" features = ["progress"] [dependencies.gix-hash] version = "^0.14.2" [dependencies.gix-transport] version = "^0.42.1" [dependencies.gix-utils] version = "^0.1.12" [dependencies.maybe-async] version = "0.2.6" [dependencies.serde] version = "1.0.114" features = ["derive"] optional = true default-features = false [dependencies.thiserror] version = "1.0.32" [dependencies.winnow] version = "0.6.0" features = ["simd"] [dev-dependencies.async-std] version = "1.9.0" features = ["attributes"] [dev-dependencies.gix-packetline] version = "^0.17.4" [features] async-client = [ "gix-transport/async-client", "async-trait", "futures-io", "futures-lite", ] blocking-client = [ "gix-transport/blocking-client", "maybe-async/is_sync", ] serde = [ "dep:serde", "bstr/serde", "gix-transport/serde", "gix-hash/serde", ] gix-protocol-0.45.1/Cargo.toml.orig000064400000000000000000000052001046102023000152250ustar 00000000000000[package] name = "gix-protocol" version = "0.45.1" repository = "https://github.com/Byron/gitoxide" license = "MIT OR Apache-2.0" description = "A crate of the gitoxide project for implementing git protocols" authors = ["Sebastian Thiel "] edition = "2021" include = ["src/**/*", "LICENSE-*", "!**/tests/**/*"] rust-version = "1.65" [lib] doctest = false [features] #! ### _Mutually exclusive client _ #! The _client_ portion of the protocol uses `gix-transport` to communicate to a server. For it to be available, one of the following features must #! be selected. #! #! Specifying both causes a compile error, preventing the use of `--all-features`. ## If set, blocking command implementations are available and will use the blocking version of the `gix-transport` crate. blocking-client = ["gix-transport/blocking-client", "maybe-async/is_sync"] ## As above, but provides async implementations instead. async-client = [ "gix-transport/async-client", "async-trait", "futures-io", "futures-lite", ] #! ### Other ## Data structures implement `serde::Serialize` and `serde::Deserialize`. serde = ["dep:serde", "bstr/serde", "gix-transport/serde", "gix-hash/serde"] [[test]] name = "blocking-client-protocol" path = "tests/blocking-protocol.rs" required-features = ["blocking-client"] [[test]] name = "async-client-protocol" path = "tests/async-protocol.rs" required-features = ["async-client"] [dependencies] gix-features = { version = "^0.38.2", path = "../gix-features", features = [ "progress", ] } gix-transport = { version = "^0.42.1", path = "../gix-transport" } gix-hash = { version = "^0.14.2", path = "../gix-hash" } gix-date = { version = "^0.8.6", path = "../gix-date" } gix-credentials = { version = "^0.24.2", path = "../gix-credentials" } gix-utils = { version = "^0.1.12", path = "../gix-utils" } thiserror = "1.0.32" serde = { version = "1.0.114", optional = true, default-features = false, features = [ "derive", ] } bstr = { version = "1.3.0", default-features = false, features = [ "std", "unicode", ] } winnow = { version = "0.6.0", features = ["simd"] } # for async-client async-trait = { version = "0.1.51", optional = true } futures-io = { version = "0.3.16", optional = true } futures-lite = { workspace = true, optional = true } maybe-async = "0.2.6" document-features = { version = "0.2.0", optional = true } [dev-dependencies] async-std = { version = "1.9.0", features = ["attributes"] } gix-packetline = { path = "../gix-packetline", version = "^0.17.4" } gix-testtools = { path = "../tests/tools" } [package.metadata.docs.rs] features = ["blocking-client", "document-features", "serde"] gix-protocol-0.45.1/LICENSE-APACHE000064400000000000000000000247461046102023000143020ustar 00000000000000 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. gix-protocol-0.45.1/LICENSE-MIT000064400000000000000000000017771046102023000140110ustar 00000000000000Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. gix-protocol-0.45.1/src/command/mod.rs000064400000000000000000000215541046102023000157020ustar 00000000000000//! 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::V0 | 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::V0 | 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(ToOwned::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::V0 | 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(ToString::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; gix-protocol-0.45.1/src/command/tests.rs000064400000000000000000000132241046102023000162600ustar 00000000000000mod 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)], ); } } } } gix-protocol-0.45.1/src/fetch/arguments/async_io.rs000064400000000000000000000046001046102023000204000ustar 00000000000000use 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 + Unpin + 'a>, client::Error> { if self.haves.is_empty() { assert!(add_done_argument, "If there are no haves, is_done must be true."); } match self.version { gix_transport::Protocol::V0 | 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, self.trace, )?; 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()), self.trace, ) .await } } } } gix-protocol-0.45.1/src/fetch/arguments/blocking_io.rs000064400000000000000000000044161046102023000210600ustar 00000000000000use 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 + Unpin + 'a>, client::Error> { if self.haves.is_empty() { assert!(add_done_argument, "If there are no haves, is_done must be true."); } match self.version { gix_transport::Protocol::V0 | 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, self.trace, )?; 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(|(_, v)| v.is_some()).cloned(), Some(std::mem::replace(&mut self.args, retained_state).into_iter()), self.trace, ) } } } } gix-protocol-0.45.1/src/fetch/arguments/mod.rs000064400000000000000000000265211046102023000173610ustar 00000000000000use 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, trace: bool, } 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 } /// Return true if we will use a stateless mode of operation, which can be decided in conjunction with `transport_is_stateless`. /// /// * we are always stateless if the transport is stateless, i.e. doesn't support multiple interactions with a single connection. /// * we are always stateless if the protocol version is `2` /// * otherwise we may be stateful. pub fn is_stateless(&self, transport_is_stateless: bool) -> bool { #[cfg(any(feature = "async-client", feature = "blocking-client"))] let res = transport_is_stateless || self.version == gix_transport::Protocol::V2; #[cfg(not(any(feature = "async-client", feature = "blocking-client")))] let res = transport_is_stateless; res } /// 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 (and including) `seconds` as passed since UNIX epoch. pub fn deepen_since(&mut self, seconds: gix_date::SecondsSinceUnixEpoch) { debug_assert!(self.deepen_since, "'deepen-since' feature required"); if self.deepen_since { self.prefixed("deepen-since ", seconds); } } /// 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.add_feature("include-tag"); } } /// Add the given `feature`, unconditionally. /// /// Note that sending an unknown or unsupported feature may cause the remote to terminate /// the connection. Use this method if you know what you are doing *and* there is no specialized /// method for this, e.g. [`Self::use_include_tag()`]. pub fn add_feature(&mut self, feature: &str) { match self.version { gix_transport::Protocol::V0 | gix_transport::Protocol::V1 => { let features = self .features_for_first_want .as_mut() .expect("call add_feature before first want()"); features.push(feature.into()) } gix_transport::Protocol::V2 => { self.args.push(feature.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. /// If `trace` is `true`, all packetlines received or sent will be passed to the facilities of the `gix-trace` crate. #[cfg(any(feature = "async-client", feature = "blocking-client"))] pub fn new(version: gix_transport::Protocol, features: Vec, trace: bool) -> 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::V0 | 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() .filter( |(f, _)| *f != "include-tag", /* not a capability in that sense, needs to be turned on by caller later */ ) .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, trace, } } } #[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; gix-protocol-0.45.1/src/fetch/delegate.rs000064400000000000000000000306641046102023000163520ustar 00000000000000use 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 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::NestedProgress; 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 NestedProgress + 'static, refs: &[Ref], previous_response: &Response, ) -> io::Result<()>; } impl Delegate for Box { fn receive_pack( &mut self, input: impl BufRead, progress: impl NestedProgress + 'static, 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 NestedProgress + 'static, 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::NestedProgress; 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 NestedProgress + 'static, 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 NestedProgress + 'static, 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 NestedProgress + 'static, 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; gix-protocol-0.45.1/src/fetch/error.rs000064400000000000000000000011141046102023000157150ustar 00000000000000use 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), } gix-protocol-0.45.1/src/fetch/handshake.rs000064400000000000000000000017641046102023000165250ustar 00000000000000use 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 } gix-protocol-0.45.1/src/fetch/mod.rs000064400000000000000000000006651046102023000153550ustar 00000000000000mod arguments; pub use arguments::Arguments; /// #[allow(clippy::empty_docs)] 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; /// #[allow(clippy::empty_docs)] pub mod response; pub use response::Response; mod handshake; pub use handshake::upload_pack as handshake; #[cfg(test)] mod tests; gix-protocol-0.45.1/src/fetch/response/async_io.rs000064400000000000000000000174501046102023000202400ustar 00000000000000use std::io; 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.readline_str(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`. /// /// `client_expects_pack` is only relevant for V1 stateful connections, and if `false`, causes us to stop parsing when seeing `NAK`, /// and if `true` we will keep parsing until we get a pack as the client already signalled to the server that it's done. /// This way of doing things allows us to exploit knowledge about more recent versions of the protocol, which keeps code easier /// and more localized without having to support all the cruft that there is. /// /// `wants_to_negotiate` should be `false` for clones which is when we don't have sent any haves. The reason for this flag to exist /// is to predict how to parse V1 output only, and neither `client_expects_pack` nor `wants_to_negotiate` are relevant for V2. /// This ugliness is in place to avoid having to resort to an [an even more complex ugliness](https://github.com/git/git/blob/9e49351c3060e1fa6e0d2de64505b7becf157f28/fetch-pack.c#L583-L594) /// that `git` has to use to predict how many acks are supposed to be read. We also genuinely hope that this covers it all…. pub async fn from_line_reader( version: Protocol, reader: &mut (impl client::ExtendedBufRead<'_> + Unpin), client_expects_pack: bool, wants_to_negotiate: bool, ) -> Result { match version { Protocol::V0 | Protocol::V1 => { let mut line = String::new(); let mut acks = Vec::::new(); let mut shallows = Vec::::new(); let mut saw_ready = false; 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 // [a lot](https://github.com/git/git/blob/9e49351c3060e1fa6e0d2de64505b7becf157f28/fetch-pack.c#L583-L594) // to deal with this correctly. // 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.readline_str(&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.readline_str(&mut line).await?, 0, "consuming a peeked line works" ); // When the server sends ready, we know there is going to be a pack so no need to stop early. saw_ready |= matches!(acks.last(), Some(Acknowledgement::Ready)); if let Some(Acknowledgement::Nak) = acks.last().filter(|_| !client_expects_pack || !saw_ready) { if !wants_to_negotiate { continue; } break 'lines false; } }; 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.readline_str(&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, }) } } } } gix-protocol-0.45.1/src/fetch/response/blocking_io.rs000064400000000000000000000171741046102023000207160ustar 00000000000000use std::io; use gix_transport::{client, Protocol}; use crate::fetch::{ response, response::{Acknowledgement, ShallowUpdate, WantedRef}, Response, }; fn parse_v2_section<'a, T>( line: &mut String, reader: &mut impl client::ExtendedBufRead<'a>, res: &mut Vec, parse: impl Fn(&str) -> Result, ) -> Result { line.clear(); while reader.readline_str(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`. /// /// `client_expects_pack` is only relevant for V1 stateful connections, and if `false`, causes us to stop parsing when seeing `NAK`, /// and if `true` we will keep parsing until we get a pack as the client already signalled to the server that it's done. /// This way of doing things allows us to exploit knowledge about more recent versions of the protocol, which keeps code easier /// and more localized without having to support all the cruft that there is. /// /// `wants_to_negotiate` should be `false` for clones which is when we don't have sent any haves. The reason for this flag to exist /// is to predict how to parse V1 output only, and neither `client_expects_pack` nor `wants_to_negotiate` are relevant for V2. /// This ugliness is in place to avoid having to resort to an [an even more complex ugliness](https://github.com/git/git/blob/9e49351c3060e1fa6e0d2de64505b7becf157f28/fetch-pack.c#L583-L594) /// that `git` has to use to predict how many acks are supposed to be read. We also genuinely hope that this covers it all…. pub fn from_line_reader<'a>( version: Protocol, reader: &mut impl client::ExtendedBufRead<'a>, client_expects_pack: bool, wants_to_negotiate: bool, ) -> Result { match version { Protocol::V0 | Protocol::V1 => { let mut line = String::new(); let mut acks = Vec::::new(); let mut shallows = Vec::::new(); let mut saw_ready = false; 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 // [a lot](https://github.com/git/git/blob/9e49351c3060e1fa6e0d2de64505b7becf157f28/fetch-pack.c#L583-L594) // to deal with this correctly. // 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.readline_str(&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.readline_str(&mut line)?, 0, "consuming a peeked line works"); // When the server sends ready, we know there is going to be a pack so no need to stop early. saw_ready |= matches!(acks.last(), Some(Acknowledgement::Ready)); if let Some(Acknowledgement::Nak) = acks.last().filter(|_| !client_expects_pack || !saw_ready) { if !wants_to_negotiate { continue; } break 'lines false; } }; 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.readline_str(&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, }) } } } } gix-protocol-0.45.1/src/fetch/response/mod.rs000064400000000000000000000224161046102023000172110ustar 00000000000000use 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 = "serde", 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 = "serde", 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 = "serde", 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::V0 | 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; gix-protocol-0.45.1/src/fetch/tests.rs000064400000000000000000000366451046102023000157470ustar 00000000000000#[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(), false) } fn arguments_v2(features: impl IntoIterator) -> fetch::Arguments { fetch::Arguments::new(Protocol::V2, features.into_iter().map(|n| (n, None)).collect(), false) } 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, trace: bool, ) -> Result, Error> { self.inner.request(write_mode, on_into_read, trace) } 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, trace: bool, ) -> Result, Error> { self.inner.request(write_mode, on_into_read, trace) } 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 false, ), 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().copied()); 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 feature-b include-tag 00000009done " .as_bstr() ); } #[maybe_async::test(feature = "blocking-client", async(feature = "async-client", async_std::test))] async fn no_include_tag() { let mut out = Vec::new(); let mut t = transport(&mut out, true); let mut arguments = arguments_v1(["include-tag", "feature-b"].iter().copied()); assert!(arguments.can_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"003cwant ff333369de1221f9bfbbe03a3a13e9a09bc1ffff feature-b 00000009done " .as_bstr(), "it's possible to not have it enabled, even though it's advertised by the server" ); } #[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().copied()); 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()); assert!(arguments.is_stateless(true), "V2 is stateless…"); assert!(arguments.is_stateless(false), "…in all cases"); arguments.add_feature("no-progress"); 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 0010no-progress 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.add_feature("no-progress"); 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 0010no-progress 000ddeepen 1 0017deepen-since 12345 0035shallow 7b333369de1221f9bfbbe03a3a13e9a09bc1c9ff 0032want 7b333369de1221f9bfbbe03a3a13e9a09bc1c907 001fdeepen-not refs/heads/main 0032have 0000000000000000000000000000000000000000 00000012command=fetch 0001000ethin-pack 000eofs-delta 0010no-progress 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() ) } } } gix-protocol-0.45.1/src/fetch_fn.rs000064400000000000000000000162621046102023000152610ustar 00000000000000use std::borrow::Cow; use gix_features::progress::NestedProgress; 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. #[derive(Default, Debug, Copy, Clone, PartialEq, Eq, Hash)] 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. #[default] 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, } /// 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)"`". /// * If `trace` is `true`, all packetlines received or sent will be passed to the facilities of the `gix-trace` crate. /// /// _Note_ that depending on the `delegate`, the actual action performed can be `ls-refs`, `clone` or `fetch`. /// /// # WARNING - Do not use! /// /// As it will hang when having multiple negotiation rounds. #[allow(clippy::result_large_err)] #[maybe_async] // TODO: remove this without losing test coverage - we have the same but better in `gix` and it's // not really worth it to maintain the delegates here. pub async fn fetch( mut transport: T, mut delegate: D, authenticate: F, mut progress: P, fetch_mode: FetchConnection, agent: impl Into, trace: bool, ) -> Result<(), Error> where F: FnMut(credentials::helper::Action) -> credentials::protocol::Result, D: Delegate, T: client::Transport, P: NestedProgress + 'static, 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, trace, ) .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, trace).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, trace).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, trace); 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, true, /* hack, telling us we don't want this delegate approach anymore */ false, /* just as much of a hack which causes us to expect a pack immediately */ ) .await?; previous_response = if response.has_pack() { progress.step(); progress.set_name("receiving pack".into()); 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, trace).await?; } Ok(()) } fn setup_remote_progress

( progress: &mut P, reader: &mut Box + Unpin + '_>, ) where P: NestedProgress, 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); gix_transport::packetline::read::ProgressAction::Continue } }) as gix_transport::client::HandleProgress<'_>)); } gix-protocol-0.45.1/src/handshake/function.rs000064400000000000000000000105731046102023000172570ustar 00000000000000use 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 _span = gix_features::trace::detail!("gix_protocol::handshake()", service = ?service, extra_parameters = ?extra_parameters); let (server_protocol_version, refs, capabilities) = { progress.init(None, progress::steps()); progress.set_name("handshake".into()); progress.step(); let extra_parameters: Vec<_> = extra_parameters .iter() .map(|(k, v)| (k.as_str(), v.as_deref())) .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".into()); let credentials::protocol::Outcome { identity, next } = authenticate(credentials::helper::Action::get_for_url(url.clone()))? .ok_or(Error::EmptyCredentials)?; transport.set_identity(identity)?; progress.step(); progress.set_name("handshake (authenticated)".into()); 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!( matches!( actual_protocol, gix_transport::Protocol::V0 | gix_transport::Protocol::V1 ), "Only V(0|1) 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, }) } gix-protocol-0.45.1/src/handshake/mod.rs000064400000000000000000000102421046102023000162020ustar 00000000000000use 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 = "serde", 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, ultimately after possibly following `tag`, 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 annotated tag the ref points to, if present. /// /// Note that this field is also `None` if `full_ref_name` is a lightweight tag. tag: Option, /// The hash of the object the `target` ref ultimately 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 = "serde", 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("No credentials were returned at all as if the credential helper isn't functioning unknowingly")] EmptyCredentials, #[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; /// #[allow(clippy::empty_docs)] pub mod refs; gix-protocol-0.45.1/src/handshake/refs/async_io.rs000064400000000000000000000032621046102023000201720ustar 00000000000000use 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()) } gix-protocol-0.45.1/src/handshake/refs/blocking_io.rs000064400000000000000000000030531046102023000206430ustar 00000000000000use 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()) } gix-protocol-0.45.1/src/handshake/refs/mod.rs000064400000000000000000000055241046102023000171500ustar 00000000000000use bstr::BStr; use super::Ref; /// #[allow(clippy::empty_docs)] 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 } => (full_ref_name.as_ref(), Some(object), None), Ref::Symbolic { full_ref_name, tag, object, .. } => ( full_ref_name.as_ref(), Some(tag.as_deref().unwrap_or(object)), tag.as_deref().map(|_| object.as_ref()), ), 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; gix-protocol-0.45.1/src/handshake/refs/shared.rs000064400000000000000000000250241046102023000176340ustar 00000000000000use 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), tag, object, } => Ref::Symbolic { full_ref_name: path, target, tag, object, }, InternalRef::Symbolic { path, target: None, tag: None, object, } => Ref::Direct { full_ref_name: path, object, }, InternalRef::Symbolic { path, target: None, tag: Some(tag), object, } => Ref::Peeled { full_ref_name: path, tag, 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, tag: 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) => { if hex_hash.iter().all(|b| *b == b'0') && stripped == b"capabilities" { // this is a special dummy-ref just for the sake of getting capabilities across in a repo that is empty. return Ok(()); } 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(), tag: None, // TODO: figure out how annotated tags work here. 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(4, |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())); } let mut symref_target = None; let mut peeled = None; for attribute in tokens.by_ref().take(2) { 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" => { peeled = Some(gix_hash::ObjectId::from_hex(value.as_bytes())?); } b"symref-target" => { symref_target = Some(value); } _ => { return Err(Error::UnknownAttribute { attribute: attribute.to_owned().into(), line: trimmed.to_owned().into(), }) } } } _ => return Err(Error::MalformedV2RefLine(trimmed.to_owned().into())), } } if tokens.next().is_some() { return Err(Error::MalformedV2RefLine(trimmed.to_owned().into())); } Ok(match (symref_target, peeled) { (Some(target_name), peeled) => match target_name { b"(null)" => match peeled { None => Ref::Direct { full_ref_name: path.into(), object: id.ok_or(Error::InvariantViolation { message: "got 'unborn' while (null) was a symref target", })?, }, Some(peeled) => Ref::Peeled { full_ref_name: path.into(), object: peeled, tag: 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(), tag: peeled.map(|_| id), object: peeled.unwrap_or(id), target: name.into(), }, None => Ref::Unborn { full_ref_name: path.into(), target: name.into(), }, }, }, (None, Some(peeled)) => Ref::Peeled { full_ref_name: path.into(), object: peeled, tag: id.ok_or(Error::InvariantViolation { message: "got 'unborn' as tag target", })?, }, (None, None) => 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())), } } gix-protocol-0.45.1/src/handshake/refs/tests.rs000064400000000000000000000221241046102023000175260ustar 00000000000000use 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 978f927e6397113757dfec6332e7d9c7e356ac25 refs/heads/symbolic symref-target:refs/tags/v1.0 peeled:4d979abcde5cea47b079c38850828956c9382a56 " .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(), tag: None, 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") }, Ref::Symbolic { full_ref_name: "refs/heads/symbolic".into(), target: "refs/tags/v1.0".into(), tag: Some(oid("978f927e6397113757dfec6332e7d9c7e356ac25")), object: oid("4d979abcde5cea47b079c38850828956c9382a56") }, ] ); } #[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(), tag: None, 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)))) } fn readline_str(&mut self, line: &mut String) -> std::io::Result { use bstr::{BStr, ByteSlice}; let bytes: &BStr = self.0.into(); let mut lines = bytes.lines(); let res = match lines.next() { None => return Ok(0), Some(line) => line, }; self.0 = lines.as_bytes(); let len = res.len(); line.push_str(res.to_str().expect("valid UTF8 in fixture")); Ok(len) } } #[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)))) } async fn readline_str(&mut self, line: &mut String) -> std::io::Result { use bstr::{BStr, ByteSlice}; let bytes: &BStr = self.0.into(); let mut lines = bytes.lines(); let res = match lines.next() { None => return Ok(0), Some(line) => line, }; self.0 = lines.as_bytes(); let len = res.len(); line.push_str(res.to_str().expect("valid UTF8 in fixture")); Ok(len) } } gix-protocol-0.45.1/src/lib.rs000064400000000000000000000044031046102023000142450ustar 00000000000000//! 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( all(doc, feature = "document-features"), doc = ::document_features::document_features!() )] #![cfg_attr(all(doc, feature = "document-features"), 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; /// #[allow(clippy::empty_docs)] #[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"))] #[allow(clippy::empty_docs)] pub mod handshake; #[cfg(any(feature = "blocking-client", feature = "async-client"))] pub use handshake::function::handshake; /// #[allow(clippy::empty_docs)] #[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; gix-protocol-0.45.1/src/ls_refs.rs000064400000000000000000000100771046102023000151400ustar 00000000000000mod 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`. /// If `trace` is `true`, all packetlines received or sent will be passed to the facilities of the `gix-trace` crate. #[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, trace: bool, ) -> Result, Error> { let _span = gix_features::trace::detail!("gix_protocol::ls_refs()", capabilities = ?capabilities); 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".into()); 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()) }, trace, ) .await?; from_v2_refs(&mut remote_refs).await? } Err(err) => { indicate_end_of_interaction(transport, trace).await?; return Err(err.into()); } }; Ok(refs) } } gix-protocol-0.45.1/src/remote_progress.rs000064400000000000000000000073331046102023000167230ustar 00000000000000use bstr::ByteSlice; use winnow::{ combinator::{opt, preceded, terminated}, prelude::*, token::take_till, }; /// 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 = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct RemoteProgress<'a> { #[cfg_attr(feature = "serde", 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(mut line: &[u8]) -> Option> { parse_progress(&mut 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: &mut &[u8]) -> PResult { take_till(0.., |c: u8| !c.is_ascii_digit()) .try_map(gix_utils::btoi::to_signed) .parse_next(i) } fn next_optional_percentage(i: &mut &[u8]) -> PResult, ()> { opt(terminated( preceded( take_till(0.., |c: u8| c.is_ascii_digit()), parse_number.try_map(u32::try_from), ), b"%", )) .parse_next(i) } fn next_optional_number(i: &mut &[u8]) -> PResult, ()> { opt(preceded(take_till(0.., |c: u8| c.is_ascii_digit()), parse_number)).parse_next(i) } fn parse_progress<'i>(line: &mut &'i [u8]) -> PResult, ()> { let action = take_till(1.., |c| c == b':').parse_next(line)?; let percent = next_optional_percentage.parse_next(line)?; let step = next_optional_number.parse_next(line)?; let max = next_optional_number.parse_next(line)?; Ok(RemoteProgress { action: action.into(), percent, step, max, }) } gix-protocol-0.45.1/src/util.rs000064400000000000000000000023441046102023000144560ustar 00000000000000/// 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. /// If `trace` is `true`, all packetlines received or sent will be passed to the facilities of the `gix-trace` crate. #[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, trace: bool, ) -> 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, trace, )? .into_read() .await?; } Ok(()) }