…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 | +} | |