History

[git-protocol] move everything into `blocking_io` for later translation…

…into async-io, as usual.

Sebastian Thiel committed 5 years ago

Commit fa033741 parent 0d39b5d12 files · +295 −254Browse files

Showing the changes to git-protocol/src/fetch/blocking_io (10 of 12 files). Show all

Unified · Split

10 files
  1. git-protocol/src/fetch/blocking_io/arguments.rs +210 −0
  2. git-protocol/src/fetch/blocking_io/command.rs +196 −0
  3. git-protocol/src/fetch/blocking_io/delegate.rs +100 −0
  4. git-protocol/src/fetch/blocking_io/mod.rs +244 −0
  5. git-protocol/src/fetch/blocking_io/refs.rs +273 −0
  6. git-protocol/src/fetch/blocking_io/response.rs +280 −0
  7. git-protocol/src/fetch/blocking_io/tests/arguments.rs +236 −0
  8. git-protocol/src/fetch/blocking_io/tests/command.rs +165 −0
  9. git-protocol/src/fetch/blocking_io/tests/mod.rs +3 −0
  10. git-protocol/src/fetch/blocking_io/tests/refs.rs +108 −0
git-protocol/src/fetch/blocking_io/arguments.rs+210−0
@@ -0,0 +1,210 @@
1use std::{fmt, io::Write};
2
3use bstr::{BStr, BString, ByteSlice};
4
5use git_transport::{
6 client::{self, TransportV2Ext},
7 Protocol,
8};
9
10use crate::fetch::blocking_io::command::Feature;
11use crate::fetch::Command;
12
13/// The arguments passed to a server command.
14pub struct Arguments {
15 /// The active features/capabilities of the fetch invocation
16 features: Vec<Feature>,
17
18 args: Vec<BString>,
19 haves: Vec<BString>,
20
21 filter: bool,
22 shallow: bool,
23 deepen_since: bool,
24 deepen_not: bool,
25 deepen_relative: bool,
26
27 features_for_first_want: Option<Vec<String>>,
28 version: Protocol,
29}
30
31impl Arguments {
32 /// Return true if ref filters is supported.
33 pub fn can_use_filter(&self) -> bool {
34 self.filter
35 }
36 /// Return true if shallow refs are supported.
37 ///
38 /// This is relevant for partial clones when using `--depth X`.
39 pub fn can_use_shallow(&self) -> bool {
40 self.shallow
41 }
42 /// Return true if the 'deepen' capability is supported.
43 ///
44 /// This is relevant for partial clones when using `--depth X` and retrieving additional history.
45 pub fn can_use_deepen(&self) -> bool {
46 self.shallow
47 }
48 /// Return true if the 'deepen_since' capability is supported.
49 ///
50 /// This is relevant for partial clones when using `--depth X` and retrieving additional history
51 /// based on a date beyond which all history should be present.
52 pub fn can_use_deepen_since(&self) -> bool {
53 self.deepen_since
54 }
55 /// Return true if the 'deepen_not' capability is supported.
56 ///
57 /// This is relevant for partial clones when using `--depth X`.
58 pub fn can_use_deepen_not(&self) -> bool {
59 self.deepen_not
60 }
61 /// Return true if the 'deepen_relative' capability is supported.
62 ///
63 /// This is relevant for partial clones when using `--depth X`.
64 pub fn can_use_deepen_relative(&self) -> bool {
65 self.deepen_relative
66 }
67
68 /// Add the given `id` pointing to a commit to the 'want' list.
69 ///
70 /// As such it should be included in the server response as it's not present on the client.
71 pub fn want(&mut self, id: impl AsRef<git_hash::oid>) {
72 match self.features_for_first_want.take() {
73 Some(features) => self.prefixed("want ", format!("{} {}", id.as_ref(), features.join(" "))),
74 None => self.prefixed("want ", id.as_ref()),
75 }
76 }
77 /// Add the given `id` pointing to a commit to the 'have' list.
78 ///
79 /// As such it should _not_ be included in the server response as it's already present on the client.
80 pub fn have(&mut self, id: impl AsRef<git_hash::oid>) {
81 self.haves.push(format!("have {}", id.as_ref()).into());
82 }
83 /// Add the given `id` pointing to a commit to the 'shallow' list.
84 pub fn shallow(&mut self, id: impl AsRef<git_hash::oid>) {
85 assert!(self.shallow, "'shallow' feature required for 'shallow <id>'");
86 self.prefixed("shallow ", id.as_ref());
87 }
88 /// Deepen the commit history by `depth` amount of commits.
89 pub fn deepen(&mut self, depth: usize) {
90 assert!(self.shallow, "'shallow' feature required for deepen");
91 self.prefixed("deepen ", depth);
92 }
93 /// Deepen the commit history to include all commits from now to `seconds_since_unix_epoch`.
94 pub fn deepen_since(&mut self, seconds_since_unix_epoch: usize) {
95 assert!(self.deepen_since, "'deepen-since' feature required");
96 self.prefixed("deepen-since ", seconds_since_unix_epoch);
97 }
98 /// Deepen the commit history in a relative instead of absolute fashion.
99 pub fn deepen_relative(&mut self) {
100 assert!(self.deepen_relative, "'deepen-relative' feature required");
101 self.args.push("deepen-relative".into());
102 }
103 /// Do not include commits reachable by the given `ref_path` when deepening the history.
104 pub fn deepen_not(&mut self, ref_path: &BStr) {
105 assert!(self.deepen_not, "'deepen-not' feature required");
106 let mut line = BString::from("deepen-not ");
107 line.extend_from_slice(&ref_path);
108 self.args.push(line);
109 }
110 /// Set the given filter `spec` when listing references.
111 pub fn filter(&mut self, spec: &str) {
112 assert!(self.filter, "'filter' feature required");
113 self.prefixed("filter ", spec);
114 }
115 fn prefixed(&mut self, prefix: &str, value: impl fmt::Display) {
116 self.args.push(format!("{}{}", prefix, value).into());
117 }
118 pub(crate) fn new(version: Protocol, features: Vec<Feature>) -> Self {
119 let has = |name: &str| features.iter().any(|f| f.0 == name);
120 let filter = has("filter");
121 let shallow = has("shallow");
122 let mut deepen_since = shallow;
123 let mut deepen_not = shallow;
124 let mut deepen_relative = shallow;
125 let (initial_arguments, features_for_first_want) = match version {
126 Protocol::V1 => {
127 deepen_since = has("deepen-since");
128 deepen_not = has("deepen-not");
129 deepen_relative = has("deepen-relative");
130 let baked_features = features
131 .iter()
132 .map(|(n, v)| match v {
133 Some(v) => format!("{}={}", n, v),
134 None => n.to_string(),
135 })
136 .collect::<Vec<_>>();
137 (Vec::new(), Some(baked_features))
138 }
139 Protocol::V2 => (Command::Fetch.initial_arguments(&features), None),
140 };
141
142 Arguments {
143 features,
144 version,
145 args: initial_arguments,
146 haves: Vec::new(),
147 filter,
148 shallow,
149 deepen_not,
150 deepen_relative,
151 deepen_since,
152 features_for_first_want,
153 }
154 }
155 pub(crate) fn send<'a, T: client::Transport + 'a>(
156 &mut self,
157 transport: &'a mut T,
158 add_done_argument: bool,
159 ) -> Result<Box<dyn client::ExtendedBufRead + 'a>, client::Error> {
160 if self.haves.is_empty() {
161 assert!(add_done_argument, "If there are no haves, is_done must be true.");
162 }
163 match self.version {
164 git_transport::Protocol::V1 => {
165 let on_into_read = if add_done_argument {
166 client::MessageKind::Text(&b"done"[..])
167 } else {
168 client::MessageKind::Flush
169 };
170 let retained_state = if transport.is_stateful() {
171 None
172 } else {
173 Some(self.args.clone())
174 };
175 let mut line_writer =
176 transport.request(client::WriteMode::OneLfTerminatedLinePerWriteCall, on_into_read)?;
177
178 if let Some(first_arg_position) = self.args.iter().position(|l| l.starts_with_str("want ")) {
179 self.args.swap(first_arg_position, 0);
180 }
181 let had_args = !self.args.is_empty();
182 for arg in self.args.drain(..) {
183 line_writer.write_all(&arg)?;
184 }
185 if had_args {
186 line_writer.write_message(client::MessageKind::Flush)?;
187 }
188 for line in self.haves.drain(..) {
189 line_writer.write_all(&line)?;
190 }
191 if let Some(next_args) = retained_state {
192 self.args = next_args;
193 }
194 Ok(line_writer.into_read()?)
195 }
196 git_transport::Protocol::V2 => {
197 let retained_state = self.args.clone();
198 self.args.extend(self.haves.drain(..));
199 if add_done_argument {
200 self.args.push("done".into());
201 }
202 transport.invoke(
203 Command::Fetch.as_str(),
204 self.features.iter().filter(|(_, v)| v.is_some()).cloned(),
205 Some(std::mem::replace(&mut self.args, retained_state).into_iter()),
206 )
207 }
208 }
209 }
210}
git-protocol/src/fetch/blocking_io/command.rs+196−0
@@ -0,0 +1,196 @@
1use crate::fetch::agent;
2use bstr::{BString, ByteSlice};
3use git_transport::client::Capabilities;
4
5/// The kind of command to invoke on the server side.
6#[derive(PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone, Copy)]
7pub enum Command {
8 /// List references.
9 LsRefs,
10 /// Fetch a pack.
11 Fetch,
12}
13
14/// A key value pair of values known at compile time.
15pub type Feature = (&'static str, Option<&'static str>);
16
17impl Command {
18 /// Produce the name of the command as known by the server side.
19 pub fn as_str(&self) -> &'static str {
20 match self {
21 Command::LsRefs => "ls-refs",
22 Command::Fetch => "fetch",
23 }
24 }
25
26 /// Only V2
27 fn all_argument_prefixes(&self) -> &'static [&'static str] {
28 match self {
29 Command::LsRefs => &["symrefs", "peel", "ref-prefix "],
30 Command::Fetch => &[
31 "want ", // hex oid
32 "have ", // hex oid
33 "done",
34 "thin-pack",
35 "no-progress",
36 "include-tag",
37 "ofs-delta",
38 // Shallow feature/capability
39 "shallow ", // hex oid
40 "deepen ", // commit depth
41 "deepen-relative",
42 "deepen-since ", // time-stamp
43 "deepen-not ", // rev
44 // filter feature/capability
45 "filter ", // filter-spec
46 // ref-in-want feature
47 "want-ref ", // ref path
48 "sideband-all",
49 // packfile-uris feature
50 "packfile-uris ", // protocols
51 ],
52 }
53 }
54
55 fn all_features(&self, version: git_transport::Protocol) -> &'static [&'static str] {
56 match self {
57 Command::LsRefs => &[],
58 Command::Fetch => match version {
59 git_transport::Protocol::V1 => &[
60 "multi_ack",
61 "thin-pack",
62 "side-band",
63 "side-band-64k",
64 "ofs-delta",
65 "shallow",
66 "deepen-since",
67 "deepen-not",
68 "deepen-relative",
69 "no-progress",
70 "include-tag",
71 "multi_ack_detailed",
72 "allow-tip-sha1-in-want",
73 "allow-reachable-sha1-in-want",
74 "no-done",
75 "filter",
76 ],
77 git_transport::Protocol::V2 => &["shallow", "filter", "ref-in-want", "sideband-all", "packfile-uris"],
78 },
79 }
80 }
81
82 /// Compute initial arguments based on the given `features`. They are typically provided by the `default_features(…)` method.
83 /// Only useful for V2
84 pub(crate) fn initial_arguments(&self, features: &[Feature]) -> Vec<BString> {
85 match self {
86 Command::Fetch => ["thin-pack", "include-tag", "ofs-delta"]
87 .iter()
88 .map(|s| s.as_bytes().as_bstr().to_owned())
89 .chain(
90 ["sideband-all", "ref-in-want", "packfile-uris"]
91 .iter()
92 .filter(|f| features.iter().any(|(sf, _)| sf == *f))
93 .map(|f| f.as_bytes().as_bstr().to_owned()),
94 )
95 .collect(),
96 Command::LsRefs => vec![b"symrefs".as_bstr().to_owned(), b"peel".as_bstr().to_owned()],
97 }
98 }
99
100 pub(crate) fn default_features(
101 &self,
102 version: git_transport::Protocol,
103 server_capabilities: &Capabilities,
104 ) -> Vec<Feature> {
105 match self {
106 Command::Fetch => match version {
107 git_transport::Protocol::V1 => {
108 let has_multi_ack_detailed = server_capabilities.contains("multi_ack_detailed");
109 let has_sideband_64k = server_capabilities.contains("side-band-64k");
110 self.all_features(version)
111 .iter()
112 .copied()
113 .filter(|feature| match *feature {
114 "side-band" if has_sideband_64k => false,
115 "multi_ack" if has_multi_ack_detailed => false,
116 "include-tag" | "no-progress" => false,
117 feature => server_capabilities.contains(feature),
118 })
119 .map(|s| (s, None))
120 .chain(Some(agent()))
121 .collect()
122 }
123 git_transport::Protocol::V2 => {
124 let supported_features = server_capabilities
125 .iter()
126 .find_map(|c| {
127 if c.name() == Command::Fetch.as_str().as_bytes().as_bstr() {
128 c.values().map(|v| v.map(|f| f.to_owned()).collect())
129 } else {
130 None
131 }
132 })
133 .unwrap_or_else(Vec::new);
134 self.all_features(version)
135 .iter()
136 .copied()
137 .filter(|feature| supported_features.iter().any(|supported| supported == feature))
138 .map(|s| (s, None))
139 .chain(Some(agent()))
140 .collect()
141 }
142 },
143 Command::LsRefs => vec![agent()],
144 }
145 }
146 /// Panics if the given arguments and features don't match what's statically known. It's considered a bug in the delegate.
147 pub(crate) fn validate_argument_prefixes_or_panic(
148 &self,
149 version: git_transport::Protocol,
150 server: &Capabilities,
151 arguments: &[BString],
152 features: &[Feature],
153 ) {
154 let allowed = self.all_argument_prefixes();
155 for arg in arguments {
156 if allowed.iter().any(|allowed| arg.starts_with(allowed.as_bytes())) {
157 continue;
158 }
159 panic!("{}: argument {} is not known or allowed", self.as_str(), arg);
160 }
161 match version {
162 git_transport::Protocol::V1 => {
163 for (feature, _) in features {
164 if server
165 .iter()
166 .any(|c| feature.starts_with(c.name().to_str_lossy().as_ref()))
167 {
168 continue;
169 }
170 panic!("{}: capability {} is not supported", self.as_str(), feature);
171 }
172 }
173 git_transport::Protocol::V2 => {
174 let allowed = server
175 .iter()
176 .find_map(|c| {
177 if c.name() == self.as_str().as_bytes().as_bstr() {
178 c.values().map(|v| v.map(|f| f.to_string()).collect::<Vec<_>>())
179 } else {
180 None
181 }
182 })
183 .unwrap_or_default();
184 for (feature, _) in features {
185 if allowed.iter().any(|allowed| feature == allowed) {
186 continue;
187 }
188 match *feature {
189 "agent" => {}
190 _ => panic!("{}: V2 feature/capability {} is not supported", self.as_str(), feature),
191 }
192 }
193 }
194 }
195 }
196}
git-protocol/src/fetch/blocking_io/delegate.rs+100−0
@@ -0,0 +1,100 @@
1use crate::fetch::{Arguments, Ref, Response};
2use bstr::BString;
3use git_features::progress::Progress;
4use git_transport::client::Capabilities;
5use std::io;
6
7/// Defines what to do next after certain [`Delegate`] operations.
8#[derive(PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone, Copy)]
9pub enum Action {
10 /// Continue the typical flow of operations in this flow.
11 Continue,
12 /// Close the connection without making any further requests.
13 Close,
14}
15
16/// The protocol delegate is the bare minimal interface needed to fully control the [`fetch`][crate::fetch()] operation.
17///
18/// Implementations of this trait are controlled by code with intricate knowledge about how fetching works in protocol version V1 and V2,
19/// so you don't have to.
20/// Everything is tucked away behind type-safety so 'nothing can go wrong'©. Runtime assertions assure invalid
21/// features or arguments don't make it to the server in the first place.
22/// Please note that this trait mostly corresponds to what V2 would look like, even though V1 is supported as well.
23pub trait Delegate {
24 /// Called before invoking 'ls-refs' on the server to allow providing it with additional `arguments` and to enable `features`.
25 ///
26 /// Note that some arguments are preset based on typical use, and `features` are preset to maximize options.
27 /// The `server` capabilities can be used to see which additional capabilities the server supports as per the handshake which happened prior.
28 /// Note that this is called only if we are using protocol version 2.
29 fn prepare_ls_refs(
30 &mut self,
31 _server: &Capabilities,
32 _arguments: &mut Vec<BString>,
33 _features: &mut Vec<(&str, Option<&str>)>,
34 ) {
35 }
36
37 /// Called before invoking the 'fetch' interaction with `features` pre-filled for typical use
38 /// and to maximize capabilities to allow aborting an interaction early.
39 ///
40 /// `refs` is a list of known references on the remote based on the handshake or a prior call to ls_refs.
41 /// These can be used to abort early in case the refs are already known here.
42 ///
43 /// As there will be another call allowing to post arguments conveniently in the correct format, i.e. `want hex-oid`,
44 /// there is no way to set arguments at this time.
45 ///
46 /// `version` is the actually supported version as reported by the server, which is relevant in case the server requested a downgrade.
47 /// `server` capabilities is a list of features the server supports for your information, along with enabled `features` that the server knows about.
48 fn prepare_fetch(
49 &mut self,
50 _version: git_transport::Protocol,
51 _server: &Capabilities,
52 _features: &mut Vec<(&str, Option<&str>)>,
53 _refs: &[Ref],
54 ) -> Action {
55 Action::Continue
56 }
57
58 /// A method called repeatedly to negotiate the objects to receive in [`receive_pack(…)`][Delegate::receive_pack()].
59 ///
60 /// The first call has `previous` set to `None` as there was no previous response. Every call that follows `previous`
61 /// will be set to `Some`.
62 ///
63 /// ### If `previous` is `None`…
64 ///
65 /// Given a list of `arguments` to populate with wants, shallows, filters and other contextual information to be
66 /// sent to the server. This method is called once.
67 /// Send the objects you `have` have afterwards based on the tips of your refs, in preparation to walk down their parents
68 /// with each call to `negotiate` to find the common base(s).
69 ///
70 /// Note that you should not `want` and object that you already have.
71 /// `refs` are the the tips of on the server side, effectively the latest objects _they_ have.
72 ///
73 /// Return `Action::Close` if you know that there are no `haves` on your end to allow the server to send all of its objects
74 /// as is the case during initial clones.
75 ///
76 /// ### If `previous` is `Some`…
77 ///
78 /// Populate `arguments` with the objects you `have` starting from the tips of _your_ refs, taking into consideration
79 /// the `previous` response of the server to see which objects they acknowledged to have. You have to maintain
80 /// enough state to be able to walk down from your tips on each call, if they are not in common, and keep setting `have`
81 /// for those which are in common if that helps teaching the server about our state and to acknowledge their existence on _their_ end.
82 /// This method is called until the other side signals they are ready to send a pack.
83 /// Return `Action::Close` if you want to give up before finding a common base. This can happen if the remote repository
84 /// has radically changed so there are no bases, or they are very far in the past, causing all objects to be sent.
85 fn negotiate(&mut self, refs: &[Ref], arguments: &mut Arguments, previous: Option<&Response>) -> Action;
86
87 /// Receive a pack provided from the given `input`.
88 ///
89 /// Use `progress` to emit your own progress messages when decoding the pack.
90 ///
91 /// `refs` of the remote side are provided for convenience, along with the parsed `previous` response in case you want
92 /// to check additional acks.
93 fn receive_pack(
94 &mut self,
95 input: impl io::BufRead,
96 progress: impl Progress,
97 refs: &[Ref],
98 previous: &Response,
99 ) -> io::Result<()>;
100}
git-protocol/src/fetch/blocking_io/mod.rs+244−0
@@ -0,0 +1,244 @@
1///
2pub mod arguments;
3///
4pub mod command;
5///
6pub mod delegate;
7///
8pub mod refs;
9///
10pub mod response;
11#[cfg(test)]
12mod tests;
13
14mod error {
15 use crate::fetch::blocking_io::{refs, response};
16 use git_transport::client;
17 use quick_error::quick_error;
18 use std::io;
19
20 use crate::credentials;
21
22 quick_error! {
23 /// The error used in [`fetch()`].
24 #[derive(Debug)]
25 #[allow(missing_docs)]
26 pub enum Error {
27 PackIo(err: io::Error) {
28 display("Could not read streaming pack file")
29 from()
30 source(err)
31 }
32 Credentials(err: credentials::Error) {
33 display("Failed to obtain, approve or reject credentials")
34 from()
35 source(err)
36 }
37 Transport(err: client::Error) {
38 display("An error occurred on the transport layer while fetching data")
39 from()
40 source(err)
41 }
42 SymrefWithoutValue {
43 display("A symref 'capability' is expected to have a value")
44 }
45 Ref(err: refs::Error) {
46 display("A reference could not be parsed or invariants were not met")
47 from()
48 source(err)
49 }
50 Response(err: response::Error) {
51 display("The server response could not be parsed")
52 from()
53 source(err)
54 }
55 }
56 }
57}
58pub use error::Error;
59
60mod fetch {
61 use crate::{
62 credentials,
63 fetch::{
64 blocking_io::{refs, refs::InternalRef},
65 Action, Arguments, Command, Delegate, Error, Ref, Response,
66 },
67 };
68 use git_features::{progress, progress::Progress};
69 use git_transport::{
70 client,
71 client::{SetServiceResponse, TransportV2Ext},
72 Service,
73 };
74 use std::io;
75
76 /// Perform a 'fetch' operation with the server using `transport`, with `delegate` handling all server interactions.
77 ///
78 /// * `authenticate(operation_to_perform)` is used to receive credentials for the connection and potentially store it
79 /// if the server indicates 'permission denied'. Note that not all transport support authentication or authorization.
80 /// * `progress` is used to emit progress messages.
81 ///
82 /// _Note_ that depending on the `delegate`, the actual action performed can be `ls-refs`, `clone` or `fetch`.
83 pub fn fetch<F>(
84 mut transport: impl client::Transport,
85 delegate: &mut impl Delegate,
86 mut authenticate: F,
87 mut progress: impl Progress,
88 ) -> Result<(), Error>
89 where
90 F: FnMut(credentials::Action<'_>) -> credentials::Result,
91 {
92 let (protocol_version, mut parsed_refs, capabilities, call_ls_refs) = {
93 progress.init(None, progress::steps());
94 progress.set_name("handshake");
95 progress.step();
96 let result = transport.handshake(Service::UploadPack);
97 let SetServiceResponse {
98 actual_protocol,
99 capabilities,
100 refs,
101 } = match result {
102 Ok(v) => Ok(v),
103 Err(client::Error::Io { ref err }) if err.kind() == io::ErrorKind::PermissionDenied => {
104 drop(result); // needed to workaround this: https://github.com/rust-lang/rust/issues/76149
105 let url = transport.to_url();
106 progress.set_name("authentication");
107 let credentials::Outcome { identity, next } =
108 authenticate(credentials::Action::Fill(&url))?.expect("FILL provides an identity");
109 transport.set_identity(identity)?;
110 progress.step();
111 progress.set_name("handshake (authenticated)");
112 match transport.handshake(Service::UploadPack) {
113 Ok(v) => {
114 authenticate(next.approve())?;
115 Ok(v)
116 }
117 // Still no permission? Reject the credentials.
118 Err(client::Error::Io { err }) if err.kind() == io::ErrorKind::PermissionDenied => {
119 authenticate(next.reject())?;
120 Err(client::Error::Io { err })
121 }
122 // Otherwise, do nothing, as we don't know if it actually got to try the credentials.
123 // If they were previously stored, they remain. In the worst case, the user has to enter them again
124 // next time they try.
125 Err(err) => Err(err),
126 }
127 }
128 Err(err) => Err(err),
129 }?;
130
131 let mut parsed_refs = Vec::<InternalRef>::new();
132 refs::from_capabilities(&mut parsed_refs, capabilities.iter())?;
133
134 let call_ls_refs = match refs {
135 Some(mut refs) => {
136 assert_eq!(
137 actual_protocol,
138 git_transport::Protocol::V1,
139 "Only V1 auto-responds with refs"
140 );
141 refs::from_v1_refs_received_as_part_of_handshake(&mut parsed_refs, &mut refs)?;
142 false
143 }
144 None => true,
145 };
146 (
147 actual_protocol,
148 parsed_refs.into_iter().map(Into::into).collect::<Vec<Ref>>(),
149 capabilities,
150 call_ls_refs,
151 )
152 }; // this scope is needed, see https://github.com/rust-lang/rust/issues/76149
153
154 if transport.desired_protocol_version() != protocol_version {
155 progress.info(format!(
156 "server did not support protocol {} and downgraded to {}",
157 transport.desired_protocol_version() as usize,
158 protocol_version as usize,
159 ));
160 }
161
162 if call_ls_refs {
163 assert_eq!(
164 protocol_version,
165 git_transport::Protocol::V2,
166 "Only V2 needs a separate request to get specific refs"
167 );
168
169 let ls_refs = Command::LsRefs;
170 let mut ls_features = ls_refs.default_features(protocol_version, &capabilities);
171 let mut ls_args = ls_refs.initial_arguments(&ls_features);
172 delegate.prepare_ls_refs(&capabilities, &mut ls_args, &mut ls_features);
173 ls_refs.validate_argument_prefixes_or_panic(protocol_version, &capabilities, &ls_args, &ls_features);
174
175 progress.step();
176 progress.set_name("list refs");
177 let mut remote_refs = transport.invoke(
178 ls_refs.as_str(),
179 ls_features.into_iter(),
180 if ls_args.is_empty() {
181 None
182 } else {
183 Some(ls_args.into_iter())
184 },
185 )?;
186 refs::from_v2_refs(&mut parsed_refs, &mut remote_refs)?;
187 }
188
189 let fetch = Command::Fetch;
190 let mut fetch_features = fetch.default_features(protocol_version, &capabilities);
191 let next = delegate.prepare_fetch(protocol_version, &capabilities, &mut fetch_features, &parsed_refs);
192 fetch.validate_argument_prefixes_or_panic(protocol_version, &capabilities, &[], &fetch_features);
193
194 if next == Action::Close {
195 transport.close()?;
196 return Ok(());
197 }
198
199 Response::check_required_features(protocol_version, &fetch_features)?;
200 let sideband_all = fetch_features.iter().any(|(n, _)| *n == "sideband-all");
201 let mut arguments = Arguments::new(protocol_version, fetch_features);
202 let mut previous_response = None::<Response>;
203 let mut round = 1;
204 'negotiation: loop {
205 progress.step();
206 progress.set_name(format!("negotiate (round {})", round));
207 round += 1;
208 let action = delegate.negotiate(&parsed_refs, &mut arguments, previous_response.as_ref());
209 let mut reader = arguments.send(&mut transport, action == Action::Close)?;
210 if sideband_all {
211 setup_remote_progress(&mut progress, &mut reader);
212 }
213 let response = Response::from_line_reader(protocol_version, &mut reader)?;
214 previous_response = if response.has_pack() {
215 progress.step();
216 progress.set_name("receiving pack");
217 if !sideband_all {
218 setup_remote_progress(&mut progress, &mut reader);
219 }
220 delegate.receive_pack(reader, progress, &parsed_refs, &response)?;
221 break 'negotiation;
222 } else {
223 match action {
224 Action::Close => break 'negotiation,
225 Action::Continue => Some(response),
226 }
227 }
228 }
229 Ok(())
230 }
231
232 fn setup_remote_progress(
233 progress: &mut impl Progress,
234 reader: &mut Box<dyn git_transport::client::ExtendedBufRead + '_>,
235 ) {
236 reader.set_progress_handler(Some(Box::new({
237 let mut remote_progress = progress.add_child("remote");
238 move |is_err: bool, data: &[u8]| {
239 crate::RemoteProgress::translate_to_progress(is_err, data, &mut remote_progress)
240 }
241 }) as git_transport::client::HandleProgress));
242 }
243}
244pub use fetch::fetch;
git-protocol/src/fetch/blocking_io/refs.rs+273−0
@@ -0,0 +1,273 @@
1use bstr::{BString, ByteSlice};
2use quick_error::quick_error;
3use std::io;
4
5quick_error! {
6 /// The error returned when parsing References/refs from the server response.
7 #[derive(Debug)]
8 #[allow(missing_docs)]
9 pub enum Error {
10 Io(err: io::Error) {
11 display("An IO error occurred while reading refs from the server")
12 from()
13 source(err)
14 }
15 Id(err: git_hash::decode::Error) {
16 display("Failed to hex-decode object hash")
17 from()
18 source(err)
19 }
20 MalformedSymref(symref: BString) {
21 display("'{}' could not be parsed. A symref is expected to look like <NAME>:<target>.", symref)
22 }
23 MalformedV1RefLine(line: String) {
24 display("'{}' could not be parsed. A V1 ref line should be '<hex-hash> <path>'.", line)
25 }
26 MalformedV2RefLine(line: String) {
27 display("'{}' could not be parsed. A V2 ref line should be '<hex-hash> <path>[ (peeled|symref-target):<value>'.", line)
28 }
29 UnkownAttribute(attribute: String, line: String) {
30 display("The ref attribute '{}' is unknown. Found in line '{}'", attribute, line)
31 }
32 InvariantViolation(message: &'static str) {
33 display("{}", message)
34 }
35 }
36}
37
38/// A git reference, commonly referred to as 'ref', as returned by a git server before sending a pack.
39#[derive(PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone)]
40#[cfg_attr(feature = "serde1", derive(serde::Serialize, serde::Deserialize))]
41pub enum Ref {
42 /// A ref pointing to a `tag` object, which in turns points to an `object`, usually a commit
43 Peeled {
44 /// The path at which the ref is located, like `/refs/heads/main`.
45 path: BString,
46 /// The hash of the tag the ref points to.
47 tag: git_hash::ObjectId,
48 /// The hash of the object the `tag` points to.
49 object: git_hash::ObjectId,
50 },
51 /// A ref pointing to a commit object
52 Direct {
53 /// The path at which the ref is located, like `/refs/heads/main`.
54 path: BString,
55 /// The hash of the object the ref points to.
56 object: git_hash::ObjectId,
57 },
58 /// A symbolic ref pointing to `target` ref, which in turn points to an `object`
59 Symbolic {
60 /// The path at which the symbolic ref is located, like `/refs/heads/main`.
61 path: BString,
62 /// The path of the ref the symbolic ref points to.
63 target: BString,
64 /// The hash of the object the `target` ref points to.
65 object: git_hash::ObjectId,
66 },
67}
68
69impl Ref {
70 /// Provide shared fields referring to the ref itself, namely `(path, object id)`.
71 /// In case of peeled refs, the tag object itself is returned as it is what the path refers to.
72 pub fn unpack(&self) -> (&BString, &git_hash::ObjectId) {
73 match self {
74 Ref::Direct { path, object, .. }
75 | Ref::Peeled { path, tag: object, .. } // the tag acts as reference
76 | Ref::Symbolic { path, object, .. } => (path, object),
77 }
78 }
79}
80
81impl From<InternalRef> for Ref {
82 fn from(v: InternalRef) -> Self {
83 match v {
84 InternalRef::Symbolic { path, target, object } => Ref::Symbolic { path, target, object },
85 InternalRef::Peeled { path, tag, object } => Ref::Peeled { path, tag, object },
86 InternalRef::Direct { path, object } => Ref::Direct { path, object },
87 InternalRef::SymbolicForLookup { .. } => {
88 unreachable!("this case should have been removed during processing")
89 }
90 }
91 }
92}
93
94#[cfg_attr(test, derive(PartialEq, Eq, Debug, Clone))]
95pub(crate) enum InternalRef {
96 /// A ref pointing to a `tag` object, which in turns points to an `object`, usually a commit
97 Peeled {
98 path: BString,
99 tag: git_hash::ObjectId,
100 object: git_hash::ObjectId,
101 },
102 /// A ref pointing to a commit object
103 Direct { path: BString, object: git_hash::ObjectId },
104 /// A symbolic ref pointing to `target` ref, which in turn points to an `object`
105 Symbolic {
106 path: BString,
107 target: BString,
108 object: git_hash::ObjectId,
109 },
110 /// extracted from V1 capabilities, which contain some important symbolic refs along with their targets
111 /// These don't contain the Id
112 SymbolicForLookup { path: BString, target: BString },
113}
114
115impl InternalRef {
116 fn unpack_direct(self) -> Option<(BString, git_hash::ObjectId)> {
117 match self {
118 InternalRef::Direct { path, object } => Some((path, object)),
119 _ => None,
120 }
121 }
122 fn lookup_symbol_has_path(&self, predicate_path: &str) -> bool {
123 matches!(self, InternalRef::SymbolicForLookup { path, .. } if path == predicate_path)
124 }
125}
126
127pub(crate) fn from_capabilities<'a>(
128 out_refs: &mut Vec<InternalRef>,
129 capabilities: impl Iterator<Item = git_transport::client::capabilities::Capability<'a>>,
130) -> Result<(), Error> {
131 let symref_values = capabilities.filter_map(|c| {
132 if c.name() == b"symref".as_bstr() {
133 c.value().map(ToOwned::to_owned)
134 } else {
135 None
136 }
137 });
138 for symref in symref_values {
139 let (left, right) = symref.split_at(
140 symref
141 .find_byte(b':')
142 .ok_or_else(|| Error::MalformedSymref(symref.to_owned()))?,
143 );
144 if left.is_empty() || right.is_empty() {
145 return Err(Error::MalformedSymref(symref.to_owned()));
146 }
147 out_refs.push(InternalRef::SymbolicForLookup {
148 path: left.into(),
149 target: right[1..].into(),
150 })
151 }
152 Ok(())
153}
154
155pub(crate) fn from_v2_refs(out_refs: &mut Vec<Ref>, in_refs: &mut dyn io::BufRead) -> Result<(), Error> {
156 let mut line = String::new();
157 loop {
158 line.clear();
159 let bytes_read = in_refs.read_line(&mut line)?;
160 if bytes_read == 0 {
161 break;
162 }
163 let trimmed = line.trim_end();
164 let mut tokens = trimmed.splitn(3, ' ');
165 match (tokens.next(), tokens.next()) {
166 (Some(hex_hash), Some(path)) => {
167 let id = git_hash::ObjectId::from_hex(hex_hash.as_bytes())?;
168 if path.is_empty() {
169 return Err(Error::MalformedV2RefLine(trimmed.to_owned()));
170 }
171 out_refs.push(if let Some(attribute) = tokens.next() {
172 let mut tokens = attribute.splitn(2, ':');
173 match (tokens.next(), tokens.next()) {
174 (Some(attribute), Some(value)) => {
175 if value.is_empty() {
176 return Err(Error::MalformedV2RefLine(trimmed.to_owned()));
177 }
178 match attribute {
179 "peeled" => Ref::Peeled {
180 path: path.into(),
181 object: git_hash::ObjectId::from_hex(value.as_bytes())?,
182 tag: id,
183 },
184 "symref-target" => Ref::Symbolic {
185 path: path.into(),
186 object: id,
187 target: value.into(),
188 },
189 _ => return Err(Error::UnkownAttribute(attribute.to_owned(), trimmed.to_owned())),
190 }
191 }
192 _ => return Err(Error::MalformedV2RefLine(trimmed.to_owned())),
193 }
194 } else {
195 Ref::Direct {
196 object: id,
197 path: path.into(),
198 }
199 });
200 }
201 _ => return Err(Error::MalformedV2RefLine(trimmed.to_owned())),
202 }
203 }
204 Ok(())
205}
206
207pub(crate) fn from_v1_refs_received_as_part_of_handshake(
208 out_refs: &mut Vec<InternalRef>,
209 in_refs: &mut dyn io::BufRead,
210) -> Result<(), Error> {
211 let number_of_possible_symbolic_refs_for_lookup = out_refs.len();
212 let mut line = String::new();
213 loop {
214 line.clear();
215 let bytes_read = in_refs.read_line(&mut line)?;
216 if bytes_read == 0 {
217 break;
218 }
219 let trimmed = line.trim_end();
220 let (hex_hash, path) = trimmed.split_at(
221 trimmed
222 .find(' ')
223 .ok_or_else(|| Error::MalformedV1RefLine(trimmed.to_owned()))?,
224 );
225 let path = &path[1..];
226 if path.is_empty() {
227 return Err(Error::MalformedV1RefLine(trimmed.to_owned()));
228 }
229 match path.strip_suffix("^{}") {
230 Some(stripped) => {
231 let (previous_path, tag) =
232 out_refs
233 .pop()
234 .and_then(InternalRef::unpack_direct)
235 .ok_or(Error::InvariantViolation(
236 "Expecting peeled refs to be preceded by direct refs",
237 ))?;
238 if previous_path != stripped {
239 return Err(Error::InvariantViolation(
240 "Expecting peeled refs to have the same base path as the previous, unpeeled one",
241 ));
242 }
243 out_refs.push(InternalRef::Peeled {
244 path: previous_path,
245 tag,
246 object: git_hash::ObjectId::from_hex(hex_hash.as_bytes())?,
247 });
248 }
249 None => {
250 let object = git_hash::ObjectId::from_hex(hex_hash.as_bytes())?;
251 match out_refs
252 .iter()
253 .take(number_of_possible_symbolic_refs_for_lookup)
254 .position(|r| r.lookup_symbol_has_path(path))
255 {
256 Some(position) => match out_refs.swap_remove(position) {
257 InternalRef::SymbolicForLookup { path: _, target } => out_refs.push(InternalRef::Symbolic {
258 path: path.into(),
259 object,
260 target,
261 }),
262 _ => unreachable!("Bug in lookup_symbol_has_path - must return lookup symbols"),
263 },
264 None => out_refs.push(InternalRef::Direct {
265 object,
266 path: path.into(),
267 }),
268 };
269 }
270 }
271 }
272 Ok(())
273}
git-protocol/src/fetch/blocking_io/response.rs+280−0
@@ -0,0 +1,280 @@
1use crate::fetch::blocking_io::command::Feature;
2use git_transport::{client, Protocol};
3use quick_error::quick_error;
4use std::io;
5
6quick_error! {
7 /// The error used in the [response module][crate::fetch::response].
8 #[derive(Debug)]
9 #[allow(missing_docs)]
10 pub enum Error {
11 Io(err: io::Error) {
12 display("Failed to read from line reader")
13 from()
14 source(err)
15 }
16 Transport(err: client::Error) {
17 display("An error occurred when decoding a line")
18 from()
19 source(err)
20 }
21 MissingServerCapability(feature: &'static str) {
22 display("Currently we require feature '{}', which is not supported by the server", feature)
23 }
24 UnknownLineType(line: String) {
25 display("Encountered an unknown line prefix in '{}'", line)
26 }
27 UnknownSectionHeader(header: String) {
28 display("Unknown or unsupported header: '{}'", header)
29 }
30 }
31}
32
33/// An 'ACK' line received from the server.
34#[derive(PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone, Copy)]
35#[cfg_attr(feature = "serde1", derive(serde::Serialize, serde::Deserialize))]
36pub enum Acknowledgement {
37 /// The contained `id` is in common.
38 Common(git_hash::ObjectId),
39 /// The server is ready to receive more lines.
40 Ready,
41 /// The server isn't ready yet.
42 Nak,
43}
44
45/// A shallow line received from the server.
46#[derive(PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone, Copy)]
47#[cfg_attr(feature = "serde1", derive(serde::Serialize, serde::Deserialize))]
48pub enum ShallowUpdate {
49 /// Shallow the given `id`.
50 Shallow(git_hash::ObjectId),
51 /// Don't shallow the given `id` anymore.
52 Unshallow(git_hash::ObjectId),
53}
54
55impl ShallowUpdate {
56 /// Parse a `ShallowUpdate` from a `line` as received to the server.
57 fn from_line(line: &str) -> Result<ShallowUpdate, Error> {
58 let mut tokens = line.trim_end().splitn(2, ' ');
59 match (tokens.next(), tokens.next()) {
60 (Some(prefix), Some(id)) => {
61 let id =
62 git_hash::ObjectId::from_hex(id.as_bytes()).map_err(|_| Error::UnknownLineType(line.to_owned()))?;
63 Ok(match prefix {
64 "shallow" => ShallowUpdate::Shallow(id),
65 "unshallow" => ShallowUpdate::Unshallow(id),
66 _ => return Err(Error::UnknownLineType(line.to_owned())),
67 })
68 }
69 _ => unreachable!("cannot have an entirely empty line"),
70 }
71 }
72}
73
74impl Acknowledgement {
75 /// Parse an `Acknowledgement` from a `line` as received to the server.
76 fn from_line(line: &str) -> Result<Acknowledgement, Error> {
77 let mut tokens = line.trim_end().splitn(3, ' ');
78 Ok(match (tokens.next(), tokens.next(), tokens.next()) {
79 (Some(first), id, description) => match first {
80 "ready" => Acknowledgement::Ready, // V2
81 "NAK" => Acknowledgement::Nak, // V1
82 "ACK" => {
83 let id = match id {
84 Some(id) => git_hash::ObjectId::from_hex(id.as_bytes())
85 .map_err(|_| Error::UnknownLineType(line.to_owned()))?,
86 None => return Err(Error::UnknownLineType(line.to_owned())),
87 };
88 if let Some(description) = description {
89 match description {
90 "common" => {}
91 "ready" => return Ok(Acknowledgement::Ready),
92 _ => return Err(Error::UnknownLineType(line.to_owned())),
93 }
94 }
95 Acknowledgement::Common(id)
96 }
97 _ => return Err(Error::UnknownLineType(line.to_owned())),
98 },
99 (None, _, _) => unreachable!("cannot have an entirely empty line"),
100 })
101 }
102 /// Returns the hash of the acknowledged object if this instance acknowledges a common one.
103 pub fn id(&self) -> Option<&git_hash::ObjectId> {
104 match self {
105 Acknowledgement::Common(id) => Some(id),
106 _ => None,
107 }
108 }
109}
110
111/// A representation of a complete fetch response
112pub struct Response {
113 acks: Vec<Acknowledgement>,
114 shallows: Vec<ShallowUpdate>,
115 has_pack: bool,
116}
117
118impl Response {
119 /// Return true if the response has a pack which can be read next.
120 pub fn has_pack(&self) -> bool {
121 self.has_pack
122 }
123
124 /// Return an error if the given `features` don't contain the required ones for the given `version` of the protocol.
125 ///
126 /// Even though technically any set of features supported by the server could work, we only implement the ones that
127 /// make it easy to maintain all versions with a single code base that aims to be and remain maintainable.
128 pub fn check_required_features(version: Protocol, features: &[Feature]) -> Result<(), Error> {
129 match version {
130 Protocol::V1 => {
131 let has = |name: &str| features.iter().any(|f| f.0 == name);
132 // Let's focus on V2 standards, and simply not support old servers to keep our code simpler
133 if !has("multi_ack_detailed") {
134 return Err(Error::MissingServerCapability("multi_ack_detailed"));
135 }
136 // It's easy to NOT do sideband for us, but then again, everyone supports it.
137 // CORRECTION: If side-band is off, it would send the packfile without packet line encoding,
138 // which is nothing we ever want to deal with (despite it being more efficient). In V2, this
139 // is not even an option anymore, sidebands are always present.
140 if !has("side-band") && !has("side-band-64k") {
141 return Err(Error::MissingServerCapability("side-band OR side-band-64k"));
142 }
143 }
144 Protocol::V2 => {}
145 }
146 Ok(())
147 }
148 /// Parse a response of the given `version` of the protocol from `reader`.
149 pub fn from_line_reader(version: Protocol, reader: &mut impl client::ExtendedBufRead) -> Result<Response, Error> {
150 match version {
151 Protocol::V1 => {
152 let mut line = String::new();
153 let mut acks = Vec::<Acknowledgement>::new();
154 let mut shallows = Vec::<ShallowUpdate>::new();
155 let has_pack = 'lines: loop {
156 line.clear();
157 let peeked_line = match reader.peek_data_line() {
158 Some(Ok(Ok(line))) => String::from_utf8_lossy(line),
159 // This special case (hang/block forver) deals with a single NAK being a legitimate EOF sometimes
160 // Note that this might block forever in stateful connections as there it's not really clear
161 // if something will be following or not by just looking at the response. Instead you have to know
162 // the arguments sent to the server and count response lines based on intricate knowledge on how the
163 // server works.
164 // For now this is acceptable, as V2 can be used as a workaround, which also is the default.
165 Some(Err(err)) if err.kind() == io::ErrorKind::UnexpectedEof => break 'lines false,
166 Some(Err(err)) => return Err(err.into()),
167 Some(Ok(Err(err))) => return Err(err.into()),
168 None => {
169 // maybe we saw a shallow flush packet, let's reset and retry
170 reader.read_line(&mut line)?;
171 reader.reset(Protocol::V1);
172 match reader.peek_data_line() {
173 Some(Ok(Ok(line))) => String::from_utf8_lossy(line),
174 Some(Err(err)) => return Err(err.into()),
175 Some(Ok(Err(err))) => return Err(err.into()),
176 None => break 'lines false, // EOF
177 }
178 }
179 };
180
181 // with a friendly server, we just assume that a non-ack line is a pack line
182 // which is our hint to stop here.
183 match Acknowledgement::from_line(&peeked_line) {
184 Ok(ack) => match ack.id() {
185 Some(id) => {
186 if !acks.iter().any(|a| a.id() == Some(id)) {
187 acks.push(ack);
188 }
189 }
190 None => acks.push(ack),
191 },
192 Err(_) => match ShallowUpdate::from_line(&peeked_line) {
193 Ok(shallow) => {
194 shallows.push(shallow);
195 }
196 Err(_) => break 'lines true,
197 },
198 };
199 assert_ne!(reader.read_line(&mut line)?, 0, "consuming a peeked line works");
200 };
201 Ok(Response {
202 acks,
203 shallows,
204 has_pack,
205 })
206 }
207 Protocol::V2 => {
208 // NOTE: We only read acknowledgements and scrub to the pack file, until we have use for the other features
209 let mut line = String::new();
210 reader.reset(Protocol::V2);
211 let mut acks = Vec::<Acknowledgement>::new();
212 let mut shallows = Vec::<ShallowUpdate>::new();
213 let has_pack = 'section: loop {
214 line.clear();
215 if reader.read_line(&mut line)? == 0 {
216 return Err(Error::Io(io::Error::new(
217 io::ErrorKind::UnexpectedEof,
218 "Could not read message headline",
219 )));
220 };
221
222 match line.trim_end() {
223 "acknowledgments" => {
224 if parse_section(&mut line, reader, &mut acks, Acknowledgement::from_line)? {
225 break 'section false;
226 }
227 }
228 "shallow-info" => {
229 if parse_section(&mut line, reader, &mut shallows, ShallowUpdate::from_line)? {
230 break 'section false;
231 }
232 }
233 "packfile" => {
234 // what follows is the packfile itself, which can be read with a sideband enabled reader
235 break 'section true;
236 }
237 _ => return Err(Error::UnknownSectionHeader(line)),
238 }
239 };
240 Ok(Response {
241 acks,
242 shallows,
243 has_pack,
244 })
245 }
246 }
247 }
248
249 /// Return all acknowledgements [parsed previously][Response::from_line_reader()].
250 pub fn acknowledgements(&self) -> &[Acknowledgement] {
251 &self.acks
252 }
253
254 /// Return all shallow update lines [parsed previously][Response::from_line_reader()].
255 pub fn shallow_updates(&self) -> &[ShallowUpdate] {
256 &self.shallows
257 }
258}
259
260fn parse_section<T>(
261 line: &mut String,
262 reader: &mut impl client::ExtendedBufRead,
263 res: &mut Vec<T>,
264 parse: impl Fn(&str) -> Result<T, Error>,
265) -> Result<bool, Error> {
266 line.clear();
267 while reader.read_line(line)? != 0 {
268 res.push(parse(line)?);
269 line.clear();
270 }
271 // End of message, or end of section?
272 Ok(if reader.stopped_at() == Some(client::MessageKind::Delimiter) {
273 // try reading more sections
274 reader.reset(Protocol::V2);
275 false
276 } else {
277 // we are done, there is no pack
278 true
279 })
280}
git-protocol/src/fetch/blocking_io/tests/arguments.rs+236−0
@@ -0,0 +1,236 @@
1use crate::fetch;
2use bstr::ByteSlice;
3use git_transport::{
4 client,
5 client::{Error, Identity, MessageKind, RequestWriter, SetServiceResponse, WriteMode},
6 Protocol, Service,
7};
8use std::io;
9
10fn arguments_v1(features: impl IntoIterator<Item = &'static str>) -> fetch::Arguments {
11 fetch::Arguments::new(Protocol::V1, features.into_iter().map(|n| (n, None)).collect())
12}
13
14fn arguments_v2(features: impl IntoIterator<Item = &'static str>) -> fetch::Arguments {
15 fetch::Arguments::new(Protocol::V2, features.into_iter().map(|n| (n, None)).collect())
16}
17
18struct Transport<T: client::Transport> {
19 inner: T,
20 stateful: bool,
21}
22
23impl<T: client::Transport> client::Transport for Transport<T> {
24 fn handshake(&mut self, service: Service) -> Result<SetServiceResponse<'_>, Error> {
25 self.inner.handshake(service)
26 }
27
28 fn set_identity(&mut self, identity: Identity) -> Result<(), Error> {
29 self.inner.set_identity(identity)
30 }
31
32 fn request(&mut self, write_mode: WriteMode, on_into_read: MessageKind) -> Result<RequestWriter<'_>, Error> {
33 self.inner.request(write_mode, on_into_read)
34 }
35
36 fn close(&mut self) -> Result<(), Error> {
37 self.inner.close()
38 }
39
40 fn to_url(&self) -> String {
41 self.inner.to_url()
42 }
43
44 fn desired_protocol_version(&self) -> Protocol {
45 self.inner.desired_protocol_version()
46 }
47
48 fn is_stateful(&self) -> bool {
49 self.stateful
50 }
51}
52
53fn transport(
54 out: &mut Vec<u8>,
55 stateful: bool,
56) -> Transport<git_transport::client::git::Connection<io::Cursor<Vec<u8>>, &mut Vec<u8>>> {
57 Transport {
58 inner: git_transport::client::git::Connection::new(
59 io::Cursor::new(Vec::new()),
60 out,
61 Protocol::V1, // does not matter
62 b"does/not/matter".as_bstr().to_owned(),
63 None::<(&str, _)>,
64 git_transport::client::git::ConnectMode::Process, // avoid header to be sent
65 ),
66 stateful,
67 }
68}
69
70fn id(hex: &str) -> git_hash::ObjectId {
71 git_hash::ObjectId::from_hex(hex.as_bytes()).expect("expect valid hex id")
72}
73
74mod v1 {
75 use crate::fetch::blocking_io::tests::arguments::{arguments_v1, id, transport};
76 use bstr::ByteSlice;
77
78 #[test]
79 fn haves_and_wants_for_clone() {
80 let mut out = Vec::new();
81 let mut t = transport(&mut out, true);
82 let mut arguments = arguments_v1(["feature-a", "feature-b"].iter().cloned());
83
84 arguments.want(id("7b333369de1221f9bfbbe03a3a13e9a09bc1c907"));
85 arguments.want(id("ff333369de1221f9bfbbe03a3a13e9a09bc1ffff"));
86 arguments.send(&mut t, true).expect("sending to buffer to work");
87 assert_eq!(
88 out.as_bstr(),
89 b"0046want 7b333369de1221f9bfbbe03a3a13e9a09bc1c907 feature-a feature-b
900032want ff333369de1221f9bfbbe03a3a13e9a09bc1ffff
9100000009done
92"
93 .as_bstr()
94 );
95 }
96
97 #[test]
98 fn haves_and_wants_for_fetch_stateless() {
99 let mut out = Vec::new();
100 let mut t = transport(&mut out, false);
101 let mut arguments = arguments_v1(["feature-a", "shallow", "deepen-since", "deepen-not"].iter().copied());
102
103 arguments.deepen(1);
104 arguments.shallow(id("7b333369de1221f9bfbbe03a3a13e9a09bc1c9ff"));
105 arguments.want(id("7b333369de1221f9bfbbe03a3a13e9a09bc1c907"));
106 arguments.deepen_since(12345);
107 arguments.deepen_not("refs/heads/main".into());
108 arguments.have(id("0000000000000000000000000000000000000000"));
109 arguments.send(&mut t, false).expect("sending to buffer to work");
110
111 arguments.have(id("1111111111111111111111111111111111111111"));
112 arguments.send(&mut t, true).expect("sending to buffer to work");
113 assert_eq!(
114 out.as_bstr(),
115 b"005cwant 7b333369de1221f9bfbbe03a3a13e9a09bc1c907 feature-a shallow deepen-since deepen-not
1160035shallow 7b333369de1221f9bfbbe03a3a13e9a09bc1c9ff
117000ddeepen 1
1180017deepen-since 12345
119001fdeepen-not refs/heads/main
12000000032have 0000000000000000000000000000000000000000
1210000005cwant 7b333369de1221f9bfbbe03a3a13e9a09bc1c907 feature-a shallow deepen-since deepen-not
1220035shallow 7b333369de1221f9bfbbe03a3a13e9a09bc1c9ff
123000ddeepen 1
1240017deepen-since 12345
125001fdeepen-not refs/heads/main
12600000032have 1111111111111111111111111111111111111111
1270009done
128"
129 .as_bstr()
130 );
131 }
132
133 #[test]
134 fn haves_and_wants_for_fetch_stateful() {
135 let mut out = Vec::new();
136 let mut t = transport(&mut out, true);
137 let mut arguments = arguments_v1(["feature-a", "shallow"].iter().copied());
138
139 arguments.deepen(1);
140 arguments.want(id("7b333369de1221f9bfbbe03a3a13e9a09bc1c907"));
141 arguments.have(id("0000000000000000000000000000000000000000"));
142 arguments.send(&mut t, false).expect("sending to buffer to work");
143
144 arguments.have(id("1111111111111111111111111111111111111111"));
145 arguments.send(&mut t, true).expect("sending to buffer to work");
146 assert_eq!(
147 out.as_bstr(),
148 b"0044want 7b333369de1221f9bfbbe03a3a13e9a09bc1c907 feature-a shallow
149000ddeepen 1
15000000032have 0000000000000000000000000000000000000000
15100000032have 1111111111111111111111111111111111111111
1520009done
153"
154 .as_bstr()
155 );
156 }
157}
158
159mod v2 {
160 use crate::fetch::blocking_io::tests::arguments::{arguments_v2, id, transport};
161 use bstr::ByteSlice;
162
163 #[test]
164 fn haves_and_wants_for_clone_stateful() {
165 let mut out = Vec::new();
166 let mut t = transport(&mut out, true);
167 let mut arguments = arguments_v2(["feature-a", "shallow"].iter().copied());
168
169 arguments.deepen(1);
170 arguments.deepen_relative();
171 arguments.want(id("7b333369de1221f9bfbbe03a3a13e9a09bc1c907"));
172 arguments.want(id("ff333369de1221f9bfbbe03a3a13e9a09bc1ffff"));
173 arguments.send(&mut t, true).expect("sending to buffer to work");
174 assert_eq!(
175 out.as_bstr(),
176 b"0012command=fetch
1770001000ethin-pack
1780010include-tag
179000eofs-delta
180000ddeepen 1
1810014deepen-relative
1820032want 7b333369de1221f9bfbbe03a3a13e9a09bc1c907
1830032want ff333369de1221f9bfbbe03a3a13e9a09bc1ffff
1840009done
1850000"
186 .as_bstr(), "we filter features/capabilities without value as these apparently sholdn't be listed (remote dies otherwise)"
187 );
188 }
189
190 #[test]
191 fn haves_and_wants_for_fetch_stateless_and_stateful() {
192 for is_stateful in &[false, true] {
193 let mut out = Vec::new();
194 let mut t = transport(&mut out, *is_stateful);
195 let mut arguments = arguments_v2(Some("shallow"));
196
197 arguments.deepen(1);
198 arguments.deepen_since(12345);
199 arguments.shallow(id("7b333369de1221f9bfbbe03a3a13e9a09bc1c9ff"));
200 arguments.want(id("7b333369de1221f9bfbbe03a3a13e9a09bc1c907"));
201 arguments.deepen_not("refs/heads/main".into());
202 arguments.have(id("0000000000000000000000000000000000000000"));
203 arguments.send(&mut t, false).expect("sending to buffer to work");
204
205 arguments.have(id("1111111111111111111111111111111111111111"));
206 arguments.send(&mut t, true).expect("sending to buffer to work");
207 assert_eq!(
208 out.as_bstr(),
209 b"0012command=fetch
2100001000ethin-pack
2110010include-tag
212000eofs-delta
213000ddeepen 1
2140017deepen-since 12345
2150035shallow 7b333369de1221f9bfbbe03a3a13e9a09bc1c9ff
2160032want 7b333369de1221f9bfbbe03a3a13e9a09bc1c907
217001fdeepen-not refs/heads/main
2180032have 0000000000000000000000000000000000000000
21900000012command=fetch
2200001000ethin-pack
2210010include-tag
222000eofs-delta
223000ddeepen 1
2240017deepen-since 12345
2250035shallow 7b333369de1221f9bfbbe03a3a13e9a09bc1c9ff
2260032want 7b333369de1221f9bfbbe03a3a13e9a09bc1c907
227001fdeepen-not refs/heads/main
2280032have 1111111111111111111111111111111111111111
2290009done
2300000"
231 .as_bstr(),
232 "V2 is stateless by default, so it repeats all but 'haves' in each request"
233 );
234 }
235 }
236}
git-protocol/src/fetch/blocking_io/tests/command.rs+165−0
@@ -0,0 +1,165 @@
1mod v1 {
2 fn capabilities(input: &str) -> git_transport::client::Capabilities {
3 git_transport::client::Capabilities::from_bytes(format!("\0{}", input).as_bytes())
4 .expect("valid input capabilities")
5 .0
6 }
7
8 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";
9 mod fetch {
10 mod default_features {
11 use crate::fetch::blocking_io::tests::command::v1::{capabilities, GITHUB_CAPABILITIES};
12 use crate::fetch::{self, Command};
13
14 #[test]
15 fn it_chooses_the_best_multi_ack_and_sideband() {
16 assert_eq!(
17 Command::Fetch.default_features(
18 git_transport::Protocol::V1,
19 &capabilities("multi_ack side-band side-band-64k multi_ack_detailed")
20 ),
21 &[("side-band-64k", None), ("multi_ack_detailed", None), fetch::agent()]
22 );
23 }
24
25 #[test]
26 fn it_chooses_all_supported_non_stacking_capabilities_and_leaves_no_progress() {
27 assert_eq!(
28 Command::Fetch.default_features(git_transport::Protocol::V1, &capabilities(GITHUB_CAPABILITIES)),
29 &[
30 ("multi_ack", None),
31 ("thin-pack", None),
32 ("side-band", None),
33 ("ofs-delta", None),
34 ("shallow", None),
35 ("deepen-since", None),
36 ("deepen-not", None),
37 ("deepen-relative", None),
38 ("allow-tip-sha1-in-want", None),
39 ("allow-reachable-sha1-in-want", None),
40 ("no-done", None),
41 ("filter", None),
42 fetch::agent()
43 ],
44 "we don't enforce include-tag or no-progress"
45 );
46 }
47 }
48 }
49}
50
51mod v2 {
52 use git_transport::client::Capabilities;
53
54 fn capabilities(command: &str, input: &str) -> Capabilities {
55 Capabilities::from_lines(Some(Ok("version 2".into())), format!("{}={}", command, input))
56 .expect("valid input for V2 capabilities")
57 }
58
59 mod fetch {
60 mod default_features {
61 use crate::fetch::blocking_io::tests::command::v2::capabilities;
62 use crate::fetch::{self, Command};
63
64 #[test]
65 fn all_features() {
66 assert_eq!(
67 Command::Fetch.default_features(
68 git_transport::Protocol::V2,
69 &capabilities("fetch", "shallow filter ref-in-want sideband-all packfile-uris")
70 ),
71 ["shallow", "filter", "ref-in-want", "sideband-all", "packfile-uris"]
72 .iter()
73 .map(|s| (*s, None))
74 .chain(Some(fetch::agent()))
75 .collect::<Vec<_>>()
76 )
77 }
78 }
79
80 mod initial_arguments {
81 use bstr::ByteSlice;
82
83 use crate::fetch::blocking_io::tests::command::v2::capabilities;
84 use crate::fetch::Command;
85
86 #[test]
87 fn for_all_features() {
88 assert_eq!(
89 Command::Fetch.initial_arguments(&Command::Fetch.default_features(
90 git_transport::Protocol::V2,
91 &capabilities("fetch", "shallow filter ref-in-want sideband-all packfile-uris")
92 )),
93 [
94 "thin-pack",
95 "include-tag",
96 "ofs-delta",
97 "sideband-all",
98 "ref-in-want",
99 "packfile-uris"
100 ]
101 .iter()
102 .map(|s| s.as_bytes().as_bstr().to_owned())
103 .collect::<Vec<_>>()
104 )
105 }
106 }
107 }
108
109 mod ls_refs {
110 mod default_features {
111 use crate::fetch::blocking_io::tests::command::v2::capabilities;
112 use crate::fetch::{self, Command};
113
114 #[test]
115 fn default_as_there_are_no_features() {
116 assert_eq!(
117 Command::LsRefs.default_features(
118 git_transport::Protocol::V2,
119 &capabilities("something-else", "does not matter as there are none")
120 ),
121 &[fetch::agent()]
122 );
123 }
124 }
125
126 mod validate {
127 use bstr::ByteSlice;
128
129 use crate::fetch::blocking_io::tests::command::v2::capabilities;
130 use crate::fetch::Command;
131
132 #[test]
133 fn ref_prefixes_can_always_be_used() {
134 Command::LsRefs.validate_argument_prefixes_or_panic(
135 git_transport::Protocol::V2,
136 &capabilities("something else", "do-not-matter"),
137 &[b"ref-prefix hello/".as_bstr().into()],
138 &[],
139 );
140 }
141
142 #[test]
143 #[should_panic]
144 fn unknown_argument() {
145 Command::LsRefs.validate_argument_prefixes_or_panic(
146 git_transport::Protocol::V2,
147 &capabilities("other", "do-not-matter"),
148 &[b"definitely-nothing-we-know".as_bstr().into()],
149 &[],
150 );
151 }
152
153 #[test]
154 #[should_panic]
155 fn unknown_feature() {
156 Command::LsRefs.validate_argument_prefixes_or_panic(
157 git_transport::Protocol::V2,
158 &capabilities("other", "do-not-matter"),
159 &[],
160 &[("some-feature-that-does-not-exist", None)],
161 );
162 }
163 }
164 }
165}
git-protocol/src/fetch/blocking_io/tests/mod.rs+3−0
@@ -0,0 +1,3 @@
1mod arguments;
2mod command;
3mod refs;
git-protocol/src/fetch/blocking_io/tests/refs.rs+108−0
@@ -0,0 +1,108 @@
1use std::io;
2
3use git_transport::client;
4
5use crate::fetch::{blocking_io::refs::InternalRef, refs, Ref};
6
7fn oid(hex_sha: &str) -> git_hash::ObjectId {
8 git_hash::ObjectId::from_hex(hex_sha.as_bytes()).expect("valid input")
9}
10
11#[test]
12fn extract_references_from_v2_refs() {
13 let input: &mut dyn io::BufRead = &mut "808e50d724f604f69ab93c6da2919c014667bedb HEAD symref-target:refs/heads/main
14808e50d724f604f69ab93c6da2919c014667bedb refs/heads/main
157fe1b98b39423b71e14217aa299a03b7c937d656 refs/tags/foo peeled:808e50d724f604f69ab93c6da2919c014667bedb
167fe1b98b39423b71e14217aa299a03b7c937d6ff refs/tags/blaz
17"
18 .as_bytes();
19 let mut out = Vec::new();
20 refs::from_v2_refs(&mut out, input).expect("no failure on valid input");
21
22 assert_eq!(
23 out,
24 vec![
25 Ref::Symbolic {
26 path: "HEAD".into(),
27 target: "refs/heads/main".into(),
28 object: oid("808e50d724f604f69ab93c6da2919c014667bedb")
29 },
30 Ref::Direct {
31 path: "refs/heads/main".into(),
32 object: oid("808e50d724f604f69ab93c6da2919c014667bedb")
33 },
34 Ref::Peeled {
35 path: "refs/tags/foo".into(),
36 tag: oid("7fe1b98b39423b71e14217aa299a03b7c937d656"),
37 object: oid("808e50d724f604f69ab93c6da2919c014667bedb")
38 },
39 Ref::Direct {
40 path: "refs/tags/blaz".into(),
41 object: oid("7fe1b98b39423b71e14217aa299a03b7c937d6ff")
42 },
43 ]
44 )
45}
46
47#[test]
48fn extract_references_from_v1_refs() {
49 let input: &mut dyn io::BufRead = &mut "73a6868963993a3328e7d8fe94e5a6ac5078a944 HEAD
5073a6868963993a3328e7d8fe94e5a6ac5078a944 refs/heads/main
518e472f9ccc7d745927426cbb2d9d077de545aa4e refs/pull/13/head
52dce0ea858eef7ff61ad345cc5cdac62203fb3c10 refs/tags/git-commitgraph-v0.0.0
5321c9b7500cb144b3169a6537961ec2b9e865be81 refs/tags/git-commitgraph-v0.0.0^{}"
54 .as_bytes();
55 let mut out = vec![InternalRef::SymbolicForLookup {
56 path: "HEAD".into(),
57 target: "refs/heads/main".into(),
58 }];
59 refs::from_v1_refs_received_as_part_of_handshake(&mut out, input).expect("no failure from valid input");
60 assert_eq!(
61 out,
62 vec![
63 InternalRef::Symbolic {
64 path: "HEAD".into(),
65 target: "refs/heads/main".into(),
66 object: oid("73a6868963993a3328e7d8fe94e5a6ac5078a944")
67 },
68 InternalRef::Direct {
69 path: "refs/heads/main".into(),
70 object: oid("73a6868963993a3328e7d8fe94e5a6ac5078a944")
71 },
72 InternalRef::Direct {
73 path: "refs/pull/13/head".into(),
74 object: oid("8e472f9ccc7d745927426cbb2d9d077de545aa4e")
75 },
76 InternalRef::Peeled {
77 path: "refs/tags/git-commitgraph-v0.0.0".into(),
78 tag: oid("dce0ea858eef7ff61ad345cc5cdac62203fb3c10"),
79 object: oid("21c9b7500cb144b3169a6537961ec2b9e865be81")
80 }
81 ]
82 )
83}
84
85#[test]
86fn extract_symbolic_references_from_capabilities() -> Result<(), client::Error> {
87 let caps = client::Capabilities::from_bytes(
88 b"\0unrelated symref=HEAD:refs/heads/main symref=ANOTHER:refs/heads/foo agent=git/2.28.0",
89 )?
90 .0;
91 let mut out = Vec::new();
92 refs::from_capabilities(&mut out, caps.iter()).expect("a working example");
93
94 assert_eq!(
95 out,
96 vec![
97 InternalRef::SymbolicForLookup {
98 path: "HEAD".into(),
99 target: "refs/heads/main".into()
100 },
101 InternalRef::SymbolicForLookup {
102 path: "ANOTHER".into(),
103 target: "refs/heads/foo".into()
104 }
105 ]
106 );
107 Ok(())
108}