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(
418 client: &UreqHttpClient,
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(
447 client: &UreqHttpClient,
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(
487 client: &UreqHttpClient,
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(
539 client: &UreqHttpClient,
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(
575 client: &UreqHttpClient,
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(
602 client: &UreqHttpClient,
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> {
642 pub client: &'a UreqHttpClient,
644 pub git_dir: &'a Path,
646 pub format: ObjectFormat,
648 pub remote: &'a RemoteUrl,
650 pub wants: Vec<ObjectId>,
652 pub haves: Option<Vec<ObjectId>>,
655 pub shallow: Vec<ObjectId>,
657 pub deepen: Option<u32>,
659 pub promisor: bool,
661 pub max_input_size: Option<u64>,
664 pub filter: Option<sley_odb::PackObjectFilter>,
665 pub deepen_since: Option<i64>,
666 pub deepen_not: Vec<String>,
667 pub deepen_relative: bool,
668 pub git_protocol: Option<&'a str>,
669 pub omit_haves: bool,
675}
676
677pub fn install_fetch_pack_via_http_upload_pack(
678 request: HttpFetchPackRequest<'_>,
679 credentials: &mut dyn CredentialProvider,
680 progress: &mut dyn ProgressSink,
681) -> Result<Vec<ProtocolV2FetchShallowInfo>> {
682 if request.wants.is_empty() {
683 return Ok(Vec::new());
684 }
685 let local_db = FileObjectDatabase::from_git_dir(request.git_dir, request.format);
686 if !request_replays_shallow_boundary(request.deepen, request.deepen_since, &request.deepen_not)
690 && request.filter.is_none()
691 && all_wants_present(&local_db, &request.wants)?
692 {
693 return Ok(Vec::new());
694 }
695 let upload_request = build_http_upload_pack_request(
696 request.wants,
697 request.shallow,
698 request.deepen,
699 request.deepen_since,
700 request.deepen_not,
701 request.filter.as_ref(),
702 );
703 let haves = request_haves(
704 request.git_dir,
705 request.format,
706 request.omit_haves,
707 request.haves.clone(),
708 )?;
709 if request.deepen.is_none() {
710 let mut response = http_upload_pack_post(
711 request.client,
712 request.remote,
713 &upload_request,
714 haves,
715 credentials,
716 request.git_protocol,
717 )?;
718 if request.promisor {
719 install_upload_pack_packfile_promisor_response_from_reader(
720 request.format,
721 &mut response.body,
722 &local_db,
723 request.max_input_size,
724 )?;
725 } else {
726 install_upload_pack_packfile_response_from_reader(
727 request.format,
728 &mut response.body,
729 &ProgressInstaller::new(&local_db, progress),
730 request.max_input_size,
731 )?;
732 }
733 return Ok(Vec::new());
734 }
735
736 let mut response = http_upload_pack_post(
737 request.client,
738 request.remote,
739 &upload_request,
740 haves,
741 credentials,
742 request.git_protocol,
743 )?;
744 let shallow_info = if request.promisor {
745 let (shallow_info, _) = install_upload_pack_shallow_packfile_promisor_response_from_reader(
746 request.format,
747 &mut response.body,
748 &local_db,
749 request.max_input_size,
750 )?;
751 shallow_info
752 } else {
753 let (shallow_info, _) = install_upload_pack_shallow_packfile_response_from_reader(
754 request.format,
755 &mut response.body,
756 &ProgressInstaller::new(&local_db, progress),
757 request.max_input_size,
758 )?;
759 shallow_info
760 };
761 Ok(shallow_info)
762}
763
764pub fn install_fetch_pack_via_http_protocol_v2_fetch(
765 request: HttpFetchPackRequest<'_>,
766 handshake: &TransportHandshake,
767 credentials: &mut dyn CredentialProvider,
768 progress: &mut dyn ProgressSink,
769) -> Result<Vec<ProtocolV2FetchShallowInfo>> {
770 if request.wants.is_empty() {
771 return Ok(Vec::new());
772 }
773 let local_db = FileObjectDatabase::from_git_dir(request.git_dir, request.format);
774 if !request_replays_shallow_boundary(request.deepen, request.deepen_since, &request.deepen_not)
775 && request.filter.is_none()
776 && all_wants_present(&local_db, &request.wants)?
777 {
778 return Ok(Vec::new());
779 }
780 let haves = request_haves(
781 request.git_dir,
782 request.format,
783 request.omit_haves,
784 request.haves.clone(),
785 )?;
786 let fetch = protocol_v2_fetch_request_from_upload_pack_semantics(
787 request.wants,
788 haves,
789 request.shallow,
790 request.deepen,
791 request.deepen_since,
792 request.deepen_not,
793 request.deepen_relative,
794 request.filter.as_ref(),
795 handshake,
796 )?;
797 let sideband_all = fetch.sideband_all;
798 let mut response = http_protocol_v2_fetch_post(
799 request.client,
800 request.remote,
801 request.format,
802 handshake,
803 fetch,
804 credentials,
805 request.git_protocol,
806 )?;
807 let (header, _install) = if request.promisor {
808 install_protocol_v2_fetch_promisor_response_from_reader(
809 request.format,
810 &mut response.body,
811 sideband_all,
812 &local_db,
813 request.max_input_size,
814 )?
815 } else {
816 install_protocol_v2_fetch_response_from_reader(
817 request.format,
818 &mut response.body,
819 sideband_all,
820 &ProgressInstaller::new(&local_db, progress),
821 request.max_input_size,
822 )?
823 };
824 Ok(shallow_info_from_protocol_v2_fetch_header(&header))
825}
826
827fn all_wants_present(db: &FileObjectDatabase, wants: &[ObjectId]) -> Result<bool> {
828 for want in wants {
829 if !db.contains(want)? {
830 return Ok(false);
831 }
832 }
833 Ok(true)
834}
835
836fn request_haves(
837 git_dir: &Path,
838 format: ObjectFormat,
839 omit_haves: bool,
840 custom_haves: Option<Vec<ObjectId>>,
841) -> Result<Vec<ObjectId>> {
842 if omit_haves {
843 Ok(Vec::new())
844 } else if let Some(haves) = custom_haves {
845 Ok(haves)
846 } else {
847 crate::local::local_have_oids(git_dir, format)
848 }
849}
850
851#[cfg(test)]
852mod tests {
853 use super::*;
854 use sley_protocol::{
855 read_protocol_v2_fetch_response, write_protocol_v2_fetch_response,
856 write_protocol_v2_ls_refs_response, ProtocolV2FetchResponseSection,
857 ProtocolV2FetchShallowInfo, ProtocolV2LsRefsRecord, ProtocolVersion, RefAdvertisement,
858 };
859
860 fn sample_v2_handshake() -> TransportHandshake {
861 TransportHandshake {
862 protocol: ProtocolVersion::V2,
863 capabilities: vec![
864 Capability {
865 name: "ls-refs".into(),
866 value: Some("peel symrefs".into()),
867 },
868 Capability {
869 name: "agent".into(),
870 value: Some("git/2.54.0".into()),
871 },
872 Capability {
873 name: "object-format".into(),
874 value: Some("sha1".into()),
875 },
876 ],
877 }
878 }
879
880 #[test]
881 fn protocol_v2_ls_refs_command_request_includes_agent_and_object_format() {
882 let handshake = sample_v2_handshake();
883 let command = protocol_v2_ls_refs_command_request(ObjectFormat::Sha1, &handshake)
884 .expect("test operation should succeed");
885 assert_eq!(command.command, "ls-refs");
886 assert_eq!(
887 command.capabilities,
888 vec![
889 Capability {
890 name: "agent".into(),
891 value: Some(format!("git/{UPSTREAM_GIT_COMPAT_VERSION}")),
892 },
893 Capability {
894 name: "object-format".into(),
895 value: Some("sha1".into()),
896 },
897 ]
898 );
899 assert_eq!(
900 ProtocolV2LsRefsRequest::from_command_request(&command)
901 .expect("test operation should succeed"),
902 ProtocolV2LsRefsRequest {
903 peel: true,
904 symrefs: true,
905 unborn: false,
906 ref_prefixes: vec!["HEAD".into(), "refs/heads/".into(), "refs/tags/".into(),],
907 }
908 );
909 }
910
911 #[test]
912 fn protocol_v2_ls_refs_command_request_omits_object_format_when_unadvertised() {
913 let handshake = TransportHandshake {
914 protocol: ProtocolVersion::V2,
915 capabilities: vec![
916 Capability {
917 name: "ls-refs".into(),
918 value: None,
919 },
920 Capability {
921 name: "agent".into(),
922 value: Some("git/2.54.0".into()),
923 },
924 ],
925 };
926 let command = protocol_v2_ls_refs_command_request(ObjectFormat::Sha1, &handshake)
927 .expect("test operation should succeed");
928 assert_eq!(
929 command.capabilities,
930 vec![Capability {
931 name: "agent".into(),
932 value: Some(format!("git/{UPSTREAM_GIT_COMPAT_VERSION}")),
933 }]
934 );
935 }
936
937 #[test]
938 fn protocol_v2_ls_refs_round_trip_bridges_into_ref_advertisement_set() {
939 let handshake = sample_v2_handshake();
940 let command = protocol_v2_ls_refs_command_request(ObjectFormat::Sha1, &handshake)
941 .expect("test operation should succeed");
942 let head = ObjectId::from_hex(
943 ObjectFormat::Sha1,
944 "1111111111111111111111111111111111111111",
945 )
946 .expect("test operation should succeed");
947 let tag = ObjectId::from_hex(
948 ObjectFormat::Sha1,
949 "2222222222222222222222222222222222222222",
950 )
951 .expect("test operation should succeed");
952 let tag_peeled = ObjectId::from_hex(
953 ObjectFormat::Sha1,
954 "3333333333333333333333333333333333333333",
955 )
956 .expect("test operation should succeed");
957 let records = vec![
958 ProtocolV2LsRefsRecord::Ref(sley_protocol::ProtocolV2LsRefsRef {
959 oid: head.clone(),
960 name: "HEAD".into(),
961 peeled: None,
962 symref_target: Some("refs/heads/main".into()),
963 attributes: Vec::new(),
964 }),
965 ProtocolV2LsRefsRecord::Ref(sley_protocol::ProtocolV2LsRefsRef {
966 oid: head.clone(),
967 name: "refs/heads/main".into(),
968 peeled: None,
969 symref_target: None,
970 attributes: Vec::new(),
971 }),
972 ProtocolV2LsRefsRecord::Ref(sley_protocol::ProtocolV2LsRefsRef {
973 oid: tag.clone(),
974 name: "refs/tags/v1".into(),
975 peeled: Some(tag_peeled.clone()),
976 symref_target: None,
977 attributes: Vec::new(),
978 }),
979 ];
980
981 let mut request_body = Vec::new();
982 write_protocol_v2_command_request(&mut request_body, &command)
983 .expect("test operation should succeed");
984 let mut response_body = Vec::new();
985 write_protocol_v2_ls_refs_response(&mut response_body, &records)
986 .expect("test operation should succeed");
987
988 let set = read_protocol_v2_ls_refs_response_as_ref_advertisement_set(
989 ObjectFormat::Sha1,
990 &mut response_body.as_slice(),
991 )
992 .expect("test operation should succeed");
993 assert_eq!(
994 set,
995 RefAdvertisementSet {
996 protocol: ProtocolVersion::V2,
997 refs: vec![
998 RefAdvertisement {
999 oid: head.clone(),
1000 name: "HEAD".into(),
1001 capabilities: vec![Capability {
1002 name: "symref".into(),
1003 value: Some("HEAD:refs/heads/main".into()),
1004 }],
1005 },
1006 RefAdvertisement {
1007 oid: head,
1008 name: "refs/heads/main".into(),
1009 capabilities: Vec::new(),
1010 },
1011 RefAdvertisement {
1012 oid: tag,
1013 name: "refs/tags/v1".into(),
1014 capabilities: Vec::new(),
1015 },
1016 RefAdvertisement {
1017 oid: tag_peeled,
1018 name: "refs/tags/v1^{}".into(),
1019 capabilities: Vec::new(),
1020 },
1021 ],
1022 shallow: Vec::new(),
1023 }
1024 );
1025 assert!(!request_body.is_empty());
1026 }
1027
1028 fn sample_v2_fetch_handshake() -> TransportHandshake {
1029 TransportHandshake {
1030 protocol: ProtocolVersion::V2,
1031 capabilities: vec![
1032 Capability {
1033 name: "fetch".into(),
1034 value: Some("shallow sideband-all".into()),
1035 },
1036 Capability {
1037 name: "agent".into(),
1038 value: Some("git/2.54.0".into()),
1039 },
1040 Capability {
1041 name: "object-format".into(),
1042 value: Some("sha1".into()),
1043 },
1044 ],
1045 }
1046 }
1047
1048 #[test]
1049 fn protocol_v2_fetch_command_request_includes_agent_object_format_and_deepen() {
1050 let handshake = sample_v2_fetch_handshake();
1051 let want = ObjectId::from_hex(
1052 ObjectFormat::Sha1,
1053 "1111111111111111111111111111111111111111",
1054 )
1055 .expect("test operation should succeed");
1056 let have = ObjectId::from_hex(
1057 ObjectFormat::Sha1,
1058 "2222222222222222222222222222222222222222",
1059 )
1060 .expect("test operation should succeed");
1061 let shallow = ObjectId::from_hex(
1062 ObjectFormat::Sha1,
1063 "3333333333333333333333333333333333333333",
1064 )
1065 .expect("test operation should succeed");
1066 let fetch = protocol_v2_fetch_request_from_upload_pack_semantics(
1067 vec![want.clone()],
1068 vec![have.clone()],
1069 vec![shallow.clone()],
1070 Some(3),
1071 None,
1072 Vec::new(),
1073 false,
1074 None,
1075 &handshake,
1076 )
1077 .expect("test operation should succeed");
1078 assert!(fetch.sideband_all);
1079 assert!(fetch.done);
1080 let command = protocol_v2_fetch_command_request(ObjectFormat::Sha1, &handshake, &fetch)
1081 .expect("test operation should succeed");
1082 assert_eq!(command.command, "fetch");
1083 assert_eq!(
1084 command.capabilities,
1085 vec![
1086 Capability {
1087 name: "agent".into(),
1088 value: Some(format!("git/{UPSTREAM_GIT_COMPAT_VERSION}")),
1089 },
1090 Capability {
1091 name: "object-format".into(),
1092 value: Some("sha1".into()),
1093 },
1094 ]
1095 );
1096 assert_eq!(
1097 ProtocolV2FetchRequest::from_command_request(ObjectFormat::Sha1, &command)
1098 .expect("test operation should succeed"),
1099 ProtocolV2FetchRequest {
1100 wants: vec![want],
1101 haves: vec![have],
1102 shallow: vec![shallow],
1103 deepen: Some(3),
1104 thin_pack: true,
1105 include_tag: true,
1106 ofs_delta: true,
1107 done: true,
1108 sideband_all: true,
1109 ..ProtocolV2FetchRequest::default()
1110 }
1111 );
1112 }
1113
1114 #[test]
1115 fn protocol_v2_fetch_round_trip_extracts_shallow_info_and_packfile_sections() {
1116 let handshake = sample_v2_fetch_handshake();
1117 let want = ObjectId::from_hex(
1118 ObjectFormat::Sha1,
1119 "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
1120 )
1121 .expect("test operation should succeed");
1122 let shallow = ObjectId::from_hex(
1123 ObjectFormat::Sha1,
1124 "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
1125 )
1126 .expect("test operation should succeed");
1127 let fetch = protocol_v2_fetch_request_from_upload_pack_semantics(
1128 vec![want],
1129 Vec::new(),
1130 vec![shallow.clone()],
1131 Some(1),
1132 None,
1133 Vec::new(),
1134 false,
1135 None,
1136 &handshake,
1137 )
1138 .expect("test operation should succeed");
1139 let command = protocol_v2_fetch_command_request(ObjectFormat::Sha1, &handshake, &fetch)
1140 .expect("test operation should succeed");
1141 let mut request_body = Vec::new();
1142 write_protocol_v2_command_request(&mut request_body, &command)
1143 .expect("test operation should succeed");
1144
1145 let sections = vec![
1146 ProtocolV2FetchResponseSection::ShallowInfo(vec![ProtocolV2FetchShallowInfo::Shallow(
1147 shallow,
1148 )]),
1149 ProtocolV2FetchResponseSection::Packfile(vec![b"PACK-test".to_vec()]),
1150 ];
1151 let mut response_body = Vec::new();
1152 write_protocol_v2_fetch_response(&mut response_body, §ions)
1153 .expect("test operation should succeed");
1154 let parsed =
1155 read_protocol_v2_fetch_response(ObjectFormat::Sha1, &mut response_body.as_slice())
1156 .expect("test operation should succeed");
1157 assert_eq!(parsed, sections);
1158 assert_eq!(
1159 parsed.first(),
1160 Some(&ProtocolV2FetchResponseSection::ShallowInfo(vec![
1161 ProtocolV2FetchShallowInfo::Shallow(
1162 ObjectId::from_hex(
1163 ObjectFormat::Sha1,
1164 "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
1165 )
1166 .expect("test operation should succeed")
1167 )
1168 ]))
1169 );
1170 assert!(!request_body.is_empty());
1171 }
1172}