…into async-io, as usual.
Showing the changes to git-protocol/src/fetch/blocking_io (10 of 12 files). Show all
Unified · Split
| @@ -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 | } | ||
| @@ -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 | } | ||
| @@ -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 | } | ||
| @@ -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; | ||
| @@ -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 | } | ||
| @@ -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 | } | ||
| @@ -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 | } | ||
| @@ -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 | } | ||
| @@ -0,0 +1,3 @@ | |||
| 1 | mod arguments; | ||
| 2 | mod command; | ||
| 3 | mod refs; | ||
| @@ -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 | } | ||