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, Connection, ConnectOptions, 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 git_protocol_header(&self) -> Option<&str> {
90 None
91 }
92
93 fn smart_http_enabled(&self) -> bool {
96 true
97 }
98}
99
100impl<C: HttpClient> HttpClient for std::sync::Arc<C> {
103 fn get(&self, url: &str, git_protocol: Option<&str>) -> Result<Vec<u8>> {
104 (**self).get(url, git_protocol)
105 }
106
107 fn post(
108 &self,
109 url: &str,
110 content_type: &str,
111 accept: &str,
112 body: &[u8],
113 git_protocol: Option<&str>,
114 ) -> Result<Vec<u8>> {
115 (**self).post(url, content_type, accept, body, git_protocol)
116 }
117
118 fn git_protocol_header(&self) -> Option<&str> {
119 (**self).git_protocol_header()
120 }
121
122 fn smart_http_enabled(&self) -> bool {
123 (**self).smart_http_enabled()
124 }
125}
126
127const UPLOAD_PACK: &str = "git-upload-pack";
128
129fn strip_service_advertisement(body: &[u8]) -> Result<&[u8]> {
138 let mut cur = Cursor::new(body);
139 let start = cur.position();
140 match pkt_line::read_packet(&mut cur)? {
141 Some(pkt_line::Packet::Data(line)) if line.starts_with("# service=") => {
142 match pkt_line::read_packet(&mut cur)? {
144 Some(pkt_line::Packet::Flush) | None => {}
145 _ => {
146 return Ok(body);
148 }
149 }
150 let pos = cur.position() as usize;
151 Ok(&body[pos..])
152 }
153 _ => {
154 cur.set_position(start);
155 Ok(body)
156 }
157 }
158}
159
160#[derive(Clone, Debug)]
162struct AdvRef {
163 name: String,
164 oid: ObjectId,
165}
166
167struct Discovery {
170 protocol_version: u8,
171 refs: Vec<AdvRef>,
172 caps: HashSet<String>,
173 head_symref: Option<String>,
174 object_format: String,
175}
176
177fn parse_advertisement(body: &[u8]) -> Result<Discovery> {
185 let mut cur = Cursor::new(body);
186
187 let first = match pkt_line::read_packet(&mut cur)? {
189 None | Some(pkt_line::Packet::Flush) => {
190 return Ok(Discovery {
192 protocol_version: 0,
193 refs: Vec::new(),
194 caps: HashSet::new(),
195 head_symref: None,
196 object_format: "sha1".to_owned(),
197 });
198 }
199 Some(pkt_line::Packet::Data(s)) => s,
200 Some(other) => {
201 return Err(Error::Message(format!(
202 "unexpected first advertisement packet: {other:?}"
203 )))
204 }
205 };
206 if first.trim_end() == "version 2" {
207 let mut caps = HashSet::new();
209 loop {
210 match pkt_line::read_packet(&mut cur)? {
211 None | Some(pkt_line::Packet::Flush) => break,
212 Some(pkt_line::Packet::Data(s)) => {
213 caps.insert(s.trim_end().to_owned());
214 }
215 Some(_) => break,
216 }
217 }
218 let object_format = caps
219 .iter()
220 .find_map(|c| c.strip_prefix("object-format="))
221 .unwrap_or("sha1")
222 .to_owned();
223 return Ok(Discovery {
224 protocol_version: 2,
225 refs: Vec::new(),
226 caps,
227 head_symref: None,
228 object_format,
229 });
230 }
231
232 cur.set_position(0);
234 let mut refs = Vec::new();
235 let mut caps: HashSet<String> = HashSet::new();
236 let mut head_symref = None;
237 let mut first_ref_line = true;
238 loop {
239 match pkt_line::read_packet(&mut cur)? {
240 None | Some(pkt_line::Packet::Flush) => break,
241 Some(pkt_line::Packet::Data(line)) => {
242 let line = line.trim_end_matches('\n');
243 if line.starts_with("version ") {
244 continue;
245 }
246 if line.starts_with("shallow ") || line.starts_with("unshallow ") {
247 continue;
248 }
249 let (payload, cap_part) = match line.split_once('\0') {
250 Some((p, c)) => (p.trim(), Some(c)),
251 None => (line.trim(), None),
252 };
253 let Some((oid_hex, refname)) =
254 payload.split_once('\t').or_else(|| payload.split_once(' '))
255 else {
256 continue;
257 };
258 let oid_hex = oid_hex.trim();
259 let refname = refname.trim();
260 if first_ref_line {
261 if let Some(raw_caps) = cap_part {
262 for cap in raw_caps.split_whitespace() {
263 if let Some(target) = cap.strip_prefix("symref=HEAD:") {
264 head_symref = Some(target.to_owned());
265 }
266 caps.insert(cap.to_owned());
267 }
268 }
269 first_ref_line = false;
270 }
271 if refname.is_empty() {
272 continue;
273 }
274 if oid_hex.bytes().all(|b| b == b'0') {
276 continue;
277 }
278 let oid = ObjectId::from_hex(oid_hex).map_err(|e| {
279 Error::Message(format!("bad oid in advertisement: {oid_hex}: {e}"))
280 })?;
281 refs.push(AdvRef {
282 name: refname.to_owned(),
283 oid,
284 });
285 }
286 Some(other) => {
287 return Err(Error::Message(format!(
288 "unexpected packet in advertisement: {other:?}"
289 )))
290 }
291 }
292 }
293 let object_format = caps
294 .iter()
295 .find_map(|c| c.strip_prefix("object-format="))
296 .unwrap_or("sha1")
297 .to_owned();
298 Ok(Discovery {
299 protocol_version: if caps.contains("version 1") { 1 } else { 0 },
300 refs,
301 caps,
302 head_symref,
303 object_format,
304 })
305}
306
307fn info_refs_url(repo_url: &str) -> String {
309 let base = repo_url.trim_end_matches('/');
310 let mut url = format!("{base}/info/refs");
311 url.push_str(if url.contains('?') { "&" } else { "?" });
312 url.push_str("service=");
313 url.push_str(UPLOAD_PACK);
314 url
315}
316
317fn upload_pack_url(repo_url: &str) -> String {
319 let base = repo_url.trim_end_matches('/');
320 format!("{base}/{UPLOAD_PACK}")
321}
322
323pub struct SmartHttpConnection {
332 repo_url: String,
333 adv_refs: Vec<(String, ObjectId)>,
334 caps: Vec<String>,
335 head_symref: Option<String>,
336 protocol_version: u8,
337 object_format: String,
338 service: Service,
340 empty_reader: Cursor<Vec<u8>>,
341 sink: Vec<u8>,
342}
343
344impl SmartHttpConnection {
345 #[must_use]
347 pub fn repo_url(&self) -> &str {
348 &self.repo_url
349 }
350
351 #[must_use]
353 pub fn object_format(&self) -> &str {
354 &self.object_format
355 }
356
357 #[must_use]
359 pub fn service(&self) -> Service {
360 self.service
361 }
362}
363
364impl Connection for SmartHttpConnection {
365 fn reader(&mut self) -> &mut dyn Read {
366 &mut self.empty_reader
367 }
368
369 fn writer(&mut self) -> &mut dyn Write {
370 &mut self.sink
371 }
372
373 fn advertised_refs(&self) -> &[(String, ObjectId)] {
374 &self.adv_refs
375 }
376
377 fn capabilities(&self) -> &[String] {
378 &self.caps
379 }
380
381 fn head_symref(&self) -> Option<&str> {
382 self.head_symref.as_deref()
383 }
384
385 fn protocol_version(&self) -> u8 {
386 self.protocol_version
387 }
388}
389
390pub struct SmartHttpTransport<C: HttpClient> {
397 client: C,
398}
399
400impl<C: HttpClient> SmartHttpTransport<C> {
401 pub fn new(client: C) -> Self {
403 Self { client }
404 }
405
406 pub fn client(&self) -> &C {
408 &self.client
409 }
410
411 pub fn push(
428 &self,
429 local_git_dir: &Path,
430 repo_url: &str,
431 refs: &[crate::transfer::PushRefSpec],
432 opts: &crate::transfer::PushOptions,
433 progress: &mut dyn Progress,
434 ) -> Result<crate::transfer::PushOutcome> {
435 crate::push::push_http(&self.client, local_git_dir, repo_url, refs, opts, progress)
436 }
437
438 fn discover(
445 &self,
446 repo_url: &str,
447 _service: Service,
448 git_protocol: Option<&str>,
449 ) -> Result<Discovery> {
450 let url = info_refs_url(repo_url);
451 let gp = git_protocol.or_else(|| self.client.git_protocol_header());
452 let body = self.client.get(&url, gp)?;
453 let stripped = strip_service_advertisement(&body)?;
454 parse_advertisement(stripped)
455 }
456}
457
458fn git_protocol_for_version(version: u8) -> Option<String> {
461 if version >= 1 {
462 Some(format!("version={version}"))
463 } else {
464 None
465 }
466}
467
468impl<C: HttpClient> Transport for SmartHttpTransport<C> {
469 fn connect(
470 &self,
471 url: &str,
472 service: Service,
473 opts: &ConnectOptions,
474 ) -> Result<Box<dyn Connection>> {
475 crate::net_trace::net_trace!(
480 "http(s) discover {url} (service={}, request protocol v{})",
481 service.wire_name(),
482 opts.protocol_version
483 );
484 let gp = git_protocol_for_version(opts.protocol_version);
485 let disc = self.discover(url, service, gp.as_deref())?;
486 let adv_refs: Vec<(String, ObjectId)> = disc
487 .refs
488 .iter()
489 .filter(|r| r.name != "HEAD" && !r.name.ends_with("^{}"))
490 .map(|r| (r.name.clone(), r.oid))
491 .collect();
492 let caps: Vec<String> = disc.caps.iter().cloned().collect();
493 crate::net_trace::net_trace!(
494 "http(s) discovered: protocol v{}, {} ref(s) advertised",
495 disc.protocol_version,
496 adv_refs.len()
497 );
498 Ok(Box::new(SmartHttpConnection {
499 repo_url: url.to_owned(),
500 adv_refs,
501 caps,
502 head_symref: disc.head_symref,
503 protocol_version: disc.protocol_version,
504 object_format: disc.object_format,
505 service,
506 empty_reader: Cursor::new(Vec::new()),
507 sink: Vec::new(),
508 }))
509 }
510}
511
512fn read_pkt_payload(r: &mut impl Read) -> std::io::Result<Option<Vec<u8>>> {
514 let mut len_buf = [0u8; 4];
515 match r.read_exact(&mut len_buf) {
516 Ok(()) => {}
517 Err(e) if e.kind() == std::io::ErrorKind::UnexpectedEof => return Ok(None),
518 Err(e) => return Err(e),
519 }
520 let len_str = std::str::from_utf8(&len_buf)
521 .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?;
522 let len = usize::from_str_radix(len_str, 16)
523 .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?;
524 match len {
525 0..=2 => Ok(None),
526 n if n <= 4 => Err(std::io::Error::new(
527 std::io::ErrorKind::InvalidData,
528 format!("invalid pkt-line length: {n}"),
529 )),
530 n => {
531 let mut buf = vec![0u8; n - 4];
532 r.read_exact(&mut buf)?;
533 Ok(Some(buf))
534 }
535 }
536}
537
538#[derive(Clone, Copy, PartialEq, Eq)]
540enum AckKind {
541 Bare,
543 Common,
546 Continue,
548 Ready,
550}
551
552struct Ack {
553 oid: ObjectId,
554 kind: AckKind,
555}
556
557fn parse_ack(line: &str) -> Option<Ack> {
558 let rest = line.strip_prefix("ACK ")?;
559 let hex = rest.split_whitespace().next()?;
560 let oid = ObjectId::from_hex(hex).ok()?;
561 let tail = rest.strip_prefix(hex).unwrap_or("").trim();
562 let kind = if tail.contains("continue") {
563 AckKind::Continue
564 } else if tail.contains("common") {
565 AckKind::Common
566 } else if tail.contains("ready") {
567 AckKind::Ready
568 } else {
569 AckKind::Bare
570 };
571 Some(Ack { oid, kind })
572}
573
574struct RoundResult {
576 acks: Vec<Ack>,
577 got_pack: bool,
578 shallow: Vec<ObjectId>,
581 unshallow: Vec<ObjectId>,
583}
584
585fn read_sideband_pack(
589 r: &mut impl Read,
590 out: &mut Vec<u8>,
591 progress: &mut dyn Progress,
592) -> Result<()> {
593 let mut seen_pack = false;
594 let mut pending: Vec<u8> = Vec::new();
595 loop {
596 let mut len_buf = [0u8; 4];
597 match r.read_exact(&mut len_buf) {
598 Ok(()) => {}
599 Err(e) if e.kind() == std::io::ErrorKind::UnexpectedEof => break,
600 Err(e) => return Err(e.into()),
601 }
602 let len_str = std::str::from_utf8(&len_buf)
603 .map_err(|_| Error::Message("bad pkt length".to_owned()))?;
604 let len = usize::from_str_radix(len_str, 16)
605 .map_err(|_| Error::Message("bad pkt length".to_owned()))?;
606 match len {
607 0 => {
608 if seen_pack {
609 break;
610 }
611 continue;
612 }
613 1 | 2 => continue,
614 n if n <= 4 => {
615 return Err(Error::Message(format!(
616 "invalid pkt-line length in side-band stream: {n}"
617 )))
618 }
619 _ => {}
620 }
621 let mut payload = vec![0u8; len - 4];
622 r.read_exact(&mut payload)?;
623 if payload.is_empty() {
624 continue;
625 }
626 match payload[0] {
627 1 => append_pack_data(&payload[1..], out, &mut pending, &mut seen_pack),
628 2 => progress.message(&payload[1..]),
629 3 => {
630 return Err(Error::Message(format!(
631 "remote error: {}",
632 String::from_utf8_lossy(&payload[1..]).trim_end()
633 )))
634 }
635 _ => append_pack_data(&payload, out, &mut pending, &mut seen_pack),
636 }
637 }
638 Ok(())
639}
640
641fn append_pack_data(data: &[u8], out: &mut Vec<u8>, pending: &mut Vec<u8>, seen_pack: &mut bool) {
644 if *seen_pack {
645 out.extend_from_slice(data);
646 return;
647 }
648 pending.extend_from_slice(data);
649 if let Some(pos) = pending.windows(4).position(|w| w == b"PACK") {
650 *seen_pack = true;
651 out.extend_from_slice(&pending[pos..]);
652 pending.clear();
653 } else if pending.len() > 3 {
654 let keep_from = pending.len() - 3;
655 pending.drain(..keep_from);
656 }
657}
658
659fn read_stateless_response(
664 resp: &[u8],
665 sideband: bool,
666 expect_shallow: bool,
667 pack_buf: &mut Vec<u8>,
668 progress: &mut dyn Progress,
669) -> Result<RoundResult> {
670 let mut cur = Cursor::new(resp);
671 let mut acks = Vec::new();
672 let mut got_pack = false;
673 let mut shallow = Vec::new();
674 let mut unshallow = Vec::new();
675
676 if expect_shallow {
680 loop {
681 let start = cur.position() as usize;
682 match pkt_line::read_packet(&mut cur)? {
683 None | Some(pkt_line::Packet::Flush) => break,
684 Some(pkt_line::Packet::Data(line)) => {
685 let line = line.trim_end_matches('\n');
686 if let Some(rest) = line.strip_prefix("shallow ") {
687 if let Ok(oid) = ObjectId::from_hex(rest.trim()) {
688 shallow.push(oid);
689 }
690 } else if let Some(rest) = line.strip_prefix("unshallow ") {
691 if let Ok(oid) = ObjectId::from_hex(rest.trim()) {
692 unshallow.push(oid);
693 }
694 } else {
695 cur.set_position(start as u64);
696 break;
697 }
698 }
699 Some(_) => break,
700 }
701 }
702 }
703
704 loop {
705 let start = cur.position() as usize;
706 let Some(payload) = read_pkt_payload(&mut cur)? else {
707 break;
708 };
709 if payload.is_empty() {
710 continue;
711 }
712 let is_pack = (sideband
713 && payload.first() == Some(&1)
714 && payload.get(1..5) == Some(b"PACK"))
715 || payload.starts_with(b"PACK");
716 if is_pack {
717 got_pack = true;
718 cur.set_position(start as u64);
719 if sideband {
720 read_sideband_pack(&mut cur, pack_buf, progress)?;
721 } else {
722 pack_buf.extend_from_slice(&resp[start..]);
723 }
724 break;
725 }
726 let text = String::from_utf8_lossy(&payload);
727 let line = text.trim_end_matches('\n');
728 if let Some(err) = line.strip_prefix("ERR ") {
729 return Err(Error::Message(format!("remote upload-pack error: {err}")));
730 }
731 if line == "NAK" {
732 continue;
733 }
734 if let Some(ack) = parse_ack(line) {
735 acks.push(ack);
736 }
737 }
738 Ok(RoundResult {
739 acks,
740 got_pack,
741 shallow,
742 unshallow,
743 })
744}
745
746fn build_fetch_caps(caps: &HashSet<String>) -> String {
749 let mut enabled = Vec::new();
750 let multi_ack_detailed = caps.contains("multi_ack_detailed");
751 if multi_ack_detailed {
752 enabled.push("multi_ack_detailed");
753 }
754 if multi_ack_detailed && caps.contains("no-done") {
755 enabled.push("no-done");
756 }
757 for want in [
758 "side-band-64k",
759 "thin-pack",
760 "no-progress",
761 "include-tag",
762 "ofs-delta",
763 ] {
764 if caps.contains(want) {
765 enabled.push(want);
766 }
767 }
768 if enabled.is_empty() {
769 String::new()
770 } else {
771 format!(" {}", enabled.join(" "))
772 }
773}
774
775fn next_flush(count: usize) -> usize {
777 const LARGE_FLUSH: usize = 16384;
778 if count < LARGE_FLUSH {
779 count * 2
780 } else {
781 count * 11 / 10
782 }
783}
784
785fn append_shallow_request_v0_http(
790 req: &mut Vec<u8>,
791 caps: &HashSet<String>,
792 local_shallow: &[ObjectId],
793 opts: &FetchOptions,
794) -> Result<()> {
795 for oid in local_shallow {
796 pkt_line::write_line_to_vec(req, &format!("shallow {}", oid.to_hex()))?;
797 }
798 if opts.unshallow {
799 pkt_line::write_line_to_vec(req, &format!("deepen {}", crate::shallow::INFINITE_DEPTH))?;
800 } else if let Some(depth) = opts.depth.filter(|d| *d > 0) {
801 pkt_line::write_line_to_vec(req, &format!("deepen {depth}"))?;
802 }
803 if let Some(since) = opts.deepen_since.as_deref().filter(|s| !s.trim().is_empty()) {
804 if caps.contains("deepen-since") {
805 let value = crate::shallow::deepen_since_wire_value(since);
806 pkt_line::write_line_to_vec(req, &format!("deepen-since {value}"))?;
807 }
808 }
809 if caps.contains("deepen-not") {
810 for excl in &opts.deepen_not {
811 let excl = excl.trim();
812 if !excl.is_empty() {
813 pkt_line::write_line_to_vec(req, &format!("deepen-not {excl}"))?;
814 }
815 }
816 }
817 Ok(())
818}
819
820fn negotiate_pack_http(
824 client: &dyn HttpClient,
825 local_git_dir: &Path,
826 repo_url: &str,
827 caps: &HashSet<String>,
828 advertised: &[AdvRef],
829 wants: &[ObjectId],
830 opts: &FetchOptions,
831 local_shallow: &[ObjectId],
832 progress: &mut dyn Progress,
833) -> Result<(Vec<u8>, crate::fetch::ShallowUpdate)> {
834 let post_url = upload_pack_url(repo_url);
835 let content_type = format!("application/x-{UPLOAD_PACK}-request");
836 let accept = format!("application/x-{UPLOAD_PACK}-result");
837 let fetch_caps = build_fetch_caps(caps);
838 let sideband = caps.contains("side-band-64k");
839 let multi_ack_detailed = caps.contains("multi_ack_detailed");
840 let no_done = multi_ack_detailed && caps.contains("no-done");
841
842 let shallow_request = opts.has_deepen_request() || !local_shallow.is_empty();
845
846 let want_set: HashSet<ObjectId> = wants.iter().copied().collect();
847
848 let mut state = Vec::new();
852 let first = wants[0];
853 pkt_line::write_line_to_vec(&mut state, &format!("want {}{}", first.to_hex(), fetch_caps))?;
854 for w in wants.iter().skip(1) {
855 pkt_line::write_line_to_vec(&mut state, &format!("want {}", w.to_hex()))?;
856 }
857 append_shallow_request_v0_http(&mut state, caps, local_shallow, opts)?;
858 pkt_line::write_flush(&mut state)?;
859
860 let mut shallow_update = crate::fetch::ShallowUpdate::default();
861
862 let local_repo = crate::repo::Repository::open(local_git_dir, None)?;
865 let mut negotiator = SkippingNegotiator::new(local_repo);
866 if !shallow_request {
867 for w in wants {
868 if negotiator.repo().odb.read(w).is_ok() {
869 negotiator.add_tip(*w)?;
870 }
871 }
872 let mut tips: Vec<ObjectId> = Vec::new();
873 for prefix in ["refs/heads/", "refs/tags/"] {
874 if let Ok(entries) = crate::refs::list_refs(local_git_dir, prefix) {
875 for (_, oid) in entries {
876 if negotiator.repo().odb.read(&oid).is_ok() {
877 tips.push(oid);
878 }
879 }
880 }
881 }
882 if let Ok(h) = crate::refs::resolve_ref(local_git_dir, "HEAD") {
883 if negotiator.repo().odb.read(&h).is_ok() {
884 tips.push(h);
885 }
886 }
887 tips.sort_by_key(ObjectId::to_hex);
888 tips.dedup();
889 for t in tips {
890 if want_set.contains(&t) {
891 continue;
892 }
893 negotiator.add_tip(t)?;
894 }
895 for e in advertised {
896 if want_set.contains(&e.oid) {
897 continue;
898 }
899 if negotiator.repo().odb.read(&e.oid).is_ok() {
900 negotiator.known_common(e.oid)?;
901 }
902 }
903 }
904
905 let mut pack_buf: Vec<u8> = Vec::new();
906 let mut got_ready = false;
907 let mut got_pack = false;
908 let mut shallow_applied = false;
909
910 const INITIAL_FLUSH: usize = 16;
911 let mut count: usize = 0;
912 let mut flush_at: usize = INITIAL_FLUSH;
913 let mut round = Vec::new();
914 while let Some(oid) = negotiator.next_have()? {
917 pkt_line::write_line_to_vec(&mut round, &format!("have {}", oid.to_hex()))?;
918 count += 1;
919 if count < flush_at {
920 continue;
921 }
922 flush_at = next_flush(count);
923
924 let mut req = state.clone();
925 req.extend_from_slice(&round);
926 pkt_line::write_flush(&mut req)?;
927 round.clear();
928
929 let resp = client.post(&post_url, &content_type, &accept, &req, None)?;
930 let round_result =
931 read_stateless_response(&resp, sideband, shallow_request, &mut pack_buf, progress)?;
932 if shallow_request && !shallow_applied {
933 shallow_update.shallow.extend(round_result.shallow.iter().copied());
934 shallow_update.unshallow.extend(round_result.unshallow.iter().copied());
935 shallow_applied = true;
936 }
937 for ack in &round_result.acks {
938 if matches!(ack.kind, AckKind::Bare) {
939 continue;
940 }
941 let was_common = negotiator.ack(ack.oid)?;
942 if matches!(ack.kind, AckKind::Common) && !was_common {
943 pkt_line::write_line_to_vec(&mut state, &format!("have {}", ack.oid.to_hex()))?;
944 }
945 if matches!(ack.kind, AckKind::Ready) {
946 got_ready = true;
947 }
948 }
949 if round_result.got_pack {
950 got_pack = true;
951 break;
952 }
953 if got_ready {
954 break;
955 }
956 }
957
958 if !(got_pack || got_ready && no_done) {
961 let mut req = state.clone();
962 pkt_line::write_line_to_vec(&mut req, "done")?;
963 pkt_line::write_flush(&mut req)?;
964 let resp = client.post(&post_url, &content_type, &accept, &req, None)?;
965 let round_result =
966 read_stateless_response(&resp, sideband, shallow_request, &mut pack_buf, progress)?;
967 if shallow_request && !shallow_applied {
968 shallow_update.shallow.extend(round_result.shallow);
969 shallow_update.unshallow.extend(round_result.unshallow);
970 }
971 }
972
973 Ok((pack_buf, shallow_update))
974}
975
976struct MatchPlan {
981 matched: Vec<crate::transfer::MatchedRef>,
982 wants: HashSet<ObjectId>,
983 seen: HashSet<String>,
984}
985
986fn match_refspecs(
987 remote_refs: &[(String, ObjectId)],
988 positive: &[RefspecItem],
989 negatives: &[RefspecItem],
990) -> MatchPlan {
991 let mut matched: Vec<crate::transfer::MatchedRef> = Vec::new();
992 let mut wants: HashSet<ObjectId> = HashSet::new();
993 let mut seen: HashSet<String> = HashSet::new();
994 for (name, oid) in remote_refs {
995 if ref_excluded(name, negatives) {
996 continue;
997 }
998 if let Some(local_ref) = match_positive(name, positive) {
999 if seen.insert(name.clone()) {
1000 wants.insert(*oid);
1001 matched.push(crate::transfer::MatchedRef {
1002 remote_ref: name.clone(),
1003 local_ref,
1004 oid: *oid,
1005 force: refspecs_force(name, positive),
1006 is_tag: name.starts_with("refs/tags/"),
1007 });
1008 }
1009 }
1010 }
1011 MatchPlan {
1012 matched,
1013 wants,
1014 seen,
1015 }
1016}
1017
1018pub fn http_fetch(
1039 client: &dyn HttpClient,
1040 local_git_dir: &Path,
1041 repo_url: &str,
1042 opts: &FetchOptions,
1043 progress: &mut dyn Progress,
1044) -> Result<FetchOutcome> {
1045 use crate::net_trace::net_trace;
1046 net_trace!(
1047 "http_fetch: begin — {} ({} refspec(s), tags={:?})",
1048 repo_url,
1049 opts.refspecs.len(),
1050 opts.tags
1051 );
1052 let disc = {
1055 let url = info_refs_url(repo_url);
1056 let body = client.get(&url, client.git_protocol_header())?;
1057 let stripped = strip_service_advertisement(&body)?;
1058 parse_advertisement(stripped)?
1059 };
1060 net_trace!(
1061 "http_fetch: discovered protocol v{}, {} ref(s)",
1062 disc.protocol_version,
1063 disc.refs.len()
1064 );
1065 if disc.protocol_version >= 2 {
1066 net_trace!("http_fetch: delegating to v2 stateless fetch");
1067 return http_fetch_v2(client, local_git_dir, repo_url, &disc, opts, progress);
1068 }
1069
1070 let local_odb = open_odb(local_git_dir);
1071
1072 let default_branch = disc
1073 .head_symref
1074 .as_deref()
1075 .map(|t| t.strip_prefix("refs/heads/").unwrap_or(t).to_owned());
1076
1077 let remote_refs: Vec<(String, ObjectId)> = disc
1078 .refs
1079 .iter()
1080 .filter(|r| r.name != "HEAD" && !r.name.ends_with("^{}"))
1081 .map(|r| (r.name.clone(), r.oid))
1082 .collect();
1083
1084 let mut positive: Vec<RefspecItem> = Vec::new();
1086 let mut negatives: Vec<RefspecItem> = Vec::new();
1087 for spec in &opts.refspecs {
1088 let item = parse_fetch_refspec(spec)
1089 .map_err(|e| Error::Message(format!("invalid refspec '{spec}': {e}")))?;
1090 if item.negative {
1091 negatives.push(item);
1092 } else {
1093 positive.push(item);
1094 }
1095 }
1096 for spec in &opts.negative_refspecs {
1097 let item = parse_fetch_refspec(spec)
1098 .map_err(|e| Error::Message(format!("invalid negative refspec '{spec}': {e}")))?;
1099 negatives.push(item);
1100 }
1101
1102 let MatchPlan {
1104 mut matched,
1105 mut wants,
1106 mut seen,
1107 } = match_refspecs(&remote_refs, &positive, &negatives);
1108
1109 if opts.tags != TagMode::None {
1113 for (name, oid) in &remote_refs {
1114 if !name.starts_with("refs/tags/") {
1115 continue;
1116 }
1117 if seen.contains(name) || ref_excluded(name, &negatives) {
1118 continue;
1119 }
1120 seen.insert(name.clone());
1121 wants.insert(*oid);
1122 matched.push(crate::transfer::MatchedRef {
1123 remote_ref: name.clone(),
1124 local_ref: Some(name.clone()),
1125 oid: *oid,
1126 force: false,
1127 is_tag: true,
1128 });
1129 }
1130 }
1131
1132 let local_shallow = crate::shallow::load_shallow_oids(local_git_dir)?;
1136 let shallow_request = opts.has_deepen_request() || !local_shallow.is_empty();
1137 let need: Vec<ObjectId> = if shallow_request {
1138 wants.iter().copied().collect()
1139 } else {
1140 wants
1141 .iter()
1142 .copied()
1143 .filter(|oid| !local_odb.exists(oid))
1144 .collect()
1145 };
1146
1147 let mut shallow_update = crate::fetch::ShallowUpdate::default();
1148
1149 if !need.is_empty() && !opts.dry_run {
1150 let (pack, su) = negotiate_pack_http(
1151 client,
1152 local_git_dir,
1153 repo_url,
1154 &disc.caps,
1155 &disc.refs,
1156 &need,
1157 opts,
1158 &local_shallow,
1159 progress,
1160 )?;
1161 shallow_update = su;
1162 if !pack.is_empty() {
1163 if pack.len() < 12 || &pack[0..4] != b"PACK" {
1164 return Err(Error::Message(
1165 "did not receive a valid pack from HTTP fetch".to_owned(),
1166 ));
1167 }
1168 let mut cursor = Cursor::new(pack);
1169 crate::unpack_objects::unpack_objects(
1170 &mut cursor,
1171 &local_odb,
1172 &crate::unpack_objects::UnpackOptions {
1173 quiet: true,
1174 ..Default::default()
1175 },
1176 )?;
1177 }
1178 }
1179
1180 if !opts.dry_run {
1182 crate::shallow::apply_shallow_updates(
1183 local_git_dir,
1184 &shallow_update.shallow,
1185 &shallow_update.unshallow,
1186 )?;
1187 }
1188
1189 if opts.tags == TagMode::Following {
1191 retain_following_tags(&local_odb, &mut matched, &wants);
1192 }
1193
1194 let local_repo = if opts.dry_run {
1196 None
1197 } else {
1198 crate::repo::Repository::open(local_git_dir, None).ok()
1199 };
1200
1201 let mut updates: Vec<RefUpdate> = Vec::new();
1202 if opts.prune {
1203 prune_tracking_refs(
1204 local_git_dir,
1205 &positive,
1206 &remote_refs,
1207 opts.dry_run,
1208 &mut updates,
1209 )?;
1210 }
1211
1212 for m in &matched {
1213 let Some(local_ref) = &m.local_ref else {
1214 updates.push(RefUpdate {
1215 remote_ref: m.remote_ref.clone(),
1216 local_ref: None,
1217 old_oid: None,
1218 new_oid: Some(m.oid),
1219 mode: UpdateMode::NoChangeNeeded,
1220 note: Some("not stored (empty destination)".to_owned()),
1221 });
1222 continue;
1223 };
1224 let old = crate::refs::resolve_ref(local_git_dir, local_ref).ok();
1225 let mode = classify_update(old.as_ref(), &m.oid, m.force, m.is_tag, local_repo.as_ref());
1226 let write = matches!(
1227 mode,
1228 UpdateMode::New | UpdateMode::FastForward | UpdateMode::Forced
1229 );
1230 if write && !opts.dry_run {
1231 crate::refs::write_ref(local_git_dir, local_ref, &m.oid)?;
1232 }
1233 updates.push(RefUpdate {
1234 remote_ref: m.remote_ref.clone(),
1235 local_ref: Some(local_ref.clone()),
1236 old_oid: old,
1237 new_oid: Some(m.oid),
1238 mode,
1239 note: None,
1240 });
1241 }
1242
1243 net_trace!("http_fetch: done — {} ref update(s)", updates.len());
1244 Ok(FetchOutcome {
1245 updates,
1246 default_branch,
1247 new_shallow: shallow_update.shallow,
1248 new_unshallow: shallow_update.unshallow,
1249 })
1250}
1251
1252fn http_fetch_v2(
1263 client: &dyn HttpClient,
1264 local_git_dir: &Path,
1265 repo_url: &str,
1266 disc: &Discovery,
1267 opts: &FetchOptions,
1268 progress: &mut dyn Progress,
1269) -> Result<FetchOutcome> {
1270 let local_odb = open_odb(local_git_dir);
1271 let server_caps: Vec<String> = disc.caps.iter().cloned().collect();
1275
1276 let post_url = upload_pack_url(repo_url);
1277 let content_type = format!("application/x-{UPLOAD_PACK}-request");
1278 let accept = format!("application/x-{UPLOAD_PACK}-result");
1279 let git_protocol = "version=2";
1281
1282 let (remote_refs, head_symref) = {
1284 let req =
1285 crate::fetch::build_v2_ls_refs_request(&server_caps, &local_odb, opts.tags, &opts.refspecs)?;
1286 let resp = client.post(&post_url, &content_type, &accept, &req, Some(git_protocol))?;
1287 let mut cur = Cursor::new(resp);
1288 crate::fetch::parse_v2_ls_refs_response(&mut cur)?
1289 };
1290 let default_branch = head_symref
1291 .as_deref()
1292 .map(|t| t.strip_prefix("refs/heads/").unwrap_or(t).to_owned());
1293
1294 let mut positive: Vec<RefspecItem> = Vec::new();
1296 let mut negatives: Vec<RefspecItem> = Vec::new();
1297 for spec in &opts.refspecs {
1298 let item = parse_fetch_refspec(spec)
1299 .map_err(|e| Error::Message(format!("invalid refspec '{spec}': {e}")))?;
1300 if item.negative {
1301 negatives.push(item);
1302 } else {
1303 positive.push(item);
1304 }
1305 }
1306 for spec in &opts.negative_refspecs {
1307 let item = parse_fetch_refspec(spec)
1308 .map_err(|e| Error::Message(format!("invalid negative refspec '{spec}': {e}")))?;
1309 negatives.push(item);
1310 }
1311
1312 let MatchPlan {
1314 mut matched,
1315 mut wants,
1316 mut seen,
1317 } = match_refspecs(&remote_refs, &positive, &negatives);
1318
1319 if opts.tags != TagMode::None {
1323 for (name, oid) in &remote_refs {
1324 if !name.starts_with("refs/tags/") {
1325 continue;
1326 }
1327 if seen.contains(name) || ref_excluded(name, &negatives) {
1328 continue;
1329 }
1330 seen.insert(name.clone());
1331 wants.insert(*oid);
1332 matched.push(crate::transfer::MatchedRef {
1333 remote_ref: name.clone(),
1334 local_ref: Some(name.clone()),
1335 oid: *oid,
1336 force: false,
1337 is_tag: true,
1338 });
1339 }
1340 }
1341
1342 let local_shallow = crate::shallow::load_shallow_oids(local_git_dir)?;
1346 let shallow_request = opts.has_deepen_request() || !local_shallow.is_empty();
1347 let need: Vec<ObjectId> = if shallow_request {
1348 wants.iter().copied().collect()
1349 } else {
1350 wants
1351 .iter()
1352 .copied()
1353 .filter(|oid| !local_odb.exists(oid))
1354 .collect()
1355 };
1356
1357 let mut shallow_update = crate::fetch::ShallowUpdate::default();
1358
1359 if !need.is_empty() && !opts.dry_run {
1360 let deepen = crate::fetch::V2DeepenArgs::from_opts(opts, &local_shallow);
1361 let (pack, su) = negotiate_pack_v2_http(
1362 client,
1363 local_git_dir,
1364 &post_url,
1365 &content_type,
1366 &accept,
1367 git_protocol,
1368 &server_caps,
1369 &local_odb,
1370 &need,
1371 &deepen,
1372 progress,
1373 )?;
1374 shallow_update = su;
1375 if !pack.is_empty() {
1376 if pack.len() < 12 || &pack[0..4] != b"PACK" {
1377 return Err(Error::Message(
1378 "did not receive a valid pack from v2 HTTP fetch".to_owned(),
1379 ));
1380 }
1381 let mut cursor = Cursor::new(pack);
1382 crate::unpack_objects::unpack_objects(
1383 &mut cursor,
1384 &local_odb,
1385 &crate::unpack_objects::UnpackOptions {
1386 quiet: true,
1387 ..Default::default()
1388 },
1389 )?;
1390 }
1391 }
1392
1393 if !opts.dry_run {
1395 crate::shallow::apply_shallow_updates(
1396 local_git_dir,
1397 &shallow_update.shallow,
1398 &shallow_update.unshallow,
1399 )?;
1400 }
1401
1402 if opts.tags == TagMode::Following {
1404 retain_following_tags(&local_odb, &mut matched, &wants);
1405 }
1406
1407 let local_repo = if opts.dry_run {
1409 None
1410 } else {
1411 crate::repo::Repository::open(local_git_dir, None).ok()
1412 };
1413
1414 let mut updates: Vec<RefUpdate> = Vec::new();
1415 if opts.prune {
1416 prune_tracking_refs(
1417 local_git_dir,
1418 &positive,
1419 &remote_refs,
1420 opts.dry_run,
1421 &mut updates,
1422 )?;
1423 }
1424
1425 for m in &matched {
1426 let Some(local_ref) = &m.local_ref else {
1427 updates.push(RefUpdate {
1428 remote_ref: m.remote_ref.clone(),
1429 local_ref: None,
1430 old_oid: None,
1431 new_oid: Some(m.oid),
1432 mode: UpdateMode::NoChangeNeeded,
1433 note: Some("not stored (empty destination)".to_owned()),
1434 });
1435 continue;
1436 };
1437 let old = crate::refs::resolve_ref(local_git_dir, local_ref).ok();
1438 let mode = classify_update(old.as_ref(), &m.oid, m.force, m.is_tag, local_repo.as_ref());
1439 let write = matches!(
1440 mode,
1441 UpdateMode::New | UpdateMode::FastForward | UpdateMode::Forced
1442 );
1443 if write && !opts.dry_run {
1444 crate::refs::write_ref(local_git_dir, local_ref, &m.oid)?;
1445 }
1446 updates.push(RefUpdate {
1447 remote_ref: m.remote_ref.clone(),
1448 local_ref: Some(local_ref.clone()),
1449 old_oid: old,
1450 new_oid: Some(m.oid),
1451 mode,
1452 note: None,
1453 });
1454 }
1455
1456 crate::net_trace::net_trace!("http_fetch (v2): done — {} ref update(s)", updates.len());
1457 Ok(FetchOutcome {
1458 updates,
1459 default_branch,
1460 new_shallow: shallow_update.shallow,
1461 new_unshallow: shallow_update.unshallow,
1462 })
1463}
1464
1465#[allow(clippy::too_many_arguments)]
1480fn negotiate_pack_v2_http(
1481 client: &dyn HttpClient,
1482 local_git_dir: &Path,
1483 post_url: &str,
1484 content_type: &str,
1485 accept: &str,
1486 git_protocol: &str,
1487 server_caps: &[String],
1488 local_odb: &crate::odb::Odb,
1489 wants: &[ObjectId],
1490 deepen: &crate::fetch::V2DeepenArgs,
1491 progress: &mut dyn Progress,
1492) -> Result<(Vec<u8>, crate::fetch::ShallowUpdate)> {
1493 if wants.is_empty() {
1494 return Ok((Vec::new(), crate::fetch::ShallowUpdate::default()));
1495 }
1496 let object_format = crate::fetch::v2_object_format(server_caps, local_odb);
1497 let cap_echo = protocol_v2::cap_lines_for_command_request(server_caps);
1498 let sideband_all = protocol_v2::fetch_supports_sideband_all(server_caps);
1499
1500 let shallow_request = deepen.is_shallow_request();
1504
1505 let haves = if shallow_request {
1509 Vec::new()
1510 } else {
1511 crate::fetch::v2_local_haves(local_git_dir, wants)?
1512 };
1513
1514 let mut pack = Vec::new();
1515 let mut shallow_update = crate::fetch::ShallowUpdate::default();
1516
1517 if haves.is_empty() {
1519 let mut req = Vec::new();
1520 crate::fetch::write_v2_fetch_request(
1521 &mut req,
1522 &object_format,
1523 &cap_echo,
1524 wants,
1525 &[],
1526 sideband_all,
1527 deepen,
1528 true,
1529 )?;
1530 let resp = client.post(post_url, content_type, accept, &req, Some(git_protocol))?;
1531 let mut cur = Cursor::new(resp);
1532 crate::fetch::read_v2_fetch_pack_response(&mut cur, &mut pack, &mut shallow_update, progress)?;
1533 return Ok((pack, shallow_update));
1534 }
1535
1536 const INITIAL_FLUSH: usize = 16;
1540 let mut flush_at: usize = INITIAL_FLUSH.min(haves.len());
1541 loop {
1542 if flush_at < haves.len() {
1543 let mut req = Vec::new();
1545 crate::fetch::write_v2_fetch_request(
1546 &mut req,
1547 &object_format,
1548 &cap_echo,
1549 wants,
1550 &haves[..flush_at],
1551 sideband_all,
1552 deepen,
1553 false,
1554 )?;
1555 let resp = client.post(post_url, content_type, accept, &req, Some(git_protocol))?;
1556 let mut cur = Cursor::new(resp);
1557 let ack = crate::fetch::read_v2_acknowledgments(&mut cur)?;
1558 if let Some(round) = ack {
1559 if round.ready {
1560 crate::fetch::read_v2_fetch_pack_response(
1562 &mut cur,
1563 &mut pack,
1564 &mut shallow_update,
1565 progress,
1566 )?;
1567 return Ok((pack, shallow_update));
1568 }
1569 } else {
1570 crate::fetch::read_v2_fetch_pack_response(
1572 &mut cur,
1573 &mut pack,
1574 &mut shallow_update,
1575 progress,
1576 )?;
1577 return Ok((pack, shallow_update));
1578 }
1579 flush_at = next_flush(flush_at).min(haves.len());
1580 continue;
1581 }
1582
1583 let mut req = Vec::new();
1585 crate::fetch::write_v2_fetch_request(
1586 &mut req,
1587 &object_format,
1588 &cap_echo,
1589 wants,
1590 &haves,
1591 sideband_all,
1592 deepen,
1593 true,
1594 )?;
1595 let resp = client.post(post_url, content_type, accept, &req, Some(git_protocol))?;
1596 let mut cur = Cursor::new(resp);
1597 crate::fetch::read_v2_fetch_pack_response(&mut cur, &mut pack, &mut shallow_update, progress)?;
1598 return Ok((pack, shallow_update));
1599 }
1600}
1601
1602fn retain_following_tags(
1604 odb: &crate::odb::Odb,
1605 matched: &mut Vec<crate::transfer::MatchedRef>,
1606 wants: &HashSet<ObjectId>,
1607) {
1608 let roots: Vec<ObjectId> = matched.iter().filter(|m| !m.is_tag).map(|m| m.oid).collect();
1609 let closure = reachable_closure(odb, &roots);
1610 matched.retain(|m| {
1611 if !m.is_tag {
1612 return true;
1613 }
1614 let peeled = peel_tag_target(odb, m.oid);
1615 let have = odb.exists(&m.oid);
1616 have && (closure.contains(&m.oid) || closure.contains(&peeled) || wants.contains(&peeled))
1617 });
1618}
1619
1620fn peel_tag_target(odb: &crate::odb::Odb, oid: ObjectId) -> ObjectId {
1621 let mut current = oid;
1622 for _ in 0..16 {
1623 let Ok(obj) = odb.read(¤t) else {
1624 return current;
1625 };
1626 if obj.kind != crate::objects::ObjectKind::Tag {
1627 return current;
1628 }
1629 match crate::objects::parse_tag(&obj.data) {
1630 Ok(t) => current = t.object,
1631 Err(_) => return current,
1632 }
1633 }
1634 current
1635}
1636
1637fn reachable_closure(odb: &crate::odb::Odb, roots: &[ObjectId]) -> HashSet<ObjectId> {
1638 use crate::objects::{parse_commit, parse_tag, parse_tree, ObjectKind};
1639 let mut seen: HashSet<ObjectId> = HashSet::new();
1640 let mut stack: Vec<ObjectId> = roots.to_vec();
1641 while let Some(oid) = stack.pop() {
1642 if !seen.insert(oid) {
1643 continue;
1644 }
1645 let Ok(obj) = odb.read(&oid) else {
1646 continue;
1647 };
1648 match obj.kind {
1649 ObjectKind::Commit => {
1650 if let Ok(c) = parse_commit(&obj.data) {
1651 stack.push(c.tree);
1652 for p in c.parents {
1653 stack.push(p);
1654 }
1655 }
1656 }
1657 ObjectKind::Tree => {
1658 if let Ok(entries) = parse_tree(&obj.data) {
1659 for e in entries {
1660 stack.push(e.oid);
1661 }
1662 }
1663 }
1664 ObjectKind::Tag => {
1665 if let Ok(t) = parse_tag(&obj.data) {
1666 stack.push(t.object);
1667 }
1668 }
1669 ObjectKind::Blob => {}
1670 }
1671 }
1672 seen
1673}
1674
1675pub fn discovery_advertisement(conn: &SmartHttpConnection) -> Advertisement {
1678 Advertisement {
1679 refs: conn.adv_refs.clone(),
1680 capabilities: conn.caps.clone(),
1681 head_symref: conn.head_symref.clone(),
1682 protocol_version: conn.protocol_version,
1683 }
1684}
1685
1686#[cfg(test)]
1687mod tests {
1688 use super::*;
1689
1690 #[test]
1691 fn strips_smart_service_preamble() {
1692 let mut body = Vec::new();
1693 pkt_line::write_line_to_vec(&mut body, "# service=git-upload-pack\n").unwrap();
1694 body.extend_from_slice(b"0000");
1695 let oid = "1".repeat(40);
1696 let line = format!("{oid} refs/heads/main\0multi_ack_detailed side-band-64k");
1697 pkt_line::write_line_to_vec(&mut body, &line).unwrap();
1698 body.extend_from_slice(b"0000");
1699
1700 let stripped = strip_service_advertisement(&body).unwrap();
1701 let disc = parse_advertisement(stripped).unwrap();
1702 assert_eq!(disc.protocol_version, 0);
1703 assert_eq!(disc.refs.len(), 1);
1704 assert_eq!(disc.refs[0].name, "refs/heads/main");
1705 assert!(disc.caps.contains("side-band-64k"));
1706 }
1707
1708 #[test]
1709 fn parses_symref_and_caps() {
1710 let mut body = Vec::new();
1711 let main = "2".repeat(40);
1712 let head = format!(
1713 "{main} HEAD\0multi_ack_detailed symref=HEAD:refs/heads/main object-format=sha1"
1714 );
1715 pkt_line::write_line_to_vec(&mut body, &head).unwrap();
1716 let r = format!("{main} refs/heads/main");
1717 pkt_line::write_line_to_vec(&mut body, &r).unwrap();
1718 body.extend_from_slice(b"0000");
1719
1720 let disc = parse_advertisement(&body).unwrap();
1721 assert_eq!(disc.head_symref.as_deref(), Some("refs/heads/main"));
1722 assert_eq!(disc.object_format, "sha1");
1723 assert!(disc.refs.iter().any(|r| r.name == "HEAD"));
1726 assert!(disc.refs.iter().any(|r| r.name == "refs/heads/main"));
1727 }
1728
1729 #[test]
1730 fn detects_v2_preamble() {
1731 let mut body = Vec::new();
1732 pkt_line::write_line_to_vec(&mut body, "version 2").unwrap();
1733 pkt_line::write_line_to_vec(&mut body, "ls-refs").unwrap();
1734 pkt_line::write_line_to_vec(&mut body, "object-format=sha256").unwrap();
1735 body.extend_from_slice(b"0000");
1736 let disc = parse_advertisement(&body).unwrap();
1737 assert_eq!(disc.protocol_version, 2);
1738 assert_eq!(disc.object_format, "sha256");
1739 }
1740
1741 #[test]
1742 fn url_helpers() {
1743 assert_eq!(
1744 info_refs_url("http://h/r.git"),
1745 "http://h/r.git/info/refs?service=git-upload-pack"
1746 );
1747 assert_eq!(
1748 info_refs_url("http://h/r.git/"),
1749 "http://h/r.git/info/refs?service=git-upload-pack"
1750 );
1751 assert_eq!(upload_pack_url("http://h/r.git/"), "http://h/r.git/git-upload-pack");
1752 }
1753}