1use std::path::Path;
16
17use sley_core::{
18 Capability, GitError, ObjectFormat, ObjectId, Result, UPSTREAM_GIT_COMPAT_VERSION,
19};
20use sley_fetch::{
21 install_protocol_v2_fetch_response_packfile,
22 install_protocol_v2_fetch_response_promisor_packfile,
23 install_upload_pack_raw_promisor_response, install_upload_pack_raw_response,
24};
25use sley_odb::FileObjectDatabase;
26use sley_protocol::{
27 GitService, ProtocolV2CommandOptions, ProtocolV2CommandRequest, ProtocolV2FetchRequest,
28 ProtocolV2FetchResponseSection, ProtocolV2FetchShallowInfo, ProtocolV2LsRefsRequest,
29 RefAdvertisement, RefAdvertisementSet, TransportHandshake, UploadPackFeatures,
30 UploadPackNegotiationRequest, UploadPackRawPackfileResponse, UploadPackRequest,
31 encode_protocol_v2_command_options, parse_protocol_v2_fetch_features,
32 parse_upload_pack_features, protocol_v2_object_format, read_protocol_v2_fetch_response,
33 read_protocol_v2_fetch_sideband_all_response,
34 read_protocol_v2_ls_refs_response_as_ref_advertisement_set,
35 read_upload_pack_raw_packfile_response,
36 read_upload_pack_shallow_info_and_raw_packfile_response, smart_http_advertisement_content_type,
37 smart_http_rpc_request_content_type, smart_http_rpc_result_content_type,
38 validate_protocol_v2_fetch_command_request, validate_protocol_v2_ls_refs_command_request,
39 write_protocol_v2_command_request, write_upload_pack_negotiation_request,
40 write_upload_pack_request,
41};
42use sley_transport::{
43 HttpClient, HttpResponse, RemoteTransport, RemoteUrl, ServiceDiscoveryPayload, UreqHttpClient,
44 git_credential_basic_authorization, http_smart_info_refs_url, http_smart_rpc_url,
45 parse_remote_url, read_service_discovery_response,
46};
47
48use crate::CredentialProvider;
49use crate::credentials::{credential_request_for_url, http_url_credential};
50
51pub fn remote_url_is_http(url: &str) -> Result<bool> {
57 Ok(matches!(
58 parse_remote_url(url)?.transport,
59 RemoteTransport::Http | RemoteTransport::Https
60 ))
61}
62
63pub fn new_http_client() -> UreqHttpClient {
65 UreqHttpClient::new()
66}
67
68pub fn http_send_with_auth(
74 remote: &RemoteUrl,
75 credentials: &mut dyn CredentialProvider,
76 mut perform: impl FnMut(Option<&str>) -> Result<HttpResponse>,
77) -> Result<HttpResponse> {
78 let initial = http_url_credential(remote);
79 let initial_header = match &initial {
80 Some(credential) => git_credential_basic_authorization(credential)?,
81 None => None,
82 };
83 let response = perform(initial_header.as_deref())?;
84 if response.status != 401 {
85 return Ok(response);
86 }
87 let mut request = credential_request_for_url(remote);
88 if request.username.is_none() {
89 request.username = initial.and_then(|credential| credential.username);
90 }
91 let Some(filled) = credentials.fill(request)? else {
92 return Ok(response);
93 };
94 let Some(header) = git_credential_basic_authorization(&filled)? else {
95 return Ok(response);
96 };
97 let retry = perform(Some(&header))?;
98 if retry.status != 401 {
99 credentials.approve(&filled)?;
100 } else {
101 credentials.reject(&filled)?;
102 }
103 Ok(retry)
104}
105
106pub fn http_authorization_headers(auth: Option<&str>) -> Vec<(&str, &str)> {
108 match auth {
109 Some(value) => vec![("Authorization", value)],
110 None => Vec::new(),
111 }
112}
113
114pub fn http_check_status(response: &HttpResponse, url: &str) -> Result<()> {
116 if (200..300).contains(&response.status) {
117 Ok(())
118 } else if response.status == 401 {
119 Err(GitError::Command(format!(
120 "authentication failed for {url}"
121 )))
122 } else {
123 Err(GitError::Command(format!(
124 "unexpected HTTP status {} for {url}",
125 response.status
126 )))
127 }
128}
129
130pub fn http_validate_content_type(response: &HttpResponse, expected: &str) -> Result<()> {
132 let actual = response
133 .content_type
134 .as_deref()
135 .unwrap_or("")
136 .split(';')
137 .next()
138 .unwrap_or("")
139 .trim();
140 if actual.eq_ignore_ascii_case(expected) {
141 Ok(())
142 } else {
143 Err(GitError::InvalidFormat(format!(
144 "unexpected content type {actual:?}, expected {expected:?}"
145 )))
146 }
147}
148
149#[derive(Debug, Clone, PartialEq, Eq)]
152pub struct HttpServiceAdvertisements {
153 pub set: RefAdvertisementSet,
154 pub handshake: Option<TransportHandshake>,
155}
156
157pub fn http_advertised_refs(
161 format: ObjectFormat,
162 mut response: HttpResponse,
163) -> Result<RefAdvertisementSet> {
164 let discovery = read_service_discovery_response(format, &mut response.body)?;
165 match discovery.payload {
166 ServiceDiscoveryPayload::AdvertisedRefs(set) => Ok(set),
167 ServiceDiscoveryPayload::ProtocolV2(_) => Err(GitError::Unsupported(
168 "protocol v2 advertisements over HTTP require an ls-refs RPC; use http_service_advertisements".into(),
169 )),
170 }
171}
172
173fn protocol_v2_ls_refs_command_request(
174 format: ObjectFormat,
175 handshake: &TransportHandshake,
176) -> Result<ProtocolV2CommandRequest> {
177 let ls_refs = ProtocolV2LsRefsRequest {
178 peel: true,
179 symrefs: true,
180 unborn: false,
181 ref_prefixes: vec!["HEAD".into(), "refs/heads/".into(), "refs/tags/".into()],
182 };
183 let mut command = ls_refs.to_command_request()?;
184 let mut options = ProtocolV2CommandOptions::default();
185 if handshake
186 .capabilities
187 .iter()
188 .any(|capability| capability.name == "agent")
189 {
190 options.agent = Some(format!("git/{UPSTREAM_GIT_COMPAT_VERSION}"));
191 }
192 if handshake
193 .capabilities
194 .iter()
195 .any(|capability| capability.name == "object-format")
196 {
197 let advertised_format = protocol_v2_object_format(&handshake.capabilities)?;
198 if advertised_format != format {
199 return Err(GitError::InvalidObjectId(format!(
200 "remote repository uses {}, local repository uses {}",
201 advertised_format.name(),
202 format.name()
203 )));
204 }
205 options.object_format = Some(format);
206 }
207 command.capabilities = encode_protocol_v2_command_options(&options)?;
208 validate_protocol_v2_ls_refs_command_request(handshake, &command)?;
209 Ok(command)
210}
211
212fn protocol_v2_fetch_command_request(
213 format: ObjectFormat,
214 handshake: &TransportHandshake,
215 fetch: &ProtocolV2FetchRequest,
216) -> Result<ProtocolV2CommandRequest> {
217 let mut command = fetch.to_command_request()?;
218 let mut options = ProtocolV2CommandOptions::default();
219 if handshake
220 .capabilities
221 .iter()
222 .any(|capability| capability.name == "agent")
223 {
224 options.agent = Some(format!("git/{UPSTREAM_GIT_COMPAT_VERSION}"));
225 }
226 if handshake
227 .capabilities
228 .iter()
229 .any(|capability| capability.name == "object-format")
230 {
231 let advertised_format = protocol_v2_object_format(&handshake.capabilities)?;
232 if advertised_format != format {
233 return Err(GitError::InvalidObjectId(format!(
234 "remote repository uses {}, local repository uses {}",
235 advertised_format.name(),
236 format.name()
237 )));
238 }
239 options.object_format = Some(format);
240 }
241 command.capabilities = encode_protocol_v2_command_options(&options)?;
242 validate_protocol_v2_fetch_command_request(handshake, format, &command)?;
243 Ok(command)
244}
245
246fn protocol_v2_fetch_request_from_upload_pack_semantics(
247 wants: Vec<ObjectId>,
248 haves: Vec<ObjectId>,
249 shallow: Vec<ObjectId>,
250 deepen: Option<u32>,
251 handshake: &TransportHandshake,
252) -> Result<ProtocolV2FetchRequest> {
253 let sideband_all = parse_protocol_v2_fetch_features(&handshake.capabilities)?
254 .map(|features| features.sideband_all)
255 .unwrap_or(false);
256 Ok(ProtocolV2FetchRequest {
257 wants,
258 haves,
259 shallow,
260 deepen,
261 done: true,
262 sideband_all,
263 ..ProtocolV2FetchRequest::default()
264 })
265}
266
267fn shallow_info_from_protocol_v2_fetch_sections(
268 sections: &[ProtocolV2FetchResponseSection],
269) -> Vec<ProtocolV2FetchShallowInfo> {
270 let mut shallow_info = Vec::new();
271 for section in sections {
272 if let ProtocolV2FetchResponseSection::ShallowInfo(entries) = section {
273 shallow_info.extend(entries.clone());
274 }
275 }
276 shallow_info
277}
278
279fn http_protocol_v2_ls_refs_advertisements(
280 client: &UreqHttpClient,
281 remote: &RemoteUrl,
282 format: ObjectFormat,
283 service: GitService,
284 handshake: TransportHandshake,
285 credentials: &mut dyn CredentialProvider,
286) -> Result<RefAdvertisementSet> {
287 let command = protocol_v2_ls_refs_command_request(format, &handshake)?;
288 let url = http_smart_rpc_url(remote, service)?;
289 let mut body = Vec::new();
290 write_protocol_v2_command_request(&mut body, &command)?;
291 let content_type = smart_http_rpc_request_content_type(service)?;
292 let mut response = http_send_with_auth(remote, credentials, |auth| {
293 client.post(
294 &url,
295 &content_type,
296 &http_authorization_headers(auth),
297 &body,
298 )
299 })?;
300 http_check_status(&response, &url)?;
301 http_validate_content_type(&response, &smart_http_rpc_result_content_type(service)?)?;
302 read_protocol_v2_ls_refs_response_as_ref_advertisement_set(format, &mut response.body)
303}
304
305pub fn http_service_advertisements(
308 client: &UreqHttpClient,
309 remote: &RemoteUrl,
310 format: ObjectFormat,
311 service: GitService,
312 credentials: &mut dyn CredentialProvider,
313) -> Result<HttpServiceAdvertisements> {
314 let url = http_smart_info_refs_url(remote, service)?;
315 let mut response = http_send_with_auth(remote, credentials, |auth| {
316 client.get(&url, &http_authorization_headers(auth))
317 })?;
318 http_check_status(&response, &url)?;
319 http_validate_content_type(&response, &smart_http_advertisement_content_type(service)?)?;
320 let discovery = read_service_discovery_response(format, &mut response.body)?;
321 match discovery.payload {
322 ServiceDiscoveryPayload::AdvertisedRefs(set) => Ok(HttpServiceAdvertisements {
323 set,
324 handshake: None,
325 }),
326 ServiceDiscoveryPayload::ProtocolV2(handshake) => {
327 let set = http_protocol_v2_ls_refs_advertisements(
328 client,
329 remote,
330 format,
331 service,
332 handshake.clone(),
333 credentials,
334 )?;
335 Ok(HttpServiceAdvertisements {
336 set,
337 handshake: Some(handshake),
338 })
339 }
340 }
341}
342
343pub fn http_upload_pack_advertisements(
345 client: &UreqHttpClient,
346 remote: &RemoteUrl,
347 format: ObjectFormat,
348 credentials: &mut dyn CredentialProvider,
349) -> Result<(Vec<RefAdvertisement>, UploadPackFeatures)> {
350 let discovered =
351 http_service_advertisements(client, remote, format, GitService::UploadPack, credentials)?;
352 let features = upload_pack_features_from_advertisements(&discovered.set.refs)?;
353 Ok((discovered.set.refs, features))
354}
355
356fn upload_pack_features_from_advertisements(
357 advertisements: &[RefAdvertisement],
358) -> Result<UploadPackFeatures> {
359 Ok(advertisements
360 .first()
361 .map(|advertisement| parse_upload_pack_features(&advertisement.capabilities))
362 .transpose()?
363 .unwrap_or_default())
364}
365
366fn http_upload_pack_post(
371 client: &UreqHttpClient,
372 remote: &RemoteUrl,
373 request: &UploadPackRequest,
374 haves: Vec<ObjectId>,
375 credentials: &mut dyn CredentialProvider,
376) -> Result<HttpResponse> {
377 let url = http_smart_rpc_url(remote, GitService::UploadPack)?;
378 let mut body = Vec::new();
379 write_upload_pack_request(&mut body, Some(request))?;
380 write_upload_pack_negotiation_request(
381 &mut body,
382 &UploadPackNegotiationRequest { haves, done: true },
383 )?;
384 let content_type = smart_http_rpc_request_content_type(GitService::UploadPack)?;
385 let response = http_send_with_auth(remote, credentials, |auth| {
386 client.post(
387 &url,
388 &content_type,
389 &http_authorization_headers(auth),
390 &body,
391 )
392 })?;
393 http_check_status(&response, &url)?;
394 http_validate_content_type(
395 &response,
396 &smart_http_rpc_result_content_type(GitService::UploadPack)?,
397 )?;
398 Ok(response)
399}
400
401pub fn http_upload_pack_fetch_response(
406 client: &UreqHttpClient,
407 remote: &RemoteUrl,
408 format: ObjectFormat,
409 request: UploadPackRequest,
410 haves: Vec<ObjectId>,
411 credentials: &mut dyn CredentialProvider,
412) -> Result<UploadPackRawPackfileResponse> {
413 let mut response = http_upload_pack_post(client, remote, &request, haves, credentials)?;
414 read_upload_pack_raw_packfile_response(format, &mut response.body)
415}
416
417pub fn http_upload_pack_shallow_fetch_response(
424 client: &UreqHttpClient,
425 remote: &RemoteUrl,
426 format: ObjectFormat,
427 request: UploadPackRequest,
428 haves: Vec<ObjectId>,
429 credentials: &mut dyn CredentialProvider,
430) -> Result<(
431 Vec<ProtocolV2FetchShallowInfo>,
432 UploadPackRawPackfileResponse,
433)> {
434 let mut response = http_upload_pack_post(client, remote, &request, haves, credentials)?;
435 read_upload_pack_shallow_info_and_raw_packfile_response(format, &mut response.body)
436}
437
438pub fn http_protocol_v2_fetch_response(
443 client: &UreqHttpClient,
444 remote: &RemoteUrl,
445 format: ObjectFormat,
446 handshake: &TransportHandshake,
447 fetch: ProtocolV2FetchRequest,
448 credentials: &mut dyn CredentialProvider,
449) -> Result<Vec<ProtocolV2FetchResponseSection>> {
450 let command = protocol_v2_fetch_command_request(format, handshake, &fetch)?;
451 let url = http_smart_rpc_url(remote, GitService::UploadPack)?;
452 let mut body = Vec::new();
453 write_protocol_v2_command_request(&mut body, &command)?;
454 let content_type = smart_http_rpc_request_content_type(GitService::UploadPack)?;
455 let mut response = http_send_with_auth(remote, credentials, |auth| {
456 client.post(
457 &url,
458 &content_type,
459 &http_authorization_headers(auth),
460 &body,
461 )
462 })?;
463 http_check_status(&response, &url)?;
464 http_validate_content_type(
465 &response,
466 &smart_http_rpc_result_content_type(GitService::UploadPack)?,
467 )?;
468 if fetch.sideband_all {
469 Ok(read_protocol_v2_fetch_sideband_all_response(format, &mut response.body)?.sections)
470 } else {
471 read_protocol_v2_fetch_response(format, &mut response.body)
472 }
473}
474
475pub struct HttpFetchPackRequest<'a> {
486 pub client: &'a UreqHttpClient,
488 pub git_dir: &'a Path,
490 pub format: ObjectFormat,
492 pub remote: &'a RemoteUrl,
494 pub wants: Vec<ObjectId>,
496 pub shallow: Vec<ObjectId>,
498 pub deepen: Option<u32>,
500 pub promisor: bool,
502}
503
504pub fn install_fetch_pack_via_http_upload_pack(
505 request: HttpFetchPackRequest<'_>,
506 credentials: &mut dyn CredentialProvider,
507) -> Result<Vec<ProtocolV2FetchShallowInfo>> {
508 if request.wants.is_empty() {
509 return Ok(Vec::new());
510 }
511 let local_db = FileObjectDatabase::from_git_dir(request.git_dir, request.format);
512 if request.deepen.is_none() && all_wants_present(&local_db, &request.wants)? {
516 return Ok(Vec::new());
517 }
518 let upload_request = UploadPackRequest {
519 wants: request.wants,
520 capabilities: shallow_request_capabilities(request.deepen),
521 shallow: request.shallow,
522 deepen: request.deepen,
523 ..UploadPackRequest::default()
524 };
525 let haves = crate::local::local_have_oids(request.git_dir, request.format)?;
526 let (shallow_info, response) = if request.deepen.is_some() {
527 http_upload_pack_shallow_fetch_response(
528 request.client,
529 request.remote,
530 request.format,
531 upload_request,
532 haves,
533 credentials,
534 )?
535 } else {
536 let response = http_upload_pack_fetch_response(
537 request.client,
538 request.remote,
539 request.format,
540 upload_request,
541 haves,
542 credentials,
543 )?;
544 (Vec::new(), response)
545 };
546 if request.promisor {
547 install_upload_pack_raw_promisor_response(&response, &local_db)?;
548 } else {
549 install_upload_pack_raw_response(&response, &local_db)?;
550 }
551 Ok(shallow_info)
552}
553
554pub fn install_fetch_pack_via_http_protocol_v2_fetch(
555 request: HttpFetchPackRequest<'_>,
556 handshake: &TransportHandshake,
557 credentials: &mut dyn CredentialProvider,
558) -> Result<Vec<ProtocolV2FetchShallowInfo>> {
559 if request.wants.is_empty() {
560 return Ok(Vec::new());
561 }
562 let local_db = FileObjectDatabase::from_git_dir(request.git_dir, request.format);
563 if request.deepen.is_none() && all_wants_present(&local_db, &request.wants)? {
564 return Ok(Vec::new());
565 }
566 let haves = crate::local::local_have_oids(request.git_dir, request.format)?;
567 let fetch = protocol_v2_fetch_request_from_upload_pack_semantics(
568 request.wants,
569 haves,
570 request.shallow,
571 request.deepen,
572 handshake,
573 )?;
574 let sections = http_protocol_v2_fetch_response(
575 request.client,
576 request.remote,
577 request.format,
578 handshake,
579 fetch,
580 credentials,
581 )?;
582 let shallow_info = shallow_info_from_protocol_v2_fetch_sections(§ions);
583 if request.promisor {
584 install_protocol_v2_fetch_response_promisor_packfile(§ions, &local_db)?;
585 } else {
586 install_protocol_v2_fetch_response_packfile(§ions, &local_db)?;
587 }
588 Ok(shallow_info)
589}
590
591fn all_wants_present(db: &FileObjectDatabase, wants: &[ObjectId]) -> Result<bool> {
592 for want in wants {
593 if !db.contains(want)? {
594 return Ok(false);
595 }
596 }
597 Ok(true)
598}
599
600fn shallow_request_capabilities(deepen: Option<u32>) -> Vec<Capability> {
605 if deepen.is_some() {
606 vec![Capability {
607 name: "shallow".into(),
608 value: None,
609 }]
610 } else {
611 Vec::new()
612 }
613}
614
615#[cfg(test)]
616mod tests {
617 use super::*;
618 use sley_protocol::{
619 ProtocolV2FetchResponseSection, ProtocolV2FetchShallowInfo, ProtocolV2LsRefsRecord,
620 ProtocolVersion, RefAdvertisement, read_protocol_v2_fetch_response,
621 write_protocol_v2_fetch_response, write_protocol_v2_ls_refs_response,
622 };
623
624 fn sample_v2_handshake() -> TransportHandshake {
625 TransportHandshake {
626 protocol: ProtocolVersion::V2,
627 capabilities: vec![
628 Capability {
629 name: "ls-refs".into(),
630 value: Some("peel symrefs".into()),
631 },
632 Capability {
633 name: "agent".into(),
634 value: Some("git/2.54.0".into()),
635 },
636 Capability {
637 name: "object-format".into(),
638 value: Some("sha1".into()),
639 },
640 ],
641 }
642 }
643
644 #[test]
645 fn protocol_v2_ls_refs_command_request_includes_agent_and_object_format() {
646 let handshake = sample_v2_handshake();
647 let command = protocol_v2_ls_refs_command_request(ObjectFormat::Sha1, &handshake)
648 .expect("test operation should succeed");
649 assert_eq!(command.command, "ls-refs");
650 assert_eq!(
651 command.capabilities,
652 vec![
653 Capability {
654 name: "agent".into(),
655 value: Some(format!("git/{UPSTREAM_GIT_COMPAT_VERSION}")),
656 },
657 Capability {
658 name: "object-format".into(),
659 value: Some("sha1".into()),
660 },
661 ]
662 );
663 assert_eq!(
664 ProtocolV2LsRefsRequest::from_command_request(&command)
665 .expect("test operation should succeed"),
666 ProtocolV2LsRefsRequest {
667 peel: true,
668 symrefs: true,
669 unborn: false,
670 ref_prefixes: vec!["HEAD".into(), "refs/heads/".into(), "refs/tags/".into(),],
671 }
672 );
673 }
674
675 #[test]
676 fn protocol_v2_ls_refs_command_request_omits_object_format_when_unadvertised() {
677 let handshake = TransportHandshake {
678 protocol: ProtocolVersion::V2,
679 capabilities: vec![
680 Capability {
681 name: "ls-refs".into(),
682 value: None,
683 },
684 Capability {
685 name: "agent".into(),
686 value: Some("git/2.54.0".into()),
687 },
688 ],
689 };
690 let command = protocol_v2_ls_refs_command_request(ObjectFormat::Sha1, &handshake)
691 .expect("test operation should succeed");
692 assert_eq!(
693 command.capabilities,
694 vec![Capability {
695 name: "agent".into(),
696 value: Some(format!("git/{UPSTREAM_GIT_COMPAT_VERSION}")),
697 }]
698 );
699 }
700
701 #[test]
702 fn protocol_v2_ls_refs_round_trip_bridges_into_ref_advertisement_set() {
703 let handshake = sample_v2_handshake();
704 let command = protocol_v2_ls_refs_command_request(ObjectFormat::Sha1, &handshake)
705 .expect("test operation should succeed");
706 let head = ObjectId::from_hex(
707 ObjectFormat::Sha1,
708 "1111111111111111111111111111111111111111",
709 )
710 .expect("test operation should succeed");
711 let tag = ObjectId::from_hex(
712 ObjectFormat::Sha1,
713 "2222222222222222222222222222222222222222",
714 )
715 .expect("test operation should succeed");
716 let tag_peeled = ObjectId::from_hex(
717 ObjectFormat::Sha1,
718 "3333333333333333333333333333333333333333",
719 )
720 .expect("test operation should succeed");
721 let records = vec![
722 ProtocolV2LsRefsRecord::Ref(sley_protocol::ProtocolV2LsRefsRef {
723 oid: head.clone(),
724 name: "HEAD".into(),
725 peeled: None,
726 symref_target: Some("refs/heads/main".into()),
727 attributes: Vec::new(),
728 }),
729 ProtocolV2LsRefsRecord::Ref(sley_protocol::ProtocolV2LsRefsRef {
730 oid: head.clone(),
731 name: "refs/heads/main".into(),
732 peeled: None,
733 symref_target: None,
734 attributes: Vec::new(),
735 }),
736 ProtocolV2LsRefsRecord::Ref(sley_protocol::ProtocolV2LsRefsRef {
737 oid: tag.clone(),
738 name: "refs/tags/v1".into(),
739 peeled: Some(tag_peeled.clone()),
740 symref_target: None,
741 attributes: Vec::new(),
742 }),
743 ];
744
745 let mut request_body = Vec::new();
746 write_protocol_v2_command_request(&mut request_body, &command)
747 .expect("test operation should succeed");
748 let mut response_body = Vec::new();
749 write_protocol_v2_ls_refs_response(&mut response_body, &records)
750 .expect("test operation should succeed");
751
752 let set = read_protocol_v2_ls_refs_response_as_ref_advertisement_set(
753 ObjectFormat::Sha1,
754 &mut response_body.as_slice(),
755 )
756 .expect("test operation should succeed");
757 assert_eq!(
758 set,
759 RefAdvertisementSet {
760 protocol: ProtocolVersion::V2,
761 refs: vec![
762 RefAdvertisement {
763 oid: head.clone(),
764 name: "HEAD".into(),
765 capabilities: vec![Capability {
766 name: "symref".into(),
767 value: Some("HEAD:refs/heads/main".into()),
768 }],
769 },
770 RefAdvertisement {
771 oid: head,
772 name: "refs/heads/main".into(),
773 capabilities: Vec::new(),
774 },
775 RefAdvertisement {
776 oid: tag,
777 name: "refs/tags/v1".into(),
778 capabilities: Vec::new(),
779 },
780 RefAdvertisement {
781 oid: tag_peeled,
782 name: "refs/tags/v1^{}".into(),
783 capabilities: Vec::new(),
784 },
785 ],
786 shallow: Vec::new(),
787 }
788 );
789 assert!(!request_body.is_empty());
790 }
791
792 fn sample_v2_fetch_handshake() -> TransportHandshake {
793 TransportHandshake {
794 protocol: ProtocolVersion::V2,
795 capabilities: vec![
796 Capability {
797 name: "fetch".into(),
798 value: Some("shallow sideband-all".into()),
799 },
800 Capability {
801 name: "agent".into(),
802 value: Some("git/2.54.0".into()),
803 },
804 Capability {
805 name: "object-format".into(),
806 value: Some("sha1".into()),
807 },
808 ],
809 }
810 }
811
812 #[test]
813 fn protocol_v2_fetch_command_request_includes_agent_object_format_and_deepen() {
814 let handshake = sample_v2_fetch_handshake();
815 let want = ObjectId::from_hex(
816 ObjectFormat::Sha1,
817 "1111111111111111111111111111111111111111",
818 )
819 .expect("test operation should succeed");
820 let have = ObjectId::from_hex(
821 ObjectFormat::Sha1,
822 "2222222222222222222222222222222222222222",
823 )
824 .expect("test operation should succeed");
825 let shallow = ObjectId::from_hex(
826 ObjectFormat::Sha1,
827 "3333333333333333333333333333333333333333",
828 )
829 .expect("test operation should succeed");
830 let fetch = protocol_v2_fetch_request_from_upload_pack_semantics(
831 vec![want.clone()],
832 vec![have.clone()],
833 vec![shallow.clone()],
834 Some(3),
835 &handshake,
836 )
837 .expect("test operation should succeed");
838 assert!(fetch.sideband_all);
839 assert!(fetch.done);
840 let command = protocol_v2_fetch_command_request(ObjectFormat::Sha1, &handshake, &fetch)
841 .expect("test operation should succeed");
842 assert_eq!(command.command, "fetch");
843 assert_eq!(
844 command.capabilities,
845 vec![
846 Capability {
847 name: "agent".into(),
848 value: Some(format!("git/{UPSTREAM_GIT_COMPAT_VERSION}")),
849 },
850 Capability {
851 name: "object-format".into(),
852 value: Some("sha1".into()),
853 },
854 ]
855 );
856 assert_eq!(
857 ProtocolV2FetchRequest::from_command_request(ObjectFormat::Sha1, &command)
858 .expect("test operation should succeed"),
859 ProtocolV2FetchRequest {
860 wants: vec![want],
861 haves: vec![have],
862 shallow: vec![shallow],
863 deepen: Some(3),
864 done: true,
865 sideband_all: true,
866 ..ProtocolV2FetchRequest::default()
867 }
868 );
869 }
870
871 #[test]
872 fn protocol_v2_fetch_round_trip_extracts_shallow_info_and_packfile_sections() {
873 let handshake = sample_v2_fetch_handshake();
874 let want = ObjectId::from_hex(
875 ObjectFormat::Sha1,
876 "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
877 )
878 .expect("test operation should succeed");
879 let shallow = ObjectId::from_hex(
880 ObjectFormat::Sha1,
881 "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
882 )
883 .expect("test operation should succeed");
884 let fetch = protocol_v2_fetch_request_from_upload_pack_semantics(
885 vec![want],
886 Vec::new(),
887 vec![shallow.clone()],
888 Some(1),
889 &handshake,
890 )
891 .expect("test operation should succeed");
892 let command = protocol_v2_fetch_command_request(ObjectFormat::Sha1, &handshake, &fetch)
893 .expect("test operation should succeed");
894 let mut request_body = Vec::new();
895 write_protocol_v2_command_request(&mut request_body, &command)
896 .expect("test operation should succeed");
897
898 let sections = vec![
899 ProtocolV2FetchResponseSection::ShallowInfo(vec![ProtocolV2FetchShallowInfo::Shallow(
900 shallow,
901 )]),
902 ProtocolV2FetchResponseSection::Packfile(vec![b"PACK-test".to_vec()]),
903 ];
904 let mut response_body = Vec::new();
905 write_protocol_v2_fetch_response(&mut response_body, §ions)
906 .expect("test operation should succeed");
907 let parsed =
908 read_protocol_v2_fetch_response(ObjectFormat::Sha1, &mut response_body.as_slice())
909 .expect("test operation should succeed");
910 assert_eq!(parsed, sections);
911 assert_eq!(
912 shallow_info_from_protocol_v2_fetch_sections(&parsed),
913 vec![ProtocolV2FetchShallowInfo::Shallow(
914 ObjectId::from_hex(
915 ObjectFormat::Sha1,
916 "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
917 )
918 .expect("test operation should succeed")
919 )]
920 );
921 assert!(!request_body.is_empty());
922 }
923}