1use std::io::Read;
16use std::path::Path;
17
18use crate::install::{
19 ProgressInstaller, install_protocol_v2_fetch_promisor_response_from_reader,
20 install_protocol_v2_fetch_response_from_reader,
21 install_upload_pack_packfile_promisor_response_from_reader,
22 install_upload_pack_packfile_response_from_reader,
23 install_upload_pack_shallow_packfile_promisor_response_from_reader,
24 install_upload_pack_shallow_packfile_response_from_reader,
25 shallow_info_from_protocol_v2_fetch_header,
26};
27use sley_config::GitConfig;
28use sley_core::{
29 Capability, GitError, ObjectFormat, ObjectId, Result, UPSTREAM_GIT_COMPAT_VERSION,
30};
31use sley_odb::FileObjectDatabase;
32use sley_protocol::{
33 GitService, ProtocolV2CommandOptions, ProtocolV2CommandRequest, ProtocolV2FetchAcknowledgment,
34 ProtocolV2FetchRequest, ProtocolV2FetchResponseSection, ProtocolV2FetchShallowInfo,
35 ProtocolV2LsRefsRequest, ProtocolVersion, RefAdvertisement, RefAdvertisementSet,
36 TransportHandshake, UploadPackFeatures, UploadPackNegotiationRequest, UploadPackRequest,
37 encode_protocol_v2_command_options, parse_protocol_v2_fetch_features,
38 parse_upload_pack_features, protocol_v2_object_format,
39 read_protocol_v2_fetch_negotiation_response, read_protocol_v2_fetch_response,
40 read_protocol_v2_fetch_sideband_all_response,
41 read_protocol_v2_ls_refs_response_as_ref_advertisement_set,
42 smart_http_advertisement_content_type, smart_http_rpc_request_content_type,
43 trace_protocol_v2_advertisement_read, validate_protocol_v2_fetch_command_request,
44 validate_protocol_v2_ls_refs_command_request, write_protocol_v2_command_request,
45 write_upload_pack_negotiation_request, write_upload_pack_request,
46};
47use sley_transport::{
48 GitProtocolHeader, HttpClient, HttpResponse, RemoteTransport, RemoteUrl,
49 ServiceDiscoveryPayload, ServiceDiscoveryResponse, UreqHttpClient, encode_git_protocol_header,
50 git_credential_basic_authorization, http_smart_info_refs_url, http_smart_rpc_url,
51 parse_remote_url, read_service_discovery_response,
52};
53
54use crate::credentials::{credential_request_for_url, http_url_credential};
55use crate::{CredentialProvider, ProgressSink};
56
57pub fn remote_url_is_http(url: &str) -> Result<bool> {
63 Ok(matches!(
64 parse_remote_url(url)?.transport,
65 RemoteTransport::Http | RemoteTransport::Https
66 ))
67}
68
69pub struct HttpOperationBatch {
71 client: UreqHttpClient,
72}
73
74impl HttpOperationBatch {
75 pub fn new() -> Self {
76 Self {
77 client: UreqHttpClient::new(),
78 }
79 }
80 pub fn client(&self) -> &UreqHttpClient {
81 &self.client
82 }
83}
84
85impl Default for HttpOperationBatch {
86 fn default() -> Self {
87 Self::new()
88 }
89}
90
91pub fn new_http_client() -> UreqHttpClient {
92 UreqHttpClient::new()
93}
94
95pub fn http_send_with_auth(
101 remote: &RemoteUrl,
102 credentials: &mut dyn CredentialProvider,
103 mut perform: impl FnMut(Option<&str>) -> Result<HttpResponse>,
104) -> Result<HttpResponse> {
105 let initial = http_url_credential(remote);
106 let initial_header = match &initial {
107 Some(credential) => git_credential_basic_authorization(credential)?,
108 None => None,
109 };
110 let response = perform(initial_header.as_deref())?;
111 if response.status != 401 {
112 return Ok(response);
113 }
114 let mut request = credential_request_for_url(remote);
115 if request.username.is_none() {
116 request.username = initial.and_then(|credential| credential.username);
117 }
118 let Some(filled) = credentials.fill(request)? else {
119 return Ok(response);
120 };
121 let Some(header) = git_credential_basic_authorization(&filled)? else {
122 return Ok(response);
123 };
124 let retry = perform(Some(&header))?;
125 if retry.status != 401 {
126 credentials.approve(&filled)?;
127 } else {
128 credentials.reject(&filled)?;
129 }
130 Ok(retry)
131}
132
133pub fn http_protocol_version_from_config(config: Option<&GitConfig>) -> Option<ProtocolVersion> {
135 match config.and_then(|config| config.get("protocol", None, "version")) {
136 Some("0") => Some(ProtocolVersion::V0),
137 Some("1") => Some(ProtocolVersion::V1),
138 Some("2") => Some(ProtocolVersion::V2),
139 _ => Some(ProtocolVersion::V2),
140 }
141}
142
143pub fn http_git_protocol_header_value(config: Option<&GitConfig>) -> Result<Option<String>> {
145 http_git_protocol_header_value_for_service(config, GitService::UploadPack)
146}
147
148pub fn http_git_protocol_header_value_for_service(
153 config: Option<&GitConfig>,
154 service: GitService,
155) -> Result<Option<String>> {
156 let mut version = http_protocol_version_from_config(config);
157 if matches!(version, Some(ProtocolVersion::V2)) && service != GitService::UploadPack {
158 version = Some(ProtocolVersion::V0);
159 }
160 match version {
161 Some(ProtocolVersion::V0) => Ok(None),
162 Some(version) => encode_git_protocol_header(&GitProtocolHeader {
163 protocol: Some(version),
164 extra_parameters: Vec::new(),
165 }),
166 None => Ok(None),
167 }
168}
169
170pub fn http_request_headers<'a>(
172 auth: Option<&'a str>,
173 git_protocol: Option<&'a str>,
174) -> Vec<(&'a str, &'a str)> {
175 let mut headers = Vec::with_capacity(2);
176 if let Some(value) = git_protocol {
177 headers.push(("Git-Protocol", value));
178 }
179 if let Some(value) = auth {
180 headers.push(("Authorization", value));
181 }
182 headers
183}
184
185pub fn http_authorization_headers(auth: Option<&str>) -> Vec<(&str, &str)> {
187 http_request_headers(auth, None)
188}
189
190pub fn http_check_status(response: &HttpResponse, url: &str) -> Result<()> {
192 if (200..300).contains(&response.status) {
193 Ok(())
194 } else if response.status == 401 {
195 Err(GitError::Command(format!(
196 "authentication failed for {url}"
197 )))
198 } else {
199 Err(GitError::Command(format!(
200 "unexpected HTTP status {} for {url}",
201 response.status
202 )))
203 }
204}
205
206pub fn http_validate_content_type(response: &HttpResponse, expected: &str) -> Result<()> {
208 let actual = response
209 .content_type
210 .as_deref()
211 .unwrap_or("")
212 .split(';')
213 .next()
214 .unwrap_or("")
215 .trim();
216 if actual.eq_ignore_ascii_case(expected) {
217 Ok(())
218 } else {
219 Err(GitError::InvalidFormat(format!(
220 "unexpected content type {actual:?}, expected {expected:?}"
221 )))
222 }
223}
224
225#[derive(Debug, Clone, PartialEq, Eq)]
228pub struct HttpServiceAdvertisements {
229 pub set: RefAdvertisementSet,
230 pub handshake: Option<TransportHandshake>,
231}
232
233#[derive(Debug, Clone, PartialEq, Eq)]
235pub struct HttpUploadPackDiscovery {
236 pub advertisements: HttpServiceAdvertisements,
237 pub features: UploadPackFeatures,
238 pub object_format: ObjectFormat,
239}
240
241pub fn http_advertised_refs(
245 format: ObjectFormat,
246 mut response: HttpResponse,
247) -> Result<RefAdvertisementSet> {
248 let discovery = read_http_service_discovery_response(format, &mut response.body)?;
249 match discovery.payload {
250 ServiceDiscoveryPayload::AdvertisedRefs(set) => Ok(set),
251 ServiceDiscoveryPayload::ProtocolV2(_) => Err(GitError::Unsupported(
252 "protocol v2 advertisements over HTTP require an ls-refs RPC; use http_service_advertisements".into(),
253 )),
254 }
255}
256
257fn protocol_v2_ls_refs_command_request(
258 format: ObjectFormat,
259 handshake: &TransportHandshake,
260) -> Result<ProtocolV2CommandRequest> {
261 let ls_refs = ProtocolV2LsRefsRequest {
262 peel: true,
263 symrefs: true,
264 unborn: false,
265 ref_prefixes: vec!["HEAD".into(), "refs/heads/".into(), "refs/tags/".into()],
266 };
267 let mut command = ls_refs.to_command_request()?;
268 let mut options = ProtocolV2CommandOptions::default();
269 if handshake
270 .capabilities
271 .iter()
272 .any(|capability| capability.name == "agent")
273 {
274 options.agent = Some(format!("git/{UPSTREAM_GIT_COMPAT_VERSION}"));
275 }
276 if handshake
277 .capabilities
278 .iter()
279 .any(|capability| capability.name == "object-format")
280 {
281 let advertised_format = protocol_v2_object_format(&handshake.capabilities)?;
282 if advertised_format != format {
283 return Err(GitError::InvalidObjectId(format!(
284 "remote repository uses {}, local repository uses {}",
285 advertised_format.name(),
286 format.name()
287 )));
288 }
289 options.object_format = Some(format);
290 }
291 command.capabilities = encode_protocol_v2_command_options(&options)?;
292 validate_protocol_v2_ls_refs_command_request(handshake, &command)?;
293 Ok(command)
294}
295
296fn protocol_v2_fetch_command_request(
297 format: ObjectFormat,
298 handshake: &TransportHandshake,
299 fetch: &ProtocolV2FetchRequest,
300) -> Result<ProtocolV2CommandRequest> {
301 let mut command = fetch.to_command_request()?;
302 let mut options = ProtocolV2CommandOptions::default();
303 if handshake
304 .capabilities
305 .iter()
306 .any(|capability| capability.name == "agent")
307 {
308 options.agent = Some(format!("git/{UPSTREAM_GIT_COMPAT_VERSION}"));
309 }
310 if handshake
311 .capabilities
312 .iter()
313 .any(|capability| capability.name == "object-format")
314 {
315 let advertised_format = protocol_v2_object_format(&handshake.capabilities)?;
316 if advertised_format != format {
317 return Err(GitError::InvalidObjectId(format!(
318 "remote repository uses {}, local repository uses {}",
319 advertised_format.name(),
320 format.name()
321 )));
322 }
323 options.object_format = Some(format);
324 }
325 command.capabilities = encode_protocol_v2_command_options(&options)?;
326 validate_protocol_v2_fetch_command_request(handshake, format, &command)?;
327 Ok(command)
328}
329
330#[allow(clippy::too_many_arguments)]
331fn protocol_v2_fetch_request_from_upload_pack_semantics(
332 wants: Vec<ObjectId>,
333 haves: Vec<ObjectId>,
334 shallow: Vec<ObjectId>,
335 deepen: Option<u32>,
336 deepen_since: Option<i64>,
337 deepen_not: Vec<String>,
338 deepen_relative: bool,
339 filter: Option<&sley_odb::PackObjectFilter>,
340 handshake: &TransportHandshake,
341) -> Result<ProtocolV2FetchRequest> {
342 let v2_features =
343 parse_protocol_v2_fetch_features(&handshake.capabilities)?.unwrap_or_default();
344 Ok(ProtocolV2FetchRequest {
345 wants,
346 haves,
347 shallow,
348 deepen,
349 deepen_since: deepen_since_u64(deepen_since),
350 deepen_not,
351 deepen_relative,
352 filter: filter.and_then(crate::local::upload_pack_filter_protocol_spec),
353 thin_pack: true,
354 include_tag: true,
355 ofs_delta: true,
356 done: true,
357 wait_for_done: false,
360 sideband_all: v2_features.sideband_all,
361 ..ProtocolV2FetchRequest::default()
362 })
363}
364
365fn deepen_since_u64(deepen_since: Option<i64>) -> Option<u64> {
366 deepen_since.and_then(|value| u64::try_from(value).ok())
367}
368
369fn build_http_upload_pack_request(
370 wants: Vec<ObjectId>,
371 shallow: Vec<ObjectId>,
372 deepen: Option<u32>,
373 deepen_since: Option<i64>,
374 deepen_not: Vec<String>,
375 filter: Option<&sley_odb::PackObjectFilter>,
376) -> UploadPackRequest {
377 UploadPackRequest {
378 wants,
379 capabilities: upload_pack_request_capabilities(
380 deepen,
381 filter
382 .and_then(crate::local::upload_pack_filter_protocol_spec)
383 .is_some(),
384 ),
385 shallow,
386 deepen,
387 deepen_since: deepen_since_u64(deepen_since),
388 deepen_not,
389 filter: filter.and_then(crate::local::upload_pack_filter_protocol_spec),
390 }
391}
392
393fn upload_pack_request_capabilities(deepen: Option<u32>, filter: bool) -> Vec<Capability> {
394 let mut capabilities = Vec::new();
395 capabilities.push(Capability {
403 name: "side-band-64k".into(),
404 value: None,
405 });
406 if deepen.is_some() {
407 capabilities.push(Capability {
408 name: "shallow".into(),
409 value: None,
410 });
411 }
412 if filter {
413 capabilities.push(Capability {
414 name: "filter".into(),
415 value: None,
416 });
417 }
418 capabilities
419}
420
421fn request_replays_shallow_boundary(
422 deepen: Option<u32>,
423 deepen_since: Option<i64>,
424 deepen_not: &[String],
425) -> bool {
426 deepen.is_some() || deepen_since.is_some() || !deepen_not.is_empty()
427}
428
429fn http_protocol_v2_ls_refs_advertisements<C: HttpClient + ?Sized>(
430 client: &C,
431 remote: &RemoteUrl,
432 format: ObjectFormat,
433 service: GitService,
434 handshake: TransportHandshake,
435 credentials: &mut dyn CredentialProvider,
436 git_protocol: Option<&str>,
437) -> Result<RefAdvertisementSet> {
438 let command = protocol_v2_ls_refs_command_request(format, &handshake)?;
439 let url = http_smart_rpc_url(remote, service)?;
440 let mut body = Vec::new();
441 write_protocol_v2_command_request(&mut body, &command)?;
442 let content_type = smart_http_rpc_request_content_type(service)?;
443 let mut response = http_send_with_auth(remote, credentials, |auth| {
444 client.post(
445 &url,
446 &content_type,
447 &http_request_headers(auth, git_protocol),
448 &body,
449 )
450 })?;
451 http_check_status(&response, &url)?;
452 read_protocol_v2_ls_refs_response_as_ref_advertisement_set(format, &mut response.body)
453}
454
455pub fn http_service_advertisements<C: HttpClient + ?Sized>(
458 client: &C,
459 remote: &RemoteUrl,
460 format: ObjectFormat,
461 service: GitService,
462 credentials: &mut dyn CredentialProvider,
463 config: Option<&GitConfig>,
464) -> Result<HttpServiceAdvertisements> {
465 http_service_advertisements_with_expected_format(
466 client,
467 remote,
468 Some(format),
469 service,
470 credentials,
471 config,
472 )
473}
474
475fn http_service_advertisements_with_expected_format<C: HttpClient + ?Sized>(
476 client: &C,
477 remote: &RemoteUrl,
478 expected_format: Option<ObjectFormat>,
479 service: GitService,
480 credentials: &mut dyn CredentialProvider,
481 config: Option<&GitConfig>,
482) -> Result<HttpServiceAdvertisements> {
483 let _packet_trace_identity = sley_protocol::scoped_packet_trace_identity("git");
488 let git_protocol = http_git_protocol_header_value_for_service(config, service)?;
489 let url = http_smart_info_refs_url(remote, service)?;
490 let mut response = http_send_with_auth(remote, credentials, |auth| {
491 client.get(&url, &http_request_headers(auth, git_protocol.as_deref()))
492 })?;
493 http_check_status(&response, &url)?;
494 http_validate_content_type(&response, &smart_http_advertisement_content_type(service)?)?;
495 let discovery = read_http_service_discovery_response(
496 expected_format.unwrap_or(ObjectFormat::Sha1),
497 &mut response.body,
498 )?;
499 let object_format = service_discovery_object_format(&discovery)?;
500 if let Some(expected) = expected_format
501 && object_format != expected
502 {
503 return Err(GitError::InvalidObjectId(format!(
504 "remote repository uses {}, local repository uses {}",
505 object_format.name(),
506 expected.name()
507 )));
508 }
509 match discovery.payload {
510 ServiceDiscoveryPayload::AdvertisedRefs(set) => Ok(HttpServiceAdvertisements {
511 set,
512 handshake: None,
513 }),
514 ServiceDiscoveryPayload::ProtocolV2(handshake) => {
515 let set = http_protocol_v2_ls_refs_advertisements(
516 client,
517 remote,
518 object_format,
519 service,
520 handshake.clone(),
521 credentials,
522 git_protocol.as_deref(),
523 )?;
524 Ok(HttpServiceAdvertisements {
525 set,
526 handshake: Some(handshake),
527 })
528 }
529 }
530}
531
532fn service_discovery_object_format(discovery: &ServiceDiscoveryResponse) -> Result<ObjectFormat> {
533 match &discovery.payload {
534 ServiceDiscoveryPayload::ProtocolV2(handshake) => {
535 protocol_v2_object_format(&handshake.capabilities)
536 }
537 ServiceDiscoveryPayload::AdvertisedRefs(set) => Ok(set
538 .refs
539 .first()
540 .map(|reference| parse_upload_pack_features(&reference.capabilities))
541 .transpose()?
542 .and_then(|features| features.object_format)
543 .unwrap_or(ObjectFormat::Sha1)),
544 }
545}
546
547pub fn http_discover_upload_pack<C: HttpClient + ?Sized>(
549 client: &C,
550 remote: &RemoteUrl,
551 credentials: &mut dyn CredentialProvider,
552 config: Option<&GitConfig>,
553) -> Result<HttpUploadPackDiscovery> {
554 let advertisements = http_service_advertisements_with_expected_format(
555 client,
556 remote,
557 None,
558 GitService::UploadPack,
559 credentials,
560 config,
561 )?;
562 let features =
563 http_upload_pack_features(&advertisements.set.refs, advertisements.handshake.as_ref())?;
564 let object_format = features.object_format.unwrap_or(ObjectFormat::Sha1);
565 Ok(HttpUploadPackDiscovery {
566 advertisements,
567 features,
568 object_format,
569 })
570}
571
572fn read_http_service_discovery_response(
573 format: ObjectFormat,
574 reader: &mut impl Read,
575) -> Result<ServiceDiscoveryResponse> {
576 let mut bytes = Vec::new();
577 reader.read_to_end(&mut bytes)?;
578 let first = read_service_discovery_response(format, &mut bytes.as_slice());
579 let alternate = match format {
580 ObjectFormat::Sha1 => ObjectFormat::Sha256,
581 ObjectFormat::Sha256 => ObjectFormat::Sha1,
582 };
583 match first {
584 Ok(discovery) => Ok(discovery),
585 Err(original) => {
586 let retry = read_service_discovery_response(alternate, &mut bytes.as_slice());
587 match retry {
588 Ok(discovery) if discovery_advertises_object_format(&discovery, alternate) => {
589 Ok(discovery)
590 }
591 _ => Err(original),
592 }
593 }
594 }
595}
596
597fn discovery_advertises_object_format(
598 discovery: &ServiceDiscoveryResponse,
599 format: ObjectFormat,
600) -> bool {
601 match &discovery.payload {
602 ServiceDiscoveryPayload::AdvertisedRefs(set) => set.refs.first().is_some_and(|reference| {
603 reference.capabilities.iter().any(|capability| {
604 capability.name == "object-format"
605 && capability.value.as_deref() == Some(format.name())
606 })
607 }),
608 ServiceDiscoveryPayload::ProtocolV2(handshake) => {
609 handshake.capabilities.iter().any(|capability| {
610 capability.name == "object-format"
611 && capability.value.as_deref() == Some(format.name())
612 })
613 }
614 }
615}
616
617pub fn http_upload_pack_advertisements<C: HttpClient + ?Sized>(
619 client: &C,
620 remote: &RemoteUrl,
621 format: ObjectFormat,
622 credentials: &mut dyn CredentialProvider,
623 config: Option<&GitConfig>,
624) -> Result<(Vec<RefAdvertisement>, UploadPackFeatures)> {
625 let discovered = http_service_advertisements(
626 client,
627 remote,
628 format,
629 GitService::UploadPack,
630 credentials,
631 config,
632 )?;
633 let features = http_upload_pack_features(&discovered.set.refs, discovered.handshake.as_ref())?;
634 Ok((discovered.set.refs, features))
635}
636
637pub fn http_upload_pack_features(
640 advertisements: &[RefAdvertisement],
641 handshake: Option<&TransportHandshake>,
642) -> Result<UploadPackFeatures> {
643 if let Some(handshake) = handshake {
644 let v2 = parse_protocol_v2_fetch_features(&handshake.capabilities)?.unwrap_or_default();
645 let mut features = UploadPackFeatures {
646 object_format: Some(protocol_v2_object_format(&handshake.capabilities)?),
647 shallow: v2.shallow,
648 deepen_since: v2.shallow,
649 deepen_not: v2.shallow,
650 filter: v2.filter,
651 ..UploadPackFeatures::default()
652 };
653 if let Some(first) = advertisements.first() {
654 let bridged = parse_upload_pack_features(&first.capabilities)?;
655 features.symrefs = bridged.symrefs;
656 }
657 return Ok(features);
658 }
659 Ok(advertisements
660 .first()
661 .map(|advertisement| parse_upload_pack_features(&advertisement.capabilities))
662 .transpose()?
663 .unwrap_or_default())
664}
665
666fn http_upload_pack_post<C: HttpClient + ?Sized>(
672 client: &C,
673 remote: &RemoteUrl,
674 request: &UploadPackRequest,
675 haves: Vec<ObjectId>,
676 credentials: &mut dyn CredentialProvider,
677 git_protocol: Option<&str>,
678 post_buffer: usize,
679) -> Result<HttpResponse> {
680 let url = http_smart_rpc_url(remote, GitService::UploadPack)?;
681 let mut body = Vec::new();
682 write_upload_pack_request(&mut body, Some(request))?;
683 write_upload_pack_negotiation_request(
684 &mut body,
685 &UploadPackNegotiationRequest { haves, done: true },
686 )?;
687 let content_type = smart_http_rpc_request_content_type(GitService::UploadPack)?;
688 let response = http_send_with_auth(remote, credentials, |auth| {
689 http_post_rpc_body(
690 client,
691 &url,
692 &content_type,
693 &http_request_headers(auth, git_protocol),
694 &body,
695 post_buffer,
696 )
697 })?;
698 http_check_status(&response, &url)?;
699 Ok(response)
705}
706
707pub fn http_protocol_v2_fetch_response<C: HttpClient + ?Sized>(
712 client: &C,
713 remote: &RemoteUrl,
714 format: ObjectFormat,
715 handshake: &TransportHandshake,
716 fetch: ProtocolV2FetchRequest,
717 credentials: &mut dyn CredentialProvider,
718 config: Option<&GitConfig>,
719) -> Result<Vec<ProtocolV2FetchResponseSection>> {
720 let git_protocol = http_git_protocol_header_value(config)?;
721 let post_buffer = http_post_buffer(config);
722 let sideband_all = fetch.sideband_all;
723 let mut response = http_protocol_v2_fetch_post(
724 client,
725 remote,
726 format,
727 handshake,
728 fetch,
729 credentials,
730 HttpRpcOptions {
731 git_protocol: git_protocol.as_deref(),
732 post_buffer,
733 },
734 )?;
735 if sideband_all {
736 Ok(read_protocol_v2_fetch_sideband_all_response(format, &mut response.body)?.sections)
737 } else {
738 read_protocol_v2_fetch_response(format, &mut response.body)
739 }
740}
741
742#[derive(Clone, Copy)]
743struct HttpRpcOptions<'a> {
744 git_protocol: Option<&'a str>,
745 post_buffer: usize,
746}
747
748fn http_protocol_v2_fetch_post<C: HttpClient + ?Sized>(
749 client: &C,
750 remote: &RemoteUrl,
751 format: ObjectFormat,
752 handshake: &TransportHandshake,
753 fetch: ProtocolV2FetchRequest,
754 credentials: &mut dyn CredentialProvider,
755 options: HttpRpcOptions<'_>,
756) -> Result<HttpResponse> {
757 let command = protocol_v2_fetch_command_request(format, handshake, &fetch)?;
758 let url = http_smart_rpc_url(remote, GitService::UploadPack)?;
759 let mut body = Vec::new();
760 write_protocol_v2_command_request(&mut body, &command)?;
761 let content_type = smart_http_rpc_request_content_type(GitService::UploadPack)?;
762 let response = http_send_with_auth(remote, credentials, |auth| {
763 http_post_rpc_body(
764 client,
765 &url,
766 &content_type,
767 &http_request_headers(auth, options.git_protocol),
768 &body,
769 options.post_buffer,
770 )
771 })?;
772 http_check_status(&response, &url)?;
773 Ok(response)
774}
775
776pub struct HttpFetchPackRequest<'a, C: HttpClient + ?Sized> {
787 pub client: &'a C,
791 pub git_dir: &'a Path,
793 pub format: ObjectFormat,
795 pub remote: &'a RemoteUrl,
797 pub wants: Vec<ObjectId>,
799 pub haves: Option<Vec<ObjectId>>,
802 pub shallow: Vec<ObjectId>,
804 pub deepen: Option<u32>,
806 pub promisor: bool,
808 pub max_input_size: Option<u64>,
811 pub filter: Option<sley_odb::PackObjectFilter>,
812 pub deepen_since: Option<i64>,
813 pub deepen_not: Vec<String>,
814 pub deepen_relative: bool,
815 pub git_protocol: Option<&'a str>,
816 pub post_buffer: usize,
819 pub omit_haves: bool,
825}
826
827pub fn install_fetch_pack_via_http_upload_pack<C: HttpClient + ?Sized>(
828 request: HttpFetchPackRequest<'_, C>,
829 credentials: &mut dyn CredentialProvider,
830 progress: &mut dyn ProgressSink,
831) -> Result<Vec<ProtocolV2FetchShallowInfo>> {
832 if request.wants.is_empty() {
833 return Ok(Vec::new());
834 }
835 let local_db = FileObjectDatabase::from_git_dir(request.git_dir, request.format);
836 if !request_replays_shallow_boundary(request.deepen, request.deepen_since, &request.deepen_not)
840 && request.filter.is_none()
841 && all_wants_present(&local_db, &request.wants)?
842 {
843 return Ok(Vec::new());
844 }
845 let upload_request = build_http_upload_pack_request(
846 request.wants,
847 request.shallow,
848 request.deepen,
849 request.deepen_since,
850 request.deepen_not,
851 request.filter.as_ref(),
852 );
853 let haves = request_haves(
854 request.git_dir,
855 request.format,
856 request.omit_haves,
857 request.haves.clone(),
858 )?;
859 if request.deepen.is_none() {
860 let mut response = http_upload_pack_post(
861 request.client,
862 request.remote,
863 &upload_request,
864 haves,
865 credentials,
866 request.git_protocol,
867 request.post_buffer,
868 )?;
869 if request.promisor {
870 install_upload_pack_packfile_promisor_response_from_reader(
871 request.format,
872 &mut response.body,
873 &local_db,
874 request.max_input_size,
875 )?;
876 } else {
877 install_upload_pack_packfile_response_from_reader(
878 request.format,
879 &mut response.body,
880 &ProgressInstaller::new(&local_db, progress),
881 request.max_input_size,
882 )?;
883 }
884 return Ok(Vec::new());
885 }
886
887 let mut response = http_upload_pack_post(
888 request.client,
889 request.remote,
890 &upload_request,
891 haves,
892 credentials,
893 request.git_protocol,
894 request.post_buffer,
895 )?;
896 let shallow_info = if request.promisor {
897 let (shallow_info, _) = install_upload_pack_shallow_packfile_promisor_response_from_reader(
898 request.format,
899 &mut response.body,
900 &local_db,
901 request.max_input_size,
902 )?;
903 shallow_info
904 } else {
905 let (shallow_info, _) = install_upload_pack_shallow_packfile_response_from_reader(
906 request.format,
907 &mut response.body,
908 &ProgressInstaller::new(&local_db, progress),
909 request.max_input_size,
910 )?;
911 shallow_info
912 };
913 Ok(shallow_info)
914}
915
916pub fn install_fetch_pack_via_http_protocol_v2_fetch<C: HttpClient + ?Sized>(
917 request: HttpFetchPackRequest<'_, C>,
918 handshake: &TransportHandshake,
919 credentials: &mut dyn CredentialProvider,
920 progress: &mut dyn ProgressSink,
921) -> Result<Vec<ProtocolV2FetchShallowInfo>> {
922 if request.wants.is_empty() {
923 return Ok(Vec::new());
924 }
925 trace_protocol_v2_advertisement_read(handshake)?;
926 let local_db = FileObjectDatabase::from_git_dir(request.git_dir, request.format);
927 if !request_replays_shallow_boundary(request.deepen, request.deepen_since, &request.deepen_not)
928 && request.filter.is_none()
929 && all_wants_present(&local_db, &request.wants)?
930 {
931 return Ok(Vec::new());
932 }
933 let haves = request_negotiation_haves(
934 request.git_dir,
935 request.format,
936 request.omit_haves,
937 request.haves.clone(),
938 )?;
939 let mut fetch = protocol_v2_fetch_request_from_upload_pack_semantics(
940 request.wants,
941 haves,
942 request.shallow,
943 request.deepen,
944 request.deepen_since,
945 request.deepen_not,
946 request.deepen_relative,
947 request.filter.as_ref(),
948 handshake,
949 )?;
950 let sideband_all = fetch.sideband_all;
951 let all_haves = std::mem::take(&mut fetch.haves);
952 const INITIAL_HAVE_BATCH: usize = 16;
953 let mut sent_haves = all_haves.len().min(INITIAL_HAVE_BATCH);
954 fetch.haves = all_haves[..sent_haves].to_vec();
955 fetch.done = all_haves.is_empty();
956
957 loop {
958 let sent_done = fetch.done;
959 let wait_for_done = fetch.wait_for_done;
960 let mut response = http_protocol_v2_fetch_post(
961 request.client,
962 request.remote,
963 request.format,
964 handshake,
965 fetch.clone(),
966 credentials,
967 HttpRpcOptions {
968 git_protocol: request.git_protocol,
969 post_buffer: request.post_buffer,
970 },
971 )?;
972
973 let has_packfile = if sent_done {
974 true
975 } else {
976 let negotiation = read_protocol_v2_fetch_negotiation_response(
977 request.format,
978 &mut response.body,
979 sideband_all,
980 wait_for_done,
981 )?;
982 if negotiation.has_following_sections {
983 true
984 } else {
985 let ready = negotiation
986 .acknowledgments
987 .iter()
988 .any(|ack| matches!(ack, ProtocolV2FetchAcknowledgment::Ready));
989 if ready || sent_haves == all_haves.len() {
990 fetch.done = true;
991 fetch.haves = all_haves.clone();
992 } else {
993 sent_haves = (sent_haves * 2).min(all_haves.len());
994 fetch.haves = all_haves[..sent_haves].to_vec();
995 }
996 false
997 }
998 };
999 if !has_packfile {
1000 continue;
1001 }
1002
1003 let (header, _install) = if request.promisor {
1004 install_protocol_v2_fetch_promisor_response_from_reader(
1005 request.format,
1006 &mut response.body,
1007 sideband_all,
1008 &local_db,
1009 request.max_input_size,
1010 )?
1011 } else {
1012 install_protocol_v2_fetch_response_from_reader(
1013 request.format,
1014 &mut response.body,
1015 sideband_all,
1016 &ProgressInstaller::new(&local_db, progress),
1017 request.max_input_size,
1018 )?
1019 };
1020 return Ok(shallow_info_from_protocol_v2_fetch_header(&header));
1021 }
1022}
1023
1024pub(crate) fn http_post_buffer(config: Option<&GitConfig>) -> usize {
1025 const DEFAULT_HTTP_POST_BUFFER: usize = 1 << 20;
1026 config
1027 .and_then(|config| config.get("http", None, "postBuffer"))
1028 .and_then(sley_config::parse_config_int)
1029 .and_then(|bytes| usize::try_from(bytes).ok())
1030 .filter(|bytes| *bytes > 0)
1031 .unwrap_or(DEFAULT_HTTP_POST_BUFFER)
1032}
1033
1034fn http_post_rpc_body<C: HttpClient + ?Sized>(
1035 client: &C,
1036 url: &str,
1037 content_type: &str,
1038 headers: &[(&str, &str)],
1039 body: &[u8],
1040 post_buffer: usize,
1041) -> Result<HttpResponse> {
1042 if body.len() > post_buffer {
1043 let mut reader = std::io::Cursor::new(body);
1044 client.post_reader(url, content_type, headers, &mut reader)
1045 } else {
1046 client.post(url, content_type, headers, body)
1047 }
1048}
1049
1050fn all_wants_present(db: &FileObjectDatabase, wants: &[ObjectId]) -> Result<bool> {
1051 for want in wants {
1052 if !db.contains(want)? {
1053 return Ok(false);
1054 }
1055 }
1056 Ok(true)
1057}
1058
1059fn request_haves(
1060 git_dir: &Path,
1061 format: ObjectFormat,
1062 omit_haves: bool,
1063 custom_haves: Option<Vec<ObjectId>>,
1064) -> Result<Vec<ObjectId>> {
1065 if omit_haves {
1066 Ok(Vec::new())
1067 } else if let Some(haves) = custom_haves {
1068 Ok(haves)
1069 } else {
1070 crate::local::local_have_oids(git_dir, format)
1071 }
1072}
1073
1074fn request_negotiation_haves(
1075 git_dir: &Path,
1076 format: ObjectFormat,
1077 omit_haves: bool,
1078 custom_haves: Option<Vec<ObjectId>>,
1079) -> Result<Vec<ObjectId>> {
1080 if omit_haves {
1081 Ok(Vec::new())
1082 } else if let Some(haves) = custom_haves {
1083 Ok(haves)
1084 } else {
1085 crate::local::local_negotiation_have_oids(git_dir, format)
1086 }
1087}
1088
1089#[cfg(test)]
1090mod tests {
1091 use super::*;
1092 use sley_protocol::{
1093 ProtocolV2FetchResponseSection, ProtocolV2FetchShallowInfo, ProtocolV2LsRefsRecord,
1094 ProtocolVersion, RefAdvertisement, read_protocol_v2_fetch_response,
1095 write_protocol_v2_fetch_response, write_protocol_v2_ls_refs_response,
1096 };
1097 use std::sync::Mutex;
1098
1099 #[test]
1100 fn protocol_v1_header_is_sent_for_upload_and_receive_discovery() {
1101 let config = GitConfig::parse(b"[protocol]\n\tversion = 1\n").expect("config");
1102 assert_eq!(
1103 http_git_protocol_header_value_for_service(Some(&config), GitService::UploadPack)
1104 .expect("upload-pack header")
1105 .as_deref(),
1106 Some("version=1")
1107 );
1108 assert_eq!(
1109 http_git_protocol_header_value_for_service(Some(&config), GitService::ReceivePack)
1110 .expect("receive-pack header")
1111 .as_deref(),
1112 Some("version=1")
1113 );
1114
1115 let config = GitConfig::parse(b"[protocol]\n\tversion = 2\n").expect("config");
1116 assert_eq!(
1117 http_git_protocol_header_value_for_service(Some(&config), GitService::ReceivePack)
1118 .expect("receive-pack fallback"),
1119 None
1120 );
1121 }
1122
1123 struct PostModeClient {
1124 mode: Mutex<Option<&'static str>>,
1125 }
1126
1127 impl PostModeClient {
1128 fn response() -> Result<HttpResponse> {
1129 Ok(HttpResponse {
1130 status: 200,
1131 content_type: None,
1132 body: Box::new(std::io::empty()),
1133 })
1134 }
1135 }
1136
1137 impl HttpClient for PostModeClient {
1138 fn get(&self, _url: &str, _headers: &[(&str, &str)]) -> Result<HttpResponse> {
1139 Self::response()
1140 }
1141
1142 fn post(
1143 &self,
1144 _url: &str,
1145 _content_type: &str,
1146 _headers: &[(&str, &str)],
1147 _body: &[u8],
1148 ) -> Result<HttpResponse> {
1149 *self.mode.lock().expect("post mode") = Some("buffered");
1150 Self::response()
1151 }
1152
1153 fn post_reader(
1154 &self,
1155 _url: &str,
1156 _content_type: &str,
1157 _headers: &[(&str, &str)],
1158 body: &mut dyn Read,
1159 ) -> Result<HttpResponse> {
1160 let mut consumed = Vec::new();
1161 body.read_to_end(&mut consumed)?;
1162 *self.mode.lock().expect("post mode") = Some("chunked");
1163 Self::response()
1164 }
1165 }
1166
1167 #[test]
1168 fn http_post_buffer_selects_buffered_or_chunked_rpc_body() {
1169 let config = GitConfig::parse(b"[http]\n\tpostBuffer = 64k\n").expect("config");
1170 assert_eq!(http_post_buffer(Some(&config)), 64 * 1024);
1171
1172 let client = PostModeClient {
1173 mode: Mutex::new(None),
1174 };
1175 http_post_rpc_body(
1176 &client,
1177 "http://example.test/rpc",
1178 "request",
1179 &[],
1180 b"1234",
1181 4,
1182 )
1183 .expect("buffered post");
1184 assert_eq!(*client.mode.lock().expect("mode"), Some("buffered"));
1185
1186 http_post_rpc_body(
1187 &client,
1188 "http://example.test/rpc",
1189 "request",
1190 &[],
1191 b"12345",
1192 4,
1193 )
1194 .expect("chunked post");
1195 assert_eq!(*client.mode.lock().expect("mode"), Some("chunked"));
1196 }
1197
1198 #[test]
1199 fn v1_upload_request_omits_unadvertised_ofs_delta() {
1200 let capabilities = upload_pack_request_capabilities(None, false);
1201 assert!(
1202 capabilities
1203 .iter()
1204 .any(|capability| capability.name == "side-band-64k")
1205 );
1206 assert!(
1207 capabilities
1208 .iter()
1209 .all(|capability| capability.name != "ofs-delta")
1210 );
1211 }
1212
1213 #[test]
1214 fn v1_discovery_adopts_explicit_sha256_object_format() {
1215 let response = ServiceDiscoveryResponse {
1216 announcement: sley_transport::ServiceAnnouncement {
1217 service: GitService::UploadPack,
1218 },
1219 payload: ServiceDiscoveryPayload::AdvertisedRefs(RefAdvertisementSet {
1220 protocol: ProtocolVersion::V1,
1221 refs: vec![RefAdvertisement {
1222 oid: ObjectId::null(ObjectFormat::Sha256),
1223 name: "capabilities^{}".into(),
1224 capabilities: vec![Capability {
1225 name: "object-format".into(),
1226 value: Some("sha256".into()),
1227 }],
1228 }],
1229 shallow: Vec::new(),
1230 }),
1231 };
1232 let mut encoded = Vec::new();
1233 sley_transport::write_service_discovery_response(&mut encoded, &response)
1234 .expect("discovery response");
1235
1236 let parsed =
1237 read_http_service_discovery_response(ObjectFormat::Sha1, &mut encoded.as_slice())
1238 .expect("adaptive discovery");
1239 let ServiceDiscoveryPayload::AdvertisedRefs(set) = parsed.payload else {
1240 panic!("expected advertised refs");
1241 };
1242 assert_eq!(set.protocol, ProtocolVersion::V1);
1243 assert_eq!(set.refs[0].oid.format(), ObjectFormat::Sha256);
1244 }
1245
1246 struct PostRecorder {
1250 result_content_type: String,
1251 post_calls: std::sync::atomic::AtomicUsize,
1252 }
1253
1254 impl HttpClient for PostRecorder {
1255 fn get(&self, _url: &str, _headers: &[(&str, &str)]) -> Result<HttpResponse> {
1256 Err(GitError::Command(
1257 "recording client received an unexpected GET".into(),
1258 ))
1259 }
1260
1261 fn post(
1262 &self,
1263 _url: &str,
1264 _content_type: &str,
1265 _headers: &[(&str, &str)],
1266 _body: &[u8],
1267 ) -> Result<HttpResponse> {
1268 self.post_calls
1269 .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
1270 Ok(HttpResponse {
1271 status: 200,
1272 content_type: Some(self.result_content_type.clone()),
1273 body: Box::new(std::io::Cursor::new(Vec::new())),
1274 })
1275 }
1276 }
1277
1278 #[test]
1279 fn http_upload_pack_post_uses_injected_client() {
1280 let recorder = PostRecorder {
1281 result_content_type: sley_protocol::smart_http_rpc_result_content_type(
1282 GitService::UploadPack,
1283 )
1284 .expect("content type"),
1285 post_calls: std::sync::atomic::AtomicUsize::new(0),
1286 };
1287 let remote = parse_remote_url("http://example.invalid/repo.git").expect("url");
1288 let want = ObjectId::from_hex(
1289 ObjectFormat::Sha1,
1290 "1111111111111111111111111111111111111111",
1291 )
1292 .expect("oid");
1293 let request = UploadPackRequest {
1294 wants: vec![want],
1295 capabilities: Vec::new(),
1296 shallow: Vec::new(),
1297 deepen: None,
1298 deepen_since: None,
1299 deepen_not: Vec::new(),
1300 filter: None,
1301 };
1302 let mut credentials = crate::NoCredentials;
1303 let response = http_upload_pack_post(
1304 &recorder,
1305 &remote,
1306 &request,
1307 Vec::new(),
1308 &mut credentials,
1309 None,
1310 1 << 20,
1311 )
1312 .expect("post via injected client should succeed");
1313 assert_eq!(response.status, 200);
1314 assert_eq!(
1315 recorder
1316 .post_calls
1317 .load(std::sync::atomic::Ordering::Relaxed),
1318 1,
1319 "the injected client must own the pack-fetch POST dial"
1320 );
1321 }
1322
1323 fn sample_v2_handshake() -> TransportHandshake {
1324 TransportHandshake {
1325 protocol: ProtocolVersion::V2,
1326 capabilities: vec![
1327 Capability {
1328 name: "ls-refs".into(),
1329 value: Some("peel symrefs".into()),
1330 },
1331 Capability {
1332 name: "agent".into(),
1333 value: Some("git/2.54.0".into()),
1334 },
1335 Capability {
1336 name: "object-format".into(),
1337 value: Some("sha1".into()),
1338 },
1339 ],
1340 }
1341 }
1342
1343 #[test]
1344 fn protocol_v2_ls_refs_command_request_includes_agent_and_object_format() {
1345 let handshake = sample_v2_handshake();
1346 let command = protocol_v2_ls_refs_command_request(ObjectFormat::Sha1, &handshake)
1347 .expect("test operation should succeed");
1348 assert_eq!(command.command, "ls-refs");
1349 assert_eq!(
1350 command.capabilities,
1351 vec![
1352 Capability {
1353 name: "agent".into(),
1354 value: Some(format!("git/{UPSTREAM_GIT_COMPAT_VERSION}")),
1355 },
1356 Capability {
1357 name: "object-format".into(),
1358 value: Some("sha1".into()),
1359 },
1360 ]
1361 );
1362 assert_eq!(
1363 ProtocolV2LsRefsRequest::from_command_request(&command)
1364 .expect("test operation should succeed"),
1365 ProtocolV2LsRefsRequest {
1366 peel: true,
1367 symrefs: true,
1368 unborn: false,
1369 ref_prefixes: vec!["HEAD".into(), "refs/heads/".into(), "refs/tags/".into(),],
1370 }
1371 );
1372 }
1373
1374 #[test]
1375 fn protocol_v2_ls_refs_command_request_omits_object_format_when_unadvertised() {
1376 let handshake = TransportHandshake {
1377 protocol: ProtocolVersion::V2,
1378 capabilities: vec![
1379 Capability {
1380 name: "ls-refs".into(),
1381 value: None,
1382 },
1383 Capability {
1384 name: "agent".into(),
1385 value: Some("git/2.54.0".into()),
1386 },
1387 ],
1388 };
1389 let command = protocol_v2_ls_refs_command_request(ObjectFormat::Sha1, &handshake)
1390 .expect("test operation should succeed");
1391 assert_eq!(
1392 command.capabilities,
1393 vec![Capability {
1394 name: "agent".into(),
1395 value: Some(format!("git/{UPSTREAM_GIT_COMPAT_VERSION}")),
1396 }]
1397 );
1398 }
1399
1400 #[test]
1401 fn protocol_v2_ls_refs_round_trip_bridges_into_ref_advertisement_set() {
1402 let handshake = sample_v2_handshake();
1403 let command = protocol_v2_ls_refs_command_request(ObjectFormat::Sha1, &handshake)
1404 .expect("test operation should succeed");
1405 let head = ObjectId::from_hex(
1406 ObjectFormat::Sha1,
1407 "1111111111111111111111111111111111111111",
1408 )
1409 .expect("test operation should succeed");
1410 let tag = ObjectId::from_hex(
1411 ObjectFormat::Sha1,
1412 "2222222222222222222222222222222222222222",
1413 )
1414 .expect("test operation should succeed");
1415 let tag_peeled = ObjectId::from_hex(
1416 ObjectFormat::Sha1,
1417 "3333333333333333333333333333333333333333",
1418 )
1419 .expect("test operation should succeed");
1420 let records = vec![
1421 ProtocolV2LsRefsRecord::Ref(sley_protocol::ProtocolV2LsRefsRef {
1422 oid: head.clone(),
1423 name: "HEAD".into(),
1424 peeled: None,
1425 symref_target: Some("refs/heads/main".into()),
1426 attributes: Vec::new(),
1427 }),
1428 ProtocolV2LsRefsRecord::Ref(sley_protocol::ProtocolV2LsRefsRef {
1429 oid: head.clone(),
1430 name: "refs/heads/main".into(),
1431 peeled: None,
1432 symref_target: None,
1433 attributes: Vec::new(),
1434 }),
1435 ProtocolV2LsRefsRecord::Ref(sley_protocol::ProtocolV2LsRefsRef {
1436 oid: tag.clone(),
1437 name: "refs/tags/v1".into(),
1438 peeled: Some(tag_peeled.clone()),
1439 symref_target: None,
1440 attributes: Vec::new(),
1441 }),
1442 ];
1443
1444 let mut request_body = Vec::new();
1445 write_protocol_v2_command_request(&mut request_body, &command)
1446 .expect("test operation should succeed");
1447 let mut response_body = Vec::new();
1448 write_protocol_v2_ls_refs_response(&mut response_body, &records)
1449 .expect("test operation should succeed");
1450
1451 let set = read_protocol_v2_ls_refs_response_as_ref_advertisement_set(
1452 ObjectFormat::Sha1,
1453 &mut response_body.as_slice(),
1454 )
1455 .expect("test operation should succeed");
1456 assert_eq!(
1457 set,
1458 RefAdvertisementSet {
1459 protocol: ProtocolVersion::V2,
1460 refs: vec![
1461 RefAdvertisement {
1462 oid: head.clone(),
1463 name: "HEAD".into(),
1464 capabilities: vec![Capability {
1465 name: "symref".into(),
1466 value: Some("HEAD:refs/heads/main".into()),
1467 }],
1468 },
1469 RefAdvertisement {
1470 oid: head,
1471 name: "refs/heads/main".into(),
1472 capabilities: Vec::new(),
1473 },
1474 RefAdvertisement {
1475 oid: tag,
1476 name: "refs/tags/v1".into(),
1477 capabilities: Vec::new(),
1478 },
1479 RefAdvertisement {
1480 oid: tag_peeled,
1481 name: "refs/tags/v1^{}".into(),
1482 capabilities: Vec::new(),
1483 },
1484 ],
1485 shallow: Vec::new(),
1486 }
1487 );
1488 assert!(!request_body.is_empty());
1489 }
1490
1491 fn sample_v2_fetch_handshake() -> TransportHandshake {
1492 TransportHandshake {
1493 protocol: ProtocolVersion::V2,
1494 capabilities: vec![
1495 Capability {
1496 name: "fetch".into(),
1497 value: Some("shallow sideband-all".into()),
1498 },
1499 Capability {
1500 name: "agent".into(),
1501 value: Some("git/2.54.0".into()),
1502 },
1503 Capability {
1504 name: "object-format".into(),
1505 value: Some("sha1".into()),
1506 },
1507 ],
1508 }
1509 }
1510
1511 #[test]
1512 fn protocol_v2_fetch_command_request_includes_agent_object_format_and_deepen() {
1513 let handshake = sample_v2_fetch_handshake();
1514 let want = ObjectId::from_hex(
1515 ObjectFormat::Sha1,
1516 "1111111111111111111111111111111111111111",
1517 )
1518 .expect("test operation should succeed");
1519 let have = ObjectId::from_hex(
1520 ObjectFormat::Sha1,
1521 "2222222222222222222222222222222222222222",
1522 )
1523 .expect("test operation should succeed");
1524 let shallow = ObjectId::from_hex(
1525 ObjectFormat::Sha1,
1526 "3333333333333333333333333333333333333333",
1527 )
1528 .expect("test operation should succeed");
1529 let fetch = protocol_v2_fetch_request_from_upload_pack_semantics(
1530 vec![want.clone()],
1531 vec![have.clone()],
1532 vec![shallow.clone()],
1533 Some(3),
1534 None,
1535 Vec::new(),
1536 false,
1537 None,
1538 &handshake,
1539 )
1540 .expect("test operation should succeed");
1541 assert!(fetch.sideband_all);
1542 assert!(fetch.done);
1543 let command = protocol_v2_fetch_command_request(ObjectFormat::Sha1, &handshake, &fetch)
1544 .expect("test operation should succeed");
1545 assert_eq!(command.command, "fetch");
1546 assert_eq!(
1547 command.capabilities,
1548 vec![
1549 Capability {
1550 name: "agent".into(),
1551 value: Some(format!("git/{UPSTREAM_GIT_COMPAT_VERSION}")),
1552 },
1553 Capability {
1554 name: "object-format".into(),
1555 value: Some("sha1".into()),
1556 },
1557 ]
1558 );
1559 assert_eq!(
1560 ProtocolV2FetchRequest::from_command_request(ObjectFormat::Sha1, &command)
1561 .expect("test operation should succeed"),
1562 ProtocolV2FetchRequest {
1563 wants: vec![want],
1564 haves: vec![have],
1565 shallow: vec![shallow],
1566 deepen: Some(3),
1567 thin_pack: true,
1568 include_tag: true,
1569 ofs_delta: true,
1570 done: true,
1571 sideband_all: true,
1572 ..ProtocolV2FetchRequest::default()
1573 }
1574 );
1575 }
1576
1577 #[test]
1578 fn protocol_v2_fetch_round_trip_extracts_shallow_info_and_packfile_sections() {
1579 let handshake = sample_v2_fetch_handshake();
1580 let want = ObjectId::from_hex(
1581 ObjectFormat::Sha1,
1582 "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
1583 )
1584 .expect("test operation should succeed");
1585 let shallow = ObjectId::from_hex(
1586 ObjectFormat::Sha1,
1587 "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
1588 )
1589 .expect("test operation should succeed");
1590 let fetch = protocol_v2_fetch_request_from_upload_pack_semantics(
1591 vec![want],
1592 Vec::new(),
1593 vec![shallow.clone()],
1594 Some(1),
1595 None,
1596 Vec::new(),
1597 false,
1598 None,
1599 &handshake,
1600 )
1601 .expect("test operation should succeed");
1602 let command = protocol_v2_fetch_command_request(ObjectFormat::Sha1, &handshake, &fetch)
1603 .expect("test operation should succeed");
1604 let mut request_body = Vec::new();
1605 write_protocol_v2_command_request(&mut request_body, &command)
1606 .expect("test operation should succeed");
1607
1608 let sections = vec![
1609 ProtocolV2FetchResponseSection::ShallowInfo(vec![ProtocolV2FetchShallowInfo::Shallow(
1610 shallow,
1611 )]),
1612 ProtocolV2FetchResponseSection::Packfile(vec![b"PACK-test".to_vec()]),
1613 ];
1614 let mut response_body = Vec::new();
1615 write_protocol_v2_fetch_response(&mut response_body, §ions)
1616 .expect("test operation should succeed");
1617 let parsed =
1618 read_protocol_v2_fetch_response(ObjectFormat::Sha1, &mut response_body.as_slice())
1619 .expect("test operation should succeed");
1620 assert_eq!(parsed, sections);
1621 assert_eq!(
1622 parsed.first(),
1623 Some(&ProtocolV2FetchResponseSection::ShallowInfo(vec![
1624 ProtocolV2FetchShallowInfo::Shallow(
1625 ObjectId::from_hex(
1626 ObjectFormat::Sha1,
1627 "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
1628 )
1629 .expect("test operation should succeed")
1630 )
1631 ]))
1632 );
1633 assert!(!request_body.is_empty());
1634 }
1635}