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