1use std::collections::HashSet;
10use std::io::{Read, Write};
11use std::path::Path;
12
13use crate::environment::RepositoryOptions;
14use crate::error::{Error, Result};
15use crate::fetch_negotiator::SkippingNegotiator;
16use crate::objects::ObjectId;
17use crate::pkt_line;
18use crate::protocol_v2;
19use crate::refspec::RefspecItem;
20use crate::repo::Repository;
21
22type LsRefsAdvertised = (
23 Vec<(String, ObjectId)>,
24 Option<String>,
25 std::collections::HashMap<String, ObjectId>,
26 Option<ObjectId>,
27);
28use crate::progress::{poll_cancel, Cancel, ProgressSink};
29use crate::transfer::{
30 classify_update, prune_tracking_refs, ref_excluded, validate_fetch_depth_local,
31 validate_fetch_depth_server, FetchOptions, FetchOutcome, RefUpdate, UpdateMode,
32};
33use crate::transport::Connection;
34
35pub trait Progress: ProgressSink {
43 fn message(&mut self, _bytes: &[u8]) {}
45
46 fn remote_sideband_progress(&self) -> bool {
48 false
49 }
50
51 fn wants_received_objects(&self) -> bool {
58 false
59 }
60
61 fn received_objects(&mut self, _counts: crate::progress::ReceivedObjects) {}
67
68 fn resumable_transfer(&mut self, _event: &crate::resumable::ResumableEvent) {}
71}
72
73pub struct NoProgress;
75
76impl Progress for NoProgress {}
77
78impl ProgressSink for NoProgress {}
79
80const INITIAL_FLUSH: usize = 16;
83const PIPESAFE_FLUSH: usize = 32;
84
85fn next_flush_count(count: usize) -> usize {
86 if count < PIPESAFE_FLUSH {
87 count * 2
88 } else {
89 count + PIPESAFE_FLUSH
90 }
91}
92
93const LARGE_FLUSH: usize = 16384;
95
96const MAX_IN_VAIN: usize = 256;
99
100fn next_v2_flush_count(count: usize) -> usize {
103 if count < LARGE_FLUSH {
104 count * 2
105 } else {
106 count + count / 10
107 }
108}
109
110#[derive(Clone, Copy, PartialEq, Eq)]
111enum AckKind {
112 Bare,
114 Common,
115 Continue,
116 Ready,
117}
118
119fn parse_ack(line: &str) -> Option<(ObjectId, AckKind)> {
120 if line == "NAK" {
121 return None;
122 }
123 let rest = line.strip_prefix("ACK ")?;
124 let hex = rest.split_whitespace().next()?;
125 let oid = ObjectId::from_hex(hex).ok()?;
126 let tail = rest.strip_prefix(hex).unwrap_or("").trim();
127 let kind = if tail.contains("continue") {
128 AckKind::Continue
129 } else if tail.contains("common") {
130 AckKind::Common
131 } else if tail.contains("ready") {
132 AckKind::Ready
133 } else {
134 AckKind::Bare
135 };
136 Some((oid, kind))
137}
138
139fn read_ack_round(reader: &mut dyn Read, negotiator: &mut SkippingNegotiator<'_>) -> Result<()> {
142 let mut reader = reader;
143 while let Some(pkt) = pkt_line::read_packet(&mut reader)? {
144 match pkt {
145 pkt_line::Packet::Flush => break,
146 pkt_line::Packet::Data(ln) => {
147 let ln = ln.trim_end();
148 if ln == "NAK" {
149 break;
152 }
153 let Some((ack_oid, kind)) = parse_ack(ln) else {
154 break;
155 };
156 if kind == AckKind::Bare {
157 break;
158 }
159 let _ = negotiator.ack(ack_oid)?;
160 }
161 _ => {}
162 }
163 }
164 Ok(())
165}
166
167fn read_sideband_pack(
172 r: &mut dyn Read,
173 target: &mut crate::pack_receive::PackReceiveTarget<'_>,
174 progress: &mut dyn Progress,
175 cancel: &dyn Cancel,
176) -> Result<()> {
177 crate::pack_receive::read_sideband_pack_to(r, target, progress, cancel)
178}
179
180#[derive(Default)]
184pub(crate) struct ShallowUpdate {
185 pub(crate) shallow: Vec<ObjectId>,
186 pub(crate) unshallow: Vec<ObjectId>,
187}
188
189fn append_filter_request_v0(req: &mut Vec<u8>, wire_filter: &str) -> Result<()> {
195 pkt_line::write_line_to_vec(req, &format!("filter {wire_filter}"))?;
196 Ok(())
197}
198
199fn append_shallow_request_v0(
200 req: &mut Vec<u8>,
201 server_caps: &str,
202 local_shallow: &[ObjectId],
203 opts: &FetchOptions,
204) -> Result<()> {
205 let sends_shallow = !local_shallow.is_empty()
206 || opts.wire_deepen().is_some()
207 || opts
208 .deepen_since
209 .as_deref()
210 .is_some_and(|s| !s.trim().is_empty())
211 || opts.deepen_not.iter().any(|s| !s.trim().is_empty());
212 if sends_shallow && !server_caps.contains("shallow") {
213 return Err(Error::FetchShallowUnsupported);
214 }
215 for oid in local_shallow {
216 pkt_line::write_line_to_vec(req, &format!("shallow {}", oid.to_hex()))?;
217 }
218 if let Some((depth, relative)) = opts.wire_deepen() {
219 if relative && !server_caps.contains("deepen-relative") {
220 return Err(Error::FetchDeepenRelativeUnsupported);
221 }
222 pkt_line::write_line_to_vec(req, &format!("deepen {depth}"))?;
223 if relative {
224 pkt_line::write_line_to_vec(req, "deepen-relative")?;
225 }
226 }
227 if let Some(since) = opts
228 .deepen_since
229 .as_deref()
230 .filter(|s| !s.trim().is_empty())
231 {
232 if server_caps.contains("deepen-since") {
233 let value = crate::shallow::deepen_since_wire_value(since);
234 pkt_line::write_line_to_vec(req, &format!("deepen-since {value}"))?;
235 }
236 }
237 if server_caps.contains("deepen-not") {
238 for excl in &opts.deepen_not {
239 let excl = excl.trim();
240 if !excl.is_empty() {
241 pkt_line::write_line_to_vec(req, &format!("deepen-not {excl}"))?;
242 }
243 }
244 }
245 Ok(())
246}
247
248#[allow(clippy::too_many_arguments)]
262fn negotiate_pack(
263 local: &Repository,
264 conn: &mut dyn Connection,
265 wants: &[ObjectId],
266 opts: &mut FetchOptions,
267 local_shallow: &[ObjectId],
268 wire_filter: Option<&str>,
269 progress: &mut dyn Progress,
270 cancel: &dyn Cancel,
271 skip_local_haves: bool,
272) -> Result<(Option<std::path::PathBuf>, ShallowUpdate)> {
273 crate::trace::trace_span!("grit.fetch.negotiate");
274 let want_set: HashSet<ObjectId> = wants.iter().copied().collect();
275
276 let Some(first_want) = wants.first().copied() else {
277 return Ok((None, ShallowUpdate::default()));
278 };
279
280 let shallow_request = opts.has_deepen_request() || !local_shallow.is_empty();
285 let skip_haves = shallow_request || skip_local_haves;
286
287 let advertised: Vec<(String, ObjectId)> = conn.advertised_refs().to_vec();
292 let server_caps: String = conn.capabilities().join(" ");
293 validate_fetch_depth_server(opts, Some(&server_caps), None)?;
294
295 let mut caps = if opts.remote_progress {
297 String::from(" multi_ack_detailed side-band-64k thin-pack include-tag ofs-delta agent=grit")
298 } else {
299 String::from(
300 " multi_ack_detailed side-band-64k thin-pack no-progress include-tag ofs-delta agent=grit",
301 )
302 };
303 if opts.wire_deepen().is_some_and(|(_, relative)| relative) {
304 if !server_caps.contains("deepen-relative") {
305 return Err(Error::FetchDeepenRelativeUnsupported);
306 }
307 caps.push_str(" deepen-relative");
308 }
309 if wire_filter.is_some() {
310 caps.push_str(" filter");
311 }
312
313 let mut req: Vec<u8> = Vec::new();
314 let w0 = format!("want {}{}", first_want.to_hex(), caps);
315 pkt_line::write_line_to_vec(&mut req, &w0)?;
316 for w in wants.iter().skip(1) {
317 pkt_line::write_line_to_vec(&mut req, &format!("want {}", w.to_hex()))?;
318 }
319 if wants.len() == 1 && !skip_haves {
323 pkt_line::write_line_to_vec(&mut req, &format!("want {}", first_want.to_hex()))?;
324 }
325 if let Some(spec) = wire_filter {
326 append_filter_request_v0(&mut req, spec)?;
327 }
328 append_shallow_request_v0(&mut req, &server_caps, local_shallow, opts)?;
329 req.extend_from_slice(b"0000");
330 conn.writer().write_all(&req)?;
331 conn.writer().flush()?;
332
333 let mut negotiator = SkippingNegotiator::new(local);
337 if !skip_haves {
338 add_negotiation_tips(&mut negotiator, &want_set)?;
339 let mut local_by_name: std::collections::HashMap<String, ObjectId> =
343 std::collections::HashMap::new();
344 for prefix in ["refs/heads/", "refs/tags/"] {
345 if let Ok(entries) = crate::refs::list_refs_in(local, prefix) {
346 for (name, oid) in entries {
347 local_by_name.insert(name, oid);
348 }
349 }
350 }
351 for (name, oid) in &advertised {
352 if want_set.contains(oid) {
353 continue;
354 }
355 let Some(local_oid) = local_by_name.get(name) else {
356 continue;
357 };
358 if local_oid != oid {
359 continue;
360 }
361 if let Ok(Some(c)) = negotiator.repo().odb().peel_to_commit(*oid) {
362 negotiator.known_common(c)?;
363 }
364 }
365 }
366
367 let mut shallow_update = ShallowUpdate::default();
372 if shallow_request {
373 let (sh, unsh) = crate::shallow::read_shallow_info_section(&mut conn.reader())?;
374 shallow_update.shallow = sh;
375 shallow_update.unshallow = unsh;
376 } else if skip_local_haves && conn.stateless_rpc() {
377 read_ack_round(conn.reader(), &mut negotiator)?;
378 } else if conn.stateless_rpc() {
379 read_ack_round(conn.reader(), &mut negotiator)?;
382 }
383
384 let mut count: usize = 0;
386 let mut flush_at: usize = INITIAL_FLUSH;
387 let mut pending: Vec<u8> = Vec::new();
388 let mut flushes: i32 = 0;
389 if !skip_haves {
390 while let Some(oid) = negotiator.next_have()? {
391 poll_cancel(progress, cancel)?;
392 pkt_line::write_line_to_vec(&mut pending, &format!("have {}", oid.to_hex()))?;
393 count += 1;
394 if flush_at <= count {
395 pending.extend_from_slice(b"0000");
396 conn.writer().write_all(&pending)?;
397 conn.writer().flush()?;
398 pending.clear();
399 flush_at = next_flush_count(count);
400 flushes += 1;
401 if count == INITIAL_FLUSH && !conn.stateless_rpc() {
404 continue;
405 }
406 read_ack_round(conn.reader(), &mut negotiator)?;
407 flushes -= 1;
408 }
409 }
410 }
411 if !pending.is_empty() {
412 pending.extend_from_slice(b"0000");
413 conn.writer().write_all(&pending)?;
414 conn.writer().flush()?;
415 flushes += 1;
416 }
417 while flushes > 0 {
418 read_ack_round(conn.reader(), &mut negotiator)?;
419 flushes -= 1;
420 }
421
422 let mut tail = Vec::new();
424 pkt_line::write_line_to_vec(&mut tail, "done")?;
425 conn.writer().write_all(&tail)?;
426 conn.writer().flush()?;
427
428 if shallow_request && conn.stateless_rpc() {
430 let (sh, unsh) = crate::shallow::read_shallow_info_section(&mut conn.reader())?;
431 shallow_update.shallow.extend(sh);
432 shallow_update.unshallow.extend(unsh);
433 }
434
435 match pkt_line::read_packet(&mut conn.reader())? {
436 None => return Err(Error::Message("unexpected EOF after done".to_owned())),
437 Some(pkt_line::Packet::Flush) => {
438 return Err(Error::Message("unexpected flush after done".to_owned()))
439 }
440 Some(pkt_line::Packet::Data(ln)) => {
441 let ln = ln.trim_end();
442 if ln != "NAK" {
443 if let Some((ack_oid, kind)) = parse_ack(ln) {
444 if kind != AckKind::Bare {
445 let _ = negotiator.ack(ack_oid)?;
446 }
447 } else {
448 pkt_line::reject_err_line(ln)?;
449 }
450 }
451 }
452 Some(_) => {}
453 }
454
455 let mut pack_receive = crate::pack_receive::TempPackReceive::create(local.odb().objects_dir())?;
456 pack_receive.read_sideband(conn.reader(), progress, cancel)?;
457 let pack_path = pack_receive.finish()?;
458 Ok((pack_path, shallow_update))
459}
460
461pub(crate) fn v2_object_format(server_caps: &[String], local_odb: &crate::odb::Odb) -> String {
476 for c in server_caps {
477 if let Some(fmt) = c.strip_prefix("object-format=") {
478 let f = fmt.trim();
479 if !f.is_empty() {
480 return f.to_ascii_lowercase();
481 }
482 }
483 }
484 local_odb.hash_algo().name().to_owned()
485}
486
487fn push_unique_prefix(out: &mut Vec<String>, value: &str) {
488 if !out.iter().any(|v| v == value) {
489 out.push(value.to_owned());
490 }
491}
492
493pub(crate) fn fetch_ref_list_prefixes(refspecs: &[String]) -> Vec<String> {
500 let mut out = vec!["refs/heads/".to_owned(), "refs/tags/".to_owned()];
501 for spec in refspecs {
502 if spec.starts_with('^') {
503 continue;
504 }
505 let raw = spec.strip_prefix('+').unwrap_or(spec.as_str());
506 let src = raw.split_once(':').map(|(s, _)| s).unwrap_or(raw).trim();
507 if src.is_empty() {
508 continue;
509 }
510 if src == "HEAD" {
511 push_unique_prefix(&mut out, "HEAD");
512 continue;
513 }
514 if let Some(star) = src.find('*') {
515 let prefix = &src[..star];
516 if prefix.is_empty() {
517 continue;
518 }
519 if prefix.starts_with("refs/") {
520 push_unique_prefix(&mut out, prefix);
521 } else {
522 push_unique_prefix(&mut out, &format!("refs/heads/{prefix}"));
523 }
524 continue;
525 }
526 if src.starts_with("refs/") {
527 push_unique_prefix(&mut out, src);
528 } else {
529 push_unique_prefix(&mut out, &format!("refs/heads/{src}"));
530 push_unique_prefix(&mut out, &format!("refs/{src}"));
532 }
533 }
534 out
535}
536
537pub(crate) fn v2_ls_refs_prefixes(
545 refspecs: &[String],
546 tags: crate::transfer::TagMode,
547) -> Vec<String> {
548 let mut out = Vec::new();
549 for spec in refspecs {
550 if spec.starts_with('^') {
551 continue;
552 }
553 let raw = spec.strip_prefix('+').unwrap_or(spec.as_str());
554 let src = raw.split_once(':').map_or(raw, |(s, _)| s).trim();
555 if src.is_empty() || src == "HEAD" || ObjectId::from_hex(src).is_ok() {
556 continue;
557 }
558 if let Some(star) = src.find('*') {
559 let prefix = &src[..star];
560 if prefix.starts_with("refs/") {
561 push_unique_prefix(&mut out, prefix);
562 } else if !prefix.is_empty() {
563 push_unique_prefix(&mut out, &format!("refs/heads/{prefix}"));
564 }
565 continue;
566 }
567 if src.starts_with("refs/") {
568 push_unique_prefix(&mut out, src);
569 continue;
570 }
571 for candidate in [
572 src.to_owned(),
573 format!("refs/{src}"),
574 format!("refs/tags/{src}"),
575 format!("refs/heads/{src}"),
576 format!("refs/remotes/{src}"),
577 format!("refs/remotes/{src}/HEAD"),
578 ] {
579 push_unique_prefix(&mut out, &candidate);
580 }
581 }
582 if out.is_empty() {
583 return fetch_ref_list_prefixes(refspecs);
584 }
585 if tags != crate::transfer::TagMode::None {
586 push_unique_prefix(&mut out, "refs/tags/");
587 }
588 out
589}
590
591#[must_use]
593pub(crate) fn parse_v2_ls_refs_unborn_head_symref(line: &str) -> Option<String> {
594 const PREFIX: &str = "unborn HEAD symref-target:";
595 let target = line.strip_prefix(PREFIX)?;
596 let target = target.split_whitespace().next()?.to_owned();
597 crate::refs::is_valid_advertised_symref_target(&target).then_some(target)
598}
599
600pub(crate) fn parse_ls_refs_v2_line(
607 line: &str,
608) -> Option<(String, ObjectId, Option<String>, Option<ObjectId>)> {
609 const SYM: &str = " symref-target:";
610 const PEEL: &str = " peeled:";
611 let (oid_hex, after_oid) = line.split_once(' ')?;
612 let oid = ObjectId::from_hex(oid_hex).ok()?;
613
614 let sym_at = after_oid.find(SYM);
616 let peel_at = after_oid.find(PEEL);
617 let name_end = match (sym_at, peel_at) {
618 (Some(a), Some(b)) => a.min(b),
619 (Some(a), None) => a,
620 (None, Some(b)) => b,
621 (None, None) => after_oid.len(),
622 };
623 let name = after_oid[..name_end].trim().to_owned();
624 if name.is_empty() {
625 return None;
626 }
627 let symref_target = sym_at.map(|pos| {
628 let tail = &after_oid[pos + SYM.len()..];
629 let end = tail.find(' ').unwrap_or(tail.len());
630 tail[..end].to_owned()
631 });
632 let advertised_peel = peel_at.and_then(|pos| {
633 let tail = &after_oid[pos + PEEL.len()..];
634 let end = tail.find(' ').unwrap_or(tail.len());
635 ObjectId::from_hex(tail[..end].trim()).ok()
636 });
637 Some((name, oid, symref_target, advertised_peel))
638}
639
640fn v2_ls_refs(
649 conn: &mut dyn Connection,
650 server_caps: &[String],
651 local_odb: &crate::odb::Odb,
652 tags: crate::transfer::TagMode,
653 refspecs: &[String],
654 promisor_reply: Option<&str>,
655) -> Result<LsRefsAdvertised> {
656 let req = build_v2_ls_refs_request(server_caps, local_odb, tags, refspecs, promisor_reply)?;
657 conn.writer().write_all(&req)?;
658 conn.writer().flush()?;
659 parse_v2_ls_refs_response(conn.reader())
660}
661
662pub(crate) fn build_v2_ls_refs_request(
672 server_caps: &[String],
673 local_odb: &crate::odb::Odb,
674 tags: crate::transfer::TagMode,
675 refspecs: &[String],
676 promisor_reply: Option<&str>,
677) -> Result<Vec<u8>> {
678 let object_format = v2_object_format(server_caps, local_odb);
679 let cap_echo = command_cap_echo(server_caps, promisor_reply);
680
681 let mut req: Vec<u8> = Vec::new();
682 pkt_line::write_line(&mut req, "command=ls-refs")?;
683 if cap_echo.iter().any(|c| c.starts_with("object-format=")) {
686 for line in &cap_echo {
687 pkt_line::write_line(&mut req, line)?;
688 }
689 } else {
690 for line in &cap_echo {
691 pkt_line::write_line(&mut req, line)?;
692 }
693 pkt_line::write_line(&mut req, &format!("object-format={object_format}"))?;
694 }
695 pkt_line::write_delim(&mut req)?;
696 pkt_line::write_line(&mut req, "symrefs")?;
697 if crate::protocol_v2::server_advertises_ls_refs_unborn(server_caps) {
698 pkt_line::write_line(&mut req, "unborn")?;
699 }
700 pkt_line::write_line(&mut req, "peel")?;
701
702 pkt_line::write_line(&mut req, "ref-prefix HEAD")?;
706 for p in &v2_ls_refs_prefixes(refspecs, tags) {
707 pkt_line::write_line(&mut req, &format!("ref-prefix {p}"))?;
708 }
709 pkt_line::write_flush(&mut req)?;
710 Ok(req)
711}
712
713pub(crate) fn parse_v2_ls_refs_response(reader: &mut dyn Read) -> Result<LsRefsAdvertised> {
719 let mut advertised: Vec<(String, ObjectId)> = Vec::new();
721 let mut head_symref: Option<String> = None;
722 let mut head_oid: Option<ObjectId> = None;
723 let mut advertised_peel: std::collections::HashMap<String, ObjectId> =
724 std::collections::HashMap::new();
725 let mut reader = reader;
726 loop {
727 match pkt_line::read_packet(&mut reader)? {
728 None | Some(pkt_line::Packet::Flush) | Some(pkt_line::Packet::Delim) => break,
729 Some(pkt_line::Packet::ResponseEnd) => break,
730 Some(pkt_line::Packet::Data(line)) => {
731 let line = line.trim_end_matches('\n');
732 pkt_line::reject_err_line(line)?;
733 if let Some(t) = parse_v2_ls_refs_unborn_head_symref(line) {
734 head_symref = Some(t);
735 continue;
736 }
737 let Some((name, oid, symref_target, peel)) = parse_ls_refs_v2_line(line) else {
738 continue;
739 };
740 if name.contains("^{") || name.ends_with("^{}") {
741 continue;
742 }
743 if name == "HEAD" {
744 head_oid = Some(oid);
745 if let Some(t) =
746 symref_target.filter(|t| crate::refs::is_valid_advertised_symref_target(t))
747 {
748 head_symref = Some(t);
749 }
750 continue;
752 }
753 if crate::refs::is_valid_fetch_advertised_ref(&name) {
754 if let Some(p) = peel {
755 advertised_peel.insert(name.clone(), p);
756 }
757 advertised.push((name, oid));
758 }
759 }
760 }
761 }
762 Ok((advertised, head_symref, advertised_peel, head_oid))
763}
764
765pub(crate) struct V2LocalHaves<'r> {
771 negotiator: SkippingNegotiator<'r>,
772 taken: Vec<ObjectId>,
773 exhausted: bool,
774}
775
776impl V2LocalHaves<'_> {
777 fn first(&mut self, n: usize) -> Result<&[ObjectId]> {
779 while self.taken.len() < n && !self.exhausted {
780 match self.negotiator.next_have()? {
781 Some(oid) => self.taken.push(oid),
782 None => self.exhausted = true,
783 }
784 }
785 Ok(&self.taken[..self.taken.len().min(n)])
786 }
787
788 #[cfg(test)]
790 fn all(&mut self) -> Result<&[ObjectId]> {
791 self.first(usize::MAX)
792 }
793}
794
795pub(crate) fn v2_local_haves<'r>(
804 local: &'r Repository,
805 exclude: &[ObjectId],
806) -> Result<V2LocalHaves<'r>> {
807 let exclude: HashSet<ObjectId> = exclude.iter().copied().collect();
808 let mut negotiator = SkippingNegotiator::new(local);
809 add_negotiation_tips(&mut negotiator, &exclude)?;
810 Ok(V2LocalHaves {
811 negotiator,
812 taken: Vec::new(),
813 exhausted: false,
814 })
815}
816
817fn add_negotiation_tips(
822 negotiator: &mut SkippingNegotiator<'_>,
823 wants: &HashSet<ObjectId>,
824) -> Result<()> {
825 let local = negotiator.repo();
826 let mut tips: Vec<ObjectId> = Vec::new();
827 let mut seen_tip: HashSet<ObjectId> = HashSet::new();
828 let mut push_tip = |negotiator: &SkippingNegotiator<'_>, oid: ObjectId| {
829 if let Ok(Some(c)) = negotiator.repo().odb().peel_to_commit(oid) {
830 if !wants.contains(&c) && seen_tip.insert(c) {
831 tips.push(c);
832 }
833 }
834 };
835 for prefix in [
836 "refs/heads/",
837 "refs/remotes/",
838 "refs/tags/",
839 crate::bundle_uri::BUNDLE_REFS_PREFIX,
840 ] {
841 if let Ok(entries) = crate::refs::list_refs_in(local, prefix) {
842 for (_, oid) in entries {
843 push_tip(negotiator, oid);
844 }
845 }
846 }
847 if let Ok(h) = crate::refs::resolve_ref_in(local, "HEAD") {
848 push_tip(negotiator, h);
849 }
850 tips.sort_unstable();
851 for t in tips {
852 negotiator.add_tip(t)?;
853 }
854 Ok(())
855}
856
857#[derive(Clone, Copy)]
859enum V2FetchMode<'a> {
860 Refs {
863 promisor_reply: Option<&'a str>,
865 },
866 ObjectWants,
869}
870
871fn command_cap_echo(server_caps: &[String], promisor_reply: Option<&str>) -> Vec<String> {
875 let mut echo = protocol_v2::cap_lines_for_command_request(server_caps);
876 if let Some(reply) = promisor_reply {
877 echo.push(format!(
878 "{}={reply}",
879 protocol_v2::PROMISOR_REMOTE_CAPABILITY
880 ));
881 }
882 echo
883}
884
885fn v2_promisor_handshake(
896 local: &Repository,
897 local_cfg: &crate::config::ConfigSet,
898 server_caps: &[String],
899 opts: &mut FetchOptions,
900) -> Result<Option<crate::promisor_remote::PromisorHandshake>> {
901 use crate::net_trace::net_trace;
902 use crate::promisor_remote::{PromisorAdvertisement, PromisorHandshake};
903 let Some(value) = protocol_v2::server_promisor_remote_info(server_caps) else {
904 return Ok(None);
905 };
906 let advertisement = PromisorAdvertisement::parse(value);
907 if advertisement.remotes.is_empty() {
908 return Ok(None);
909 }
910 let auto_filter = opts.filter.is_none() && opts.auto_filter;
911 let partial_fetch = opts.filter.is_some()
912 || auto_filter
913 || opts
914 .remote_name
915 .as_deref()
916 .is_some_and(|name| crate::promisor_remote::is_promisor_remote(local_cfg, name));
917 let mut handshake =
918 PromisorHandshake::evaluate(local_cfg, &advertisement, partial_fetch, auto_filter);
919 if !opts.dry_run {
920 handshake.store_fields(local.git_dir(), local_cfg)?;
921 }
922 if auto_filter {
923 opts.filter = handshake.resolved_filter;
924 }
925 net_trace!(
926 opts.network_trace,
927 opts.diagnostics.as_ref(),
928 "fetch_remote: server advertised {} promisor remote(s), accepted [{}]",
929 advertisement.remotes.len(),
930 handshake.decision.accepted_names().join(", ")
931 );
932 Ok(Some(handshake))
933}
934
935#[allow(clippy::too_many_arguments)]
950fn negotiate_pack_v2(
951 local: &Repository,
952 conn: &mut dyn Connection,
953 server_caps: &[String],
954 wants: &[ObjectId],
955 deepen: &V2DeepenArgs,
956 wire_filter: Option<&str>,
957 progress: &mut dyn Progress,
958 cancel: &dyn Cancel,
959 mode: V2FetchMode<'_>,
960) -> Result<(Option<std::path::PathBuf>, ShallowUpdate)> {
961 if wants.is_empty() {
962 return Ok((None, ShallowUpdate::default()));
963 }
964 let (skip_local_haves, promisor_reply) = match mode {
965 V2FetchMode::Refs { promisor_reply } => (false, promisor_reply),
966 V2FetchMode::ObjectWants => (true, None),
967 };
968 let local_odb = local.odb();
969 let object_format = v2_object_format(server_caps, local_odb);
970 let cap_echo = command_cap_echo(server_caps, promisor_reply);
971 let sideband_all = protocol_v2::fetch_supports_sideband_all(server_caps);
972
973 let mut haves = if skip_local_haves {
980 None
981 } else {
982 let exclude: &[ObjectId] = if deepen.is_shallow_request() {
983 &[]
984 } else {
985 wants
986 };
987 Some(v2_local_haves(local, exclude)?)
988 };
989
990 let mut pack_receive = crate::pack_receive::TempPackReceive::create(local_odb.objects_dir())?;
991 let mut shallow_update = ShallowUpdate::default();
992 let mut common: Vec<ObjectId> = Vec::new();
993 let mut common_set: HashSet<ObjectId> = HashSet::new();
994 let mut batch = INITIAL_FLUSH;
995 let mut in_vain: usize = 0;
996 let mut seen_ack = false;
997 let mut round_haves: Vec<ObjectId> = Vec::new();
998 loop {
999 round_haves.clear();
1000 round_haves.extend_from_slice(&common);
1001 let added = match haves.as_mut() {
1002 Some(haves) => {
1003 poll_cancel(progress, cancel)?;
1004 let previous = haves.taken.len();
1005 haves.first(previous.saturating_add(batch))?;
1006 round_haves.extend_from_slice(&haves.taken[previous..]);
1007 haves.taken.len() - previous
1008 }
1009 None => 0,
1010 };
1011 in_vain += added;
1012 let done = added == 0 || (seen_ack && in_vain >= MAX_IN_VAIN);
1013 write_v2_fetch_request(
1014 conn.writer(),
1015 &object_format,
1016 &cap_echo,
1017 wants,
1018 &round_haves,
1019 sideband_all,
1020 deepen,
1021 wire_filter,
1022 done,
1023 )?;
1024 if done {
1025 break;
1026 }
1027 match read_v2_acknowledgments(conn.reader(), sideband_all)? {
1028 Some(round) if !round.ready => {
1032 for oid in round.common {
1033 if let Some(haves) = haves.as_mut() {
1034 haves.negotiator.ack(oid)?;
1035 }
1036 if common_set.insert(oid) {
1037 common.push(oid);
1038 in_vain = 0;
1039 }
1040 seen_ack = true;
1041 }
1042 batch = next_v2_flush_count(batch);
1043 }
1044 _ => break,
1047 }
1048 }
1049 read_v2_fetch_pack_response(
1050 conn.reader(),
1051 &mut Vec::new(),
1052 Some(&mut pack_receive),
1053 &mut shallow_update,
1054 progress,
1055 cancel,
1056 sideband_all,
1057 )?;
1058 Ok((pack_receive.finish()?, shallow_update))
1059}
1060
1061#[derive(Clone, Default)]
1066pub(crate) struct V2DeepenArgs {
1067 pub(crate) local_shallow: Vec<ObjectId>,
1069 pub(crate) depth: Option<u32>,
1071 pub(crate) deepen_relative: bool,
1073 pub(crate) deepen_since: Option<String>,
1075 pub(crate) deepen_not: Vec<String>,
1077 pub(crate) remote_progress: bool,
1079}
1080
1081impl V2DeepenArgs {
1082 pub(crate) fn from_opts(opts: &FetchOptions, local_shallow: &[ObjectId]) -> Self {
1085 let (depth, deepen_relative) = opts
1086 .wire_deepen()
1087 .map(|(d, r)| (Some(d), r))
1088 .unwrap_or((None, false));
1089 Self {
1090 local_shallow: local_shallow.to_vec(),
1091 depth,
1092 deepen_relative,
1093 deepen_since: opts
1094 .deepen_since
1095 .as_deref()
1096 .filter(|s| !s.trim().is_empty())
1097 .map(crate::shallow::deepen_since_wire_value),
1098 deepen_not: opts
1099 .deepen_not
1100 .iter()
1101 .map(|s| s.trim().to_owned())
1102 .filter(|s| !s.is_empty())
1103 .collect(),
1104 remote_progress: opts.remote_progress,
1105 }
1106 }
1107
1108 pub(crate) fn is_shallow_request(&self) -> bool {
1111 self.depth.is_some()
1112 || self.deepen_relative
1113 || self.deepen_since.is_some()
1114 || !self.deepen_not.is_empty()
1115 || !self.local_shallow.is_empty()
1116 }
1117}
1118#[expect(clippy::too_many_arguments)]
1119pub(crate) fn write_v2_fetch_request(
1125 w: &mut dyn Write,
1126 object_format: &str,
1127 cap_echo: &[String],
1128 wants: &[ObjectId],
1129 haves: &[ObjectId],
1130 sideband_all: bool,
1131 deepen: &V2DeepenArgs,
1132 wire_filter: Option<&str>,
1133 send_done: bool,
1134) -> Result<()> {
1135 let mut req: Vec<u8> = Vec::new();
1136 pkt_line::write_line(&mut req, "command=fetch")?;
1137 if cap_echo.iter().any(|c| c.starts_with("object-format=")) {
1138 for line in cap_echo {
1139 pkt_line::write_line(&mut req, line)?;
1140 }
1141 } else {
1142 for line in cap_echo {
1143 pkt_line::write_line(&mut req, line)?;
1144 }
1145 pkt_line::write_line(&mut req, &format!("object-format={object_format}"))?;
1146 }
1147 pkt_line::write_delim(&mut req)?;
1148
1149 pkt_line::write_line(&mut req, "thin-pack")?;
1150 if !deepen.remote_progress {
1151 pkt_line::write_line(&mut req, "no-progress")?;
1152 }
1153 pkt_line::write_line(&mut req, "ofs-delta")?;
1154 if sideband_all {
1155 pkt_line::write_line(&mut req, "sideband-all")?;
1156 }
1157 pkt_line::write_line(&mut req, "include-tag")?;
1160
1161 for oid in &deepen.local_shallow {
1163 pkt_line::write_line(&mut req, &format!("shallow {}", oid.to_hex()))?;
1164 }
1165 if let Some(depth) = deepen.depth {
1166 pkt_line::write_line(&mut req, &format!("deepen {depth}"))?;
1167 }
1168 if deepen.deepen_relative {
1169 pkt_line::write_line(&mut req, "deepen-relative")?;
1170 }
1171 if let Some(since) = &deepen.deepen_since {
1172 pkt_line::write_line(&mut req, &format!("deepen-since {since}"))?;
1173 }
1174 for excl in &deepen.deepen_not {
1175 pkt_line::write_line(&mut req, &format!("deepen-not {excl}"))?;
1176 }
1177 if let Some(spec) = wire_filter {
1178 pkt_line::write_line(&mut req, &format!("filter {spec}"))?;
1179 }
1180
1181 for want in wants {
1182 pkt_line::write_line(&mut req, &format!("want {}", want.to_hex()))?;
1183 }
1184 for have in haves {
1185 pkt_line::write_line(&mut req, &format!("have {}", have.to_hex()))?;
1186 }
1187 if send_done {
1188 pkt_line::write_line(&mut req, "done")?;
1189 }
1190 pkt_line::write_flush(&mut req)?;
1191 w.write_all(&req)?;
1192 w.flush()?;
1193 Ok(())
1194}
1195
1196pub(crate) struct V2AckRound {
1198 pub(crate) ready: bool,
1201 pub(crate) common: Vec<ObjectId>,
1203}
1204
1205pub(crate) fn read_v2_acknowledgments(
1213 reader: &mut dyn Read,
1214 sideband_all: bool,
1215) -> Result<Option<V2AckRound>> {
1216 let mut reader = reader;
1217 let hdr = match crate::shallow::read_v2_fetch_primary_line(&mut reader, sideband_all)? {
1218 Some(s) => s,
1219 None => {
1220 return Ok(Some(V2AckRound {
1221 ready: false,
1222 common: Vec::new(),
1223 }))
1224 }
1225 };
1226 let hdr = hdr.as_str();
1227 pkt_line::reject_err_line(hdr)?;
1228 if hdr != "acknowledgments" {
1229 return Err(Error::Message(format!(
1239 "unexpected v2 fetch section before acknowledgments: {hdr}"
1240 )));
1241 }
1242 let mut ready = false;
1243 let mut common = Vec::new();
1244 loop {
1245 match pkt_line::read_packet(&mut reader)? {
1246 Some(pkt_line::Packet::Delim) | Some(pkt_line::Packet::Flush) | None => break,
1247 Some(pkt_line::Packet::ResponseEnd) => break,
1248 Some(pkt_line::Packet::Data(payload)) => {
1249 let Some(ln) = crate::shallow::v2_primary_line_from_pkt(payload, sideband_all)?
1250 else {
1251 continue;
1252 };
1253 if ln == "NAK" {
1254 continue;
1255 }
1256 if let Some(hex) = ln.strip_prefix("ACK ") {
1257 let oid = ObjectId::from_hex(hex.trim()).map_err(|_| {
1258 Error::Message(format!("unexpected acknowledgment line: '{ln}'"))
1259 })?;
1260 common.push(oid);
1261 continue;
1262 }
1263 if ln == "ready" {
1264 ready = true;
1265 continue;
1266 }
1267 return Err(Error::Message(format!(
1268 "unexpected acknowledgment line: '{ln}'"
1269 )));
1270 }
1271 }
1272 }
1273 Ok(Some(V2AckRound { ready, common }))
1274}
1275
1276fn v2_wanted_ref_ad_line(line: &str) -> bool {
1279 let line = line.trim_end();
1280 let Some((oid_hex, rest)) = line.split_once(' ') else {
1281 return false;
1282 };
1283 if ObjectId::from_hex(oid_hex.trim()).is_err() {
1284 return false;
1285 }
1286 !rest.trim().is_empty()
1287}
1288
1289pub(crate) fn read_v2_fetch_pack_response(
1298 reader: &mut dyn Read,
1299 out: &mut Vec<u8>,
1300 pack_sink: Option<&mut crate::pack_receive::TempPackReceive>,
1301 shallow_out: &mut ShallowUpdate,
1302 progress: &mut dyn Progress,
1303 cancel: &dyn Cancel,
1304 sideband_all: bool,
1305) -> Result<()> {
1306 loop {
1307 let hdr = loop {
1308 match pkt_line::read_packet(&mut &mut *reader)? {
1309 Some(pkt_line::Packet::Data(payload)) => {
1310 if let Some(line) =
1311 crate::shallow::v2_primary_line_from_pkt(payload, sideband_all)?
1312 {
1313 break line;
1314 }
1315 }
1316 Some(pkt_line::Packet::Flush) => return Ok(()),
1317 None => {
1320 return Err(Error::RemoteUnreadable {
1321 url: None,
1322 reason: crate::error::RemoteUnreadableReason::ResponseTruncated,
1323 })
1324 }
1325 Some(pkt_line::Packet::Delim) => continue,
1326 Some(other) => {
1327 return Err(Error::Message(format!(
1328 "unexpected v2 fetch response: {other:?}"
1329 )))
1330 }
1331 }
1332 };
1333 let hdr = hdr.as_str();
1334 pkt_line::reject_err_line(hdr)?;
1335 match hdr {
1336 h if h == crate::protocol_v2::FetchResponseSection::ShallowInfo.header() => {
1337 let (sh, unsh) =
1341 crate::shallow::read_shallow_info_section_sideband(&mut *reader, sideband_all)?;
1342 shallow_out.shallow.extend(sh);
1343 shallow_out.unshallow.extend(unsh);
1344 }
1345 h if h == crate::protocol_v2::FetchResponseSection::Acknowledgments.header()
1346 || h == crate::protocol_v2::FetchResponseSection::WantedRefs.header()
1347 || h == crate::protocol_v2::FetchResponseSection::PackfileUris.header() =>
1348 {
1349 skip_v2_section_until_boundary(&mut *reader, sideband_all)?;
1350 }
1351 h if h == crate::protocol_v2::FetchResponseSection::Packfile.header() => {
1352 if let Some(sink) = pack_sink {
1355 sink.read_sideband(&mut *reader, progress, cancel)?;
1356 } else {
1357 let mut target = crate::pack_receive::PackReceiveTarget::Memory(out);
1358 read_sideband_pack(&mut *reader, &mut target, progress, cancel)?;
1359 }
1360 return Ok(());
1361 }
1362 other if v2_wanted_ref_ad_line(other) => {
1363 }
1365 other => {
1366 return Err(Error::Message(format!(
1367 "unexpected v2 fetch section: {other}"
1368 )))
1369 }
1370 }
1371 }
1372}
1373
1374fn skip_v2_section_until_boundary(reader: &mut dyn Read, sideband_all: bool) -> Result<()> {
1376 loop {
1377 match pkt_line::read_packet(&mut &mut *reader)? {
1378 None | Some(pkt_line::Packet::Flush) | Some(pkt_line::Packet::Delim) => return Ok(()),
1379 Some(pkt_line::Packet::ResponseEnd) => return Ok(()),
1380 Some(pkt_line::Packet::Data(payload)) => {
1381 let _ = crate::shallow::v2_primary_line_from_pkt(payload, sideband_all)?;
1382 }
1383 }
1384 }
1385}
1386
1387#[allow(clippy::large_enum_variant)]
1389enum FetchDryRunIngest {
1390 Quarantine(crate::receive_quarantine::ReceiveQuarantine),
1391 Memory {
1392 objects_dir: std::path::PathBuf,
1393 pack: std::sync::Arc<crate::objects::odb::store::MemoryStore>,
1394 read: std::sync::Arc<dyn crate::objects::odb::store::ObjectStore>,
1395 },
1396}
1397
1398impl FetchDryRunIngest {
1399 fn create(local_odb: &crate::odb::Odb, git_dir: &Path) -> Result<Self> {
1400 use crate::objects::odb::store::MemoryStore;
1401 if local_odb.uses_files_primary() {
1402 Ok(Self::Quarantine(
1403 crate::receive_quarantine::ReceiveQuarantine::create(local_odb, git_dir)?,
1404 ))
1405 } else {
1406 let pack = std::sync::Arc::new(MemoryStore::new(local_odb.hash_algo()));
1407 Ok(Self::Memory {
1408 objects_dir: local_odb.objects_dir().to_path_buf(),
1409 pack: pack.clone(),
1410 read: local_odb.read_store(),
1411 })
1412 }
1413 }
1414
1415 fn odb(&self) -> crate::odb::Odb {
1416 match self {
1417 Self::Quarantine(q) => q.odb(),
1418 Self::Memory {
1419 objects_dir,
1420 pack,
1421 read,
1422 } => crate::odb::OdbBuilder::files(objects_dir)
1423 .primary(pack.clone())
1424 .push_read_source(read.clone())
1425 .alternates(false)
1426 .build(),
1427 }
1428 }
1429}
1430
1431pub fn fetch_remote(
1453 local_git_dir: &Path,
1454 conn: &mut dyn Connection,
1455 opts: &mut FetchOptions,
1456 progress: &mut dyn Progress,
1457 cancel: &dyn Cancel,
1458) -> Result<FetchOutcome> {
1459 let local = open_fetch_target(local_git_dir, opts)?;
1460 fetch_remote_into(&local, conn, opts, progress, cancel)
1461}
1462
1463pub(crate) fn open_fetch_target(local_git_dir: &Path, opts: &FetchOptions) -> Result<Repository> {
1465 Repository::open(
1466 local_git_dir,
1467 &RepositoryOptions::with_environment(opts.environment.clone()),
1468 )
1469}
1470
1471pub fn fetch_remote_into(
1479 local: &Repository,
1480 conn: &mut dyn Connection,
1481 opts: &mut FetchOptions,
1482 progress: &mut dyn Progress,
1483 cancel: &dyn Cancel,
1484) -> Result<FetchOutcome> {
1485 use crate::net_trace::net_trace;
1486 let local_git_dir = local.git_dir();
1487 let t_begin = std::time::Instant::now();
1490 net_trace!(
1491 opts.network_trace,
1492 opts.diagnostics.as_ref(),
1493 "fetch_remote: begin — protocol v{}, {} refspec(s), tags={:?}, depth={:?}",
1494 conn.protocol_version(),
1495 opts.refspecs.len(),
1496 opts.tags,
1497 opts.fetch_depth
1498 );
1499 validate_fetch_depth_local(local_git_dir, opts)?;
1500 let local_cfg = crate::config::ConfigSet::load(&opts.environment, Some(local_git_dir), true)?;
1501 if opts.filter.is_none() {
1502 if let Some(remote) = opts.remote_name.as_deref() {
1503 opts.filter = crate::partial_clone::stored_remote_filter(&local_cfg, remote);
1504 opts.auto_filter |=
1505 crate::partial_clone::stored_remote_filter_is_auto(&local_cfg, remote);
1506 }
1507 }
1508 if !opts.object_wants.is_empty() {
1509 return fetch_remote_object_wants(local, conn, opts, progress, cancel);
1510 }
1511 let mut promisor_handshake: Option<crate::promisor_remote::PromisorHandshake> = None;
1512 let local_odb_for_ls = local.odb();
1513
1514 let (remote_refs, default_branch, v2_caps, advertised_peel, remote_head_oid) = if conn
1521 .protocol_version()
1522 >= 2
1523 {
1524 let caps: Vec<String> = conn.capabilities().to_vec();
1525 promisor_handshake = v2_promisor_handshake(local, &local_cfg, &caps, opts)?;
1526 let promisor_reply = promisor_handshake
1527 .as_ref()
1528 .and_then(|h| h.decision.reply_value());
1529 let (refs, head_symref, peel, head_oid) = v2_ls_refs(
1530 conn,
1531 &caps,
1532 local_odb_for_ls,
1533 opts.tags,
1534 &opts.refspecs,
1535 promisor_reply.as_deref(),
1536 )?;
1537 let default_branch =
1538 head_symref.map(|t| t.strip_prefix("refs/heads/").unwrap_or(&t).to_owned());
1539 (refs, default_branch, Some(caps), peel, head_oid)
1540 } else {
1541 let default_branch = conn
1542 .head_symref()
1543 .map(|t| t.strip_prefix("refs/heads/").unwrap_or(t).to_owned());
1544 let advertised_peel = conn.advertised_tag_peel().cloned().unwrap_or_default();
1545 let remote_head_oid =
1546 crate::transfer::remote_head_oid_from_advertised(conn.advertised_refs());
1547 let remote_refs: Vec<(String, ObjectId)> = conn
1548 .advertised_refs()
1549 .iter()
1550 .filter(|(n, _)| {
1551 n != "HEAD" && !n.ends_with("^{}") && crate::refs::is_valid_fetch_advertised_ref(n)
1552 })
1553 .cloned()
1554 .collect();
1555 (
1556 remote_refs,
1557 default_branch,
1558 None,
1559 advertised_peel,
1560 remote_head_oid,
1561 )
1562 };
1563 net_trace!(
1564 opts.network_trace,
1565 opts.diagnostics.as_ref(),
1566 "fetch_remote: remote advertised {} ref(s){}",
1567 remote_refs.len(),
1568 v2_caps
1569 .as_ref()
1570 .map(|_| " (via v2 ls-refs)")
1571 .unwrap_or(" (v0/v1 advertisement)")
1572 );
1573
1574 if opts.initial_remote_fetch
1575 && remote_refs.is_empty()
1576 && !crate::transport::connection_handshake_complete(conn)
1577 {
1578 return Err(Error::RemoteUnreadable {
1579 url: None,
1580 reason: crate::error::RemoteUnreadableReason::NoHandshake,
1581 });
1582 }
1583
1584 let conn_caps: Vec<String> = conn.capabilities().to_vec();
1585 let remote_algo = crate::transfer::remote_hash_algo_from_advertisement(
1586 &[v2_caps.as_deref().unwrap_or(&[]), conn_caps.as_slice()],
1587 &remote_refs,
1588 );
1589 let reopened =
1590 crate::transfer::sync_empty_destination_object_format(local_git_dir, remote_algo, opts)?
1591 .then(|| crate::transfer::reopen_after_format_change(local))
1592 .transpose()?;
1593 let local = reopened.as_ref().unwrap_or(local);
1594 let local_git_dir = local.git_dir();
1595 let local_odb = local.odb();
1596 if opts.filter.is_some() && !local_odb.uses_files_primary() {
1597 return Err(Error::UnsupportedObjectStore {
1598 operation: "partial fetch",
1599 });
1600 }
1601
1602 let crate::transfer::FetchMatchSet {
1603 mut matched,
1604 positive,
1605 negatives,
1606 explicit_refspec_fetch,
1607 } = crate::transfer::build_fetch_matched_set(opts, &remote_refs, remote_head_oid)?;
1608 let mut matched_oids: HashSet<ObjectId> = matched.iter().map(|m| m.oid).collect();
1609 let mut seen_remote_ref: HashSet<String> =
1610 matched.iter().map(|m| m.remote_ref.clone()).collect();
1611
1612 let tags_present = if opts.tags == crate::transfer::TagMode::Following {
1626 tags_already_local(local.refs(), &remote_refs)
1627 } else {
1628 HashSet::new()
1629 };
1630 let following_only = add_wire_tags(
1631 opts.tags,
1632 &remote_refs,
1633 &advertised_peel,
1634 &negatives,
1635 &tags_present,
1636 opts.force,
1637 &mut matched,
1638 &mut matched_oids,
1639 &mut seen_remote_ref,
1640 );
1641 let fetch_commit_oids: HashSet<ObjectId> = matched
1642 .iter()
1643 .filter(|m| !m.is_tag)
1644 .map(|m| m.oid)
1645 .collect();
1646 let mut following_tag_wants =
1647 following_tag_wants_with_local_peel(&matched, &following_only, local_odb)?;
1648
1649 let local_shallow = crate::shallow::load_shallow_oids(local_git_dir)?;
1652 let shallow_request = opts.has_deepen_request() || !local_shallow.is_empty();
1653 if shallow_request {
1654 let local_shallow_set: HashSet<ObjectId> = local_shallow.iter().copied().collect();
1655 let peel_set = following_peel_commit_set_for_shallow(
1656 local_odb,
1657 &fetch_commit_oids,
1658 opts.absolute_depth_for_tags(),
1659 &local_shallow_set,
1660 )?;
1661 following_tag_wants.extend(following_tag_wants_peel_among_fetched_commits(
1662 &matched,
1663 &following_only,
1664 &peel_set,
1665 ));
1666 }
1667
1668 let wants: Vec<ObjectId> = if shallow_request {
1673 matched_oids
1674 .iter()
1675 .copied()
1676 .filter(|oid| !following_only.contains(oid) || following_tag_wants.contains(oid))
1677 .collect()
1678 } else {
1679 matched_oids
1680 .iter()
1681 .copied()
1682 .filter_map(|oid| {
1683 if following_only.contains(&oid) {
1684 return following_tag_wants.contains(&oid).then_some(Ok(oid));
1685 }
1686 match local_odb.contains(&oid) {
1687 Ok(true) => None,
1688 Ok(false) => Some(Ok(oid)),
1689 Err(err) => Some(Err(err)),
1690 }
1691 })
1692 .collect::<Result<_>>()?
1693 };
1694
1695 let mut shallow_update = ShallowUpdate::default();
1698 let mut pack_oids: HashSet<ObjectId> = HashSet::new();
1699 let mut transfer = crate::transfer::TransferStats::default();
1700
1701 net_trace!(
1702 opts.network_trace,
1703 opts.diagnostics.as_ref(),
1704 "fetch_remote: {} matched ref(s), want {} object(s){} [+{:?} since begin]",
1705 matched.len(),
1706 wants.len(),
1707 if shallow_request {
1708 " (shallow request)"
1709 } else {
1710 ""
1711 },
1712 t_begin.elapsed()
1713 );
1714
1715 let mut dry_run_ingest: Option<FetchDryRunIngest> = None;
1716
1717 let v0_caps = if v2_caps.is_none() {
1718 Some(conn.capabilities().join(" "))
1719 } else {
1720 None
1721 };
1722 let (wire_filter, filter_sent) = crate::partial_clone::wire_filter_for_capabilities(
1723 opts.filter.as_ref(),
1724 v2_caps.as_deref(),
1725 v0_caps.as_deref(),
1726 );
1727 let promisor_reply = promisor_handshake
1732 .as_ref()
1733 .and_then(|h| h.decision.reply_value());
1734 opts.mark_received_pack_promisor = opts.filter.is_some() || promisor_reply.is_some();
1735 let wire_filter = wire_filter.as_deref();
1736
1737 if wants.is_empty() {
1738 crate::transport::send_client_flush(conn)?;
1739 } else {
1740 net_trace!(
1741 opts.network_trace,
1742 opts.diagnostics.as_ref(),
1743 "fetch_remote: negotiating + fetching pack…"
1744 );
1745 let (pack_path, su) = if let Some(caps) = v2_caps.as_ref() {
1746 validate_fetch_depth_server(opts, None, Some(caps))?;
1747 let deepen = V2DeepenArgs::from_opts(opts, &local_shallow);
1748 negotiate_pack_v2(
1749 local,
1750 conn,
1751 caps,
1752 &wants,
1753 &deepen,
1754 wire_filter,
1755 progress,
1756 cancel,
1757 V2FetchMode::Refs {
1758 promisor_reply: promisor_reply.as_deref(),
1759 },
1760 )?
1761 } else {
1762 negotiate_pack(
1763 local,
1764 conn,
1765 &wants,
1766 opts,
1767 &local_shallow,
1768 wire_filter,
1769 progress,
1770 cancel,
1771 false,
1772 )?
1773 };
1774 shallow_update = su;
1775 net_trace!(
1776 opts.network_trace,
1777 opts.diagnostics.as_ref(),
1778 "fetch_remote: received pack, ingesting via index-pack…"
1779 );
1780 if let Some(path) = pack_path {
1781 if opts.pack_spool_only {
1782 net_trace!(
1783 opts.network_trace,
1784 opts.diagnostics.as_ref(),
1785 "fetch_remote: pack spooled to {} (pack_spool_only)",
1786 path.display()
1787 );
1788 return Ok(FetchOutcome {
1789 updates: Vec::new(),
1790 default_branch: None,
1791 new_shallow: crate::shallow::dedupe_shallow_list(shallow_update.shallow),
1792 new_unshallow: shallow_update.unshallow,
1793 transfer: crate::transfer::TransferStats::default(),
1794 filter_sent,
1795 promisor: promisor_handshake,
1796 });
1797 }
1798 let pack_config =
1799 crate::config::ConfigSet::load(&opts.environment, Some(local_git_dir), true)?;
1800 let index_threads = crate::parallel::index_pack_threads_for_ingest(
1801 opts.thread_policy,
1802 &pack_config,
1803 opts.index_pack_threads,
1804 )?;
1805 if opts.dry_run && dry_run_ingest.is_none() {
1806 dry_run_ingest = Some(FetchDryRunIngest::create(local_odb, local_git_dir)?);
1807 }
1808 let ingested = if let Some(target) = dry_run_ingest.as_ref() {
1809 let q_odb = target.odb();
1810 crate::index_pack::ingest_received_pack_path_with_progress(
1811 path,
1812 &q_odb,
1813 &crate::index_pack::IngestPackOptions {
1814 fix_thin: true,
1815 skip_post_index_verify: true,
1816 threads: index_threads,
1817 merged_config: Some(pack_config),
1818 environment: opts.environment.clone(),
1819 mark_promisor_pack: opts.mark_received_pack_promisor,
1820 keep_message: None,
1821 trusted_index_rows: None,
1822 },
1823 Some(progress),
1824 Some(cancel),
1825 )?
1826 } else {
1827 crate::index_pack::ingest_received_pack_path_with_progress(
1828 path,
1829 local_odb,
1830 &crate::index_pack::IngestPackOptions {
1831 fix_thin: true,
1832 skip_post_index_verify: true,
1833 threads: index_threads,
1834 merged_config: Some(pack_config),
1835 environment: opts.environment.clone(),
1836 mark_promisor_pack: opts.mark_received_pack_promisor,
1837 keep_message: None,
1838 trusted_index_rows: None,
1839 },
1840 Some(progress),
1841 Some(cancel),
1842 )?
1843 };
1844 transfer = crate::transfer::TransferStats::from_ingested_pack(&ingested);
1845 pack_oids = ingested.object_ids;
1846 }
1847 }
1848
1849 if !opts.dry_run {
1852 crate::shallow::apply_shallow_updates(
1853 local_git_dir,
1854 &shallow_update.shallow,
1855 &shallow_update.unshallow,
1856 )?;
1857 }
1858
1859 if v2_caps.is_some() && !wants.is_empty() {
1865 conn.finish_send();
1866 }
1867
1868 if opts.tags == crate::transfer::TagMode::Following {
1872 let t = std::time::Instant::now();
1873 let tag_odb = dry_run_ingest
1874 .as_ref()
1875 .map(FetchDryRunIngest::odb)
1876 .unwrap_or_else(|| local_odb.clone());
1877 retain_following_tags(
1878 &tag_odb,
1879 &mut matched,
1880 &pack_oids,
1881 &crate::shallow::load_shallow_boundaries(local_git_dir),
1882 )?;
1883 net_trace!(
1884 opts.network_trace,
1885 opts.diagnostics.as_ref(),
1886 "fetch_remote: retain_following_tags {:?}",
1887 t.elapsed()
1888 );
1889 }
1890
1891 let t_apply = std::time::Instant::now();
1893 let classify_odb = dry_run_ingest.as_ref().map(FetchDryRunIngest::odb);
1894 let classify_repo = (!opts.dry_run).then_some(local);
1895 let d_repo_open = t_apply.elapsed();
1896
1897 let mut updates: Vec<RefUpdate> = Vec::new();
1898
1899 let t_prune = std::time::Instant::now();
1900 if opts.prune {
1901 prune_tracking_refs(local, &positive, &remote_refs, opts.dry_run, &mut updates)?;
1902 }
1903 let d_prune = t_prune.elapsed();
1904
1905 let mut store_batch: Vec<(String, crate::refs::store::RawRef)> =
1906 Vec::with_capacity(matched.len());
1907
1908 let t_packed = std::time::Instant::now();
1915 let packed = if local.refs_on_disk() {
1916 Some(crate::refs::PackedRefs::load(local_git_dir)?)
1917 } else {
1918 None
1919 };
1920 let d_packed = t_packed.elapsed();
1921
1922 let mut d_resolve = std::time::Duration::ZERO;
1925 let mut d_classify = std::time::Duration::ZERO;
1926 let mut d_write = std::time::Duration::ZERO;
1927 let mut n_written = 0usize;
1928 let mut n_ancestry = 0usize; for m in &matched {
1931 let Some(local_ref) = &m.local_ref else {
1932 updates.push(RefUpdate {
1933 remote_ref: m.remote_ref.clone(),
1934 local_ref: None,
1935 old_oid: None,
1936 new_oid: Some(m.oid),
1937 mode: UpdateMode::NoChangeNeeded,
1938 note: Some("not stored (empty destination)".to_owned()),
1939 });
1940 continue;
1941 };
1942
1943 let t = std::time::Instant::now();
1944 let old = match &packed {
1945 Some(packed) => crate::refs::resolve_ref_cached(local_git_dir, local_ref, packed)
1946 .ok()
1947 .flatten(),
1948 None => local.resolve_ref_oid(local_ref).ok(),
1949 };
1950 d_resolve += t.elapsed();
1951
1952 if old.as_ref().is_some_and(|o| o != &m.oid) {
1953 n_ancestry += 1;
1954 }
1955
1956 let t = std::time::Instant::now();
1957 let mode = classify_update(
1958 old.as_ref(),
1959 &m.oid,
1960 m.force,
1961 m.dest_is_tag,
1962 classify_repo,
1963 classify_odb.as_ref(),
1964 );
1965 d_classify += t.elapsed();
1966
1967 let write = matches!(
1968 mode,
1969 UpdateMode::New | UpdateMode::FastForward | UpdateMode::Forced
1970 );
1971 crate::transfer::enforce_fetch_ref_update(
1972 local_ref,
1973 old,
1974 m.oid,
1975 mode,
1976 opts,
1977 explicit_refspec_fetch,
1978 classify_repo,
1979 )?;
1980 if write && !opts.dry_run {
1981 if !crate::refs::is_valid_storable_ref_name(local_ref) {
1982 updates.push(RefUpdate {
1983 remote_ref: m.remote_ref.clone(),
1984 local_ref: Some(local_ref.clone()),
1985 old_oid: old,
1986 new_oid: Some(m.oid),
1987 mode,
1988 note: Some("skipped (invalid ref name)".to_owned()),
1989 });
1990 continue;
1991 }
1992 store_batch.push((local_ref.clone(), crate::refs::store::RawRef::Direct(m.oid)));
1993 n_written += 1;
1994 }
1995
1996 updates.push(RefUpdate {
1997 remote_ref: m.remote_ref.clone(),
1998 local_ref: Some(local_ref.clone()),
1999 old_oid: old,
2000 new_oid: Some(m.oid),
2001 mode,
2002 note: None,
2003 });
2004 }
2005
2006 if !opts.dry_run && !store_batch.is_empty() {
2007 let t = std::time::Instant::now();
2008 let fetch_reflog = prepare_fetch_apply_reflog(local, opts)?;
2009 let fetch_reflog_ctx = fetch_reflog.as_ref().map(PreparedFetchReflog::context);
2010 crate::transfer::apply_fetch_ref_updates(
2011 local,
2012 store_batch,
2013 &mut updates,
2014 crate::transfer::FetchRefRejectHint {
2015 prune: opts.prune,
2016 positive: &positive,
2017 remote_refs: &remote_refs,
2018 remote_name: opts.remote_name.as_deref(),
2019 },
2020 opts.initial_remote_fetch,
2021 fetch_reflog_ctx.as_ref(),
2022 )?;
2023 d_write += t.elapsed();
2024 }
2025
2026 net_trace!(
2027 opts.network_trace,
2028 opts.diagnostics.as_ref(),
2029 "fetch_remote: apply {:?} — repo_open {:?}, prune {:?}, packed_load {:?}, \
2030 resolve {:?}, classify {:?} ({} ancestry-checked), write {:?} ({} written)",
2031 t_apply.elapsed(),
2032 d_repo_open,
2033 d_prune,
2034 d_packed,
2035 d_resolve,
2036 d_classify,
2037 n_ancestry,
2038 d_write,
2039 n_written
2040 );
2041
2042 net_trace!(
2043 opts.network_trace,
2044 opts.diagnostics.as_ref(),
2045 "fetch_remote: done — {} ref update(s){} [+{:?} total]",
2046 updates.len(),
2047 default_branch
2048 .as_deref()
2049 .map(|b| format!(", default branch '{b}'"))
2050 .unwrap_or_default(),
2051 t_begin.elapsed()
2052 );
2053 finish_initial_remote_fetch_layout(local, opts, default_branch.as_deref())?;
2054 if let Some(remote) = opts.remote_name.as_deref() {
2055 ensure_remote_tracking_head_symref(local, remote, default_branch.as_deref(), opts.dry_run)?;
2056 }
2057 crate::transfer::finish_anonymous_url_fetch(
2058 local_git_dir,
2059 opts,
2060 explicit_refspec_fetch,
2061 default_branch.as_deref(),
2062 &remote_refs,
2063 &matched,
2064 )?;
2065 Ok(FetchOutcome {
2066 updates,
2067 default_branch,
2068 new_shallow: crate::shallow::dedupe_shallow_list(shallow_update.shallow),
2069 new_unshallow: shallow_update.unshallow,
2070 transfer,
2071 filter_sent,
2072 promisor: promisor_handshake,
2073 })
2074}
2075
2076fn fetch_remote_object_wants(
2078 local: &Repository,
2079 conn: &mut dyn Connection,
2080 opts: &mut FetchOptions,
2081 progress: &mut dyn Progress,
2082 cancel: &dyn Cancel,
2083) -> Result<FetchOutcome> {
2084 use crate::net_trace::net_trace;
2085
2086 let t_begin = std::time::Instant::now();
2087 let local_git_dir = local.git_dir();
2088 let local_odb = local.odb();
2089 let wants = local_odb.missing_stored_oids(opts.object_wants.iter().copied())?;
2090 if wants.is_empty() {
2091 return Ok(FetchOutcome {
2092 updates: Vec::new(),
2093 default_branch: None,
2094 new_shallow: Vec::new(),
2095 new_unshallow: Vec::new(),
2096 transfer: crate::transfer::TransferStats::default(),
2097 filter_sent: false,
2098 promisor: None,
2099 });
2100 }
2101
2102 let v2_caps = if conn.protocol_version() >= 2 {
2103 Some(conn.capabilities().to_vec())
2104 } else {
2105 None
2106 };
2107 let v0_caps = if v2_caps.is_none() {
2108 Some(conn.capabilities().join(" "))
2109 } else {
2110 None
2111 };
2112 let (wire_filter, filter_sent) = crate::partial_clone::wire_filter_for_capabilities(
2113 opts.filter.as_ref(),
2114 v2_caps.as_deref(),
2115 v0_caps.as_deref(),
2116 );
2117 if opts.promisor_object_fetch {
2118 opts.mark_received_pack_promisor = true;
2119 } else {
2120 opts.mark_received_pack_promisor = opts.filter.is_some();
2121 }
2122 let wire_filter = wire_filter.as_deref();
2123 let local_shallow = crate::shallow::load_shallow_oids(local_git_dir)?;
2124 let mut transfer = crate::transfer::TransferStats::default();
2125
2126 net_trace!(
2127 opts.network_trace,
2128 opts.diagnostics.as_ref(),
2129 "fetch_remote_object_wants: {} object(s) [+{:?} since begin]",
2130 wants.len(),
2131 t_begin.elapsed()
2132 );
2133
2134 let (pack_path, shallow_update) = if let Some(caps) = v2_caps.as_ref() {
2135 validate_fetch_depth_server(opts, None, Some(caps))?;
2136 let deepen = V2DeepenArgs::from_opts(opts, &local_shallow);
2137 negotiate_pack_v2(
2138 local,
2139 conn,
2140 caps,
2141 &wants,
2142 &deepen,
2143 wire_filter,
2144 progress,
2145 cancel,
2146 V2FetchMode::ObjectWants,
2147 )?
2148 } else {
2149 negotiate_pack(
2150 local,
2151 conn,
2152 &wants,
2153 opts,
2154 &local_shallow,
2155 wire_filter,
2156 progress,
2157 cancel,
2158 true,
2159 )?
2160 };
2161 if let Some(path) = pack_path {
2162 let pack_config =
2163 crate::config::ConfigSet::load(&opts.environment, Some(local_git_dir), true)?;
2164 let index_threads = crate::parallel::index_pack_threads_for_ingest(
2165 opts.thread_policy,
2166 &pack_config,
2167 opts.index_pack_threads,
2168 )?;
2169 let ingested = crate::index_pack::ingest_received_pack_path_with_progress(
2170 path,
2171 local_odb,
2172 &crate::index_pack::IngestPackOptions {
2173 fix_thin: true,
2174 skip_post_index_verify: true,
2175 threads: index_threads,
2176 merged_config: Some(pack_config),
2177 environment: opts.environment.clone(),
2178 mark_promisor_pack: opts.mark_received_pack_promisor,
2179 keep_message: None,
2180 trusted_index_rows: None,
2181 },
2182 Some(progress),
2183 Some(cancel),
2184 )?;
2185 transfer = crate::transfer::TransferStats::from_ingested_pack(&ingested);
2186 }
2187
2188 if v2_caps.is_some() && !wants.is_empty() {
2189 conn.finish_send();
2190 }
2191
2192 if !opts.dry_run {
2193 crate::shallow::apply_shallow_updates(
2194 local_git_dir,
2195 &shallow_update.shallow,
2196 &shallow_update.unshallow,
2197 )?;
2198 }
2199
2200 Ok(FetchOutcome {
2201 updates: Vec::new(),
2202 default_branch: None,
2203 new_shallow: crate::shallow::dedupe_shallow_list(shallow_update.shallow),
2204 new_unshallow: shallow_update.unshallow,
2205 transfer,
2206 filter_sent,
2207 promisor: None,
2208 })
2209}
2210
2211pub(crate) struct PreparedFetchReflog {
2213 identity: String,
2214 action: &'static str,
2215}
2216
2217impl PreparedFetchReflog {
2218 pub(crate) fn context(&self) -> crate::transfer::FetchApplyReflog<'_> {
2219 crate::transfer::FetchApplyReflog {
2220 action: self.action,
2221 identity: &self.identity,
2222 }
2223 }
2224}
2225
2226pub(crate) fn prepare_fetch_apply_reflog(
2231 local: &Repository,
2232 opts: &FetchOptions,
2233) -> Result<Option<PreparedFetchReflog>> {
2234 if opts.initial_remote_fetch {
2235 return Ok(None);
2236 }
2237 let sig = fetch_operation_identity(&opts.environment, local.git_dir())?;
2238 let identity = crate::refs::store::format_reflog_identity(&sig.reflog_name_email(), sig.time);
2239 Ok(Some(PreparedFetchReflog {
2240 identity,
2241 action: opts.reflog_action,
2242 }))
2243}
2244
2245pub fn fetch_operation_identity(
2247 env: &crate::environment::Environment,
2248 git_dir: &Path,
2249) -> Result<crate::ident::Signature> {
2250 use crate::ident_config::ident_default_name;
2251 let cfg = crate::config::ConfigSet::load(env, Some(git_dir), true)?;
2252 let name = ident_default_name(&cfg, env);
2253 let email = cfg
2254 .get("user.email")
2255 .filter(|e| !e.trim().is_empty())
2256 .unwrap_or_else(|| "unknown".to_owned());
2257 let now = time::OffsetDateTime::now_utc();
2258 crate::ident::Signature::new(name, email, now).map_err(crate::error::Error::InvalidSignature)
2259}
2260
2261pub(crate) fn ensure_remote_tracking_head_symref(
2264 local: &Repository,
2265 remote: &str,
2266 default_branch: Option<&str>,
2267 dry_run: bool,
2268) -> Result<()> {
2269 if dry_run {
2270 return Ok(());
2271 }
2272 let Some(branch) = default_branch else {
2273 return Ok(());
2274 };
2275 let head_ref = format!("refs/remotes/{remote}/HEAD");
2276 if crate::refs::resolve_ref_in(local, &head_ref).is_ok() {
2277 return Ok(());
2278 }
2279 let target = format!("refs/remotes/{remote}/{branch}");
2280 if crate::refs::resolve_ref_in(local, &target).is_err() {
2281 return Ok(());
2282 }
2283 crate::refs::write_symbolic_ref_on_store(local.refs(), local.git_dir(), &head_ref, &target)
2284}
2285
2286pub(crate) fn finish_initial_remote_fetch_layout(
2290 local: &Repository,
2291 opts: &FetchOptions,
2292 default_branch: Option<&str>,
2293) -> Result<()> {
2294 if opts.dry_run || !opts.initial_remote_fetch {
2295 return Ok(());
2296 }
2297 let git_dir = local.git_dir();
2298 if local.refs_on_disk() {
2299 crate::refs::pack_imported_refs_for_clone(git_dir, opts.remote_name.as_deref())?;
2300 }
2301 let Some(remote) = opts.remote_name.as_deref() else {
2302 return Ok(());
2303 };
2304 let Some(branch) = default_branch else {
2305 return Ok(());
2306 };
2307 let target = format!("refs/remotes/{remote}/{branch}");
2308 let head_ref = format!("refs/remotes/{remote}/HEAD");
2309 let Ok(tip) = crate::refs::resolve_ref_in(local, &target) else {
2310 return Ok(());
2311 };
2312 crate::refs::write_symbolic_ref_on_store(local.refs(), git_dir, &head_ref, &target)?;
2313 let Some(ref log) = opts.clone_reflog else {
2314 return Ok(());
2315 };
2316 let null = ObjectId::null(local.odb().hash_algo());
2317 log.identity
2318 .validate()
2319 .map_err(crate::error::Error::InvalidSignature)?;
2320 let identity = crate::refs::store::format_reflog_identity(
2321 &log.identity.reflog_name_email(),
2322 log.identity.time,
2323 );
2324 let config = local.config()?;
2325 crate::refs::append_reflog_on_store(
2326 local.refs(),
2327 &head_ref,
2328 &null,
2329 &tip,
2330 &identity,
2331 &log.message,
2332 true,
2333 &config,
2334 )
2335}
2336
2337#[allow(clippy::too_many_arguments)]
2354pub(crate) fn add_wire_tags(
2355 mode: crate::transfer::TagMode,
2356 remote_refs: &[(String, ObjectId)],
2357 advertised_peel: &std::collections::HashMap<String, ObjectId>,
2358 negatives: &[RefspecItem],
2359 already_local: &HashSet<String>,
2360 force_all_tags: bool,
2361 matched: &mut Vec<crate::transfer::MatchedRef>,
2362 matched_oids: &mut HashSet<ObjectId>,
2363 seen_remote_ref: &mut HashSet<String>,
2364) -> HashSet<ObjectId> {
2365 let mut following_only: HashSet<ObjectId> = HashSet::new();
2366 if mode == crate::transfer::TagMode::None {
2367 return following_only;
2368 }
2369 for (name, oid) in remote_refs {
2370 if !name.starts_with("refs/tags/") {
2371 continue;
2372 }
2373 if seen_remote_ref.contains(name) || ref_excluded(name, negatives) {
2374 continue;
2375 }
2376 if mode == crate::transfer::TagMode::Following && already_local.contains(name) {
2377 continue;
2378 }
2379 seen_remote_ref.insert(name.clone());
2380 if matched_oids.insert(*oid) && mode == crate::transfer::TagMode::Following {
2384 following_only.insert(*oid);
2385 }
2386 matched.push(crate::transfer::MatchedRef {
2387 remote_ref: name.clone(),
2388 local_ref: Some(name.clone()),
2389 oid: *oid,
2390 force: force_all_tags && mode == crate::transfer::TagMode::All,
2391 is_tag: true,
2392 auto_followed: mode == crate::transfer::TagMode::Following,
2393 dest_is_tag: true,
2394 advertised_peel: advertised_peel.get(name).copied(),
2395 fetch_head: crate::fetch_request::FetchHeadPolicy::ForMerge,
2396 });
2397 }
2398 following_only
2399}
2400
2401pub(crate) fn following_tag_wants_with_local_peel(
2404 matched: &[crate::transfer::MatchedRef],
2405 following_only: &HashSet<ObjectId>,
2406 local_odb: &crate::odb::Odb,
2407) -> Result<HashSet<ObjectId>> {
2408 let mut wants = HashSet::new();
2409 for m in matched {
2410 if !m.is_tag || !following_only.contains(&m.oid) {
2411 continue;
2412 }
2413 let Some(peel) = m.advertised_peel else {
2414 continue;
2415 };
2416 if local_odb.contains(&peel)? && !local_odb.contains(&m.oid)? {
2417 wants.insert(m.oid);
2418 }
2419 }
2420 Ok(wants)
2421}
2422
2423pub(crate) fn shallow_fetched_commit_oids(
2427 odb: &crate::odb::Odb,
2428 roots: &[ObjectId],
2429 depth: u32,
2430 shallow_boundaries: &HashSet<ObjectId>,
2431) -> Result<HashSet<ObjectId>> {
2432 use crate::objects::{parse_commit, ObjectKind};
2433 use std::collections::{HashMap, VecDeque};
2434
2435 if depth == 0 {
2436 return Ok(HashSet::new());
2437 }
2438 let max_depth = usize::try_from(depth).unwrap_or(usize::MAX);
2439 let mut best_depth: HashMap<ObjectId, usize> = HashMap::new();
2440 let mut q: VecDeque<(ObjectId, usize)> = VecDeque::new();
2441 for &root in roots {
2442 best_depth.insert(root, 1);
2443 q.push_back((root, 1));
2444 }
2445 let mut out = HashSet::new();
2446 while let Some((oid, d)) = q.pop_front() {
2447 if d > max_depth {
2448 continue;
2449 }
2450 if best_depth.get(&oid).copied() != Some(d) {
2451 continue;
2452 }
2453 out.insert(oid);
2454 if d == max_depth {
2455 continue;
2456 }
2457 if shallow_boundaries.contains(&oid) {
2458 continue;
2459 }
2460 if !odb.contains(&oid)? {
2461 continue;
2462 }
2463 let Ok(obj) = odb.read(&oid) else {
2464 continue;
2465 };
2466 if obj.kind != ObjectKind::Commit {
2467 continue;
2468 }
2469 let Ok(c) = parse_commit(&obj.data) else {
2470 continue;
2471 };
2472 for p in c.parents {
2473 let nd = d.saturating_add(1);
2474 if nd > max_depth {
2475 continue;
2476 }
2477 let prev = best_depth.get(&p).copied().unwrap_or(usize::MAX);
2478 if nd < prev {
2479 best_depth.insert(p, nd);
2480 q.push_back((p, nd));
2481 }
2482 }
2483 }
2484 Ok(out)
2485}
2486
2487pub(crate) fn following_peel_commit_set_for_shallow(
2490 odb: &crate::odb::Odb,
2491 commit_tips: &HashSet<ObjectId>,
2492 depth: Option<u32>,
2493 shallow_boundaries: &HashSet<ObjectId>,
2494) -> Result<HashSet<ObjectId>> {
2495 let roots: Vec<ObjectId> = commit_tips.iter().copied().collect();
2496 match depth.filter(|d| *d > 0) {
2497 Some(d) => shallow_fetched_commit_oids(odb, &roots, d, shallow_boundaries),
2498 None => Ok(commit_tips.clone()),
2499 }
2500}
2501
2502pub(crate) fn following_tag_wants_peel_among_fetched_commits(
2506 matched: &[crate::transfer::MatchedRef],
2507 following_only: &HashSet<ObjectId>,
2508 fetch_commit_oids: &HashSet<ObjectId>,
2509) -> HashSet<ObjectId> {
2510 let mut wants = HashSet::new();
2511 for m in matched {
2512 if !m.is_tag || !following_only.contains(&m.oid) {
2513 continue;
2514 }
2515 let Some(peel) = m.advertised_peel else {
2516 continue;
2517 };
2518 if peel != m.oid && fetch_commit_oids.contains(&peel) {
2519 wants.insert(m.oid);
2520 }
2521 }
2522 wants
2523}
2524
2525pub(crate) fn retain_following_tags(
2546 local_odb: &crate::odb::Odb,
2547 matched: &mut Vec<crate::transfer::MatchedRef>,
2548 pack_oids: &HashSet<ObjectId>,
2549 shallow_boundaries: &HashSet<ObjectId>,
2550) -> Result<()> {
2551 let followed = |m: &crate::transfer::MatchedRef| m.is_tag && m.auto_followed;
2552 if !matched.iter().any(followed) {
2553 return Ok(());
2554 }
2555
2556 let roots: Vec<ObjectId> = matched
2557 .iter()
2558 .filter(|m| !m.is_tag)
2559 .map(|m| m.oid)
2560 .collect();
2561 let peeled_by_tag: std::collections::HashMap<ObjectId, ObjectId> = matched
2562 .iter()
2563 .filter(|m| followed(m))
2564 .map(|m| {
2565 let peeled = m.advertised_peel.unwrap_or_else(|| {
2566 local_odb
2567 .peel_stored_tag_target(m.oid)
2568 .ok()
2569 .flatten()
2570 .unwrap_or(m.oid)
2571 });
2572 (m.oid, peeled)
2573 })
2574 .collect();
2575 let mut local_commits: HashSet<ObjectId> = HashSet::new();
2576 let mut local_trees_and_blobs: HashSet<ObjectId> = HashSet::new();
2577 for &oid in peeled_by_tag.values() {
2578 if pack_oids.contains(&oid) || !local_odb.contains(&oid)? {
2581 continue;
2582 }
2583 match local_odb.read_info(&oid)?.kind {
2584 crate::objects::ObjectKind::Tree | crate::objects::ObjectKind::Blob => {
2585 local_trees_and_blobs.insert(oid);
2586 }
2587 crate::objects::ObjectKind::Commit => {
2588 local_commits.insert(oid);
2589 }
2590 crate::objects::ObjectKind::Tag => {}
2592 }
2593 }
2594 let local_reach = reachable_commits(local_odb, &roots, &local_commits, shallow_boundaries)?;
2595 let mut keep = Vec::with_capacity(matched.len());
2596 for m in matched.iter() {
2597 if !followed(m) {
2598 keep.push(true);
2599 continue;
2600 }
2601 let peeled = peeled_by_tag.get(&m.oid).copied().unwrap_or(m.oid);
2602 let reachable = pack_oids.contains(&peeled)
2603 || local_trees_and_blobs.contains(&peeled)
2604 || local_reach.contains(&peeled);
2605 keep.push(
2608 reachable
2609 && (m.oid == peeled || pack_oids.contains(&m.oid) || local_odb.contains(&m.oid)?),
2610 );
2611 }
2612 let mut keep = keep.into_iter();
2613 matched.retain(|_| keep.next().unwrap_or(false));
2614 Ok(())
2615}
2616
2617pub(crate) fn tags_already_local(
2635 local_refs: &dyn crate::refs::store::RefStore,
2636 remote_refs: &[(String, ObjectId)],
2637) -> HashSet<String> {
2638 remote_refs
2639 .iter()
2640 .filter(|(name, _)| name.starts_with("refs/tags/") && local_refs.resolve(name).is_ok())
2641 .map(|(name, _)| name.clone())
2642 .collect()
2643}
2644
2645pub(crate) fn reachable_commits(
2656 odb: &crate::odb::Odb,
2657 roots: &[ObjectId],
2658 targets: &HashSet<ObjectId>,
2659 shallow_boundaries: &HashSet<ObjectId>,
2660) -> Result<HashSet<ObjectId>> {
2661 use crate::objects::{parse_commit, parse_tag, ObjectKind};
2662
2663 let mut seen: HashSet<ObjectId> = HashSet::new();
2664 let mut remaining: HashSet<ObjectId> = targets.clone();
2665 if remaining.is_empty() {
2666 return Ok(seen);
2667 }
2668
2669 let graph = crate::commit_graph_file::CommitGraphChain::load(odb.objects_dir());
2674
2675 let mut stack: Vec<ObjectId> = roots.to_vec();
2676 while let Some(oid) = stack.pop() {
2677 if remaining.is_empty() {
2678 break;
2679 }
2680 if seen.contains(&oid) {
2681 continue;
2682 }
2683 if shallow_boundaries.contains(&oid) {
2684 if odb.contains(&oid)? {
2685 seen.insert(oid);
2686 remaining.remove(&oid);
2687 }
2688 continue;
2689 }
2690 if let Some(graph) = graph.as_ref() {
2691 if let Some((parents, _time)) = graph.graph_commit(&oid) {
2692 seen.insert(oid);
2693 remaining.remove(&oid);
2694 stack.extend(parents);
2695 continue;
2696 }
2697 }
2698 let Ok(obj) = odb.read(&oid) else {
2699 continue;
2700 };
2701 seen.insert(oid);
2702 remaining.remove(&oid);
2703 match obj.kind {
2704 ObjectKind::Commit => {
2705 if let Ok(c) = parse_commit(&obj.data) {
2706 for p in c.parents {
2707 stack.push(p);
2708 }
2709 }
2710 }
2711 ObjectKind::Tag => {
2712 if let Ok(t) = parse_tag(&obj.data) {
2713 stack.push(t.object);
2714 }
2715 }
2716 ObjectKind::Tree | ObjectKind::Blob => {}
2718 }
2719 }
2720 Ok(seen)
2721}
2722
2723#[cfg(test)]
2724mod fetch_advertised_ref_tests {
2725 use super::*;
2726 use crate::objects::ObjectId;
2727 use std::io::Cursor;
2728
2729 fn linear_history(len: usize) -> (tempfile::TempDir, Vec<ObjectId>) {
2730 use crate::objects::ObjectKind;
2731 let dir = tempfile::tempdir().unwrap();
2732 let repo = crate::repo::init_repository(
2733 dir.path(),
2734 true,
2735 "main",
2736 None,
2737 crate::RefStorageFormat::Files,
2738 )
2739 .unwrap();
2740 let odb = repo.odb();
2741 let tree = odb.write(ObjectKind::Tree, b"").unwrap();
2742 let mut commits: Vec<ObjectId> = Vec::new();
2743 for i in 0..len {
2744 let mut body = format!("tree {tree}\n");
2745 if let Some(parent) = commits.last() {
2746 body.push_str(&format!("parent {parent}\n"));
2747 }
2748 let time = 1_000_000 + i;
2749 body.push_str(&format!(
2750 "author T <t@e.com> {time} +0000\ncommitter T <t@e.com> {time} +0000\n\nc{i}\n"
2751 ));
2752 commits.push(odb.write(ObjectKind::Commit, body.as_bytes()).unwrap());
2753 }
2754 let tip = commits.last().copied().unwrap();
2755 crate::refs::write_ref(repo.git_dir(), "refs/heads/main", &tip).unwrap();
2756 (dir, commits)
2757 }
2758
2759 #[test]
2760 fn v2_local_haves_are_produced_only_as_far_as_asked() {
2761 let (dir, commits) = linear_history(400);
2762 let unrelated_want = ObjectId::from_bytes(&[9u8; 20]).unwrap();
2763 let local = Repository::open(dir.path(), &RepositoryOptions::empty()).unwrap();
2764
2765 let mut drained = v2_local_haves(&local, &[unrelated_want]).unwrap();
2766 let all = drained.all().unwrap().to_vec();
2767 assert!(drained.exhausted);
2768 assert_eq!(all[0], *commits.last().unwrap());
2769 assert!(
2770 all.len() > 4,
2771 "skipping still offers several haves: {}",
2772 all.len()
2773 );
2774
2775 let mut lazy = v2_local_haves(&local, &[unrelated_want]).unwrap();
2776 let first = lazy.first(4).unwrap().to_vec();
2777 assert_eq!(first.as_slice(), &all[..4]);
2778 assert_eq!(lazy.taken.len(), 4, "only the requested haves are walked");
2779 assert!(!lazy.exhausted);
2780 assert_eq!(lazy.all().unwrap(), all.as_slice());
2781 }
2782
2783 #[test]
2784 fn v2_local_haves_first_stops_at_a_short_history() {
2785 let (dir, _) = linear_history(3);
2786 let want = ObjectId::from_bytes(&[9u8; 20]).unwrap();
2787 let local = Repository::open(dir.path(), &RepositoryOptions::empty()).unwrap();
2788 let all = v2_local_haves(&local, &[want])
2789 .unwrap()
2790 .all()
2791 .unwrap()
2792 .to_vec();
2793 let mut haves = v2_local_haves(&local, &[want]).unwrap();
2794 assert_eq!(haves.first(INITIAL_FLUSH).unwrap(), all.as_slice());
2795 assert!(haves.exhausted);
2796 }
2797
2798 #[test]
2799 fn v2_ls_refs_prefixes_follow_refspecs_and_tag_mode() {
2800 use crate::transfer::TagMode;
2801
2802 let heads = vec!["+refs/heads/*:refs/remotes/origin/*".to_owned()];
2803 assert_eq!(v2_ls_refs_prefixes(&heads, TagMode::None), ["refs/heads/"]);
2804 assert_eq!(
2805 v2_ls_refs_prefixes(&heads, TagMode::Following),
2806 ["refs/heads/", "refs/tags/"]
2807 );
2808 let short = vec!["main".to_owned()];
2809 assert_eq!(
2810 v2_ls_refs_prefixes(&short, TagMode::None),
2811 [
2812 "main",
2813 "refs/main",
2814 "refs/tags/main",
2815 "refs/heads/main",
2816 "refs/remotes/main",
2817 "refs/remotes/main/HEAD",
2818 ]
2819 );
2820 let oid = vec![format!("{}:refs/heads/x", "a".repeat(40))];
2821 assert_eq!(
2822 v2_ls_refs_prefixes(&oid, TagMode::None),
2823 fetch_ref_list_prefixes(&oid)
2824 );
2825 }
2826
2827 #[test]
2828 fn fetch_ref_list_prefixes_includes_refs_dwim_for_pull() {
2829 let specs = vec!["pull/123/head:pr-123".to_owned()];
2830 let prefixes = fetch_ref_list_prefixes(&specs);
2831 assert!(prefixes.iter().any(|p| p == "refs/pull/123/head"));
2832 assert!(prefixes.iter().any(|p| p == "refs/heads/"));
2833 }
2834
2835 #[test]
2836 fn build_v2_ls_refs_request_includes_unborn_when_advertised() {
2837 use crate::odb::Odb;
2838 use crate::transfer::TagMode;
2839
2840 let caps = vec!["ls-refs=unborn".to_owned()];
2841 let odb = Odb::new(std::path::Path::new("/dev/null"));
2842 let req = build_v2_ls_refs_request(&caps, &odb, TagMode::None, &[], None).unwrap();
2843 let text = String::from_utf8_lossy(&req);
2844 assert!(
2845 text.contains("unborn"),
2846 "request must ask for unborn HEAD:\n{text}"
2847 );
2848 assert!(!text.contains("promisor-remote"), "{text}");
2849 }
2850
2851 #[test]
2852 fn v2_fetch_response_cut_before_the_pack_is_truncated() {
2853 let mut wire: Vec<u8> = Vec::new();
2855 wire.extend_from_slice(b"0014acknowledgments\n");
2856 wire.extend_from_slice(b"0008NAK\n");
2857 wire.extend_from_slice(b"0001");
2858 let err = super::read_v2_fetch_pack_response(
2859 &mut wire.as_slice(),
2860 &mut Vec::new(),
2861 None,
2862 &mut super::ShallowUpdate::default(),
2863 &mut crate::fetch::NoProgress,
2864 &crate::progress::NeverCancel,
2865 false,
2866 )
2867 .unwrap_err();
2868 assert!(
2869 matches!(
2870 err,
2871 Error::RemoteUnreadable {
2872 reason: crate::error::RemoteUnreadableReason::ResponseTruncated,
2873 ..
2874 }
2875 ),
2876 "{err:?}"
2877 );
2878 }
2879
2880 #[test]
2881 fn build_v2_ls_refs_request_carries_promisor_reply_before_args() {
2882 use crate::odb::Odb;
2883 use crate::transfer::TagMode;
2884
2885 let caps = vec!["agent=git/2.50".to_owned(), "object-format=sha1".to_owned()];
2886 let odb = Odb::new(std::path::Path::new("/dev/null"));
2887 let req = build_v2_ls_refs_request(&caps, &odb, TagMode::None, &[], Some("lop")).unwrap();
2888 let text = String::from_utf8_lossy(&req);
2889 let reply = text.find("promisor-remote=lop\n").expect("reply line");
2890 let delim = text.find("0001").expect("delim");
2891 assert!(reply < delim, "{text}");
2892 }
2893
2894 #[test]
2895 fn v2_acknowledgments_collect_acked_commits_and_ready() {
2896 let a = ObjectId::from_hex("aabbccddeeff00112233445566778899aabbccdd").unwrap();
2897 let b = ObjectId::from_hex("00112233445566778899aabbccddeeff00112233").unwrap();
2898 let mut body = Vec::new();
2899 crate::pkt_line::write_line(&mut body, "acknowledgments").unwrap();
2900 crate::pkt_line::write_line(&mut body, &format!("ACK {a}")).unwrap();
2901 crate::pkt_line::write_line(&mut body, &format!("ACK {b}")).unwrap();
2902 crate::pkt_line::write_line(&mut body, "ready").unwrap();
2903 crate::pkt_line::write_delim(&mut body).unwrap();
2904
2905 let round = read_v2_acknowledgments(&mut Cursor::new(body), false)
2906 .unwrap()
2907 .unwrap();
2908 assert!(round.ready);
2909 assert_eq!(round.common, vec![a, b]);
2910 }
2911
2912 #[test]
2913 fn v2_acknowledgments_nak_has_no_common() {
2914 let mut body = Vec::new();
2915 crate::pkt_line::write_line(&mut body, "acknowledgments").unwrap();
2916 crate::pkt_line::write_line(&mut body, "NAK").unwrap();
2917 crate::pkt_line::write_flush(&mut body).unwrap();
2918
2919 let round = read_v2_acknowledgments(&mut Cursor::new(body), false)
2920 .unwrap()
2921 .unwrap();
2922 assert!(!round.ready);
2923 assert!(round.common.is_empty());
2924 }
2925
2926 #[test]
2927 fn v2_acknowledgments_reject_malformed_ack() {
2928 let mut body = Vec::new();
2929 crate::pkt_line::write_line(&mut body, "acknowledgments").unwrap();
2930 crate::pkt_line::write_line(&mut body, "ACK not-an-oid").unwrap();
2931 crate::pkt_line::write_flush(&mut body).unwrap();
2932
2933 assert!(read_v2_acknowledgments(&mut Cursor::new(body), false).is_err());
2934 }
2935
2936 #[test]
2937 fn v2_flush_schedule_doubles_then_grows_by_a_tenth() {
2938 let mut sizes = vec![INITIAL_FLUSH];
2939 while sizes.len() < 6 {
2940 let last = *sizes.last().unwrap();
2941 sizes.push(next_v2_flush_count(last));
2942 }
2943 assert_eq!(sizes, vec![16, 32, 64, 128, 256, 512]);
2944 assert_eq!(
2945 next_v2_flush_count(LARGE_FLUSH),
2946 LARGE_FLUSH + LARGE_FLUSH / 10
2947 );
2948 }
2949
2950 #[test]
2951 fn parse_v2_ls_refs_unborn_head_symref_line() {
2952 let mut body = Vec::new();
2953 crate::pkt_line::write_line(&mut body, "unborn HEAD symref-target:refs/heads/trunk")
2954 .unwrap();
2955 crate::pkt_line::write_flush(&mut body).unwrap();
2956
2957 let (_, head, _, _) = parse_v2_ls_refs_response(&mut Cursor::new(body)).unwrap();
2958 assert_eq!(head.as_deref(), Some("refs/heads/trunk"));
2959 }
2960
2961 #[test]
2962 fn parse_v2_ls_refs_drops_traversal_ref() {
2963 let oid = ObjectId::from_hex("aabbccddeeff00112233445566778899aabbccdd").unwrap();
2964 let line = format!("{oid} refs/heads/../../../config");
2965 let mut body = Vec::new();
2966 crate::pkt_line::write_line(&mut body, &line).unwrap();
2967 crate::pkt_line::write_flush(&mut body).unwrap();
2968
2969 let (refs, _, _, _) = parse_v2_ls_refs_response(&mut Cursor::new(body)).unwrap();
2970 assert!(refs.is_empty());
2971 }
2972
2973 #[test]
2974 fn refspec_maps_traversal_to_non_storable_local_name() {
2975 use crate::refs::is_valid_storable_ref_name;
2976 use crate::refspec::parse_fetch_refspec;
2977 use crate::transfer::match_positive;
2978
2979 let spec = parse_fetch_refspec("+refs/heads/*:refs/remotes/origin/*").unwrap();
2980 let positive = vec![spec];
2981 let local = match_positive("refs/heads/../../../config", &positive)
2982 .flatten()
2983 .expect("wildcard matches malicious remote name");
2984 assert!(!is_valid_storable_ref_name(&local));
2985 }
2986
2987 #[test]
2988 fn add_wire_tags_following_skips_following_only_when_oid_already_matched() {
2989 use crate::transfer::TagMode;
2990 use std::collections::{HashMap, HashSet};
2991
2992 let shared = ObjectId::from_hex("4eda3c62a0f0a0a0a0a0a0a0a0a0a0a0a0a0a0a0").unwrap();
2993 let unreachable_tag =
2994 ObjectId::from_hex("1111111111111111111111111111111111111111").unwrap();
2995 let remote_refs = vec![
2996 ("refs/heads/rel".to_owned(), shared),
2997 ("refs/tags/light".to_owned(), shared),
2998 ("refs/tags/orphan".to_owned(), unreachable_tag),
2999 ];
3000 let advertised_peel = HashMap::new();
3001 let negatives: Vec<crate::refspec::RefspecItem> = Vec::new();
3002 let mut matched = Vec::new();
3003 let mut matched_oids = HashSet::from([shared]);
3004 let mut seen = HashSet::from(["refs/heads/rel".to_owned()]);
3005 matched.push(crate::transfer::MatchedRef {
3006 remote_ref: "refs/heads/rel".to_owned(),
3007 local_ref: Some("refs/remotes/origin/rel".to_owned()),
3008 oid: shared,
3009 force: false,
3010 is_tag: false,
3011 auto_followed: false,
3012 dest_is_tag: false,
3013 advertised_peel: None,
3014 fetch_head: crate::fetch_request::FetchHeadPolicy::ForMerge,
3015 });
3016
3017 let following_only = add_wire_tags(
3018 TagMode::Following,
3019 &remote_refs,
3020 &advertised_peel,
3021 &negatives,
3022 &HashSet::new(),
3023 false,
3024 &mut matched,
3025 &mut matched_oids,
3026 &mut seen,
3027 );
3028
3029 assert!(
3030 !following_only.contains(&shared),
3031 "shared branch/tag tip must stay wanted"
3032 );
3033 assert!(
3034 following_only.contains(&unreachable_tag),
3035 "tag-only oid stays following-only (no upfront want)"
3036 );
3037 assert_eq!(matched.iter().filter(|m| m.is_tag).count(), 2);
3038 }
3039
3040 #[test]
3041 fn following_tag_wants_includes_annotated_tag_when_peel_is_local() {
3042 use std::collections::HashSet;
3043
3044 let tag_obj = ObjectId::from_hex("bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb").unwrap();
3045 let tmp = tempfile::tempdir().expect("tempdir");
3046 let git_dir = tmp.path().join(".git");
3047 std::fs::create_dir_all(git_dir.join("objects")).expect("objects");
3048 let odb = crate::odb::Odb::new(git_dir.join("objects").as_path());
3049 let commit_bytes = b"tree 0000000000000000000000000000000000000000\nauthor T <t@example.com> 0 +0000\ncommitter T <t@example.com> 0 +0000\n\nx\n";
3051 let commit = odb
3052 .write_loose_materialize(crate::objects::ObjectKind::Commit, commit_bytes)
3053 .expect("write commit");
3054
3055 let following_only = HashSet::from([tag_obj]);
3056 let matched = vec![crate::transfer::MatchedRef {
3057 remote_ref: "refs/tags/v".to_owned(),
3058 local_ref: Some("refs/tags/v".to_owned()),
3059 oid: tag_obj,
3060 force: false,
3061 is_tag: true,
3062 auto_followed: true,
3063 dest_is_tag: true,
3064 advertised_peel: Some(commit),
3065 fetch_head: crate::fetch_request::FetchHeadPolicy::ForMerge,
3066 }];
3067 let wants = following_tag_wants_with_local_peel(&matched, &following_only, &odb).unwrap();
3068 assert!(wants.contains(&tag_obj));
3069 }
3070
3071 #[test]
3072 fn following_tag_wants_peel_among_fetched_commits_before_peel_is_local() {
3073 use std::collections::HashSet;
3074
3075 let tag_obj = ObjectId::from_hex("bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb").unwrap();
3076 let tip = ObjectId::from_hex("cccccccccccccccccccccccccccccccccccccccc").unwrap();
3077 let following_only = HashSet::from([tag_obj]);
3078 let fetch_commits = HashSet::from([tip]);
3079 let matched = vec![crate::transfer::MatchedRef {
3080 remote_ref: "refs/tags/v".to_owned(),
3081 local_ref: Some("refs/tags/v".to_owned()),
3082 oid: tag_obj,
3083 force: false,
3084 is_tag: true,
3085 auto_followed: true,
3086 dest_is_tag: true,
3087 advertised_peel: Some(tip),
3088 fetch_head: crate::fetch_request::FetchHeadPolicy::ForMerge,
3089 }];
3090 let wants = following_tag_wants_peel_among_fetched_commits(
3091 &matched,
3092 &following_only,
3093 &fetch_commits,
3094 );
3095 assert!(wants.contains(&tag_obj));
3096 }
3097
3098 #[test]
3099 fn shallow_peel_set_includes_parent_at_depth_two_not_tip_only() {
3100 use std::collections::HashSet;
3101
3102 let tmp = tempfile::tempdir().expect("tempdir");
3103 let objects = tmp.path().join("objects");
3104 std::fs::create_dir_all(&objects).expect("objects");
3105 let odb = crate::odb::Odb::new(objects.as_path());
3106 let chain = write_commit_chain(&odb, 3);
3107 let tip = *chain.last().expect("tip");
3108 let parent = chain[1];
3109 let tips = HashSet::from([tip]);
3110 let tips_only =
3111 following_peel_commit_set_for_shallow(&odb, &tips, Some(1), &HashSet::new()).unwrap();
3112 assert!(!tips_only.contains(&parent));
3113 let depth_two =
3114 following_peel_commit_set_for_shallow(&odb, &tips, Some(2), &HashSet::new()).unwrap();
3115 assert!(depth_two.contains(&parent));
3116 }
3117
3118 fn write_commit_chain(odb: &crate::odb::Odb, n: usize) -> Vec<ObjectId> {
3120 let mut chain: Vec<ObjectId> = Vec::new();
3121 for i in 0..n {
3122 let parent = chain
3123 .last()
3124 .map(|p| format!("parent {}\n", p.to_hex()))
3125 .unwrap_or_default();
3126 let body = format!(
3127 "tree 4b825dc642cb6eb9a060e54bf8d69288fbee4904\n{parent}\
3128 author T <t@example.com> {i} +0000\ncommitter T <t@example.com> {i} +0000\n\nc{i}\n"
3129 );
3130 let oid = odb
3131 .write_loose_materialize(crate::objects::ObjectKind::Commit, body.as_bytes())
3132 .expect("write commit");
3133 chain.push(oid);
3134 }
3135 chain
3136 }
3137
3138 fn matched_ref(
3139 name: &str,
3140 oid: ObjectId,
3141 peel: Option<ObjectId>,
3142 ) -> crate::transfer::MatchedRef {
3143 let is_tag = name.starts_with("refs/tags/");
3144 crate::transfer::MatchedRef {
3145 remote_ref: name.to_owned(),
3146 local_ref: Some(name.to_owned()),
3147 oid,
3148 force: false,
3149 is_tag,
3150 auto_followed: is_tag,
3151 dest_is_tag: is_tag,
3152 advertised_peel: peel,
3153 fetch_head: crate::fetch_request::FetchHeadPolicy::ForMerge,
3154 }
3155 }
3156
3157 fn kept_tags(matched: &[crate::transfer::MatchedRef]) -> Vec<&str> {
3158 let mut names: Vec<&str> = matched
3159 .iter()
3160 .filter(|m| m.is_tag)
3161 .map(|m| m.remote_ref.as_str())
3162 .collect();
3163 names.sort_unstable();
3164 names
3165 }
3166
3167 #[test]
3168 fn retain_following_tags_keeps_delivered_targets_without_reading_objects() {
3169 use std::collections::HashSet;
3170
3171 let tmp = tempfile::tempdir().expect("tempdir");
3174 let objects = tmp.path().join("objects");
3175 std::fs::create_dir_all(&objects).expect("objects");
3176 let odb = crate::odb::Odb::new(&objects);
3177 let oid = |c: char| ObjectId::from_hex(&c.to_string().repeat(40)).expect("oid");
3178 let (tip, tree, blob, tag_obj, missing_tag_obj, absent) =
3179 (oid('1'), oid('2'), oid('3'), oid('4'), oid('5'), oid('6'));
3180 let pack_oids = HashSet::from([tip, tree, blob, tag_obj]);
3181 let mut matched = vec![
3182 matched_ref("refs/heads/main", tip, None),
3183 matched_ref("refs/tags/light", tip, None),
3184 matched_ref("refs/tags/blob", blob, None),
3185 matched_ref("refs/tags/annotated-tree", tag_obj, Some(tree)),
3186 matched_ref("refs/tags/no-tag-object", missing_tag_obj, Some(tip)),
3187 matched_ref("refs/tags/undelivered", absent, None),
3188 ];
3189
3190 retain_following_tags(&odb, &mut matched, &pack_oids, &HashSet::new())
3191 .expect("retain following tags");
3192
3193 assert_eq!(
3194 kept_tags(&matched),
3195 [
3196 "refs/tags/annotated-tree",
3197 "refs/tags/blob",
3198 "refs/tags/light"
3199 ]
3200 );
3201 assert!(matched.iter().any(|m| m.remote_ref == "refs/heads/main"));
3202 }
3203
3204 #[test]
3205 fn retain_following_tags_keeps_refspec_selected_tags() {
3206 use std::collections::HashSet;
3207
3208 let tmp = tempfile::tempdir().expect("tempdir");
3209 let objects = tmp.path().join("objects");
3210 std::fs::create_dir_all(&objects).expect("objects");
3211 let odb = crate::odb::Odb::new(&objects);
3212 let peel = write_commit_chain(&odb, 1)[0];
3215 let tag_obj = ObjectId::from_hex(&"9".repeat(40)).expect("oid");
3216 let followed = matched_ref("refs/tags/followed", tag_obj, Some(peel));
3217 let mut selected = matched_ref("refs/tags/selected", tag_obj, Some(peel));
3218 selected.auto_followed = false;
3219 let mut matched = vec![followed, selected];
3220
3221 retain_following_tags(&odb, &mut matched, &HashSet::new(), &HashSet::new())
3222 .expect("retain following tags");
3223
3224 assert_eq!(kept_tags(&matched), ["refs/tags/selected"]);
3225 }
3226
3227 #[test]
3228 fn retain_following_tags_walks_commits_only_for_targets_already_local() {
3229 use std::collections::HashSet;
3230
3231 let tmp = tempfile::tempdir().expect("tempdir");
3232 let objects = tmp.path().join("objects");
3233 std::fs::create_dir_all(&objects).expect("objects");
3234 let odb = crate::odb::Odb::new(&objects);
3235 let chain = write_commit_chain(&odb, 4);
3236 let tip = chain[3];
3237 let unrelated = odb
3239 .write_loose_materialize(
3240 crate::objects::ObjectKind::Commit,
3241 b"tree 4b825dc642cb6eb9a060e54bf8d69288fbee4904\n\
3242 author T <t@example.com> 9 +0000\ncommitter T <t@example.com> 9 +0000\n\nu\n",
3243 )
3244 .expect("write commit");
3245 let blob = odb
3246 .write_loose_materialize(crate::objects::ObjectKind::Blob, b"local blob\n")
3247 .expect("write blob");
3248 let mut matched = vec![
3249 matched_ref("refs/heads/main", tip, None),
3250 matched_ref("refs/tags/old", chain[0], None),
3251 matched_ref("refs/tags/unrelated", unrelated, None),
3252 matched_ref("refs/tags/local-blob", blob, None),
3253 ];
3254
3255 retain_following_tags(&odb, &mut matched, &HashSet::new(), &HashSet::new())
3256 .expect("retain following tags");
3257 assert_eq!(
3258 kept_tags(&matched),
3259 ["refs/tags/local-blob", "refs/tags/old"]
3260 );
3261
3262 let mut matched = vec![
3264 matched_ref("refs/heads/main", tip, None),
3265 matched_ref("refs/tags/at-boundary", chain[1], None),
3266 matched_ref("refs/tags/behind-boundary", chain[0], None),
3267 ];
3268 retain_following_tags(
3269 &odb,
3270 &mut matched,
3271 &HashSet::new(),
3272 &HashSet::from([chain[1]]),
3273 )
3274 .expect("retain following tags");
3275 assert_eq!(kept_tags(&matched), ["refs/tags/at-boundary"]);
3276 }
3277
3278 #[test]
3279 fn retain_following_tags_never_lazy_fetches_in_a_partial_clone() {
3280 use std::collections::HashSet;
3281 use std::sync::atomic::{AtomicUsize, Ordering};
3282 use std::sync::Arc;
3283
3284 struct CountingFetcher(Arc<AtomicUsize>);
3285 impl crate::promisor_fetch::PromisorFetcher for CountingFetcher {
3286 fn fetch_missing(
3287 &self,
3288 _repo: &crate::repo::Repository,
3289 _oids: &[ObjectId],
3290 ) -> Result<()> {
3291 self.0.fetch_add(1, Ordering::SeqCst);
3292 Ok(())
3293 }
3294 }
3295
3296 let tmp = tempfile::tempdir().expect("tempdir");
3297 let git_dir = tmp.path().join(".git");
3298 std::fs::create_dir_all(git_dir.join("objects/pack")).expect("objects");
3299 std::fs::create_dir_all(git_dir.join("refs/heads")).expect("refs");
3300 std::fs::write(git_dir.join("HEAD"), "ref: refs/heads/main\n").expect("HEAD");
3301 std::fs::write(
3302 git_dir.join("config"),
3303 "[core]\n\trepositoryformatversion = 1\n\tbare = true\n\
3304 [extensions]\n\tpartialclone = origin\n\
3305 [remote \"origin\"]\n\turl = /nonexistent\n\tpromisor = true\n",
3306 )
3307 .expect("config");
3308 let calls = Arc::new(AtomicUsize::new(0));
3309 let repo = crate::repo::Repository::open(
3310 &git_dir,
3311 &RepositoryOptions::empty()
3312 .with_promisor_fetcher(Some(Arc::new(CountingFetcher(Arc::clone(&calls))))),
3313 )
3314 .expect("open");
3315 let odb = repo.odb();
3316 let tip = write_commit_chain(odb, 1)[0];
3317 let oid = |c: char| ObjectId::from_hex(&c.to_string().repeat(40)).expect("oid");
3318 let (promised_blob, missing_tag_obj) = (oid('7'), oid('8'));
3320 let write_tag = |target: ObjectId, kind: &str| {
3321 let body = format!(
3322 "object {}\ntype {kind}\ntag t\ntagger T <t@example.com> 0 +0000\n\nt\n",
3323 target.to_hex()
3324 );
3325 odb.write_loose_materialize(crate::objects::ObjectKind::Tag, body.as_bytes())
3326 .expect("write tag")
3327 };
3328 let local_tag_on_promised_blob = write_tag(promised_blob, "blob");
3330 let local_tag_on_missing_tag = write_tag(missing_tag_obj, "tag");
3331 let mut matched = vec![
3332 matched_ref("refs/heads/main", tip, None),
3333 matched_ref("refs/tags/light", tip, None),
3334 matched_ref("refs/tags/promised-blob", promised_blob, None),
3335 matched_ref("refs/tags/unpeeled", missing_tag_obj, None),
3336 matched_ref(
3337 "refs/tags/local-on-promised",
3338 local_tag_on_promised_blob,
3339 None,
3340 ),
3341 matched_ref(
3342 "refs/tags/local-on-missing-tag",
3343 local_tag_on_missing_tag,
3344 None,
3345 ),
3346 ];
3347
3348 retain_following_tags(odb, &mut matched, &HashSet::new(), &HashSet::new())
3349 .expect("retain following tags");
3350
3351 assert_eq!(
3352 calls.load(Ordering::SeqCst),
3353 0,
3354 "tag following must not lazy-fetch missing objects"
3355 );
3356 assert_eq!(kept_tags(&matched), ["refs/tags/light"]);
3357 }
3358
3359 #[test]
3360 fn reachable_commits_stops_once_every_target_is_found() {
3361 use std::collections::HashSet;
3362
3363 let tmp = tempfile::tempdir().expect("tempdir");
3364 let objects = tmp.path().join("objects");
3365 std::fs::create_dir_all(&objects).expect("objects");
3366 let odb = crate::odb::Odb::new(&objects);
3367 let chain = write_commit_chain(&odb, 5);
3368 let tip = chain[4];
3369 let none = HashSet::new();
3370
3371 let near = HashSet::from([chain[3]]);
3372 let seen = reachable_commits(&odb, &[tip], &near, &none).unwrap();
3373 assert!(seen.contains(&chain[3]), "reachable target is reported");
3374 assert!(
3375 !seen.contains(&chain[0]),
3376 "walk must stop before the root once every target is found"
3377 );
3378
3379 let unreachable =
3380 HashSet::from([
3381 ObjectId::from_hex("1111111111111111111111111111111111111111").unwrap(),
3382 ]);
3383 let seen = reachable_commits(&odb, &[tip], &unreachable, &none).unwrap();
3384 assert_eq!(
3385 seen,
3386 chain.iter().copied().collect::<HashSet<_>>(),
3387 "an unreachable target forces the full closure"
3388 );
3389
3390 assert!(
3391 reachable_commits(&odb, &[tip], &HashSet::new(), &none)
3392 .unwrap()
3393 .is_empty(),
3394 "no targets means no walk"
3395 );
3396 }
3397
3398 #[test]
3399 fn tags_already_local_matches_name_and_oid_across_loose_and_packed() {
3400 let tmp = tempfile::tempdir().expect("tempdir");
3401 let git_dir = tmp.path().join(".git");
3402 std::fs::create_dir_all(git_dir.join("objects")).expect("objects");
3403 std::fs::create_dir_all(git_dir.join("refs/tags")).expect("refs/tags");
3404 std::fs::write(git_dir.join("HEAD"), "ref: refs/heads/main\n").expect("HEAD");
3405
3406 let a = ObjectId::from_hex("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa").unwrap();
3407 let b = ObjectId::from_hex("bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb").unwrap();
3408 let c = ObjectId::from_hex("cccccccccccccccccccccccccccccccccccccccc").unwrap();
3409 std::fs::write(git_dir.join("refs/tags/loose"), format!("{}\n", a.to_hex()))
3410 .expect("loose tag");
3411 std::fs::write(
3412 crate::refs::store::paths::packed_refs_path(&git_dir),
3413 format!(
3414 "# pack-refs with: peeled fully-peeled sorted \n{} refs/tags/moved\n{} refs/tags/packed\n",
3415 a.to_hex(),
3416 b.to_hex()
3417 ),
3418 )
3419 .expect("packed-refs");
3420
3421 let remote_refs = vec![
3422 ("refs/heads/main".to_owned(), a),
3423 ("refs/tags/loose".to_owned(), a),
3424 ("refs/tags/packed".to_owned(), b),
3425 ("refs/tags/moved".to_owned(), c),
3426 ("refs/tags/new".to_owned(), c),
3427 ];
3428 let store = crate::refs::store::open_ref_store(&git_dir).unwrap();
3429 let present = tags_already_local(store.as_ref(), &remote_refs);
3430 assert_eq!(
3431 present,
3432 HashSet::from([
3433 "refs/tags/loose".to_owned(),
3434 "refs/tags/packed".to_owned(),
3435 "refs/tags/moved".to_owned(),
3436 ]),
3437 "every local tag name is skipped under Following"
3438 );
3439 assert!(tags_already_local(store.as_ref(), &remote_refs[..1]).is_empty());
3440 }
3441
3442 #[test]
3443 fn add_wire_tags_following_skips_tags_already_local() {
3444 use crate::transfer::TagMode;
3445 use std::collections::HashMap;
3446
3447 let tip = ObjectId::from_hex("2222222222222222222222222222222222222222").unwrap();
3448 let old_tag = ObjectId::from_hex("3333333333333333333333333333333333333333").unwrap();
3449 let new_tag = ObjectId::from_hex("4444444444444444444444444444444444444444").unwrap();
3450 let remote_refs = vec![
3451 ("refs/heads/main".to_owned(), tip),
3452 ("refs/tags/old".to_owned(), old_tag),
3453 ("refs/tags/new".to_owned(), new_tag),
3454 ];
3455 let already_local = HashSet::from(["refs/tags/old".to_owned()]);
3456
3457 for (mode, expect) in [
3458 (TagMode::Following, vec!["refs/tags/new"]),
3459 (TagMode::All, vec!["refs/tags/old", "refs/tags/new"]),
3460 ] {
3461 let mut matched = Vec::new();
3462 let mut matched_oids = HashSet::new();
3463 let mut seen = HashSet::new();
3464 add_wire_tags(
3465 mode,
3466 &remote_refs,
3467 &HashMap::new(),
3468 &[],
3469 &already_local,
3470 false,
3471 &mut matched,
3472 &mut matched_oids,
3473 &mut seen,
3474 );
3475 let names: Vec<&str> = matched.iter().map(|m| m.remote_ref.as_str()).collect();
3476 assert_eq!(names, expect, "{mode:?}");
3477 }
3478 }
3479}
3480
3481#[cfg(test)]
3482mod v2_negotiation_tests {
3483 use super::*;
3484 use crate::objects::ObjectKind;
3485 use crate::progress::NeverCancel;
3486 use crate::repo::{InitOptions, Repository};
3487 use std::io::Cursor;
3488
3489 struct AckingNeverReadyServer {
3492 caps: Vec<String>,
3493 pending: Vec<u8>,
3494 response: Cursor<Vec<u8>>,
3495 acked: Option<ObjectId>,
3496 requests: Vec<Vec<String>>,
3497 }
3498
3499 impl AckingNeverReadyServer {
3500 fn answer(&mut self) -> std::io::Result<()> {
3501 let mut lines = Vec::new();
3502 let mut cursor = Cursor::new(std::mem::take(&mut self.pending));
3503 while let Some(pkt) = pkt_line::read_packet(&mut cursor)? {
3504 if let pkt_line::Packet::Data(line) = pkt {
3505 lines.push(line.trim_end().to_owned());
3506 }
3507 }
3508 let haves: Vec<ObjectId> = lines
3509 .iter()
3510 .filter_map(|l| l.strip_prefix("have "))
3511 .filter_map(|h| ObjectId::from_hex(h).ok())
3512 .collect();
3513 let done = lines.iter().any(|l| l == "done");
3514 self.requests.push(lines);
3515 let mut out = Vec::new();
3516 if !done {
3517 let acked = *self.acked.get_or_insert(haves[0]);
3518 pkt_line::write_line(&mut out, "acknowledgments")?;
3519 if haves.contains(&acked) {
3520 pkt_line::write_line(&mut out, &format!("ACK {acked}"))?;
3521 } else {
3522 pkt_line::write_line(&mut out, "NAK")?;
3523 }
3524 pkt_line::write_flush(&mut out)?;
3525 }
3526 self.response = Cursor::new(out);
3527 Ok(())
3528 }
3529 }
3530
3531 impl Write for AckingNeverReadyServer {
3532 fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
3533 self.pending.extend_from_slice(buf);
3534 Ok(buf.len())
3535 }
3536
3537 fn flush(&mut self) -> std::io::Result<()> {
3538 if self.pending.is_empty() {
3539 return Ok(());
3540 }
3541 self.answer()
3542 }
3543 }
3544
3545 impl Connection for AckingNeverReadyServer {
3546 fn reader(&mut self) -> &mut dyn Read {
3547 &mut self.response
3548 }
3549 fn writer(&mut self) -> &mut dyn Write {
3550 self
3551 }
3552 fn advertised_refs(&self) -> &[(String, ObjectId)] {
3553 &[]
3554 }
3555 fn capabilities(&self) -> &[String] {
3556 &self.caps
3557 }
3558 fn head_symref(&self) -> Option<&str> {
3559 None
3560 }
3561 fn protocol_version(&self) -> u8 {
3562 2
3563 }
3564 fn stateless_rpc(&self) -> bool {
3565 true
3566 }
3567 }
3568
3569 #[test]
3570 fn replayed_acks_do_not_postpone_the_in_vain_cutoff() {
3571 const TIPS: usize = 2000;
3572 let tmp = tempfile::tempdir().unwrap();
3573 let repo = Repository::init(tmp.path(), &InitOptions::default()).unwrap();
3574 let git_dir = repo.git_dir().to_path_buf();
3575 let tree = repo.odb().write(ObjectKind::Tree, &[]).unwrap();
3576 for i in 0..TIPS {
3579 let when = 1_600_000_000 + i as i64;
3580 let body = format!(
3581 "tree {tree}\nauthor T <t@example.com> {when} +0000\n\
3582 committer T <t@example.com> {when} +0000\n\nroot {i}\n"
3583 );
3584 let oid = repo
3585 .odb()
3586 .write(ObjectKind::Commit, body.as_bytes())
3587 .unwrap();
3588 crate::refs::write_ref(&git_dir, &format!("refs/heads/b{i:04}"), &oid).unwrap();
3589 }
3590
3591 let mut server = AckingNeverReadyServer {
3592 caps: vec!["fetch".to_owned()],
3593 pending: Vec::new(),
3594 response: Cursor::new(Vec::new()),
3595 acked: None,
3596 requests: Vec::new(),
3597 };
3598 let want = ObjectId::from_hex("1111111111111111111111111111111111111111").unwrap();
3599 let _ = negotiate_pack_v2(
3601 &repo,
3602 &mut server,
3603 &[],
3604 &[want],
3605 &V2DeepenArgs::default(),
3606 None,
3607 &mut NoProgress,
3608 &NeverCancel,
3609 V2FetchMode::Refs {
3610 promisor_reply: None,
3611 },
3612 );
3613
3614 let last = server.requests.last().unwrap();
3615 assert!(
3616 last.iter().any(|l| l == "done"),
3617 "negotiation must end with done"
3618 );
3619 let distinct: HashSet<&str> = server
3620 .requests
3621 .iter()
3622 .flatten()
3623 .filter_map(|l| l.strip_prefix("have "))
3624 .collect();
3625 assert_eq!(distinct.len(), 16 + 32 + 64 + 128 + 256);
3629 assert!(distinct.len() < TIPS, "cutoff never fired");
3630 }
3631}