1use std::collections::HashSet;
34use std::io::{Cursor, Read, Write};
35use std::path::Path;
36
37use crate::error::{Error, Result};
38use crate::fetch::Progress;
39use crate::fetch_negotiator::SkippingNegotiator;
40use crate::objects::ObjectId;
41use crate::pkt_line;
42use crate::protocol_v2;
43use crate::refspec::{parse_fetch_refspec, RefspecItem};
44use crate::transfer::{
45 classify_update, match_positive, open_odb, prune_tracking_refs, ref_excluded, refspecs_force,
46 FetchOptions, FetchOutcome, RefUpdate, TagMode, UpdateMode,
47};
48use crate::transport::{Advertisement, ConnectOptions, Connection, Service, Transport};
49
50#[cfg(feature = "http-ureq")]
51pub mod ureq_client;
52
53pub trait HttpClient: Send + Sync {
65 fn get(&self, url: &str, git_protocol: Option<&str>) -> Result<Vec<u8>>;
71
72 fn post(
79 &self,
80 url: &str,
81 content_type: &str,
82 accept: &str,
83 body: &[u8],
84 git_protocol: Option<&str>,
85 ) -> Result<Vec<u8>>;
86
87 fn get_with_final_url(
103 &self,
104 url: &str,
105 git_protocol: Option<&str>,
106 ) -> Result<(Vec<u8>, Option<String>)> {
107 Ok((self.get(url, git_protocol)?, None))
108 }
109
110 fn git_protocol_header(&self) -> Option<&str> {
113 None
114 }
115
116 fn smart_http_enabled(&self) -> bool {
119 true
120 }
121}
122
123impl<C: HttpClient> HttpClient for std::sync::Arc<C> {
126 fn get(&self, url: &str, git_protocol: Option<&str>) -> Result<Vec<u8>> {
127 (**self).get(url, git_protocol)
128 }
129
130 fn post(
131 &self,
132 url: &str,
133 content_type: &str,
134 accept: &str,
135 body: &[u8],
136 git_protocol: Option<&str>,
137 ) -> Result<Vec<u8>> {
138 (**self).post(url, content_type, accept, body, git_protocol)
139 }
140
141 fn get_with_final_url(
142 &self,
143 url: &str,
144 git_protocol: Option<&str>,
145 ) -> Result<(Vec<u8>, Option<String>)> {
146 (**self).get_with_final_url(url, git_protocol)
147 }
148
149 fn git_protocol_header(&self) -> Option<&str> {
150 (**self).git_protocol_header()
151 }
152
153 fn smart_http_enabled(&self) -> bool {
154 (**self).smart_http_enabled()
155 }
156}
157
158const UPLOAD_PACK: &str = "git-upload-pack";
159
160fn strip_service_advertisement(body: &[u8]) -> Result<&[u8]> {
169 let mut cur = Cursor::new(body);
170 let start = cur.position();
171 match pkt_line::read_packet(&mut cur)? {
172 Some(pkt_line::Packet::Data(line)) if line.starts_with("# service=") => {
173 match pkt_line::read_packet(&mut cur)? {
175 Some(pkt_line::Packet::Flush) | None => {}
176 _ => {
177 return Ok(body);
179 }
180 }
181 let pos = cur.position() as usize;
182 Ok(&body[pos..])
183 }
184 _ => {
185 cur.set_position(start);
186 Ok(body)
187 }
188 }
189}
190
191#[derive(Clone, Debug)]
193struct AdvRef {
194 name: String,
195 oid: ObjectId,
196}
197
198struct Discovery {
201 protocol_version: u8,
202 refs: Vec<AdvRef>,
203 caps: HashSet<String>,
204 head_symref: Option<String>,
205 object_format: String,
206}
207
208fn parse_advertisement(body: &[u8]) -> Result<Discovery> {
216 let mut cur = Cursor::new(body);
217
218 let first = match pkt_line::read_packet(&mut cur)? {
220 None | Some(pkt_line::Packet::Flush) => {
221 return Ok(Discovery {
223 protocol_version: 0,
224 refs: Vec::new(),
225 caps: HashSet::new(),
226 head_symref: None,
227 object_format: "sha1".to_owned(),
228 });
229 }
230 Some(pkt_line::Packet::Data(s)) => s,
231 Some(other) => {
232 return Err(Error::Message(format!(
233 "unexpected first advertisement packet: {other:?}"
234 )))
235 }
236 };
237 if first.trim_end() == "version 2" {
238 let mut caps = HashSet::new();
240 loop {
241 match pkt_line::read_packet(&mut cur)? {
242 None | Some(pkt_line::Packet::Flush) => break,
243 Some(pkt_line::Packet::Data(s)) => {
244 caps.insert(s.trim_end().to_owned());
245 }
246 Some(_) => break,
247 }
248 }
249 let object_format = caps
250 .iter()
251 .find_map(|c| c.strip_prefix("object-format="))
252 .unwrap_or("sha1")
253 .to_owned();
254 return Ok(Discovery {
255 protocol_version: 2,
256 refs: Vec::new(),
257 caps,
258 head_symref: None,
259 object_format,
260 });
261 }
262
263 cur.set_position(0);
265 let mut refs = Vec::new();
266 let mut caps: HashSet<String> = HashSet::new();
267 let mut head_symref = None;
268 let mut first_ref_line = true;
269 loop {
270 match pkt_line::read_packet(&mut cur)? {
271 None | Some(pkt_line::Packet::Flush) => break,
272 Some(pkt_line::Packet::Data(line)) => {
273 let line = line.trim_end_matches('\n');
274 if line.starts_with("version ") {
275 continue;
276 }
277 if line.starts_with("shallow ") || line.starts_with("unshallow ") {
278 continue;
279 }
280 let (payload, cap_part) = match line.split_once('\0') {
281 Some((p, c)) => (p.trim(), Some(c)),
282 None => (line.trim(), None),
283 };
284 let Some((oid_hex, refname)) =
285 payload.split_once('\t').or_else(|| payload.split_once(' '))
286 else {
287 continue;
288 };
289 let oid_hex = oid_hex.trim();
290 let refname = refname.trim();
291 if first_ref_line {
292 if let Some(raw_caps) = cap_part {
293 for cap in raw_caps.split_whitespace() {
294 if let Some(target) = cap.strip_prefix("symref=HEAD:") {
295 head_symref = Some(target.to_owned());
296 }
297 caps.insert(cap.to_owned());
298 }
299 }
300 first_ref_line = false;
301 }
302 if refname.is_empty() {
303 continue;
304 }
305 if oid_hex.bytes().all(|b| b == b'0') {
307 continue;
308 }
309 let oid = ObjectId::from_hex(oid_hex).map_err(|e| {
310 Error::Message(format!("bad oid in advertisement: {oid_hex}: {e}"))
311 })?;
312 refs.push(AdvRef {
313 name: refname.to_owned(),
314 oid,
315 });
316 }
317 Some(other) => {
318 return Err(Error::Message(format!(
319 "unexpected packet in advertisement: {other:?}"
320 )))
321 }
322 }
323 }
324 let object_format = caps
325 .iter()
326 .find_map(|c| c.strip_prefix("object-format="))
327 .unwrap_or("sha1")
328 .to_owned();
329 Ok(Discovery {
330 protocol_version: if caps.contains("version 1") { 1 } else { 0 },
331 refs,
332 caps,
333 head_symref,
334 object_format,
335 })
336}
337
338fn info_refs_url(repo_url: &str) -> String {
340 let base = repo_url.trim_end_matches('/');
341 let mut url = format!("{base}/info/refs");
342 url.push_str(if url.contains('?') { "&" } else { "?" });
343 url.push_str("service=");
344 url.push_str(UPLOAD_PACK);
345 url
346}
347
348#[must_use]
362pub fn rebased_base_from_redirect(original_base: &str, final_url: Option<&str>) -> Option<String> {
363 let final_url = final_url?;
364 let final_path = final_url.split('?').next().unwrap_or(final_url);
365 let new_base = final_path.strip_suffix("/info/refs")?.trim_end_matches('/');
366 if new_base.is_empty() || new_base == original_base.trim_end_matches('/') {
367 return None;
368 }
369 Some(new_base.to_owned())
370}
371
372fn upload_pack_url(repo_url: &str) -> String {
374 let base = repo_url.trim_end_matches('/');
375 format!("{base}/{UPLOAD_PACK}")
376}
377
378pub struct SmartHttpConnection {
387 repo_url: String,
388 adv_refs: Vec<(String, ObjectId)>,
389 caps: Vec<String>,
390 head_symref: Option<String>,
391 protocol_version: u8,
392 object_format: String,
393 service: Service,
395 empty_reader: Cursor<Vec<u8>>,
396 sink: Vec<u8>,
397}
398
399impl SmartHttpConnection {
400 #[must_use]
402 pub fn repo_url(&self) -> &str {
403 &self.repo_url
404 }
405
406 #[must_use]
408 pub fn object_format(&self) -> &str {
409 &self.object_format
410 }
411
412 #[must_use]
414 pub fn service(&self) -> Service {
415 self.service
416 }
417}
418
419impl Connection for SmartHttpConnection {
420 fn reader(&mut self) -> &mut dyn Read {
421 &mut self.empty_reader
422 }
423
424 fn writer(&mut self) -> &mut dyn Write {
425 &mut self.sink
426 }
427
428 fn advertised_refs(&self) -> &[(String, ObjectId)] {
429 &self.adv_refs
430 }
431
432 fn capabilities(&self) -> &[String] {
433 &self.caps
434 }
435
436 fn head_symref(&self) -> Option<&str> {
437 self.head_symref.as_deref()
438 }
439
440 fn protocol_version(&self) -> u8 {
441 self.protocol_version
442 }
443}
444
445pub struct SmartHttpTransport<C: HttpClient> {
452 client: C,
453}
454
455impl<C: HttpClient> SmartHttpTransport<C> {
456 pub fn new(client: C) -> Self {
458 Self { client }
459 }
460
461 pub fn client(&self) -> &C {
463 &self.client
464 }
465
466 pub fn push(
483 &self,
484 local_git_dir: &Path,
485 repo_url: &str,
486 refs: &[crate::transfer::PushRefSpec],
487 opts: &crate::transfer::PushOptions,
488 progress: &mut dyn Progress,
489 ) -> Result<crate::transfer::PushOutcome> {
490 crate::push::push_http(&self.client, local_git_dir, repo_url, refs, opts, progress)
491 }
492
493 fn discover(
500 &self,
501 repo_url: &str,
502 _service: Service,
503 git_protocol: Option<&str>,
504 ) -> Result<Discovery> {
505 let url = info_refs_url(repo_url);
506 let gp = git_protocol.or_else(|| self.client.git_protocol_header());
507 let body = self.client.get(&url, gp)?;
508 let stripped = strip_service_advertisement(&body)?;
509 parse_advertisement(stripped)
510 }
511}
512
513fn git_protocol_for_version(version: u8) -> Option<String> {
516 if version >= 1 {
517 Some(format!("version={version}"))
518 } else {
519 None
520 }
521}
522
523impl<C: HttpClient> Transport for SmartHttpTransport<C> {
524 fn connect(
525 &self,
526 url: &str,
527 service: Service,
528 opts: &ConnectOptions,
529 ) -> Result<Box<dyn Connection>> {
530 crate::net_trace::net_trace!(
535 "http(s) discover {url} (service={}, request protocol v{})",
536 service.wire_name(),
537 opts.protocol_version
538 );
539 let gp = git_protocol_for_version(opts.protocol_version);
540 let disc = self.discover(url, service, gp.as_deref())?;
541 let adv_refs: Vec<(String, ObjectId)> = disc
542 .refs
543 .iter()
544 .filter(|r| r.name != "HEAD" && !r.name.ends_with("^{}"))
545 .map(|r| (r.name.clone(), r.oid))
546 .collect();
547 let caps: Vec<String> = disc.caps.iter().cloned().collect();
548 crate::net_trace::net_trace!(
549 "http(s) discovered: protocol v{}, {} ref(s) advertised",
550 disc.protocol_version,
551 adv_refs.len()
552 );
553 Ok(Box::new(SmartHttpConnection {
554 repo_url: url.to_owned(),
555 adv_refs,
556 caps,
557 head_symref: disc.head_symref,
558 protocol_version: disc.protocol_version,
559 object_format: disc.object_format,
560 service,
561 empty_reader: Cursor::new(Vec::new()),
562 sink: Vec::new(),
563 }))
564 }
565}
566
567fn read_pkt_payload(r: &mut impl Read) -> std::io::Result<Option<Vec<u8>>> {
569 let mut len_buf = [0u8; 4];
570 match r.read_exact(&mut len_buf) {
571 Ok(()) => {}
572 Err(e) if e.kind() == std::io::ErrorKind::UnexpectedEof => return Ok(None),
573 Err(e) => return Err(e),
574 }
575 let len_str = std::str::from_utf8(&len_buf)
576 .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?;
577 let len = usize::from_str_radix(len_str, 16)
578 .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?;
579 match len {
580 0..=2 => Ok(None),
581 n if n <= 4 => Err(std::io::Error::new(
582 std::io::ErrorKind::InvalidData,
583 format!("invalid pkt-line length: {n}"),
584 )),
585 n => {
586 let mut buf = vec![0u8; n - 4];
587 r.read_exact(&mut buf)?;
588 Ok(Some(buf))
589 }
590 }
591}
592
593#[derive(Clone, Copy, PartialEq, Eq)]
595enum AckKind {
596 Bare,
598 Common,
601 Continue,
603 Ready,
605}
606
607struct Ack {
608 oid: ObjectId,
609 kind: AckKind,
610}
611
612fn parse_ack(line: &str) -> Option<Ack> {
613 let rest = line.strip_prefix("ACK ")?;
614 let hex = rest.split_whitespace().next()?;
615 let oid = ObjectId::from_hex(hex).ok()?;
616 let tail = rest.strip_prefix(hex).unwrap_or("").trim();
617 let kind = if tail.contains("continue") {
618 AckKind::Continue
619 } else if tail.contains("common") {
620 AckKind::Common
621 } else if tail.contains("ready") {
622 AckKind::Ready
623 } else {
624 AckKind::Bare
625 };
626 Some(Ack { oid, kind })
627}
628
629struct RoundResult {
631 acks: Vec<Ack>,
632 got_pack: bool,
633 shallow: Vec<ObjectId>,
636 unshallow: Vec<ObjectId>,
638}
639
640fn read_sideband_pack(
644 r: &mut impl Read,
645 out: &mut Vec<u8>,
646 progress: &mut dyn Progress,
647) -> Result<()> {
648 let mut seen_pack = false;
649 let mut pending: Vec<u8> = Vec::new();
650 loop {
651 let mut len_buf = [0u8; 4];
652 match r.read_exact(&mut len_buf) {
653 Ok(()) => {}
654 Err(e) if e.kind() == std::io::ErrorKind::UnexpectedEof => break,
655 Err(e) => return Err(e.into()),
656 }
657 let len_str = std::str::from_utf8(&len_buf)
658 .map_err(|_| Error::Message("bad pkt length".to_owned()))?;
659 let len = usize::from_str_radix(len_str, 16)
660 .map_err(|_| Error::Message("bad pkt length".to_owned()))?;
661 match len {
662 0 => {
663 if seen_pack {
664 break;
665 }
666 continue;
667 }
668 1 | 2 => continue,
669 n if n <= 4 => {
670 return Err(Error::Message(format!(
671 "invalid pkt-line length in side-band stream: {n}"
672 )))
673 }
674 _ => {}
675 }
676 let mut payload = vec![0u8; len - 4];
677 r.read_exact(&mut payload)?;
678 if payload.is_empty() {
679 continue;
680 }
681 match payload[0] {
682 1 => append_pack_data(&payload[1..], out, &mut pending, &mut seen_pack),
683 2 => progress.message(&payload[1..]),
684 3 => {
685 return Err(Error::Message(format!(
686 "remote error: {}",
687 String::from_utf8_lossy(&payload[1..]).trim_end()
688 )))
689 }
690 _ => append_pack_data(&payload, out, &mut pending, &mut seen_pack),
691 }
692 }
693 Ok(())
694}
695
696fn append_pack_data(data: &[u8], out: &mut Vec<u8>, pending: &mut Vec<u8>, seen_pack: &mut bool) {
699 if *seen_pack {
700 out.extend_from_slice(data);
701 return;
702 }
703 pending.extend_from_slice(data);
704 if let Some(pos) = pending.windows(4).position(|w| w == b"PACK") {
705 *seen_pack = true;
706 out.extend_from_slice(&pending[pos..]);
707 pending.clear();
708 } else if pending.len() > 3 {
709 let keep_from = pending.len() - 3;
710 pending.drain(..keep_from);
711 }
712}
713
714fn read_stateless_response(
719 resp: &[u8],
720 sideband: bool,
721 expect_shallow: bool,
722 pack_buf: &mut Vec<u8>,
723 progress: &mut dyn Progress,
724) -> Result<RoundResult> {
725 let mut cur = Cursor::new(resp);
726 let mut acks = Vec::new();
727 let mut got_pack = false;
728 let mut shallow = Vec::new();
729 let mut unshallow = Vec::new();
730
731 if expect_shallow {
735 loop {
736 let start = cur.position() as usize;
737 match pkt_line::read_packet(&mut cur)? {
738 None | Some(pkt_line::Packet::Flush) => break,
739 Some(pkt_line::Packet::Data(line)) => {
740 let line = line.trim_end_matches('\n');
741 if let Some(rest) = line.strip_prefix("shallow ") {
742 if let Ok(oid) = ObjectId::from_hex(rest.trim()) {
743 shallow.push(oid);
744 }
745 } else if let Some(rest) = line.strip_prefix("unshallow ") {
746 if let Ok(oid) = ObjectId::from_hex(rest.trim()) {
747 unshallow.push(oid);
748 }
749 } else {
750 cur.set_position(start as u64);
751 break;
752 }
753 }
754 Some(_) => break,
755 }
756 }
757 }
758
759 loop {
760 let start = cur.position() as usize;
761 let Some(payload) = read_pkt_payload(&mut cur)? else {
762 break;
763 };
764 if payload.is_empty() {
765 continue;
766 }
767 let is_pack =
768 (sideband && payload.first() == Some(&1) && payload.get(1..5) == Some(b"PACK"))
769 || payload.starts_with(b"PACK");
770 if is_pack {
771 got_pack = true;
772 cur.set_position(start as u64);
773 if sideband {
774 read_sideband_pack(&mut cur, pack_buf, progress)?;
775 } else {
776 pack_buf.extend_from_slice(&resp[start..]);
777 }
778 break;
779 }
780 let text = String::from_utf8_lossy(&payload);
781 let line = text.trim_end_matches('\n');
782 if let Some(err) = line.strip_prefix("ERR ") {
783 return Err(Error::Message(format!("remote upload-pack error: {err}")));
784 }
785 if line == "NAK" {
786 continue;
787 }
788 if let Some(ack) = parse_ack(line) {
789 acks.push(ack);
790 }
791 }
792 Ok(RoundResult {
793 acks,
794 got_pack,
795 shallow,
796 unshallow,
797 })
798}
799
800fn build_fetch_caps(caps: &HashSet<String>) -> String {
803 let mut enabled = Vec::new();
804 let multi_ack_detailed = caps.contains("multi_ack_detailed");
805 if multi_ack_detailed {
806 enabled.push("multi_ack_detailed");
807 }
808 if multi_ack_detailed && caps.contains("no-done") {
809 enabled.push("no-done");
810 }
811 for want in [
812 "side-band-64k",
813 "thin-pack",
814 "no-progress",
815 "include-tag",
816 "ofs-delta",
817 ] {
818 if caps.contains(want) {
819 enabled.push(want);
820 }
821 }
822 if enabled.is_empty() {
823 String::new()
824 } else {
825 format!(" {}", enabled.join(" "))
826 }
827}
828
829fn next_flush(count: usize) -> usize {
831 const LARGE_FLUSH: usize = 16384;
832 if count < LARGE_FLUSH {
833 count * 2
834 } else {
835 count * 11 / 10
836 }
837}
838
839fn append_shallow_request_v0_http(
844 req: &mut Vec<u8>,
845 caps: &HashSet<String>,
846 local_shallow: &[ObjectId],
847 opts: &FetchOptions,
848) -> Result<()> {
849 for oid in local_shallow {
850 pkt_line::write_line_to_vec(req, &format!("shallow {}", oid.to_hex()))?;
851 }
852 if opts.unshallow {
853 pkt_line::write_line_to_vec(req, &format!("deepen {}", crate::shallow::INFINITE_DEPTH))?;
854 } else if let Some(depth) = opts.depth.filter(|d| *d > 0) {
855 pkt_line::write_line_to_vec(req, &format!("deepen {depth}"))?;
856 }
857 if let Some(since) = opts
858 .deepen_since
859 .as_deref()
860 .filter(|s| !s.trim().is_empty())
861 {
862 if caps.contains("deepen-since") {
863 let value = crate::shallow::deepen_since_wire_value(since);
864 pkt_line::write_line_to_vec(req, &format!("deepen-since {value}"))?;
865 }
866 }
867 if caps.contains("deepen-not") {
868 for excl in &opts.deepen_not {
869 let excl = excl.trim();
870 if !excl.is_empty() {
871 pkt_line::write_line_to_vec(req, &format!("deepen-not {excl}"))?;
872 }
873 }
874 }
875 Ok(())
876}
877
878fn negotiate_pack_http(
882 client: &dyn HttpClient,
883 local_git_dir: &Path,
884 repo_url: &str,
885 caps: &HashSet<String>,
886 advertised: &[AdvRef],
887 wants: &[ObjectId],
888 opts: &FetchOptions,
889 local_shallow: &[ObjectId],
890 progress: &mut dyn Progress,
891) -> Result<(Vec<u8>, crate::fetch::ShallowUpdate)> {
892 let post_url = upload_pack_url(repo_url);
893 let content_type = format!("application/x-{UPLOAD_PACK}-request");
894 let accept = format!("application/x-{UPLOAD_PACK}-result");
895 let fetch_caps = build_fetch_caps(caps);
896 let sideband = caps.contains("side-band-64k");
897 let multi_ack_detailed = caps.contains("multi_ack_detailed");
898 let no_done = multi_ack_detailed && caps.contains("no-done");
899
900 let shallow_request = opts.has_deepen_request() || !local_shallow.is_empty();
903
904 let want_set: HashSet<ObjectId> = wants.iter().copied().collect();
905
906 let mut state = Vec::new();
910 let first = wants[0];
911 pkt_line::write_line_to_vec(
912 &mut state,
913 &format!("want {}{}", first.to_hex(), fetch_caps),
914 )?;
915 for w in wants.iter().skip(1) {
916 pkt_line::write_line_to_vec(&mut state, &format!("want {}", w.to_hex()))?;
917 }
918 append_shallow_request_v0_http(&mut state, caps, local_shallow, opts)?;
919 pkt_line::write_flush(&mut state)?;
920
921 let mut shallow_update = crate::fetch::ShallowUpdate::default();
922
923 let local_repo = crate::repo::Repository::open(local_git_dir, None)?;
926 let mut negotiator = SkippingNegotiator::new(local_repo);
927 if !shallow_request {
928 for w in wants {
929 if negotiator.repo().odb.read(w).is_ok() {
930 negotiator.add_tip(*w)?;
931 }
932 }
933 let mut tips: Vec<ObjectId> = Vec::new();
934 for prefix in ["refs/heads/", "refs/tags/"] {
935 if let Ok(entries) = crate::refs::list_refs(local_git_dir, prefix) {
936 for (_, oid) in entries {
937 if negotiator.repo().odb.read(&oid).is_ok() {
938 tips.push(oid);
939 }
940 }
941 }
942 }
943 if let Ok(h) = crate::refs::resolve_ref(local_git_dir, "HEAD") {
944 if negotiator.repo().odb.read(&h).is_ok() {
945 tips.push(h);
946 }
947 }
948 tips.sort_by_key(ObjectId::to_hex);
949 tips.dedup();
950 for t in tips {
951 if want_set.contains(&t) {
952 continue;
953 }
954 negotiator.add_tip(t)?;
955 }
956 for e in advertised {
957 if want_set.contains(&e.oid) {
958 continue;
959 }
960 if negotiator.repo().odb.read(&e.oid).is_ok() {
961 negotiator.known_common(e.oid)?;
962 }
963 }
964 }
965
966 let mut pack_buf: Vec<u8> = Vec::new();
967 let mut got_ready = false;
968 let mut got_pack = false;
969 let mut shallow_applied = false;
970
971 const INITIAL_FLUSH: usize = 16;
972 let mut count: usize = 0;
973 let mut flush_at: usize = INITIAL_FLUSH;
974 let mut round = Vec::new();
975 while let Some(oid) = negotiator.next_have()? {
978 pkt_line::write_line_to_vec(&mut round, &format!("have {}", oid.to_hex()))?;
979 count += 1;
980 if count < flush_at {
981 continue;
982 }
983 flush_at = next_flush(count);
984
985 let mut req = state.clone();
986 req.extend_from_slice(&round);
987 pkt_line::write_flush(&mut req)?;
988 round.clear();
989
990 let resp = client.post(&post_url, &content_type, &accept, &req, None)?;
991 let round_result =
992 read_stateless_response(&resp, sideband, shallow_request, &mut pack_buf, progress)?;
993 if shallow_request && !shallow_applied {
994 shallow_update
995 .shallow
996 .extend(round_result.shallow.iter().copied());
997 shallow_update
998 .unshallow
999 .extend(round_result.unshallow.iter().copied());
1000 shallow_applied = true;
1001 }
1002 for ack in &round_result.acks {
1003 if matches!(ack.kind, AckKind::Bare) {
1004 continue;
1005 }
1006 let was_common = negotiator.ack(ack.oid)?;
1007 if matches!(ack.kind, AckKind::Common) && !was_common {
1008 pkt_line::write_line_to_vec(&mut state, &format!("have {}", ack.oid.to_hex()))?;
1009 }
1010 if matches!(ack.kind, AckKind::Ready) {
1011 got_ready = true;
1012 }
1013 }
1014 if round_result.got_pack {
1015 got_pack = true;
1016 break;
1017 }
1018 if got_ready {
1019 break;
1020 }
1021 }
1022
1023 if !(got_pack || got_ready && no_done) {
1026 let mut req = state.clone();
1027 pkt_line::write_line_to_vec(&mut req, "done")?;
1028 pkt_line::write_flush(&mut req)?;
1029 let resp = client.post(&post_url, &content_type, &accept, &req, None)?;
1030 let round_result =
1031 read_stateless_response(&resp, sideband, shallow_request, &mut pack_buf, progress)?;
1032 if shallow_request && !shallow_applied {
1033 shallow_update.shallow.extend(round_result.shallow);
1034 shallow_update.unshallow.extend(round_result.unshallow);
1035 }
1036 }
1037
1038 Ok((pack_buf, shallow_update))
1039}
1040
1041struct MatchPlan {
1046 matched: Vec<crate::transfer::MatchedRef>,
1047 wants: HashSet<ObjectId>,
1048 seen: HashSet<String>,
1049}
1050
1051fn match_refspecs(
1052 remote_refs: &[(String, ObjectId)],
1053 positive: &[RefspecItem],
1054 negatives: &[RefspecItem],
1055) -> MatchPlan {
1056 let mut matched: Vec<crate::transfer::MatchedRef> = Vec::new();
1057 let mut wants: HashSet<ObjectId> = HashSet::new();
1058 let mut seen: HashSet<String> = HashSet::new();
1059 for (name, oid) in remote_refs {
1060 if ref_excluded(name, negatives) {
1061 continue;
1062 }
1063 if let Some(local_ref) = match_positive(name, positive) {
1064 if seen.insert(name.clone()) {
1065 wants.insert(*oid);
1066 matched.push(crate::transfer::MatchedRef {
1067 remote_ref: name.clone(),
1068 local_ref,
1069 oid: *oid,
1070 force: refspecs_force(name, positive),
1071 is_tag: name.starts_with("refs/tags/"),
1072 });
1073 }
1074 }
1075 }
1076 MatchPlan {
1077 matched,
1078 wants,
1079 seen,
1080 }
1081}
1082
1083pub fn http_fetch(
1104 client: &dyn HttpClient,
1105 local_git_dir: &Path,
1106 repo_url: &str,
1107 opts: &FetchOptions,
1108 progress: &mut dyn Progress,
1109) -> Result<FetchOutcome> {
1110 use crate::net_trace::net_trace;
1111 net_trace!(
1112 "http_fetch: begin — {} ({} refspec(s), tags={:?})",
1113 repo_url,
1114 opts.refspecs.len(),
1115 opts.tags
1116 );
1117 let info_url = info_refs_url(repo_url);
1125 let (body, final_url) = client.get_with_final_url(&info_url, client.git_protocol_header())?;
1126 let rebased = rebased_base_from_redirect(repo_url, final_url.as_deref());
1127 let repo_url_owned;
1128 let (repo_url, disc) = match rebased {
1129 Some(new_base) => {
1130 net_trace!("http_fetch: redirected base {repo_url} -> {new_base}");
1131 let url = info_refs_url(&new_base);
1132 let body = client.get(&url, client.git_protocol_header())?;
1133 let disc = parse_advertisement(strip_service_advertisement(&body)?)?;
1134 repo_url_owned = new_base;
1135 (repo_url_owned.as_str(), disc)
1136 }
1137 None => {
1138 let disc = parse_advertisement(strip_service_advertisement(&body)?)?;
1139 (repo_url, disc)
1140 }
1141 };
1142 net_trace!(
1143 "http_fetch: discovered protocol v{}, {} ref(s)",
1144 disc.protocol_version,
1145 disc.refs.len()
1146 );
1147 if disc.protocol_version >= 2 {
1148 net_trace!("http_fetch: delegating to v2 stateless fetch");
1149 return http_fetch_v2(client, local_git_dir, repo_url, &disc, opts, progress);
1150 }
1151
1152 let local_odb = open_odb(local_git_dir);
1153
1154 let default_branch = disc
1155 .head_symref
1156 .as_deref()
1157 .map(|t| t.strip_prefix("refs/heads/").unwrap_or(t).to_owned());
1158
1159 let remote_refs: Vec<(String, ObjectId)> = disc
1160 .refs
1161 .iter()
1162 .filter(|r| r.name != "HEAD" && !r.name.ends_with("^{}"))
1163 .map(|r| (r.name.clone(), r.oid))
1164 .collect();
1165
1166 let mut positive: Vec<RefspecItem> = Vec::new();
1168 let mut negatives: Vec<RefspecItem> = Vec::new();
1169 for spec in &opts.refspecs {
1170 let item = parse_fetch_refspec(spec)
1171 .map_err(|e| Error::Message(format!("invalid refspec '{spec}': {e}")))?;
1172 if item.negative {
1173 negatives.push(item);
1174 } else {
1175 positive.push(item);
1176 }
1177 }
1178 for spec in &opts.negative_refspecs {
1179 let item = parse_fetch_refspec(spec)
1180 .map_err(|e| Error::Message(format!("invalid negative refspec '{spec}': {e}")))?;
1181 negatives.push(item);
1182 }
1183
1184 let MatchPlan {
1186 mut matched,
1187 mut wants,
1188 mut seen,
1189 } = match_refspecs(&remote_refs, &positive, &negatives);
1190
1191 if opts.tags != TagMode::None {
1195 for (name, oid) in &remote_refs {
1196 if !name.starts_with("refs/tags/") {
1197 continue;
1198 }
1199 if seen.contains(name) || ref_excluded(name, &negatives) {
1200 continue;
1201 }
1202 seen.insert(name.clone());
1203 wants.insert(*oid);
1204 matched.push(crate::transfer::MatchedRef {
1205 remote_ref: name.clone(),
1206 local_ref: Some(name.clone()),
1207 oid: *oid,
1208 force: false,
1209 is_tag: true,
1210 });
1211 }
1212 }
1213
1214 let local_shallow = crate::shallow::load_shallow_oids(local_git_dir)?;
1218 let shallow_request = opts.has_deepen_request() || !local_shallow.is_empty();
1219 let need: Vec<ObjectId> = if shallow_request {
1220 wants.iter().copied().collect()
1221 } else {
1222 wants
1223 .iter()
1224 .copied()
1225 .filter(|oid| !local_odb.exists(oid))
1226 .collect()
1227 };
1228
1229 let mut shallow_update = crate::fetch::ShallowUpdate::default();
1230
1231 if !need.is_empty() && !opts.dry_run {
1232 let (pack, su) = negotiate_pack_http(
1233 client,
1234 local_git_dir,
1235 repo_url,
1236 &disc.caps,
1237 &disc.refs,
1238 &need,
1239 opts,
1240 &local_shallow,
1241 progress,
1242 )?;
1243 shallow_update = su;
1244 if !pack.is_empty() {
1245 if pack.len() < 12 || &pack[0..4] != b"PACK" {
1246 return Err(Error::Message(
1247 "did not receive a valid pack from HTTP fetch".to_owned(),
1248 ));
1249 }
1250 let mut cursor = Cursor::new(pack);
1251 crate::unpack_objects::unpack_objects(
1252 &mut cursor,
1253 &local_odb,
1254 &crate::unpack_objects::UnpackOptions {
1255 quiet: true,
1256 ..Default::default()
1257 },
1258 )?;
1259 }
1260 }
1261
1262 if !opts.dry_run {
1264 crate::shallow::apply_shallow_updates(
1265 local_git_dir,
1266 &shallow_update.shallow,
1267 &shallow_update.unshallow,
1268 )?;
1269 }
1270
1271 if opts.tags == TagMode::Following {
1273 retain_following_tags(&local_odb, &mut matched, &wants);
1274 }
1275
1276 let local_repo = if opts.dry_run {
1278 None
1279 } else {
1280 crate::repo::Repository::open(local_git_dir, None).ok()
1281 };
1282
1283 let mut updates: Vec<RefUpdate> = Vec::new();
1284 if opts.prune {
1285 prune_tracking_refs(
1286 local_git_dir,
1287 &positive,
1288 &remote_refs,
1289 opts.dry_run,
1290 &mut updates,
1291 )?;
1292 }
1293
1294 for m in &matched {
1295 let Some(local_ref) = &m.local_ref else {
1296 updates.push(RefUpdate {
1297 remote_ref: m.remote_ref.clone(),
1298 local_ref: None,
1299 old_oid: None,
1300 new_oid: Some(m.oid),
1301 mode: UpdateMode::NoChangeNeeded,
1302 note: Some("not stored (empty destination)".to_owned()),
1303 });
1304 continue;
1305 };
1306 let old = crate::refs::resolve_ref(local_git_dir, local_ref).ok();
1307 let mode = classify_update(old.as_ref(), &m.oid, m.force, m.is_tag, local_repo.as_ref());
1308 let write = matches!(
1309 mode,
1310 UpdateMode::New | UpdateMode::FastForward | UpdateMode::Forced
1311 );
1312 if write && !opts.dry_run {
1313 crate::refs::write_ref(local_git_dir, local_ref, &m.oid)?;
1314 }
1315 updates.push(RefUpdate {
1316 remote_ref: m.remote_ref.clone(),
1317 local_ref: Some(local_ref.clone()),
1318 old_oid: old,
1319 new_oid: Some(m.oid),
1320 mode,
1321 note: None,
1322 });
1323 }
1324
1325 net_trace!("http_fetch: done — {} ref update(s)", updates.len());
1326 Ok(FetchOutcome {
1327 updates,
1328 default_branch,
1329 new_shallow: shallow_update.shallow,
1330 new_unshallow: shallow_update.unshallow,
1331 })
1332}
1333
1334fn http_fetch_v2(
1345 client: &dyn HttpClient,
1346 local_git_dir: &Path,
1347 repo_url: &str,
1348 disc: &Discovery,
1349 opts: &FetchOptions,
1350 progress: &mut dyn Progress,
1351) -> Result<FetchOutcome> {
1352 let local_odb = open_odb(local_git_dir);
1353 let server_caps: Vec<String> = disc.caps.iter().cloned().collect();
1357
1358 let post_url = upload_pack_url(repo_url);
1359 let content_type = format!("application/x-{UPLOAD_PACK}-request");
1360 let accept = format!("application/x-{UPLOAD_PACK}-result");
1361 let git_protocol = "version=2";
1363
1364 let (remote_refs, head_symref) = {
1366 let req = crate::fetch::build_v2_ls_refs_request(
1367 &server_caps,
1368 &local_odb,
1369 opts.tags,
1370 &opts.refspecs,
1371 )?;
1372 let resp = client.post(&post_url, &content_type, &accept, &req, Some(git_protocol))?;
1373 let mut cur = Cursor::new(resp);
1374 crate::fetch::parse_v2_ls_refs_response(&mut cur)?
1375 };
1376 let default_branch = head_symref
1377 .as_deref()
1378 .map(|t| t.strip_prefix("refs/heads/").unwrap_or(t).to_owned());
1379
1380 let mut positive: Vec<RefspecItem> = Vec::new();
1382 let mut negatives: Vec<RefspecItem> = Vec::new();
1383 for spec in &opts.refspecs {
1384 let item = parse_fetch_refspec(spec)
1385 .map_err(|e| Error::Message(format!("invalid refspec '{spec}': {e}")))?;
1386 if item.negative {
1387 negatives.push(item);
1388 } else {
1389 positive.push(item);
1390 }
1391 }
1392 for spec in &opts.negative_refspecs {
1393 let item = parse_fetch_refspec(spec)
1394 .map_err(|e| Error::Message(format!("invalid negative refspec '{spec}': {e}")))?;
1395 negatives.push(item);
1396 }
1397
1398 let MatchPlan {
1400 mut matched,
1401 mut wants,
1402 mut seen,
1403 } = match_refspecs(&remote_refs, &positive, &negatives);
1404
1405 if opts.tags != TagMode::None {
1409 for (name, oid) in &remote_refs {
1410 if !name.starts_with("refs/tags/") {
1411 continue;
1412 }
1413 if seen.contains(name) || ref_excluded(name, &negatives) {
1414 continue;
1415 }
1416 seen.insert(name.clone());
1417 wants.insert(*oid);
1418 matched.push(crate::transfer::MatchedRef {
1419 remote_ref: name.clone(),
1420 local_ref: Some(name.clone()),
1421 oid: *oid,
1422 force: false,
1423 is_tag: true,
1424 });
1425 }
1426 }
1427
1428 let local_shallow = crate::shallow::load_shallow_oids(local_git_dir)?;
1432 let shallow_request = opts.has_deepen_request() || !local_shallow.is_empty();
1433 let need: Vec<ObjectId> = if shallow_request {
1434 wants.iter().copied().collect()
1435 } else {
1436 wants
1437 .iter()
1438 .copied()
1439 .filter(|oid| !local_odb.exists(oid))
1440 .collect()
1441 };
1442
1443 let mut shallow_update = crate::fetch::ShallowUpdate::default();
1444
1445 if !need.is_empty() && !opts.dry_run {
1446 let deepen = crate::fetch::V2DeepenArgs::from_opts(opts, &local_shallow);
1447 let (pack, su) = negotiate_pack_v2_http(
1448 client,
1449 local_git_dir,
1450 &post_url,
1451 &content_type,
1452 &accept,
1453 git_protocol,
1454 &server_caps,
1455 &local_odb,
1456 &need,
1457 &deepen,
1458 progress,
1459 )?;
1460 shallow_update = su;
1461 if !pack.is_empty() {
1462 if pack.len() < 12 || &pack[0..4] != b"PACK" {
1463 return Err(Error::Message(
1464 "did not receive a valid pack from v2 HTTP fetch".to_owned(),
1465 ));
1466 }
1467 let mut cursor = Cursor::new(pack);
1468 crate::unpack_objects::unpack_objects(
1469 &mut cursor,
1470 &local_odb,
1471 &crate::unpack_objects::UnpackOptions {
1472 quiet: true,
1473 ..Default::default()
1474 },
1475 )?;
1476 }
1477 }
1478
1479 if !opts.dry_run {
1481 crate::shallow::apply_shallow_updates(
1482 local_git_dir,
1483 &shallow_update.shallow,
1484 &shallow_update.unshallow,
1485 )?;
1486 }
1487
1488 if opts.tags == TagMode::Following {
1490 retain_following_tags(&local_odb, &mut matched, &wants);
1491 }
1492
1493 let local_repo = if opts.dry_run {
1495 None
1496 } else {
1497 crate::repo::Repository::open(local_git_dir, None).ok()
1498 };
1499
1500 let mut updates: Vec<RefUpdate> = Vec::new();
1501 if opts.prune {
1502 prune_tracking_refs(
1503 local_git_dir,
1504 &positive,
1505 &remote_refs,
1506 opts.dry_run,
1507 &mut updates,
1508 )?;
1509 }
1510
1511 for m in &matched {
1512 let Some(local_ref) = &m.local_ref else {
1513 updates.push(RefUpdate {
1514 remote_ref: m.remote_ref.clone(),
1515 local_ref: None,
1516 old_oid: None,
1517 new_oid: Some(m.oid),
1518 mode: UpdateMode::NoChangeNeeded,
1519 note: Some("not stored (empty destination)".to_owned()),
1520 });
1521 continue;
1522 };
1523 let old = crate::refs::resolve_ref(local_git_dir, local_ref).ok();
1524 let mode = classify_update(old.as_ref(), &m.oid, m.force, m.is_tag, local_repo.as_ref());
1525 let write = matches!(
1526 mode,
1527 UpdateMode::New | UpdateMode::FastForward | UpdateMode::Forced
1528 );
1529 if write && !opts.dry_run {
1530 crate::refs::write_ref(local_git_dir, local_ref, &m.oid)?;
1531 }
1532 updates.push(RefUpdate {
1533 remote_ref: m.remote_ref.clone(),
1534 local_ref: Some(local_ref.clone()),
1535 old_oid: old,
1536 new_oid: Some(m.oid),
1537 mode,
1538 note: None,
1539 });
1540 }
1541
1542 crate::net_trace::net_trace!("http_fetch (v2): done — {} ref update(s)", updates.len());
1543 Ok(FetchOutcome {
1544 updates,
1545 default_branch,
1546 new_shallow: shallow_update.shallow,
1547 new_unshallow: shallow_update.unshallow,
1548 })
1549}
1550
1551#[allow(clippy::too_many_arguments)]
1566fn negotiate_pack_v2_http(
1567 client: &dyn HttpClient,
1568 local_git_dir: &Path,
1569 post_url: &str,
1570 content_type: &str,
1571 accept: &str,
1572 git_protocol: &str,
1573 server_caps: &[String],
1574 local_odb: &crate::odb::Odb,
1575 wants: &[ObjectId],
1576 deepen: &crate::fetch::V2DeepenArgs,
1577 progress: &mut dyn Progress,
1578) -> Result<(Vec<u8>, crate::fetch::ShallowUpdate)> {
1579 if wants.is_empty() {
1580 return Ok((Vec::new(), crate::fetch::ShallowUpdate::default()));
1581 }
1582 let object_format = crate::fetch::v2_object_format(server_caps, local_odb);
1583 let cap_echo = protocol_v2::cap_lines_for_command_request(server_caps);
1584 let sideband_all = protocol_v2::fetch_supports_sideband_all(server_caps);
1585
1586 let shallow_request = deepen.is_shallow_request();
1590
1591 let haves = if shallow_request {
1595 Vec::new()
1596 } else {
1597 crate::fetch::v2_local_haves(local_git_dir, wants)?
1598 };
1599
1600 let mut pack = Vec::new();
1601 let mut shallow_update = crate::fetch::ShallowUpdate::default();
1602
1603 if haves.is_empty() {
1605 let mut req = Vec::new();
1606 crate::fetch::write_v2_fetch_request(
1607 &mut req,
1608 &object_format,
1609 &cap_echo,
1610 wants,
1611 &[],
1612 sideband_all,
1613 deepen,
1614 true,
1615 )?;
1616 let resp = client.post(post_url, content_type, accept, &req, Some(git_protocol))?;
1617 let mut cur = Cursor::new(resp);
1618 crate::fetch::read_v2_fetch_pack_response(
1619 &mut cur,
1620 &mut pack,
1621 &mut shallow_update,
1622 progress,
1623 )?;
1624 return Ok((pack, shallow_update));
1625 }
1626
1627 const INITIAL_FLUSH: usize = 16;
1631 let mut flush_at: usize = INITIAL_FLUSH.min(haves.len());
1632 loop {
1633 if flush_at < haves.len() {
1634 let mut req = Vec::new();
1636 crate::fetch::write_v2_fetch_request(
1637 &mut req,
1638 &object_format,
1639 &cap_echo,
1640 wants,
1641 &haves[..flush_at],
1642 sideband_all,
1643 deepen,
1644 false,
1645 )?;
1646 let resp = client.post(post_url, content_type, accept, &req, Some(git_protocol))?;
1647 let mut cur = Cursor::new(resp);
1648 let ack = crate::fetch::read_v2_acknowledgments(&mut cur)?;
1649 if let Some(round) = ack {
1650 if round.ready {
1651 crate::fetch::read_v2_fetch_pack_response(
1653 &mut cur,
1654 &mut pack,
1655 &mut shallow_update,
1656 progress,
1657 )?;
1658 return Ok((pack, shallow_update));
1659 }
1660 } else {
1661 crate::fetch::read_v2_fetch_pack_response(
1663 &mut cur,
1664 &mut pack,
1665 &mut shallow_update,
1666 progress,
1667 )?;
1668 return Ok((pack, shallow_update));
1669 }
1670 flush_at = next_flush(flush_at).min(haves.len());
1671 continue;
1672 }
1673
1674 let mut req = Vec::new();
1676 crate::fetch::write_v2_fetch_request(
1677 &mut req,
1678 &object_format,
1679 &cap_echo,
1680 wants,
1681 &haves,
1682 sideband_all,
1683 deepen,
1684 true,
1685 )?;
1686 let resp = client.post(post_url, content_type, accept, &req, Some(git_protocol))?;
1687 let mut cur = Cursor::new(resp);
1688 crate::fetch::read_v2_fetch_pack_response(
1689 &mut cur,
1690 &mut pack,
1691 &mut shallow_update,
1692 progress,
1693 )?;
1694 return Ok((pack, shallow_update));
1695 }
1696}
1697
1698fn retain_following_tags(
1700 odb: &crate::odb::Odb,
1701 matched: &mut Vec<crate::transfer::MatchedRef>,
1702 wants: &HashSet<ObjectId>,
1703) {
1704 let roots: Vec<ObjectId> = matched
1705 .iter()
1706 .filter(|m| !m.is_tag)
1707 .map(|m| m.oid)
1708 .collect();
1709 let closure = reachable_closure(odb, &roots);
1710 matched.retain(|m| {
1711 if !m.is_tag {
1712 return true;
1713 }
1714 let peeled = peel_tag_target(odb, m.oid);
1715 let have = odb.exists(&m.oid);
1716 have && (closure.contains(&m.oid) || closure.contains(&peeled) || wants.contains(&peeled))
1717 });
1718}
1719
1720fn peel_tag_target(odb: &crate::odb::Odb, oid: ObjectId) -> ObjectId {
1721 let mut current = oid;
1722 for _ in 0..16 {
1723 let Ok(obj) = odb.read(¤t) else {
1724 return current;
1725 };
1726 if obj.kind != crate::objects::ObjectKind::Tag {
1727 return current;
1728 }
1729 match crate::objects::parse_tag(&obj.data) {
1730 Ok(t) => current = t.object,
1731 Err(_) => return current,
1732 }
1733 }
1734 current
1735}
1736
1737fn reachable_closure(odb: &crate::odb::Odb, roots: &[ObjectId]) -> HashSet<ObjectId> {
1738 use crate::objects::{parse_commit, parse_tag, parse_tree, ObjectKind};
1739 let mut seen: HashSet<ObjectId> = HashSet::new();
1740 let mut stack: Vec<ObjectId> = roots.to_vec();
1741 while let Some(oid) = stack.pop() {
1742 if !seen.insert(oid) {
1743 continue;
1744 }
1745 let Ok(obj) = odb.read(&oid) else {
1746 continue;
1747 };
1748 match obj.kind {
1749 ObjectKind::Commit => {
1750 if let Ok(c) = parse_commit(&obj.data) {
1751 stack.push(c.tree);
1752 for p in c.parents {
1753 stack.push(p);
1754 }
1755 }
1756 }
1757 ObjectKind::Tree => {
1758 if let Ok(entries) = parse_tree(&obj.data) {
1759 for e in entries {
1760 stack.push(e.oid);
1761 }
1762 }
1763 }
1764 ObjectKind::Tag => {
1765 if let Ok(t) = parse_tag(&obj.data) {
1766 stack.push(t.object);
1767 }
1768 }
1769 ObjectKind::Blob => {}
1770 }
1771 }
1772 seen
1773}
1774
1775pub fn discovery_advertisement(conn: &SmartHttpConnection) -> Advertisement {
1778 Advertisement {
1779 refs: conn.adv_refs.clone(),
1780 capabilities: conn.caps.clone(),
1781 head_symref: conn.head_symref.clone(),
1782 protocol_version: conn.protocol_version,
1783 }
1784}
1785
1786#[cfg(test)]
1787mod tests {
1788 use super::*;
1789
1790 #[test]
1791 fn rebase_redirect_strips_info_refs_and_query() {
1792 let base = "https://tangled.org/me/repo";
1793 assert_eq!(
1795 rebased_base_from_redirect(
1796 base,
1797 Some("https://knot.example/did:plc:xyz/info/refs?service=git-upload-pack")
1798 )
1799 .as_deref(),
1800 Some("https://knot.example/did:plc:xyz")
1801 );
1802 assert_eq!(
1804 rebased_base_from_redirect(base, Some("https://host/smart/repo/info/refs")).as_deref(),
1805 Some("https://host/smart/repo")
1806 );
1807 }
1808
1809 #[test]
1810 fn rebase_redirect_none_when_no_change_or_unknown() {
1811 let base = "https://host/smart/repo";
1812 assert_eq!(rebased_base_from_redirect(base, None), None);
1814 assert_eq!(
1816 rebased_base_from_redirect(
1817 base,
1818 Some("https://host/smart/repo/info/refs?service=git-upload-pack")
1819 ),
1820 None
1821 );
1822 assert_eq!(
1824 rebased_base_from_redirect(base, Some("https://host/elsewhere")),
1825 None
1826 );
1827 }
1828
1829 #[test]
1830 fn strips_smart_service_preamble() {
1831 let mut body = Vec::new();
1832 pkt_line::write_line_to_vec(&mut body, "# service=git-upload-pack\n").unwrap();
1833 body.extend_from_slice(b"0000");
1834 let oid = "1".repeat(40);
1835 let line = format!("{oid} refs/heads/main\0multi_ack_detailed side-band-64k");
1836 pkt_line::write_line_to_vec(&mut body, &line).unwrap();
1837 body.extend_from_slice(b"0000");
1838
1839 let stripped = strip_service_advertisement(&body).unwrap();
1840 let disc = parse_advertisement(stripped).unwrap();
1841 assert_eq!(disc.protocol_version, 0);
1842 assert_eq!(disc.refs.len(), 1);
1843 assert_eq!(disc.refs[0].name, "refs/heads/main");
1844 assert!(disc.caps.contains("side-band-64k"));
1845 }
1846
1847 #[test]
1848 fn parses_symref_and_caps() {
1849 let mut body = Vec::new();
1850 let main = "2".repeat(40);
1851 let head = format!(
1852 "{main} HEAD\0multi_ack_detailed symref=HEAD:refs/heads/main object-format=sha1"
1853 );
1854 pkt_line::write_line_to_vec(&mut body, &head).unwrap();
1855 let r = format!("{main} refs/heads/main");
1856 pkt_line::write_line_to_vec(&mut body, &r).unwrap();
1857 body.extend_from_slice(b"0000");
1858
1859 let disc = parse_advertisement(&body).unwrap();
1860 assert_eq!(disc.head_symref.as_deref(), Some("refs/heads/main"));
1861 assert_eq!(disc.object_format, "sha1");
1862 assert!(disc.refs.iter().any(|r| r.name == "HEAD"));
1865 assert!(disc.refs.iter().any(|r| r.name == "refs/heads/main"));
1866 }
1867
1868 #[test]
1869 fn detects_v2_preamble() {
1870 let mut body = Vec::new();
1871 pkt_line::write_line_to_vec(&mut body, "version 2").unwrap();
1872 pkt_line::write_line_to_vec(&mut body, "ls-refs").unwrap();
1873 pkt_line::write_line_to_vec(&mut body, "object-format=sha256").unwrap();
1874 body.extend_from_slice(b"0000");
1875 let disc = parse_advertisement(&body).unwrap();
1876 assert_eq!(disc.protocol_version, 2);
1877 assert_eq!(disc.object_format, "sha256");
1878 }
1879
1880 #[test]
1881 fn url_helpers() {
1882 assert_eq!(
1883 info_refs_url("http://h/r.git"),
1884 "http://h/r.git/info/refs?service=git-upload-pack"
1885 );
1886 assert_eq!(
1887 info_refs_url("http://h/r.git/"),
1888 "http://h/r.git/info/refs?service=git-upload-pack"
1889 );
1890 assert_eq!(
1891 upload_pack_url("http://h/r.git/"),
1892 "http://h/r.git/git-upload-pack"
1893 );
1894 }
1895}