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 @@
1+use std::{fmt, io::Write};
2+
3+use bstr::{BStr, BString, ByteSlice};
4+
5+use git_transport::{
6+ client::{self, TransportV2Ext},
7+ Protocol,
8+};
9+
10+use crate::fetch::blocking_io::command::Feature;
11+use crate::fetch::Command;
12+
13+/// The arguments passed to a server command.
14+pub 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+
31+impl 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 @@
1+use crate::fetch::agent;
2+use bstr::{BString, ByteSlice};
3+use 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)]
7+pub 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.
15+pub type Feature = (&'static str, Option<&'static str>);
16+
17+impl 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 @@
1+use crate::fetch::{Arguments, Ref, Response};
2+use bstr::BString;
3+use git_features::progress::Progress;
4+use git_transport::client::Capabilities;
5+use std::io;
6+
7+/// Defines what to do next after certain [`Delegate`] operations.
8+#[derive(PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone, Copy)]
9+pub 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.
23+pub 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+///
2+pub mod arguments;
3+///
4+pub mod command;
5+///
6+pub mod delegate;
7+///
8+pub mod refs;
9+///
10+pub mod response;
11+#[cfg(test)]
12+mod tests;
13+
14+mod 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+}
58+pub use error::Error;
59+
60+mod 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+}
244+pub use fetch::fetch;
git-protocol/src/fetch/blocking_io/refs.rs+273−0
@@ -0,0 +1,273 @@
1+use bstr::{BString, ByteSlice};
2+use quick_error::quick_error;
3+use std::io;
4+
5+quick_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))]
41+pub 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+
69+impl 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+
81+impl 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))]
95+pub(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+
115+impl 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+
127+pub(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+
155+pub(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+
207+pub(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 @@
1+use crate::fetch::blocking_io::command::Feature;
2+use git_transport::{client, Protocol};
3+use quick_error::quick_error;
4+use std::io;
5+
6+quick_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))]
36+pub 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))]
48+pub 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+
55+impl 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+
74+impl 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
112+pub struct Response {
113+ acks: Vec<Acknowledgement>,
114+ shallows: Vec<ShallowUpdate>,
115+ has_pack: bool,
116+}
117+
118+impl 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+
260+fn 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 @@
1+use crate::fetch;
2+use bstr::ByteSlice;
3+use git_transport::{
4+ client,
5+ client::{Error, Identity, MessageKind, RequestWriter, SetServiceResponse, WriteMode},
6+ Protocol, Service,
7+};
8+use std::io;
9+
10+fn 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+
14+fn 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+
18+struct Transport<T: client::Transport> {
19+ inner: T,
20+ stateful: bool,
21+}
22+
23+impl<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+
53+fn 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+
70+fn id(hex: &str) -> git_hash::ObjectId {
71+ git_hash::ObjectId::from_hex(hex.as_bytes()).expect("expect valid hex id")
72+}
73+
74+mod 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
90+0032want ff333369de1221f9bfbbe03a3a13e9a09bc1ffff
91+00000009done
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
116+0035shallow 7b333369de1221f9bfbbe03a3a13e9a09bc1c9ff
117+000ddeepen 1
118+0017deepen-since 12345
119+001fdeepen-not refs/heads/main
120+00000032have 0000000000000000000000000000000000000000
121+0000005cwant 7b333369de1221f9bfbbe03a3a13e9a09bc1c907 feature-a shallow deepen-since deepen-not
122+0035shallow 7b333369de1221f9bfbbe03a3a13e9a09bc1c9ff
123+000ddeepen 1
124+0017deepen-since 12345
125+001fdeepen-not refs/heads/main
126+00000032have 1111111111111111111111111111111111111111
127+0009done
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
149+000ddeepen 1
150+00000032have 0000000000000000000000000000000000000000
151+00000032have 1111111111111111111111111111111111111111
152+0009done
153+"
154+ .as_bstr()
155+ );
156+ }
157+}
158+
159+mod 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
177+0001000ethin-pack
178+0010include-tag
179+000eofs-delta
180+000ddeepen 1
181+0014deepen-relative
182+0032want 7b333369de1221f9bfbbe03a3a13e9a09bc1c907
183+0032want ff333369de1221f9bfbbe03a3a13e9a09bc1ffff
184+0009done
185+0000"
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
210+0001000ethin-pack
211+0010include-tag
212+000eofs-delta
213+000ddeepen 1
214+0017deepen-since 12345
215+0035shallow 7b333369de1221f9bfbbe03a3a13e9a09bc1c9ff
216+0032want 7b333369de1221f9bfbbe03a3a13e9a09bc1c907
217+001fdeepen-not refs/heads/main
218+0032have 0000000000000000000000000000000000000000
219+00000012command=fetch
220+0001000ethin-pack
221+0010include-tag
222+000eofs-delta
223+000ddeepen 1
224+0017deepen-since 12345
225+0035shallow 7b333369de1221f9bfbbe03a3a13e9a09bc1c9ff
226+0032want 7b333369de1221f9bfbbe03a3a13e9a09bc1c907
227+001fdeepen-not refs/heads/main
228+0032have 1111111111111111111111111111111111111111
229+0009done
230+0000"
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 @@
1+mod 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+
51+mod 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 @@
1+mod arguments;
2+mod command;
3+mod refs;
git-protocol/src/fetch/blocking_io/tests/refs.rs+108−0
@@ -0,0 +1,108 @@
1+use std::io;
2+
3+use git_transport::client;
4+
5+use crate::fetch::{blocking_io::refs::InternalRef, refs, Ref};
6+
7+fn oid(hex_sha: &str) -> git_hash::ObjectId {
8+ git_hash::ObjectId::from_hex(hex_sha.as_bytes()).expect("valid input")
9+}
10+
11+#[test]
12+fn extract_references_from_v2_refs() {
13+ let input: &mut dyn io::BufRead = &mut "808e50d724f604f69ab93c6da2919c014667bedb HEAD symref-target:refs/heads/main
14+808e50d724f604f69ab93c6da2919c014667bedb refs/heads/main
15+7fe1b98b39423b71e14217aa299a03b7c937d656 refs/tags/foo peeled:808e50d724f604f69ab93c6da2919c014667bedb
16+7fe1b98b39423b71e14217aa299a03b7c937d6ff 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]
48+fn extract_references_from_v1_refs() {
49+ let input: &mut dyn io::BufRead = &mut "73a6868963993a3328e7d8fe94e5a6ac5078a944 HEAD
50+73a6868963993a3328e7d8fe94e5a6ac5078a944 refs/heads/main
51+8e472f9ccc7d745927426cbb2d9d077de545aa4e refs/pull/13/head
52+dce0ea858eef7ff61ad345cc5cdac62203fb3c10 refs/tags/git-commitgraph-v0.0.0
53+21c9b7500cb144b3169a6537961ec2b9e865be81 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]
86+fn 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+}