1use std::path::Path;
16
17use crate::install::{
18 install_protocol_v2_fetch_promisor_response_from_reader,
19 install_protocol_v2_fetch_response_from_reader,
20 install_upload_pack_packfile_promisor_response_from_reader,
21 install_upload_pack_packfile_response_from_reader,
22 install_upload_pack_shallow_packfile_promisor_response_from_reader,
23 install_upload_pack_shallow_packfile_response_from_reader,
24 shallow_info_from_protocol_v2_fetch_header, ProgressInstaller,
25};
26use sley_config::GitConfig;
27use sley_core::{
28 Capability, GitError, ObjectFormat, ObjectId, Result, UPSTREAM_GIT_COMPAT_VERSION,
29};
30use sley_odb::FileObjectDatabase;
31use sley_protocol::{
32 encode_protocol_v2_command_options, parse_protocol_v2_fetch_features,
33 parse_upload_pack_features, protocol_v2_object_format, read_protocol_v2_fetch_response,
34 read_protocol_v2_fetch_sideband_all_response,
35 read_protocol_v2_ls_refs_response_as_ref_advertisement_set,
36 smart_http_advertisement_content_type, smart_http_rpc_request_content_type,
37 smart_http_rpc_result_content_type, validate_protocol_v2_fetch_command_request,
38 validate_protocol_v2_ls_refs_command_request, write_protocol_v2_command_request,
39 write_upload_pack_negotiation_request, write_upload_pack_request, GitService,
40 ProtocolV2CommandOptions, ProtocolV2CommandRequest, ProtocolV2FetchRequest,
41 ProtocolV2FetchResponseSection, ProtocolV2FetchShallowInfo, ProtocolV2LsRefsRequest,
42 ProtocolVersion, RefAdvertisement, RefAdvertisementSet, TransportHandshake, UploadPackFeatures,
43 UploadPackNegotiationRequest, UploadPackRequest,
44};
45use sley_transport::{
46 encode_git_protocol_header, git_credential_basic_authorization, http_smart_info_refs_url,
47 http_smart_rpc_url, parse_remote_url, read_service_discovery_response, GitProtocolHeader,
48 HttpClient, HttpResponse, RemoteTransport, RemoteUrl, ServiceDiscoveryPayload, UreqHttpClient,
49};
50
51use crate::credentials::{credential_request_for_url, http_url_credential};
52use crate::{CredentialProvider, ProgressSink};
53
54pub fn remote_url_is_http(url: &str) -> Result<bool> {
60 Ok(matches!(
61 parse_remote_url(url)?.transport,
62 RemoteTransport::Http | RemoteTransport::Https
63 ))
64}
65
66pub struct HttpOperationBatch {
68 client: UreqHttpClient,
69}
70
71impl HttpOperationBatch {
72 pub fn new() -> Self {
73 Self {
74 client: UreqHttpClient::new(),
75 }
76 }
77 pub fn client(&self) -> &UreqHttpClient {
78 &self.client
79 }
80}
81
82impl Default for HttpOperationBatch {
83 fn default() -> Self {
84 Self::new()
85 }
86}
87
88pub fn new_http_client() -> UreqHttpClient {
89 UreqHttpClient::new()
90}
91
92pub fn http_send_with_auth(
98 remote: &RemoteUrl,
99 credentials: &mut dyn CredentialProvider,
100 mut perform: impl FnMut(Option<&str>) -> Result<HttpResponse>,
101) -> Result<HttpResponse> {
102 let initial = http_url_credential(remote);
103 let initial_header = match &initial {
104 Some(credential) => git_credential_basic_authorization(credential)?,
105 None => None,
106 };
107 let response = perform(initial_header.as_deref())?;
108 if response.status != 401 {
109 return Ok(response);
110 }
111 let mut request = credential_request_for_url(remote);
112 if request.username.is_none() {
113 request.username = initial.and_then(|credential| credential.username);
114 }
115 let Some(filled) = credentials.fill(request)? else {
116 return Ok(response);
117 };
118 let Some(header) = git_credential_basic_authorization(&filled)? else {
119 return Ok(response);
120 };
121 let retry = perform(Some(&header))?;
122 if retry.status != 401 {
123 credentials.approve(&filled)?;
124 } else {
125 credentials.reject(&filled)?;
126 }
127 Ok(retry)
128}
129
130pub fn http_protocol_version_from_config(config: Option<&GitConfig>) -> Option<ProtocolVersion> {
132 match config.and_then(|config| config.get("protocol", None, "version")) {
133 Some("0") => Some(ProtocolVersion::V0),
134 Some("1") => Some(ProtocolVersion::V1),
135 Some("2") => Some(ProtocolVersion::V2),
136 _ => Some(ProtocolVersion::V2),
137 }
138}
139
140pub fn http_git_protocol_header_value(config: Option<&GitConfig>) -> Result<Option<String>> {
142 http_git_protocol_header_value_for_service(config, GitService::UploadPack)
143}
144
145pub fn http_git_protocol_header_value_for_service(
150 config: Option<&GitConfig>,
151 service: GitService,
152) -> Result<Option<String>> {
153 let mut version = http_protocol_version_from_config(config);
154 if matches!(version, Some(ProtocolVersion::V2)) && service != GitService::UploadPack {
155 version = Some(ProtocolVersion::V0);
156 }
157 match version {
158 Some(ProtocolVersion::V0) => Ok(None),
159 Some(version) => encode_git_protocol_header(&GitProtocolHeader {
160 protocol: Some(version),
161 extra_parameters: Vec::new(),
162 }),
163 None => Ok(None),
164 }
165}
166
167pub fn http_request_headers<'a>(
169 auth: Option<&'a str>,
170 git_protocol: Option<&'a str>,
171) -> Vec<(&'a str, &'a str)> {
172 let mut headers = Vec::with_capacity(2);
173 if let Some(value) = git_protocol {
174 headers.push(("Git-Protocol", value));
175 }
176 if let Some(value) = auth {
177 headers.push(("Authorization", value));
178 }
179 headers
180}
181
182pub fn http_authorization_headers(auth: Option<&str>) -> Vec<(&str, &str)> {
184 http_request_headers(auth, None)
185}
186
187pub fn http_check_status(response: &HttpResponse, url: &str) -> Result<()> {
189 if (200..300).contains(&response.status) {
190 Ok(())
191 } else if response.status == 401 {
192 Err(GitError::Command(format!(
193 "authentication failed for {url}"
194 )))
195 } else {
196 Err(GitError::Command(format!(
197 "unexpected HTTP status {} for {url}",
198 response.status
199 )))
200 }
201}
202
203pub fn http_validate_content_type(response: &HttpResponse, expected: &str) -> Result<()> {
205 let actual = response
206 .content_type
207 .as_deref()
208 .unwrap_or("")
209 .split(';')
210 .next()
211 .unwrap_or("")
212 .trim();
213 if actual.eq_ignore_ascii_case(expected) {
214 Ok(())
215 } else {
216 Err(GitError::InvalidFormat(format!(
217 "unexpected content type {actual:?}, expected {expected:?}"
218 )))
219 }
220}
221
222#[derive(Debug, Clone, PartialEq, Eq)]
225pub struct HttpServiceAdvertisements {
226 pub set: RefAdvertisementSet,
227 pub handshake: Option<TransportHandshake>,
228}
229
230pub fn http_advertised_refs(
234 format: ObjectFormat,
235 mut response: HttpResponse,
236) -> Result<RefAdvertisementSet> {
237 let discovery = read_service_discovery_response(format, &mut response.body)?;
238 match discovery.payload {
239 ServiceDiscoveryPayload::AdvertisedRefs(set) => Ok(set),
240 ServiceDiscoveryPayload::ProtocolV2(_) => Err(GitError::Unsupported(
241 "protocol v2 advertisements over HTTP require an ls-refs RPC; use http_service_advertisements".into(),
242 )),
243 }
244}
245
246fn protocol_v2_ls_refs_command_request(
247 format: ObjectFormat,
248 handshake: &TransportHandshake,
249) -> Result<ProtocolV2CommandRequest> {
250 let ls_refs = ProtocolV2LsRefsRequest {
251 peel: true,
252 symrefs: true,
253 unborn: false,
254 ref_prefixes: vec!["HEAD".into(), "refs/heads/".into(), "refs/tags/".into()],
255 };
256 let mut command = ls_refs.to_command_request()?;
257 let mut options = ProtocolV2CommandOptions::default();
258 if handshake
259 .capabilities
260 .iter()
261 .any(|capability| capability.name == "agent")
262 {
263 options.agent = Some(format!("git/{UPSTREAM_GIT_COMPAT_VERSION}"));
264 }
265 if handshake
266 .capabilities
267 .iter()
268 .any(|capability| capability.name == "object-format")
269 {
270 let advertised_format = protocol_v2_object_format(&handshake.capabilities)?;
271 if advertised_format != format {
272 return Err(GitError::InvalidObjectId(format!(
273 "remote repository uses {}, local repository uses {}",
274 advertised_format.name(),
275 format.name()
276 )));
277 }
278 options.object_format = Some(format);
279 }
280 command.capabilities = encode_protocol_v2_command_options(&options)?;
281 validate_protocol_v2_ls_refs_command_request(handshake, &command)?;
282 Ok(command)
283}
284
285fn protocol_v2_fetch_command_request(
286 format: ObjectFormat,
287 handshake: &TransportHandshake,
288 fetch: &ProtocolV2FetchRequest,
289) -> Result<ProtocolV2CommandRequest> {
290 let mut command = fetch.to_command_request()?;
291 let mut options = ProtocolV2CommandOptions::default();
292 if handshake
293 .capabilities
294 .iter()
295 .any(|capability| capability.name == "agent")
296 {
297 options.agent = Some(format!("git/{UPSTREAM_GIT_COMPAT_VERSION}"));
298 }
299 if handshake
300 .capabilities
301 .iter()
302 .any(|capability| capability.name == "object-format")
303 {
304 let advertised_format = protocol_v2_object_format(&handshake.capabilities)?;
305 if advertised_format != format {
306 return Err(GitError::InvalidObjectId(format!(
307 "remote repository uses {}, local repository uses {}",
308 advertised_format.name(),
309 format.name()
310 )));
311 }
312 options.object_format = Some(format);
313 }
314 command.capabilities = encode_protocol_v2_command_options(&options)?;
315 validate_protocol_v2_fetch_command_request(handshake, format, &command)?;
316 Ok(command)
317}
318
319fn protocol_v2_fetch_request_from_upload_pack_semantics(
320 wants: Vec<ObjectId>,
321 haves: Vec<ObjectId>,
322 shallow: Vec<ObjectId>,
323 deepen: Option<u32>,
324 deepen_since: Option<i64>,
325 deepen_not: Vec<String>,
326 deepen_relative: bool,
327 filter: Option<&sley_odb::PackObjectFilter>,
328 handshake: &TransportHandshake,
329) -> Result<ProtocolV2FetchRequest> {
330 let v2_features =
331 parse_protocol_v2_fetch_features(&handshake.capabilities)?.unwrap_or_default();
332 Ok(ProtocolV2FetchRequest {
333 wants,
334 haves,
335 shallow,
336 deepen,
337 deepen_since: deepen_since_u64(deepen_since),
338 deepen_not,
339 deepen_relative,
340 filter: filter.and_then(crate::local::upload_pack_filter_protocol_spec),
341 thin_pack: true,
342 include_tag: true,
343 ofs_delta: true,
344 done: true,
345 wait_for_done: v2_features.wait_for_done,
346 sideband_all: v2_features.sideband_all,
347 ..ProtocolV2FetchRequest::default()
348 })
349}
350
351fn deepen_since_u64(deepen_since: Option<i64>) -> Option<u64> {
352 deepen_since.and_then(|value| u64::try_from(value).ok())
353}
354
355fn build_http_upload_pack_request(
356 wants: Vec<ObjectId>,
357 shallow: Vec<ObjectId>,
358 deepen: Option<u32>,
359 deepen_since: Option<i64>,
360 deepen_not: Vec<String>,
361 filter: Option<&sley_odb::PackObjectFilter>,
362) -> UploadPackRequest {
363 UploadPackRequest {
364 wants,
365 capabilities: upload_pack_request_capabilities(
366 deepen,
367 filter
368 .and_then(crate::local::upload_pack_filter_protocol_spec)
369 .is_some(),
370 ),
371 shallow,
372 deepen,
373 deepen_since: deepen_since_u64(deepen_since),
374 deepen_not,
375 filter: filter.and_then(crate::local::upload_pack_filter_protocol_spec),
376 }
377}
378
379fn upload_pack_request_capabilities(deepen: Option<u32>, filter: bool) -> Vec<Capability> {
380 let mut capabilities = Vec::new();
381 capabilities.push(Capability {
387 name: "side-band-64k".into(),
388 value: None,
389 });
390 capabilities.push(Capability {
391 name: "ofs-delta".into(),
392 value: None,
393 });
394 if deepen.is_some() {
395 capabilities.push(Capability {
396 name: "shallow".into(),
397 value: None,
398 });
399 }
400 if filter {
401 capabilities.push(Capability {
402 name: "filter".into(),
403 value: None,
404 });
405 }
406 capabilities
407}
408
409fn request_replays_shallow_boundary(
410 deepen: Option<u32>,
411 deepen_since: Option<i64>,
412 deepen_not: &[String],
413) -> bool {
414 deepen.is_some() || deepen_since.is_some() || !deepen_not.is_empty()
415}
416
417fn http_protocol_v2_ls_refs_advertisements<C: HttpClient + ?Sized>(
418 client: &C,
419 remote: &RemoteUrl,
420 format: ObjectFormat,
421 service: GitService,
422 handshake: TransportHandshake,
423 credentials: &mut dyn CredentialProvider,
424 git_protocol: Option<&str>,
425) -> Result<RefAdvertisementSet> {
426 let command = protocol_v2_ls_refs_command_request(format, &handshake)?;
427 let url = http_smart_rpc_url(remote, service)?;
428 let mut body = Vec::new();
429 write_protocol_v2_command_request(&mut body, &command)?;
430 let content_type = smart_http_rpc_request_content_type(service)?;
431 let mut response = http_send_with_auth(remote, credentials, |auth| {
432 client.post(
433 &url,
434 &content_type,
435 &http_request_headers(auth, git_protocol),
436 &body,
437 )
438 })?;
439 http_check_status(&response, &url)?;
440 http_validate_content_type(&response, &smart_http_rpc_result_content_type(service)?)?;
441 read_protocol_v2_ls_refs_response_as_ref_advertisement_set(format, &mut response.body)
442}
443
444pub fn http_service_advertisements<C: HttpClient + ?Sized>(
447 client: &C,
448 remote: &RemoteUrl,
449 format: ObjectFormat,
450 service: GitService,
451 credentials: &mut dyn CredentialProvider,
452 config: Option<&GitConfig>,
453) -> Result<HttpServiceAdvertisements> {
454 let git_protocol = http_git_protocol_header_value_for_service(config, service)?;
455 let url = http_smart_info_refs_url(remote, service)?;
456 let mut response = http_send_with_auth(remote, credentials, |auth| {
457 client.get(&url, &http_request_headers(auth, git_protocol.as_deref()))
458 })?;
459 http_check_status(&response, &url)?;
460 http_validate_content_type(&response, &smart_http_advertisement_content_type(service)?)?;
461 let discovery = read_service_discovery_response(format, &mut response.body)?;
462 match discovery.payload {
463 ServiceDiscoveryPayload::AdvertisedRefs(set) => Ok(HttpServiceAdvertisements {
464 set,
465 handshake: None,
466 }),
467 ServiceDiscoveryPayload::ProtocolV2(handshake) => {
468 let set = http_protocol_v2_ls_refs_advertisements(
469 client,
470 remote,
471 format,
472 service,
473 handshake.clone(),
474 credentials,
475 git_protocol.as_deref(),
476 )?;
477 Ok(HttpServiceAdvertisements {
478 set,
479 handshake: Some(handshake),
480 })
481 }
482 }
483}
484
485pub fn http_upload_pack_advertisements<C: HttpClient + ?Sized>(
487 client: &C,
488 remote: &RemoteUrl,
489 format: ObjectFormat,
490 credentials: &mut dyn CredentialProvider,
491 config: Option<&GitConfig>,
492) -> Result<(Vec<RefAdvertisement>, UploadPackFeatures)> {
493 let discovered = http_service_advertisements(
494 client,
495 remote,
496 format,
497 GitService::UploadPack,
498 credentials,
499 config,
500 )?;
501 let features = http_upload_pack_features(&discovered.set.refs, discovered.handshake.as_ref())?;
502 Ok((discovered.set.refs, features))
503}
504
505pub fn http_upload_pack_features(
508 advertisements: &[RefAdvertisement],
509 handshake: Option<&TransportHandshake>,
510) -> Result<UploadPackFeatures> {
511 if let Some(handshake) = handshake {
512 let v2 = parse_protocol_v2_fetch_features(&handshake.capabilities)?.unwrap_or_default();
513 let mut features = UploadPackFeatures {
514 object_format: Some(protocol_v2_object_format(&handshake.capabilities)?),
515 shallow: v2.shallow,
516 deepen_since: v2.shallow,
517 deepen_not: v2.shallow,
518 filter: v2.filter,
519 ..UploadPackFeatures::default()
520 };
521 if let Some(first) = advertisements.first() {
522 let bridged = parse_upload_pack_features(&first.capabilities)?;
523 features.symrefs = bridged.symrefs;
524 }
525 return Ok(features);
526 }
527 Ok(advertisements
528 .first()
529 .map(|advertisement| parse_upload_pack_features(&advertisement.capabilities))
530 .transpose()?
531 .unwrap_or_default())
532}
533
534fn http_upload_pack_post<C: HttpClient + ?Sized>(
539 client: &C,
540 remote: &RemoteUrl,
541 request: &UploadPackRequest,
542 haves: Vec<ObjectId>,
543 credentials: &mut dyn CredentialProvider,
544 git_protocol: Option<&str>,
545) -> Result<HttpResponse> {
546 let url = http_smart_rpc_url(remote, GitService::UploadPack)?;
547 let mut body = Vec::new();
548 write_upload_pack_request(&mut body, Some(request))?;
549 write_upload_pack_negotiation_request(
550 &mut body,
551 &UploadPackNegotiationRequest { haves, done: true },
552 )?;
553 let content_type = smart_http_rpc_request_content_type(GitService::UploadPack)?;
554 let response = http_send_with_auth(remote, credentials, |auth| {
555 client.post(
556 &url,
557 &content_type,
558 &http_request_headers(auth, git_protocol),
559 &body,
560 )
561 })?;
562 http_check_status(&response, &url)?;
563 http_validate_content_type(
564 &response,
565 &smart_http_rpc_result_content_type(GitService::UploadPack)?,
566 )?;
567 Ok(response)
568}
569
570pub fn http_protocol_v2_fetch_response<C: HttpClient + ?Sized>(
575 client: &C,
576 remote: &RemoteUrl,
577 format: ObjectFormat,
578 handshake: &TransportHandshake,
579 fetch: ProtocolV2FetchRequest,
580 credentials: &mut dyn CredentialProvider,
581 config: Option<&GitConfig>,
582) -> Result<Vec<ProtocolV2FetchResponseSection>> {
583 let git_protocol = http_git_protocol_header_value(config)?;
584 let sideband_all = fetch.sideband_all;
585 let mut response = http_protocol_v2_fetch_post(
586 client,
587 remote,
588 format,
589 handshake,
590 fetch,
591 credentials,
592 git_protocol.as_deref(),
593 )?;
594 if sideband_all {
595 Ok(read_protocol_v2_fetch_sideband_all_response(format, &mut response.body)?.sections)
596 } else {
597 read_protocol_v2_fetch_response(format, &mut response.body)
598 }
599}
600
601fn http_protocol_v2_fetch_post<C: HttpClient + ?Sized>(
602 client: &C,
603 remote: &RemoteUrl,
604 format: ObjectFormat,
605 handshake: &TransportHandshake,
606 fetch: ProtocolV2FetchRequest,
607 credentials: &mut dyn CredentialProvider,
608 git_protocol: Option<&str>,
609) -> Result<HttpResponse> {
610 let command = protocol_v2_fetch_command_request(format, handshake, &fetch)?;
611 let url = http_smart_rpc_url(remote, GitService::UploadPack)?;
612 let mut body = Vec::new();
613 write_protocol_v2_command_request(&mut body, &command)?;
614 let content_type = smart_http_rpc_request_content_type(GitService::UploadPack)?;
615 let response = http_send_with_auth(remote, credentials, |auth| {
616 client.post(
617 &url,
618 &content_type,
619 &http_request_headers(auth, git_protocol),
620 &body,
621 )
622 })?;
623 http_check_status(&response, &url)?;
624 http_validate_content_type(
625 &response,
626 &smart_http_rpc_result_content_type(GitService::UploadPack)?,
627 )?;
628 Ok(response)
629}
630
631pub struct HttpFetchPackRequest<'a, C: HttpClient + ?Sized> {
642 pub client: &'a C,
646 pub git_dir: &'a Path,
648 pub format: ObjectFormat,
650 pub remote: &'a RemoteUrl,
652 pub wants: Vec<ObjectId>,
654 pub haves: Option<Vec<ObjectId>>,
657 pub shallow: Vec<ObjectId>,
659 pub deepen: Option<u32>,
661 pub promisor: bool,
663 pub max_input_size: Option<u64>,
666 pub filter: Option<sley_odb::PackObjectFilter>,
667 pub deepen_since: Option<i64>,
668 pub deepen_not: Vec<String>,
669 pub deepen_relative: bool,
670 pub git_protocol: Option<&'a str>,
671 pub omit_haves: bool,
677}
678
679pub fn install_fetch_pack_via_http_upload_pack<C: HttpClient + ?Sized>(
680 request: HttpFetchPackRequest<'_, C>,
681 credentials: &mut dyn CredentialProvider,
682 progress: &mut dyn ProgressSink,
683) -> Result<Vec<ProtocolV2FetchShallowInfo>> {
684 if request.wants.is_empty() {
685 return Ok(Vec::new());
686 }
687 let local_db = FileObjectDatabase::from_git_dir(request.git_dir, request.format);
688 if !request_replays_shallow_boundary(request.deepen, request.deepen_since, &request.deepen_not)
692 && request.filter.is_none()
693 && all_wants_present(&local_db, &request.wants)?
694 {
695 return Ok(Vec::new());
696 }
697 let upload_request = build_http_upload_pack_request(
698 request.wants,
699 request.shallow,
700 request.deepen,
701 request.deepen_since,
702 request.deepen_not,
703 request.filter.as_ref(),
704 );
705 let haves = request_haves(
706 request.git_dir,
707 request.format,
708 request.omit_haves,
709 request.haves.clone(),
710 )?;
711 if request.deepen.is_none() {
712 let mut response = http_upload_pack_post(
713 request.client,
714 request.remote,
715 &upload_request,
716 haves,
717 credentials,
718 request.git_protocol,
719 )?;
720 if request.promisor {
721 install_upload_pack_packfile_promisor_response_from_reader(
722 request.format,
723 &mut response.body,
724 &local_db,
725 request.max_input_size,
726 )?;
727 } else {
728 install_upload_pack_packfile_response_from_reader(
729 request.format,
730 &mut response.body,
731 &ProgressInstaller::new(&local_db, progress),
732 request.max_input_size,
733 )?;
734 }
735 return Ok(Vec::new());
736 }
737
738 let mut response = http_upload_pack_post(
739 request.client,
740 request.remote,
741 &upload_request,
742 haves,
743 credentials,
744 request.git_protocol,
745 )?;
746 let shallow_info = if request.promisor {
747 let (shallow_info, _) = install_upload_pack_shallow_packfile_promisor_response_from_reader(
748 request.format,
749 &mut response.body,
750 &local_db,
751 request.max_input_size,
752 )?;
753 shallow_info
754 } else {
755 let (shallow_info, _) = install_upload_pack_shallow_packfile_response_from_reader(
756 request.format,
757 &mut response.body,
758 &ProgressInstaller::new(&local_db, progress),
759 request.max_input_size,
760 )?;
761 shallow_info
762 };
763 Ok(shallow_info)
764}
765
766pub fn install_fetch_pack_via_http_protocol_v2_fetch<C: HttpClient + ?Sized>(
767 request: HttpFetchPackRequest<'_, C>,
768 handshake: &TransportHandshake,
769 credentials: &mut dyn CredentialProvider,
770 progress: &mut dyn ProgressSink,
771) -> Result<Vec<ProtocolV2FetchShallowInfo>> {
772 if request.wants.is_empty() {
773 return Ok(Vec::new());
774 }
775 let local_db = FileObjectDatabase::from_git_dir(request.git_dir, request.format);
776 if !request_replays_shallow_boundary(request.deepen, request.deepen_since, &request.deepen_not)
777 && request.filter.is_none()
778 && all_wants_present(&local_db, &request.wants)?
779 {
780 return Ok(Vec::new());
781 }
782 let haves = request_haves(
783 request.git_dir,
784 request.format,
785 request.omit_haves,
786 request.haves.clone(),
787 )?;
788 let fetch = protocol_v2_fetch_request_from_upload_pack_semantics(
789 request.wants,
790 haves,
791 request.shallow,
792 request.deepen,
793 request.deepen_since,
794 request.deepen_not,
795 request.deepen_relative,
796 request.filter.as_ref(),
797 handshake,
798 )?;
799 let sideband_all = fetch.sideband_all;
800 let mut response = http_protocol_v2_fetch_post(
801 request.client,
802 request.remote,
803 request.format,
804 handshake,
805 fetch,
806 credentials,
807 request.git_protocol,
808 )?;
809 let (header, _install) = if request.promisor {
810 install_protocol_v2_fetch_promisor_response_from_reader(
811 request.format,
812 &mut response.body,
813 sideband_all,
814 &local_db,
815 request.max_input_size,
816 )?
817 } else {
818 install_protocol_v2_fetch_response_from_reader(
819 request.format,
820 &mut response.body,
821 sideband_all,
822 &ProgressInstaller::new(&local_db, progress),
823 request.max_input_size,
824 )?
825 };
826 Ok(shallow_info_from_protocol_v2_fetch_header(&header))
827}
828
829fn all_wants_present(db: &FileObjectDatabase, wants: &[ObjectId]) -> Result<bool> {
830 for want in wants {
831 if !db.contains(want)? {
832 return Ok(false);
833 }
834 }
835 Ok(true)
836}
837
838fn request_haves(
839 git_dir: &Path,
840 format: ObjectFormat,
841 omit_haves: bool,
842 custom_haves: Option<Vec<ObjectId>>,
843) -> Result<Vec<ObjectId>> {
844 if omit_haves {
845 Ok(Vec::new())
846 } else if let Some(haves) = custom_haves {
847 Ok(haves)
848 } else {
849 crate::local::local_have_oids(git_dir, format)
850 }
851}
852
853#[cfg(test)]
854mod tests {
855 use super::*;
856 use sley_protocol::{
857 read_protocol_v2_fetch_response, write_protocol_v2_fetch_response,
858 write_protocol_v2_ls_refs_response, ProtocolV2FetchResponseSection,
859 ProtocolV2FetchShallowInfo, ProtocolV2LsRefsRecord, ProtocolVersion, RefAdvertisement,
860 };
861
862 struct PostRecorder {
866 result_content_type: String,
867 post_calls: std::sync::atomic::AtomicUsize,
868 }
869
870 impl HttpClient for PostRecorder {
871 fn get(&self, _url: &str, _headers: &[(&str, &str)]) -> Result<HttpResponse> {
872 Err(GitError::Command("recording client received an unexpected GET".into()))
873 }
874
875 fn post(
876 &self,
877 _url: &str,
878 _content_type: &str,
879 _headers: &[(&str, &str)],
880 _body: &[u8],
881 ) -> Result<HttpResponse> {
882 self.post_calls
883 .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
884 Ok(HttpResponse {
885 status: 200,
886 content_type: Some(self.result_content_type.clone()),
887 body: Box::new(std::io::Cursor::new(Vec::new())),
888 })
889 }
890 }
891
892 #[test]
893 fn http_upload_pack_post_uses_injected_client() {
894 let recorder = PostRecorder {
895 result_content_type: smart_http_rpc_result_content_type(GitService::UploadPack)
896 .expect("content type"),
897 post_calls: std::sync::atomic::AtomicUsize::new(0),
898 };
899 let remote = parse_remote_url("http://example.invalid/repo.git").expect("url");
900 let want = ObjectId::from_hex(
901 ObjectFormat::Sha1,
902 "1111111111111111111111111111111111111111",
903 )
904 .expect("oid");
905 let request = UploadPackRequest {
906 wants: vec![want],
907 capabilities: Vec::new(),
908 shallow: Vec::new(),
909 deepen: None,
910 deepen_since: None,
911 deepen_not: Vec::new(),
912 filter: None,
913 };
914 let mut credentials = crate::NoCredentials;
915 let response = http_upload_pack_post(
916 &recorder,
917 &remote,
918 &request,
919 Vec::new(),
920 &mut credentials,
921 None,
922 )
923 .expect("post via injected client should succeed");
924 assert_eq!(response.status, 200);
925 assert_eq!(
926 recorder
927 .post_calls
928 .load(std::sync::atomic::Ordering::Relaxed),
929 1,
930 "the injected client must own the pack-fetch POST dial"
931 );
932 }
933
934 fn sample_v2_handshake() -> TransportHandshake {
935 TransportHandshake {
936 protocol: ProtocolVersion::V2,
937 capabilities: vec![
938 Capability {
939 name: "ls-refs".into(),
940 value: Some("peel symrefs".into()),
941 },
942 Capability {
943 name: "agent".into(),
944 value: Some("git/2.54.0".into()),
945 },
946 Capability {
947 name: "object-format".into(),
948 value: Some("sha1".into()),
949 },
950 ],
951 }
952 }
953
954 #[test]
955 fn protocol_v2_ls_refs_command_request_includes_agent_and_object_format() {
956 let handshake = sample_v2_handshake();
957 let command = protocol_v2_ls_refs_command_request(ObjectFormat::Sha1, &handshake)
958 .expect("test operation should succeed");
959 assert_eq!(command.command, "ls-refs");
960 assert_eq!(
961 command.capabilities,
962 vec![
963 Capability {
964 name: "agent".into(),
965 value: Some(format!("git/{UPSTREAM_GIT_COMPAT_VERSION}")),
966 },
967 Capability {
968 name: "object-format".into(),
969 value: Some("sha1".into()),
970 },
971 ]
972 );
973 assert_eq!(
974 ProtocolV2LsRefsRequest::from_command_request(&command)
975 .expect("test operation should succeed"),
976 ProtocolV2LsRefsRequest {
977 peel: true,
978 symrefs: true,
979 unborn: false,
980 ref_prefixes: vec!["HEAD".into(), "refs/heads/".into(), "refs/tags/".into(),],
981 }
982 );
983 }
984
985 #[test]
986 fn protocol_v2_ls_refs_command_request_omits_object_format_when_unadvertised() {
987 let handshake = TransportHandshake {
988 protocol: ProtocolVersion::V2,
989 capabilities: vec![
990 Capability {
991 name: "ls-refs".into(),
992 value: None,
993 },
994 Capability {
995 name: "agent".into(),
996 value: Some("git/2.54.0".into()),
997 },
998 ],
999 };
1000 let command = protocol_v2_ls_refs_command_request(ObjectFormat::Sha1, &handshake)
1001 .expect("test operation should succeed");
1002 assert_eq!(
1003 command.capabilities,
1004 vec![Capability {
1005 name: "agent".into(),
1006 value: Some(format!("git/{UPSTREAM_GIT_COMPAT_VERSION}")),
1007 }]
1008 );
1009 }
1010
1011 #[test]
1012 fn protocol_v2_ls_refs_round_trip_bridges_into_ref_advertisement_set() {
1013 let handshake = sample_v2_handshake();
1014 let command = protocol_v2_ls_refs_command_request(ObjectFormat::Sha1, &handshake)
1015 .expect("test operation should succeed");
1016 let head = ObjectId::from_hex(
1017 ObjectFormat::Sha1,
1018 "1111111111111111111111111111111111111111",
1019 )
1020 .expect("test operation should succeed");
1021 let tag = ObjectId::from_hex(
1022 ObjectFormat::Sha1,
1023 "2222222222222222222222222222222222222222",
1024 )
1025 .expect("test operation should succeed");
1026 let tag_peeled = ObjectId::from_hex(
1027 ObjectFormat::Sha1,
1028 "3333333333333333333333333333333333333333",
1029 )
1030 .expect("test operation should succeed");
1031 let records = vec![
1032 ProtocolV2LsRefsRecord::Ref(sley_protocol::ProtocolV2LsRefsRef {
1033 oid: head.clone(),
1034 name: "HEAD".into(),
1035 peeled: None,
1036 symref_target: Some("refs/heads/main".into()),
1037 attributes: Vec::new(),
1038 }),
1039 ProtocolV2LsRefsRecord::Ref(sley_protocol::ProtocolV2LsRefsRef {
1040 oid: head.clone(),
1041 name: "refs/heads/main".into(),
1042 peeled: None,
1043 symref_target: None,
1044 attributes: Vec::new(),
1045 }),
1046 ProtocolV2LsRefsRecord::Ref(sley_protocol::ProtocolV2LsRefsRef {
1047 oid: tag.clone(),
1048 name: "refs/tags/v1".into(),
1049 peeled: Some(tag_peeled.clone()),
1050 symref_target: None,
1051 attributes: Vec::new(),
1052 }),
1053 ];
1054
1055 let mut request_body = Vec::new();
1056 write_protocol_v2_command_request(&mut request_body, &command)
1057 .expect("test operation should succeed");
1058 let mut response_body = Vec::new();
1059 write_protocol_v2_ls_refs_response(&mut response_body, &records)
1060 .expect("test operation should succeed");
1061
1062 let set = read_protocol_v2_ls_refs_response_as_ref_advertisement_set(
1063 ObjectFormat::Sha1,
1064 &mut response_body.as_slice(),
1065 )
1066 .expect("test operation should succeed");
1067 assert_eq!(
1068 set,
1069 RefAdvertisementSet {
1070 protocol: ProtocolVersion::V2,
1071 refs: vec![
1072 RefAdvertisement {
1073 oid: head.clone(),
1074 name: "HEAD".into(),
1075 capabilities: vec![Capability {
1076 name: "symref".into(),
1077 value: Some("HEAD:refs/heads/main".into()),
1078 }],
1079 },
1080 RefAdvertisement {
1081 oid: head,
1082 name: "refs/heads/main".into(),
1083 capabilities: Vec::new(),
1084 },
1085 RefAdvertisement {
1086 oid: tag,
1087 name: "refs/tags/v1".into(),
1088 capabilities: Vec::new(),
1089 },
1090 RefAdvertisement {
1091 oid: tag_peeled,
1092 name: "refs/tags/v1^{}".into(),
1093 capabilities: Vec::new(),
1094 },
1095 ],
1096 shallow: Vec::new(),
1097 }
1098 );
1099 assert!(!request_body.is_empty());
1100 }
1101
1102 fn sample_v2_fetch_handshake() -> TransportHandshake {
1103 TransportHandshake {
1104 protocol: ProtocolVersion::V2,
1105 capabilities: vec![
1106 Capability {
1107 name: "fetch".into(),
1108 value: Some("shallow sideband-all".into()),
1109 },
1110 Capability {
1111 name: "agent".into(),
1112 value: Some("git/2.54.0".into()),
1113 },
1114 Capability {
1115 name: "object-format".into(),
1116 value: Some("sha1".into()),
1117 },
1118 ],
1119 }
1120 }
1121
1122 #[test]
1123 fn protocol_v2_fetch_command_request_includes_agent_object_format_and_deepen() {
1124 let handshake = sample_v2_fetch_handshake();
1125 let want = ObjectId::from_hex(
1126 ObjectFormat::Sha1,
1127 "1111111111111111111111111111111111111111",
1128 )
1129 .expect("test operation should succeed");
1130 let have = ObjectId::from_hex(
1131 ObjectFormat::Sha1,
1132 "2222222222222222222222222222222222222222",
1133 )
1134 .expect("test operation should succeed");
1135 let shallow = ObjectId::from_hex(
1136 ObjectFormat::Sha1,
1137 "3333333333333333333333333333333333333333",
1138 )
1139 .expect("test operation should succeed");
1140 let fetch = protocol_v2_fetch_request_from_upload_pack_semantics(
1141 vec![want.clone()],
1142 vec![have.clone()],
1143 vec![shallow.clone()],
1144 Some(3),
1145 None,
1146 Vec::new(),
1147 false,
1148 None,
1149 &handshake,
1150 )
1151 .expect("test operation should succeed");
1152 assert!(fetch.sideband_all);
1153 assert!(fetch.done);
1154 let command = protocol_v2_fetch_command_request(ObjectFormat::Sha1, &handshake, &fetch)
1155 .expect("test operation should succeed");
1156 assert_eq!(command.command, "fetch");
1157 assert_eq!(
1158 command.capabilities,
1159 vec![
1160 Capability {
1161 name: "agent".into(),
1162 value: Some(format!("git/{UPSTREAM_GIT_COMPAT_VERSION}")),
1163 },
1164 Capability {
1165 name: "object-format".into(),
1166 value: Some("sha1".into()),
1167 },
1168 ]
1169 );
1170 assert_eq!(
1171 ProtocolV2FetchRequest::from_command_request(ObjectFormat::Sha1, &command)
1172 .expect("test operation should succeed"),
1173 ProtocolV2FetchRequest {
1174 wants: vec![want],
1175 haves: vec![have],
1176 shallow: vec![shallow],
1177 deepen: Some(3),
1178 thin_pack: true,
1179 include_tag: true,
1180 ofs_delta: true,
1181 done: true,
1182 sideband_all: true,
1183 ..ProtocolV2FetchRequest::default()
1184 }
1185 );
1186 }
1187
1188 #[test]
1189 fn protocol_v2_fetch_round_trip_extracts_shallow_info_and_packfile_sections() {
1190 let handshake = sample_v2_fetch_handshake();
1191 let want = ObjectId::from_hex(
1192 ObjectFormat::Sha1,
1193 "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
1194 )
1195 .expect("test operation should succeed");
1196 let shallow = ObjectId::from_hex(
1197 ObjectFormat::Sha1,
1198 "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
1199 )
1200 .expect("test operation should succeed");
1201 let fetch = protocol_v2_fetch_request_from_upload_pack_semantics(
1202 vec![want],
1203 Vec::new(),
1204 vec![shallow.clone()],
1205 Some(1),
1206 None,
1207 Vec::new(),
1208 false,
1209 None,
1210 &handshake,
1211 )
1212 .expect("test operation should succeed");
1213 let command = protocol_v2_fetch_command_request(ObjectFormat::Sha1, &handshake, &fetch)
1214 .expect("test operation should succeed");
1215 let mut request_body = Vec::new();
1216 write_protocol_v2_command_request(&mut request_body, &command)
1217 .expect("test operation should succeed");
1218
1219 let sections = vec![
1220 ProtocolV2FetchResponseSection::ShallowInfo(vec![ProtocolV2FetchShallowInfo::Shallow(
1221 shallow,
1222 )]),
1223 ProtocolV2FetchResponseSection::Packfile(vec![b"PACK-test".to_vec()]),
1224 ];
1225 let mut response_body = Vec::new();
1226 write_protocol_v2_fetch_response(&mut response_body, §ions)
1227 .expect("test operation should succeed");
1228 let parsed =
1229 read_protocol_v2_fetch_response(ObjectFormat::Sha1, &mut response_body.as_slice())
1230 .expect("test operation should succeed");
1231 assert_eq!(parsed, sections);
1232 assert_eq!(
1233 parsed.first(),
1234 Some(&ProtocolV2FetchResponseSection::ShallowInfo(vec![
1235 ProtocolV2FetchShallowInfo::Shallow(
1236 ObjectId::from_hex(
1237 ObjectFormat::Sha1,
1238 "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
1239 )
1240 .expect("test operation should succeed")
1241 )
1242 ]))
1243 );
1244 assert!(!request_body.is_empty());
1245 }
1246}