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 let gp = git_protocol_for_version(opts.protocol_version);
480 let disc = self.discover(url, service, gp.as_deref())?;
481 let adv_refs: Vec<(String, ObjectId)> = disc
482 .refs
483 .iter()
484 .filter(|r| r.name != "HEAD" && !r.name.ends_with("^{}"))
485 .map(|r| (r.name.clone(), r.oid))
486 .collect();
487 let caps: Vec<String> = disc.caps.iter().cloned().collect();
488 Ok(Box::new(SmartHttpConnection {
489 repo_url: url.to_owned(),
490 adv_refs,
491 caps,
492 head_symref: disc.head_symref,
493 protocol_version: disc.protocol_version,
494 object_format: disc.object_format,
495 service,
496 empty_reader: Cursor::new(Vec::new()),
497 sink: Vec::new(),
498 }))
499 }
500}
501
502fn read_pkt_payload(r: &mut impl Read) -> std::io::Result<Option<Vec<u8>>> {
504 let mut len_buf = [0u8; 4];
505 match r.read_exact(&mut len_buf) {
506 Ok(()) => {}
507 Err(e) if e.kind() == std::io::ErrorKind::UnexpectedEof => return Ok(None),
508 Err(e) => return Err(e),
509 }
510 let len_str = std::str::from_utf8(&len_buf)
511 .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?;
512 let len = usize::from_str_radix(len_str, 16)
513 .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?;
514 match len {
515 0..=2 => Ok(None),
516 n if n <= 4 => Err(std::io::Error::new(
517 std::io::ErrorKind::InvalidData,
518 format!("invalid pkt-line length: {n}"),
519 )),
520 n => {
521 let mut buf = vec![0u8; n - 4];
522 r.read_exact(&mut buf)?;
523 Ok(Some(buf))
524 }
525 }
526}
527
528#[derive(Clone, Copy, PartialEq, Eq)]
530enum AckKind {
531 Bare,
533 Common,
536 Continue,
538 Ready,
540}
541
542struct Ack {
543 oid: ObjectId,
544 kind: AckKind,
545}
546
547fn parse_ack(line: &str) -> Option<Ack> {
548 let rest = line.strip_prefix("ACK ")?;
549 let hex = rest.split_whitespace().next()?;
550 let oid = ObjectId::from_hex(hex).ok()?;
551 let tail = rest.strip_prefix(hex).unwrap_or("").trim();
552 let kind = if tail.contains("continue") {
553 AckKind::Continue
554 } else if tail.contains("common") {
555 AckKind::Common
556 } else if tail.contains("ready") {
557 AckKind::Ready
558 } else {
559 AckKind::Bare
560 };
561 Some(Ack { oid, kind })
562}
563
564struct RoundResult {
566 acks: Vec<Ack>,
567 got_pack: bool,
568 shallow: Vec<ObjectId>,
571 unshallow: Vec<ObjectId>,
573}
574
575fn read_sideband_pack(
579 r: &mut impl Read,
580 out: &mut Vec<u8>,
581 progress: &mut dyn Progress,
582) -> Result<()> {
583 let mut seen_pack = false;
584 let mut pending: Vec<u8> = Vec::new();
585 loop {
586 let mut len_buf = [0u8; 4];
587 match r.read_exact(&mut len_buf) {
588 Ok(()) => {}
589 Err(e) if e.kind() == std::io::ErrorKind::UnexpectedEof => break,
590 Err(e) => return Err(e.into()),
591 }
592 let len_str = std::str::from_utf8(&len_buf)
593 .map_err(|_| Error::Message("bad pkt length".to_owned()))?;
594 let len = usize::from_str_radix(len_str, 16)
595 .map_err(|_| Error::Message("bad pkt length".to_owned()))?;
596 match len {
597 0 => {
598 if seen_pack {
599 break;
600 }
601 continue;
602 }
603 1 | 2 => continue,
604 n if n <= 4 => {
605 return Err(Error::Message(format!(
606 "invalid pkt-line length in side-band stream: {n}"
607 )))
608 }
609 _ => {}
610 }
611 let mut payload = vec![0u8; len - 4];
612 r.read_exact(&mut payload)?;
613 if payload.is_empty() {
614 continue;
615 }
616 match payload[0] {
617 1 => append_pack_data(&payload[1..], out, &mut pending, &mut seen_pack),
618 2 => progress.message(&payload[1..]),
619 3 => {
620 return Err(Error::Message(format!(
621 "remote error: {}",
622 String::from_utf8_lossy(&payload[1..]).trim_end()
623 )))
624 }
625 _ => append_pack_data(&payload, out, &mut pending, &mut seen_pack),
626 }
627 }
628 Ok(())
629}
630
631fn append_pack_data(data: &[u8], out: &mut Vec<u8>, pending: &mut Vec<u8>, seen_pack: &mut bool) {
634 if *seen_pack {
635 out.extend_from_slice(data);
636 return;
637 }
638 pending.extend_from_slice(data);
639 if let Some(pos) = pending.windows(4).position(|w| w == b"PACK") {
640 *seen_pack = true;
641 out.extend_from_slice(&pending[pos..]);
642 pending.clear();
643 } else if pending.len() > 3 {
644 let keep_from = pending.len() - 3;
645 pending.drain(..keep_from);
646 }
647}
648
649fn read_stateless_response(
654 resp: &[u8],
655 sideband: bool,
656 expect_shallow: bool,
657 pack_buf: &mut Vec<u8>,
658 progress: &mut dyn Progress,
659) -> Result<RoundResult> {
660 let mut cur = Cursor::new(resp);
661 let mut acks = Vec::new();
662 let mut got_pack = false;
663 let mut shallow = Vec::new();
664 let mut unshallow = Vec::new();
665
666 if expect_shallow {
670 loop {
671 let start = cur.position() as usize;
672 match pkt_line::read_packet(&mut cur)? {
673 None | Some(pkt_line::Packet::Flush) => break,
674 Some(pkt_line::Packet::Data(line)) => {
675 let line = line.trim_end_matches('\n');
676 if let Some(rest) = line.strip_prefix("shallow ") {
677 if let Ok(oid) = ObjectId::from_hex(rest.trim()) {
678 shallow.push(oid);
679 }
680 } else if let Some(rest) = line.strip_prefix("unshallow ") {
681 if let Ok(oid) = ObjectId::from_hex(rest.trim()) {
682 unshallow.push(oid);
683 }
684 } else {
685 cur.set_position(start as u64);
686 break;
687 }
688 }
689 Some(_) => break,
690 }
691 }
692 }
693
694 loop {
695 let start = cur.position() as usize;
696 let Some(payload) = read_pkt_payload(&mut cur)? else {
697 break;
698 };
699 if payload.is_empty() {
700 continue;
701 }
702 let is_pack = (sideband
703 && payload.first() == Some(&1)
704 && payload.get(1..5) == Some(b"PACK"))
705 || payload.starts_with(b"PACK");
706 if is_pack {
707 got_pack = true;
708 cur.set_position(start as u64);
709 if sideband {
710 read_sideband_pack(&mut cur, pack_buf, progress)?;
711 } else {
712 pack_buf.extend_from_slice(&resp[start..]);
713 }
714 break;
715 }
716 let text = String::from_utf8_lossy(&payload);
717 let line = text.trim_end_matches('\n');
718 if let Some(err) = line.strip_prefix("ERR ") {
719 return Err(Error::Message(format!("remote upload-pack error: {err}")));
720 }
721 if line == "NAK" {
722 continue;
723 }
724 if let Some(ack) = parse_ack(line) {
725 acks.push(ack);
726 }
727 }
728 Ok(RoundResult {
729 acks,
730 got_pack,
731 shallow,
732 unshallow,
733 })
734}
735
736fn build_fetch_caps(caps: &HashSet<String>) -> String {
739 let mut enabled = Vec::new();
740 let multi_ack_detailed = caps.contains("multi_ack_detailed");
741 if multi_ack_detailed {
742 enabled.push("multi_ack_detailed");
743 }
744 if multi_ack_detailed && caps.contains("no-done") {
745 enabled.push("no-done");
746 }
747 for want in [
748 "side-band-64k",
749 "thin-pack",
750 "no-progress",
751 "include-tag",
752 "ofs-delta",
753 ] {
754 if caps.contains(want) {
755 enabled.push(want);
756 }
757 }
758 if enabled.is_empty() {
759 String::new()
760 } else {
761 format!(" {}", enabled.join(" "))
762 }
763}
764
765fn next_flush(count: usize) -> usize {
767 const LARGE_FLUSH: usize = 16384;
768 if count < LARGE_FLUSH {
769 count * 2
770 } else {
771 count * 11 / 10
772 }
773}
774
775fn append_shallow_request_v0_http(
780 req: &mut Vec<u8>,
781 caps: &HashSet<String>,
782 local_shallow: &[ObjectId],
783 opts: &FetchOptions,
784) -> Result<()> {
785 for oid in local_shallow {
786 pkt_line::write_line_to_vec(req, &format!("shallow {}", oid.to_hex()))?;
787 }
788 if opts.unshallow {
789 pkt_line::write_line_to_vec(req, &format!("deepen {}", crate::shallow::INFINITE_DEPTH))?;
790 } else if let Some(depth) = opts.depth.filter(|d| *d > 0) {
791 pkt_line::write_line_to_vec(req, &format!("deepen {depth}"))?;
792 }
793 if let Some(since) = opts.deepen_since.as_deref().filter(|s| !s.trim().is_empty()) {
794 if caps.contains("deepen-since") {
795 let value = crate::shallow::deepen_since_wire_value(since);
796 pkt_line::write_line_to_vec(req, &format!("deepen-since {value}"))?;
797 }
798 }
799 if caps.contains("deepen-not") {
800 for excl in &opts.deepen_not {
801 let excl = excl.trim();
802 if !excl.is_empty() {
803 pkt_line::write_line_to_vec(req, &format!("deepen-not {excl}"))?;
804 }
805 }
806 }
807 Ok(())
808}
809
810fn negotiate_pack_http(
814 client: &dyn HttpClient,
815 local_git_dir: &Path,
816 repo_url: &str,
817 caps: &HashSet<String>,
818 advertised: &[AdvRef],
819 wants: &[ObjectId],
820 opts: &FetchOptions,
821 local_shallow: &[ObjectId],
822 progress: &mut dyn Progress,
823) -> Result<(Vec<u8>, crate::fetch::ShallowUpdate)> {
824 let post_url = upload_pack_url(repo_url);
825 let content_type = format!("application/x-{UPLOAD_PACK}-request");
826 let accept = format!("application/x-{UPLOAD_PACK}-result");
827 let fetch_caps = build_fetch_caps(caps);
828 let sideband = caps.contains("side-band-64k");
829 let multi_ack_detailed = caps.contains("multi_ack_detailed");
830 let no_done = multi_ack_detailed && caps.contains("no-done");
831
832 let shallow_request = opts.has_deepen_request() || !local_shallow.is_empty();
835
836 let want_set: HashSet<ObjectId> = wants.iter().copied().collect();
837
838 let mut state = Vec::new();
842 let first = wants[0];
843 pkt_line::write_line_to_vec(&mut state, &format!("want {}{}", first.to_hex(), fetch_caps))?;
844 for w in wants.iter().skip(1) {
845 pkt_line::write_line_to_vec(&mut state, &format!("want {}", w.to_hex()))?;
846 }
847 append_shallow_request_v0_http(&mut state, caps, local_shallow, opts)?;
848 pkt_line::write_flush(&mut state)?;
849
850 let mut shallow_update = crate::fetch::ShallowUpdate::default();
851
852 let local_repo = crate::repo::Repository::open(local_git_dir, None)?;
855 let mut negotiator = SkippingNegotiator::new(local_repo);
856 if !shallow_request {
857 for w in wants {
858 if negotiator.repo().odb.read(w).is_ok() {
859 negotiator.add_tip(*w)?;
860 }
861 }
862 let mut tips: Vec<ObjectId> = Vec::new();
863 for prefix in ["refs/heads/", "refs/tags/"] {
864 if let Ok(entries) = crate::refs::list_refs(local_git_dir, prefix) {
865 for (_, oid) in entries {
866 if negotiator.repo().odb.read(&oid).is_ok() {
867 tips.push(oid);
868 }
869 }
870 }
871 }
872 if let Ok(h) = crate::refs::resolve_ref(local_git_dir, "HEAD") {
873 if negotiator.repo().odb.read(&h).is_ok() {
874 tips.push(h);
875 }
876 }
877 tips.sort_by_key(ObjectId::to_hex);
878 tips.dedup();
879 for t in tips {
880 if want_set.contains(&t) {
881 continue;
882 }
883 negotiator.add_tip(t)?;
884 }
885 for e in advertised {
886 if want_set.contains(&e.oid) {
887 continue;
888 }
889 if negotiator.repo().odb.read(&e.oid).is_ok() {
890 negotiator.known_common(e.oid)?;
891 }
892 }
893 }
894
895 let mut pack_buf: Vec<u8> = Vec::new();
896 let mut got_ready = false;
897 let mut got_pack = false;
898 let mut shallow_applied = false;
899
900 const INITIAL_FLUSH: usize = 16;
901 let mut count: usize = 0;
902 let mut flush_at: usize = INITIAL_FLUSH;
903 let mut round = Vec::new();
904 while let Some(oid) = negotiator.next_have()? {
907 pkt_line::write_line_to_vec(&mut round, &format!("have {}", oid.to_hex()))?;
908 count += 1;
909 if count < flush_at {
910 continue;
911 }
912 flush_at = next_flush(count);
913
914 let mut req = state.clone();
915 req.extend_from_slice(&round);
916 pkt_line::write_flush(&mut req)?;
917 round.clear();
918
919 let resp = client.post(&post_url, &content_type, &accept, &req, None)?;
920 let round_result =
921 read_stateless_response(&resp, sideband, shallow_request, &mut pack_buf, progress)?;
922 if shallow_request && !shallow_applied {
923 shallow_update.shallow.extend(round_result.shallow.iter().copied());
924 shallow_update.unshallow.extend(round_result.unshallow.iter().copied());
925 shallow_applied = true;
926 }
927 for ack in &round_result.acks {
928 if matches!(ack.kind, AckKind::Bare) {
929 continue;
930 }
931 let was_common = negotiator.ack(ack.oid)?;
932 if matches!(ack.kind, AckKind::Common) && !was_common {
933 pkt_line::write_line_to_vec(&mut state, &format!("have {}", ack.oid.to_hex()))?;
934 }
935 if matches!(ack.kind, AckKind::Ready) {
936 got_ready = true;
937 }
938 }
939 if round_result.got_pack {
940 got_pack = true;
941 break;
942 }
943 if got_ready {
944 break;
945 }
946 }
947
948 if !(got_pack || got_ready && no_done) {
951 let mut req = state.clone();
952 pkt_line::write_line_to_vec(&mut req, "done")?;
953 pkt_line::write_flush(&mut req)?;
954 let resp = client.post(&post_url, &content_type, &accept, &req, None)?;
955 let round_result =
956 read_stateless_response(&resp, sideband, shallow_request, &mut pack_buf, progress)?;
957 if shallow_request && !shallow_applied {
958 shallow_update.shallow.extend(round_result.shallow);
959 shallow_update.unshallow.extend(round_result.unshallow);
960 }
961 }
962
963 Ok((pack_buf, shallow_update))
964}
965
966struct MatchPlan {
971 matched: Vec<crate::transfer::MatchedRef>,
972 wants: HashSet<ObjectId>,
973 seen: HashSet<String>,
974}
975
976fn match_refspecs(
977 remote_refs: &[(String, ObjectId)],
978 positive: &[RefspecItem],
979 negatives: &[RefspecItem],
980) -> MatchPlan {
981 let mut matched: Vec<crate::transfer::MatchedRef> = Vec::new();
982 let mut wants: HashSet<ObjectId> = HashSet::new();
983 let mut seen: HashSet<String> = HashSet::new();
984 for (name, oid) in remote_refs {
985 if ref_excluded(name, negatives) {
986 continue;
987 }
988 if let Some(local_ref) = match_positive(name, positive) {
989 if seen.insert(name.clone()) {
990 wants.insert(*oid);
991 matched.push(crate::transfer::MatchedRef {
992 remote_ref: name.clone(),
993 local_ref,
994 oid: *oid,
995 force: refspecs_force(name, positive),
996 is_tag: name.starts_with("refs/tags/"),
997 });
998 }
999 }
1000 }
1001 MatchPlan {
1002 matched,
1003 wants,
1004 seen,
1005 }
1006}
1007
1008pub fn http_fetch(
1029 client: &dyn HttpClient,
1030 local_git_dir: &Path,
1031 repo_url: &str,
1032 opts: &FetchOptions,
1033 progress: &mut dyn Progress,
1034) -> Result<FetchOutcome> {
1035 let disc = {
1038 let url = info_refs_url(repo_url);
1039 let body = client.get(&url, client.git_protocol_header())?;
1040 let stripped = strip_service_advertisement(&body)?;
1041 parse_advertisement(stripped)?
1042 };
1043 if disc.protocol_version >= 2 {
1044 return http_fetch_v2(client, local_git_dir, repo_url, &disc, opts, progress);
1045 }
1046
1047 let local_odb = open_odb(local_git_dir);
1048
1049 let default_branch = disc
1050 .head_symref
1051 .as_deref()
1052 .map(|t| t.strip_prefix("refs/heads/").unwrap_or(t).to_owned());
1053
1054 let remote_refs: Vec<(String, ObjectId)> = disc
1055 .refs
1056 .iter()
1057 .filter(|r| r.name != "HEAD" && !r.name.ends_with("^{}"))
1058 .map(|r| (r.name.clone(), r.oid))
1059 .collect();
1060
1061 let mut positive: Vec<RefspecItem> = Vec::new();
1063 let mut negatives: Vec<RefspecItem> = Vec::new();
1064 for spec in &opts.refspecs {
1065 let item = parse_fetch_refspec(spec)
1066 .map_err(|e| Error::Message(format!("invalid refspec '{spec}': {e}")))?;
1067 if item.negative {
1068 negatives.push(item);
1069 } else {
1070 positive.push(item);
1071 }
1072 }
1073 for spec in &opts.negative_refspecs {
1074 let item = parse_fetch_refspec(spec)
1075 .map_err(|e| Error::Message(format!("invalid negative refspec '{spec}': {e}")))?;
1076 negatives.push(item);
1077 }
1078
1079 let MatchPlan {
1081 mut matched,
1082 mut wants,
1083 mut seen,
1084 } = match_refspecs(&remote_refs, &positive, &negatives);
1085
1086 if opts.tags != TagMode::None {
1090 for (name, oid) in &remote_refs {
1091 if !name.starts_with("refs/tags/") {
1092 continue;
1093 }
1094 if seen.contains(name) || ref_excluded(name, &negatives) {
1095 continue;
1096 }
1097 seen.insert(name.clone());
1098 wants.insert(*oid);
1099 matched.push(crate::transfer::MatchedRef {
1100 remote_ref: name.clone(),
1101 local_ref: Some(name.clone()),
1102 oid: *oid,
1103 force: false,
1104 is_tag: true,
1105 });
1106 }
1107 }
1108
1109 let local_shallow = crate::shallow::load_shallow_oids(local_git_dir)?;
1113 let shallow_request = opts.has_deepen_request() || !local_shallow.is_empty();
1114 let need: Vec<ObjectId> = if shallow_request {
1115 wants.iter().copied().collect()
1116 } else {
1117 wants
1118 .iter()
1119 .copied()
1120 .filter(|oid| !local_odb.exists(oid))
1121 .collect()
1122 };
1123
1124 let mut shallow_update = crate::fetch::ShallowUpdate::default();
1125
1126 if !need.is_empty() && !opts.dry_run {
1127 let (pack, su) = negotiate_pack_http(
1128 client,
1129 local_git_dir,
1130 repo_url,
1131 &disc.caps,
1132 &disc.refs,
1133 &need,
1134 opts,
1135 &local_shallow,
1136 progress,
1137 )?;
1138 shallow_update = su;
1139 if !pack.is_empty() {
1140 if pack.len() < 12 || &pack[0..4] != b"PACK" {
1141 return Err(Error::Message(
1142 "did not receive a valid pack from HTTP fetch".to_owned(),
1143 ));
1144 }
1145 let mut cursor = Cursor::new(pack);
1146 crate::unpack_objects::unpack_objects(
1147 &mut cursor,
1148 &local_odb,
1149 &crate::unpack_objects::UnpackOptions {
1150 quiet: true,
1151 ..Default::default()
1152 },
1153 )?;
1154 }
1155 }
1156
1157 if !opts.dry_run {
1159 crate::shallow::apply_shallow_updates(
1160 local_git_dir,
1161 &shallow_update.shallow,
1162 &shallow_update.unshallow,
1163 )?;
1164 }
1165
1166 if opts.tags == TagMode::Following {
1168 retain_following_tags(&local_odb, &mut matched, &wants);
1169 }
1170
1171 let local_repo = if opts.dry_run {
1173 None
1174 } else {
1175 crate::repo::Repository::open(local_git_dir, None).ok()
1176 };
1177
1178 let mut updates: Vec<RefUpdate> = Vec::new();
1179 if opts.prune {
1180 prune_tracking_refs(
1181 local_git_dir,
1182 &positive,
1183 &remote_refs,
1184 opts.dry_run,
1185 &mut updates,
1186 )?;
1187 }
1188
1189 for m in &matched {
1190 let Some(local_ref) = &m.local_ref else {
1191 updates.push(RefUpdate {
1192 remote_ref: m.remote_ref.clone(),
1193 local_ref: None,
1194 old_oid: None,
1195 new_oid: Some(m.oid),
1196 mode: UpdateMode::NoChangeNeeded,
1197 note: Some("not stored (empty destination)".to_owned()),
1198 });
1199 continue;
1200 };
1201 let old = crate::refs::resolve_ref(local_git_dir, local_ref).ok();
1202 let mode = classify_update(old.as_ref(), &m.oid, m.force, m.is_tag, local_repo.as_ref());
1203 let write = matches!(
1204 mode,
1205 UpdateMode::New | UpdateMode::FastForward | UpdateMode::Forced
1206 );
1207 if write && !opts.dry_run {
1208 crate::refs::write_ref(local_git_dir, local_ref, &m.oid)?;
1209 }
1210 updates.push(RefUpdate {
1211 remote_ref: m.remote_ref.clone(),
1212 local_ref: Some(local_ref.clone()),
1213 old_oid: old,
1214 new_oid: Some(m.oid),
1215 mode,
1216 note: None,
1217 });
1218 }
1219
1220 Ok(FetchOutcome {
1221 updates,
1222 default_branch,
1223 new_shallow: shallow_update.shallow,
1224 new_unshallow: shallow_update.unshallow,
1225 })
1226}
1227
1228fn http_fetch_v2(
1239 client: &dyn HttpClient,
1240 local_git_dir: &Path,
1241 repo_url: &str,
1242 disc: &Discovery,
1243 opts: &FetchOptions,
1244 progress: &mut dyn Progress,
1245) -> Result<FetchOutcome> {
1246 let local_odb = open_odb(local_git_dir);
1247 let server_caps: Vec<String> = disc.caps.iter().cloned().collect();
1251
1252 let post_url = upload_pack_url(repo_url);
1253 let content_type = format!("application/x-{UPLOAD_PACK}-request");
1254 let accept = format!("application/x-{UPLOAD_PACK}-result");
1255 let git_protocol = "version=2";
1257
1258 let (remote_refs, head_symref) = {
1260 let req =
1261 crate::fetch::build_v2_ls_refs_request(&server_caps, &local_odb, opts.tags, &opts.refspecs)?;
1262 let resp = client.post(&post_url, &content_type, &accept, &req, Some(git_protocol))?;
1263 let mut cur = Cursor::new(resp);
1264 crate::fetch::parse_v2_ls_refs_response(&mut cur)?
1265 };
1266 let default_branch = head_symref
1267 .as_deref()
1268 .map(|t| t.strip_prefix("refs/heads/").unwrap_or(t).to_owned());
1269
1270 let mut positive: Vec<RefspecItem> = Vec::new();
1272 let mut negatives: Vec<RefspecItem> = Vec::new();
1273 for spec in &opts.refspecs {
1274 let item = parse_fetch_refspec(spec)
1275 .map_err(|e| Error::Message(format!("invalid refspec '{spec}': {e}")))?;
1276 if item.negative {
1277 negatives.push(item);
1278 } else {
1279 positive.push(item);
1280 }
1281 }
1282 for spec in &opts.negative_refspecs {
1283 let item = parse_fetch_refspec(spec)
1284 .map_err(|e| Error::Message(format!("invalid negative refspec '{spec}': {e}")))?;
1285 negatives.push(item);
1286 }
1287
1288 let MatchPlan {
1290 mut matched,
1291 mut wants,
1292 mut seen,
1293 } = match_refspecs(&remote_refs, &positive, &negatives);
1294
1295 if opts.tags != TagMode::None {
1299 for (name, oid) in &remote_refs {
1300 if !name.starts_with("refs/tags/") {
1301 continue;
1302 }
1303 if seen.contains(name) || ref_excluded(name, &negatives) {
1304 continue;
1305 }
1306 seen.insert(name.clone());
1307 wants.insert(*oid);
1308 matched.push(crate::transfer::MatchedRef {
1309 remote_ref: name.clone(),
1310 local_ref: Some(name.clone()),
1311 oid: *oid,
1312 force: false,
1313 is_tag: true,
1314 });
1315 }
1316 }
1317
1318 let local_shallow = crate::shallow::load_shallow_oids(local_git_dir)?;
1322 let shallow_request = opts.has_deepen_request() || !local_shallow.is_empty();
1323 let need: Vec<ObjectId> = if shallow_request {
1324 wants.iter().copied().collect()
1325 } else {
1326 wants
1327 .iter()
1328 .copied()
1329 .filter(|oid| !local_odb.exists(oid))
1330 .collect()
1331 };
1332
1333 let mut shallow_update = crate::fetch::ShallowUpdate::default();
1334
1335 if !need.is_empty() && !opts.dry_run {
1336 let deepen = crate::fetch::V2DeepenArgs::from_opts(opts, &local_shallow);
1337 let (pack, su) = negotiate_pack_v2_http(
1338 client,
1339 local_git_dir,
1340 &post_url,
1341 &content_type,
1342 &accept,
1343 git_protocol,
1344 &server_caps,
1345 &local_odb,
1346 &need,
1347 &deepen,
1348 progress,
1349 )?;
1350 shallow_update = su;
1351 if !pack.is_empty() {
1352 if pack.len() < 12 || &pack[0..4] != b"PACK" {
1353 return Err(Error::Message(
1354 "did not receive a valid pack from v2 HTTP fetch".to_owned(),
1355 ));
1356 }
1357 let mut cursor = Cursor::new(pack);
1358 crate::unpack_objects::unpack_objects(
1359 &mut cursor,
1360 &local_odb,
1361 &crate::unpack_objects::UnpackOptions {
1362 quiet: true,
1363 ..Default::default()
1364 },
1365 )?;
1366 }
1367 }
1368
1369 if !opts.dry_run {
1371 crate::shallow::apply_shallow_updates(
1372 local_git_dir,
1373 &shallow_update.shallow,
1374 &shallow_update.unshallow,
1375 )?;
1376 }
1377
1378 if opts.tags == TagMode::Following {
1380 retain_following_tags(&local_odb, &mut matched, &wants);
1381 }
1382
1383 let local_repo = if opts.dry_run {
1385 None
1386 } else {
1387 crate::repo::Repository::open(local_git_dir, None).ok()
1388 };
1389
1390 let mut updates: Vec<RefUpdate> = Vec::new();
1391 if opts.prune {
1392 prune_tracking_refs(
1393 local_git_dir,
1394 &positive,
1395 &remote_refs,
1396 opts.dry_run,
1397 &mut updates,
1398 )?;
1399 }
1400
1401 for m in &matched {
1402 let Some(local_ref) = &m.local_ref else {
1403 updates.push(RefUpdate {
1404 remote_ref: m.remote_ref.clone(),
1405 local_ref: None,
1406 old_oid: None,
1407 new_oid: Some(m.oid),
1408 mode: UpdateMode::NoChangeNeeded,
1409 note: Some("not stored (empty destination)".to_owned()),
1410 });
1411 continue;
1412 };
1413 let old = crate::refs::resolve_ref(local_git_dir, local_ref).ok();
1414 let mode = classify_update(old.as_ref(), &m.oid, m.force, m.is_tag, local_repo.as_ref());
1415 let write = matches!(
1416 mode,
1417 UpdateMode::New | UpdateMode::FastForward | UpdateMode::Forced
1418 );
1419 if write && !opts.dry_run {
1420 crate::refs::write_ref(local_git_dir, local_ref, &m.oid)?;
1421 }
1422 updates.push(RefUpdate {
1423 remote_ref: m.remote_ref.clone(),
1424 local_ref: Some(local_ref.clone()),
1425 old_oid: old,
1426 new_oid: Some(m.oid),
1427 mode,
1428 note: None,
1429 });
1430 }
1431
1432 Ok(FetchOutcome {
1433 updates,
1434 default_branch,
1435 new_shallow: shallow_update.shallow,
1436 new_unshallow: shallow_update.unshallow,
1437 })
1438}
1439
1440#[allow(clippy::too_many_arguments)]
1455fn negotiate_pack_v2_http(
1456 client: &dyn HttpClient,
1457 local_git_dir: &Path,
1458 post_url: &str,
1459 content_type: &str,
1460 accept: &str,
1461 git_protocol: &str,
1462 server_caps: &[String],
1463 local_odb: &crate::odb::Odb,
1464 wants: &[ObjectId],
1465 deepen: &crate::fetch::V2DeepenArgs,
1466 progress: &mut dyn Progress,
1467) -> Result<(Vec<u8>, crate::fetch::ShallowUpdate)> {
1468 if wants.is_empty() {
1469 return Ok((Vec::new(), crate::fetch::ShallowUpdate::default()));
1470 }
1471 let object_format = crate::fetch::v2_object_format(server_caps, local_odb);
1472 let cap_echo = protocol_v2::cap_lines_for_command_request(server_caps);
1473 let sideband_all = protocol_v2::fetch_supports_sideband_all(server_caps);
1474
1475 let shallow_request = deepen.is_shallow_request();
1479
1480 let haves = if shallow_request {
1484 Vec::new()
1485 } else {
1486 crate::fetch::v2_local_haves(local_git_dir, wants)?
1487 };
1488
1489 let mut pack = Vec::new();
1490 let mut shallow_update = crate::fetch::ShallowUpdate::default();
1491
1492 if haves.is_empty() {
1494 let mut req = Vec::new();
1495 crate::fetch::write_v2_fetch_request(
1496 &mut req,
1497 &object_format,
1498 &cap_echo,
1499 wants,
1500 &[],
1501 sideband_all,
1502 deepen,
1503 true,
1504 )?;
1505 let resp = client.post(post_url, content_type, accept, &req, Some(git_protocol))?;
1506 let mut cur = Cursor::new(resp);
1507 crate::fetch::read_v2_fetch_pack_response(&mut cur, &mut pack, &mut shallow_update, progress)?;
1508 return Ok((pack, shallow_update));
1509 }
1510
1511 const INITIAL_FLUSH: usize = 16;
1515 let mut flush_at: usize = INITIAL_FLUSH.min(haves.len());
1516 loop {
1517 if flush_at < haves.len() {
1518 let mut req = Vec::new();
1520 crate::fetch::write_v2_fetch_request(
1521 &mut req,
1522 &object_format,
1523 &cap_echo,
1524 wants,
1525 &haves[..flush_at],
1526 sideband_all,
1527 deepen,
1528 false,
1529 )?;
1530 let resp = client.post(post_url, content_type, accept, &req, Some(git_protocol))?;
1531 let mut cur = Cursor::new(resp);
1532 let ack = crate::fetch::read_v2_acknowledgments(&mut cur)?;
1533 if let Some(round) = ack {
1534 if round.ready {
1535 crate::fetch::read_v2_fetch_pack_response(
1537 &mut cur,
1538 &mut pack,
1539 &mut shallow_update,
1540 progress,
1541 )?;
1542 return Ok((pack, shallow_update));
1543 }
1544 } else {
1545 crate::fetch::read_v2_fetch_pack_response(
1547 &mut cur,
1548 &mut pack,
1549 &mut shallow_update,
1550 progress,
1551 )?;
1552 return Ok((pack, shallow_update));
1553 }
1554 flush_at = next_flush(flush_at).min(haves.len());
1555 continue;
1556 }
1557
1558 let mut req = Vec::new();
1560 crate::fetch::write_v2_fetch_request(
1561 &mut req,
1562 &object_format,
1563 &cap_echo,
1564 wants,
1565 &haves,
1566 sideband_all,
1567 deepen,
1568 true,
1569 )?;
1570 let resp = client.post(post_url, content_type, accept, &req, Some(git_protocol))?;
1571 let mut cur = Cursor::new(resp);
1572 crate::fetch::read_v2_fetch_pack_response(&mut cur, &mut pack, &mut shallow_update, progress)?;
1573 return Ok((pack, shallow_update));
1574 }
1575}
1576
1577fn retain_following_tags(
1579 odb: &crate::odb::Odb,
1580 matched: &mut Vec<crate::transfer::MatchedRef>,
1581 wants: &HashSet<ObjectId>,
1582) {
1583 let roots: Vec<ObjectId> = matched.iter().filter(|m| !m.is_tag).map(|m| m.oid).collect();
1584 let closure = reachable_closure(odb, &roots);
1585 matched.retain(|m| {
1586 if !m.is_tag {
1587 return true;
1588 }
1589 let peeled = peel_tag_target(odb, m.oid);
1590 let have = odb.exists(&m.oid);
1591 have && (closure.contains(&m.oid) || closure.contains(&peeled) || wants.contains(&peeled))
1592 });
1593}
1594
1595fn peel_tag_target(odb: &crate::odb::Odb, oid: ObjectId) -> ObjectId {
1596 let mut current = oid;
1597 for _ in 0..16 {
1598 let Ok(obj) = odb.read(¤t) else {
1599 return current;
1600 };
1601 if obj.kind != crate::objects::ObjectKind::Tag {
1602 return current;
1603 }
1604 match crate::objects::parse_tag(&obj.data) {
1605 Ok(t) => current = t.object,
1606 Err(_) => return current,
1607 }
1608 }
1609 current
1610}
1611
1612fn reachable_closure(odb: &crate::odb::Odb, roots: &[ObjectId]) -> HashSet<ObjectId> {
1613 use crate::objects::{parse_commit, parse_tag, parse_tree, ObjectKind};
1614 let mut seen: HashSet<ObjectId> = HashSet::new();
1615 let mut stack: Vec<ObjectId> = roots.to_vec();
1616 while let Some(oid) = stack.pop() {
1617 if !seen.insert(oid) {
1618 continue;
1619 }
1620 let Ok(obj) = odb.read(&oid) else {
1621 continue;
1622 };
1623 match obj.kind {
1624 ObjectKind::Commit => {
1625 if let Ok(c) = parse_commit(&obj.data) {
1626 stack.push(c.tree);
1627 for p in c.parents {
1628 stack.push(p);
1629 }
1630 }
1631 }
1632 ObjectKind::Tree => {
1633 if let Ok(entries) = parse_tree(&obj.data) {
1634 for e in entries {
1635 stack.push(e.oid);
1636 }
1637 }
1638 }
1639 ObjectKind::Tag => {
1640 if let Ok(t) = parse_tag(&obj.data) {
1641 stack.push(t.object);
1642 }
1643 }
1644 ObjectKind::Blob => {}
1645 }
1646 }
1647 seen
1648}
1649
1650pub fn discovery_advertisement(conn: &SmartHttpConnection) -> Advertisement {
1653 Advertisement {
1654 refs: conn.adv_refs.clone(),
1655 capabilities: conn.caps.clone(),
1656 head_symref: conn.head_symref.clone(),
1657 protocol_version: conn.protocol_version,
1658 }
1659}
1660
1661#[cfg(test)]
1662mod tests {
1663 use super::*;
1664
1665 #[test]
1666 fn strips_smart_service_preamble() {
1667 let mut body = Vec::new();
1668 pkt_line::write_line_to_vec(&mut body, "# service=git-upload-pack\n").unwrap();
1669 body.extend_from_slice(b"0000");
1670 let oid = "1".repeat(40);
1671 let line = format!("{oid} refs/heads/main\0multi_ack_detailed side-band-64k");
1672 pkt_line::write_line_to_vec(&mut body, &line).unwrap();
1673 body.extend_from_slice(b"0000");
1674
1675 let stripped = strip_service_advertisement(&body).unwrap();
1676 let disc = parse_advertisement(stripped).unwrap();
1677 assert_eq!(disc.protocol_version, 0);
1678 assert_eq!(disc.refs.len(), 1);
1679 assert_eq!(disc.refs[0].name, "refs/heads/main");
1680 assert!(disc.caps.contains("side-band-64k"));
1681 }
1682
1683 #[test]
1684 fn parses_symref_and_caps() {
1685 let mut body = Vec::new();
1686 let main = "2".repeat(40);
1687 let head = format!(
1688 "{main} HEAD\0multi_ack_detailed symref=HEAD:refs/heads/main object-format=sha1"
1689 );
1690 pkt_line::write_line_to_vec(&mut body, &head).unwrap();
1691 let r = format!("{main} refs/heads/main");
1692 pkt_line::write_line_to_vec(&mut body, &r).unwrap();
1693 body.extend_from_slice(b"0000");
1694
1695 let disc = parse_advertisement(&body).unwrap();
1696 assert_eq!(disc.head_symref.as_deref(), Some("refs/heads/main"));
1697 assert_eq!(disc.object_format, "sha1");
1698 assert!(disc.refs.iter().any(|r| r.name == "HEAD"));
1701 assert!(disc.refs.iter().any(|r| r.name == "refs/heads/main"));
1702 }
1703
1704 #[test]
1705 fn detects_v2_preamble() {
1706 let mut body = Vec::new();
1707 pkt_line::write_line_to_vec(&mut body, "version 2").unwrap();
1708 pkt_line::write_line_to_vec(&mut body, "ls-refs").unwrap();
1709 pkt_line::write_line_to_vec(&mut body, "object-format=sha256").unwrap();
1710 body.extend_from_slice(b"0000");
1711 let disc = parse_advertisement(&body).unwrap();
1712 assert_eq!(disc.protocol_version, 2);
1713 assert_eq!(disc.object_format, "sha256");
1714 }
1715
1716 #[test]
1717 fn url_helpers() {
1718 assert_eq!(
1719 info_refs_url("http://h/r.git"),
1720 "http://h/r.git/info/refs?service=git-upload-pack"
1721 );
1722 assert_eq!(
1723 info_refs_url("http://h/r.git/"),
1724 "http://h/r.git/info/refs?service=git-upload-pack"
1725 );
1726 assert_eq!(upload_pack_url("http://h/r.git/"), "http://h/r.git/git-upload-pack");
1727 }
1728}