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