…into async-io, as usual.
Showing the changes to git-protocol/src/fetch/mod.rs (1 of 12 files). Show all
Unified · Split
| @@ -1,237 +1,18 @@ | |||
| 1 | use crate::credentials; | 1 | #[cfg(feature = "blocking-client")] |
| 2 | use git_features::{progress, progress::Progress}; | 2 | mod blocking_io; |
| 3 | use git_transport::{ | 3 | #[cfg(feature = "blocking-client")] |
| 4 | client::{self, SetServiceResponse, TransportV2Ext}, | 4 | pub use blocking_io::{ |
| 5 | Service, | 5 | arguments::Arguments, |
| 6 | command::Command, | ||
| 7 | delegate::{Action, Delegate}, | ||
| 8 | fetch, refs, | ||
| 9 | refs::Ref, | ||
| 10 | response, | ||
| 11 | response::Response, | ||
| 12 | Error, | ||
| 6 | }; | 13 | }; |
| 7 | use quick_error::quick_error; | ||
| 8 | use std::io; | ||
| 9 | |||
| 10 | mod refs; | ||
| 11 | use refs::InternalRef; | ||
| 12 | pub use refs::Ref; | ||
| 13 | |||
| 14 | mod command; | ||
| 15 | pub use command::Command; | ||
| 16 | |||
| 17 | mod arguments; | ||
| 18 | pub use arguments::Arguments; | ||
| 19 | |||
| 20 | /// | ||
| 21 | pub mod response; | ||
| 22 | pub use response::Response; | ||
| 23 | |||
| 24 | mod delegate; | ||
| 25 | pub use delegate::{Action, Delegate}; | ||
| 26 | |||
| 27 | #[cfg(test)] | ||
| 28 | mod tests; | ||
| 29 | |||
| 30 | quick_error! { | ||
| 31 | /// The error used in [`fetch()`]. | ||
| 32 | #[derive(Debug)] | ||
| 33 | #[allow(missing_docs)] | ||
| 34 | pub enum Error { | ||
| 35 | PackIo(err: io::Error) { | ||
| 36 | display("Could not read streaming pack file") | ||
| 37 | from() | ||
| 38 | source(err) | ||
| 39 | } | ||
| 40 | Credentials(err: credentials::Error) { | ||
| 41 | display("Failed to obtain, approve or reject credentials") | ||
| 42 | from() | ||
| 43 | source(err) | ||
| 44 | } | ||
| 45 | Transport(err: client::Error) { | ||
| 46 | display("An error occurred on the transport layer while fetching data") | ||
| 47 | from() | ||
| 48 | source(err) | ||
| 49 | } | ||
| 50 | SymrefWithoutValue { | ||
| 51 | display("A symref 'capability' is expected to have a value") | ||
| 52 | } | ||
| 53 | Ref(err: refs::Error) { | ||
| 54 | display("A reference could not be parsed or invariants were not met") | ||
| 55 | from() | ||
| 56 | source(err) | ||
| 57 | } | ||
| 58 | Response(err: response::Error) { | ||
| 59 | display("The server response could not be parsed") | ||
| 60 | from() | ||
| 61 | source(err) | ||
| 62 | } | ||
| 63 | } | ||
| 64 | } | ||
| 65 | 14 | ||
| 66 | /// Returns the name of the agent as key-value pair, commonly used in HTTP headers. | 15 | /// Returns the name of the agent as key-value pair, commonly used in HTTP headers. |
| 67 | pub fn agent() -> (&'static str, Option<&'static str>) { | 16 | pub fn agent() -> (&'static str, Option<&'static str>) { |
| 68 | ("agent", Some(concat!("git/oxide-", env!("CARGO_PKG_VERSION")))) | 17 | ("agent", Some(concat!("git/oxide-", env!("CARGO_PKG_VERSION")))) |
| 69 | } | 18 | } |
| 70 | |||
| 71 | /// Perform a 'fetch' operation with the server using `transport`, with `delegate` handling all server interactions. | ||
| 72 | /// | ||
| 73 | /// * `authenticate(operation_to_perform)` is used to receive credentials for the connection and potentially store it | ||
| 74 | /// if the server indicates 'permission denied'. Note that not all transport support authentication or authorization. | ||
| 75 | /// * `progress` is used to emit progress messages. | ||
| 76 | /// | ||
| 77 | /// _Note_ that depending on the `delegate`, the actual action performed can be `ls-refs`, `clone` or `fetch`. | ||
| 78 | pub fn fetch<F>( | ||
| 79 | mut transport: impl client::Transport, | ||
| 80 | delegate: &mut impl Delegate, | ||
| 81 | mut authenticate: F, | ||
| 82 | mut progress: impl Progress, | ||
| 83 | ) -> Result<(), Error> | ||
| 84 | where | ||
| 85 | F: FnMut(credentials::Action<'_>) -> credentials::Result, | ||
| 86 | { | ||
| 87 | let (protocol_version, mut parsed_refs, capabilities, call_ls_refs) = { | ||
| 88 | progress.init(None, progress::steps()); | ||
| 89 | progress.set_name("handshake"); | ||
| 90 | progress.step(); | ||
| 91 | let result = transport.handshake(Service::UploadPack); | ||
| 92 | let SetServiceResponse { | ||
| 93 | actual_protocol, | ||
| 94 | capabilities, | ||
| 95 | refs, | ||
| 96 | } = match result { | ||
| 97 | Ok(v) => Ok(v), | ||
| 98 | Err(client::Error::Io { ref err }) if err.kind() == io::ErrorKind::PermissionDenied => { | ||
| 99 | drop(result); // needed to workaround this: https://github.com/rust-lang/rust/issues/76149 | ||
| 100 | let url = transport.to_url(); | ||
| 101 | progress.set_name("authentication"); | ||
| 102 | let credentials::Outcome { identity, next } = | ||
| 103 | authenticate(credentials::Action::Fill(&url))?.expect("FILL provides an identity"); | ||
| 104 | transport.set_identity(identity)?; | ||
| 105 | progress.step(); | ||
| 106 | progress.set_name("handshake (authenticated)"); | ||
| 107 | match transport.handshake(Service::UploadPack) { | ||
| 108 | Ok(v) => { | ||
| 109 | authenticate(next.approve())?; | ||
| 110 | Ok(v) | ||
| 111 | } | ||
| 112 | // Still no permission? Reject the credentials. | ||
| 113 | Err(client::Error::Io { err }) if err.kind() == io::ErrorKind::PermissionDenied => { | ||
| 114 | authenticate(next.reject())?; | ||
| 115 | Err(client::Error::Io { err }) | ||
| 116 | } | ||
| 117 | // Otherwise, do nothing, as we don't know if it actually got to try the credentials. | ||
| 118 | // If they were previously stored, they remain. In the worst case, the user has to enter them again | ||
| 119 | // next time they try. | ||
| 120 | Err(err) => Err(err), | ||
| 121 | } | ||
| 122 | } | ||
| 123 | Err(err) => Err(err), | ||
| 124 | }?; | ||
| 125 | |||
| 126 | let mut parsed_refs = Vec::<InternalRef>::new(); | ||
| 127 | refs::from_capabilities(&mut parsed_refs, capabilities.iter())?; | ||
| 128 | |||
| 129 | let call_ls_refs = match refs { | ||
| 130 | Some(mut refs) => { | ||
| 131 | assert_eq!( | ||
| 132 | actual_protocol, | ||
| 133 | git_transport::Protocol::V1, | ||
| 134 | "Only V1 auto-responds with refs" | ||
| 135 | ); | ||
| 136 | refs::from_v1_refs_received_as_part_of_handshake(&mut parsed_refs, &mut refs)?; | ||
| 137 | false | ||
| 138 | } | ||
| 139 | None => true, | ||
| 140 | }; | ||
| 141 | ( | ||
| 142 | actual_protocol, | ||
| 143 | parsed_refs.into_iter().map(Into::into).collect::<Vec<Ref>>(), | ||
| 144 | capabilities, | ||
| 145 | call_ls_refs, | ||
| 146 | ) | ||
| 147 | }; // this scope is needed, see https://github.com/rust-lang/rust/issues/76149 | ||
| 148 | |||
| 149 | if transport.desired_protocol_version() != protocol_version { | ||
| 150 | progress.info(format!( | ||
| 151 | "server did not support protocol {} and downgraded to {}", | ||
| 152 | transport.desired_protocol_version() as usize, | ||
| 153 | protocol_version as usize, | ||
| 154 | )); | ||
| 155 | } | ||
| 156 | |||
| 157 | if call_ls_refs { | ||
| 158 | assert_eq!( | ||
| 159 | protocol_version, | ||
| 160 | git_transport::Protocol::V2, | ||
| 161 | "Only V2 needs a separate request to get specific refs" | ||
| 162 | ); | ||
| 163 | |||
| 164 | let ls_refs = Command::LsRefs; | ||
| 165 | let mut ls_features = ls_refs.default_features(protocol_version, &capabilities); | ||
| 166 | let mut ls_args = ls_refs.initial_arguments(&ls_features); | ||
| 167 | delegate.prepare_ls_refs(&capabilities, &mut ls_args, &mut ls_features); | ||
| 168 | ls_refs.validate_argument_prefixes_or_panic(protocol_version, &capabilities, &ls_args, &ls_features); | ||
| 169 | |||
| 170 | progress.step(); | ||
| 171 | progress.set_name("list refs"); | ||
| 172 | let mut remote_refs = transport.invoke( | ||
| 173 | ls_refs.as_str(), | ||
| 174 | ls_features.into_iter(), | ||
| 175 | if ls_args.is_empty() { | ||
| 176 | None | ||
| 177 | } else { | ||
| 178 | Some(ls_args.into_iter()) | ||
| 179 | }, | ||
| 180 | )?; | ||
| 181 | refs::from_v2_refs(&mut parsed_refs, &mut remote_refs)?; | ||
| 182 | } | ||
| 183 | |||
| 184 | let fetch = Command::Fetch; | ||
| 185 | let mut fetch_features = fetch.default_features(protocol_version, &capabilities); | ||
| 186 | let next = delegate.prepare_fetch(protocol_version, &capabilities, &mut fetch_features, &parsed_refs); | ||
| 187 | fetch.validate_argument_prefixes_or_panic(protocol_version, &capabilities, &[], &fetch_features); | ||
| 188 | |||
| 189 | if next == Action::Close { | ||
| 190 | transport.close()?; | ||
| 191 | return Ok(()); | ||
| 192 | } | ||
| 193 | |||
| 194 | Response::check_required_features(protocol_version, &fetch_features)?; | ||
| 195 | let sideband_all = fetch_features.iter().any(|(n, _)| *n == "sideband-all"); | ||
| 196 | let mut arguments = Arguments::new(protocol_version, fetch_features); | ||
| 197 | let mut previous_response = None::<Response>; | ||
| 198 | let mut round = 1; | ||
| 199 | 'negotiation: loop { | ||
| 200 | progress.step(); | ||
| 201 | progress.set_name(format!("negotiate (round {})", round)); | ||
| 202 | round += 1; | ||
| 203 | let action = delegate.negotiate(&parsed_refs, &mut arguments, previous_response.as_ref()); | ||
| 204 | let mut reader = arguments.send(&mut transport, action == Action::Close)?; | ||
| 205 | if sideband_all { | ||
| 206 | setup_remote_progress(&mut progress, &mut reader); | ||
| 207 | } | ||
| 208 | let response = Response::from_line_reader(protocol_version, &mut reader)?; | ||
| 209 | previous_response = if response.has_pack() { | ||
| 210 | progress.step(); | ||
| 211 | progress.set_name("receiving pack"); | ||
| 212 | if !sideband_all { | ||
| 213 | setup_remote_progress(&mut progress, &mut reader); | ||
| 214 | } | ||
| 215 | delegate.receive_pack(reader, progress, &parsed_refs, &response)?; | ||
| 216 | break 'negotiation; | ||
| 217 | } else { | ||
| 218 | match action { | ||
| 219 | Action::Close => break 'negotiation, | ||
| 220 | Action::Continue => Some(response), | ||
| 221 | } | ||
| 222 | } | ||
| 223 | } | ||
| 224 | Ok(()) | ||
| 225 | } | ||
| 226 | |||
| 227 | fn setup_remote_progress( | ||
| 228 | progress: &mut impl Progress, | ||
| 229 | reader: &mut Box<dyn git_transport::client::ExtendedBufRead + '_>, | ||
| 230 | ) { | ||
| 231 | reader.set_progress_handler(Some(Box::new({ | ||
| 232 | let mut remote_progress = progress.add_child("remote"); | ||
| 233 | move |is_err: bool, data: &[u8]| { | ||
| 234 | crate::RemoteProgress::translate_to_progress(is_err, data, &mut remote_progress) | ||
| 235 | } | ||
| 236 | }) as git_transport::client::HandleProgress)); | ||
| 237 | } | ||