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, open_odb, 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) {}
66}
67
68pub struct NoProgress;
70
71impl Progress for NoProgress {}
72
73impl ProgressSink for NoProgress {}
74
75const INITIAL_FLUSH: usize = 16;
78const PIPESAFE_FLUSH: usize = 32;
79
80fn next_flush_count(count: usize) -> usize {
81 if count < PIPESAFE_FLUSH {
82 count * 2
83 } else {
84 count + PIPESAFE_FLUSH
85 }
86}
87
88const LARGE_FLUSH: usize = 16384;
90
91const MAX_IN_VAIN: usize = 256;
94
95fn next_v2_flush_count(count: usize) -> usize {
98 if count < LARGE_FLUSH {
99 count * 2
100 } else {
101 count + count / 10
102 }
103}
104
105#[derive(Clone, Copy, PartialEq, Eq)]
106enum AckKind {
107 Bare,
109 Common,
110 Continue,
111 Ready,
112}
113
114fn parse_ack(line: &str) -> Option<(ObjectId, AckKind)> {
115 if line == "NAK" {
116 return None;
117 }
118 let rest = line.strip_prefix("ACK ")?;
119 let hex = rest.split_whitespace().next()?;
120 let oid = ObjectId::from_hex(hex).ok()?;
121 let tail = rest.strip_prefix(hex).unwrap_or("").trim();
122 let kind = if tail.contains("continue") {
123 AckKind::Continue
124 } else if tail.contains("common") {
125 AckKind::Common
126 } else if tail.contains("ready") {
127 AckKind::Ready
128 } else {
129 AckKind::Bare
130 };
131 Some((oid, kind))
132}
133
134fn read_ack_round(reader: &mut dyn Read, negotiator: &mut SkippingNegotiator<'_>) -> Result<()> {
137 let mut reader = reader;
138 while let Some(pkt) = pkt_line::read_packet(&mut reader)? {
139 match pkt {
140 pkt_line::Packet::Flush => break,
141 pkt_line::Packet::Data(ln) => {
142 let ln = ln.trim_end();
143 if ln == "NAK" {
144 break;
147 }
148 let Some((ack_oid, kind)) = parse_ack(ln) else {
149 break;
150 };
151 if kind == AckKind::Bare {
152 break;
153 }
154 let _ = negotiator.ack(ack_oid)?;
155 }
156 _ => {}
157 }
158 }
159 Ok(())
160}
161
162fn read_sideband_pack(
167 r: &mut dyn Read,
168 target: &mut crate::pack_receive::PackReceiveTarget<'_>,
169 progress: &mut dyn Progress,
170 cancel: &dyn Cancel,
171) -> Result<()> {
172 crate::pack_receive::read_sideband_pack_to(r, target, progress, cancel)
173}
174
175#[derive(Default)]
179pub(crate) struct ShallowUpdate {
180 pub(crate) shallow: Vec<ObjectId>,
181 pub(crate) unshallow: Vec<ObjectId>,
182}
183
184fn append_filter_request_v0(req: &mut Vec<u8>, wire_filter: &str) -> Result<()> {
190 pkt_line::write_line_to_vec(req, &format!("filter {wire_filter}"))?;
191 Ok(())
192}
193
194fn append_shallow_request_v0(
195 req: &mut Vec<u8>,
196 server_caps: &str,
197 local_shallow: &[ObjectId],
198 opts: &FetchOptions,
199) -> Result<()> {
200 let sends_shallow = !local_shallow.is_empty()
201 || opts.wire_deepen().is_some()
202 || opts
203 .deepen_since
204 .as_deref()
205 .is_some_and(|s| !s.trim().is_empty())
206 || opts.deepen_not.iter().any(|s| !s.trim().is_empty());
207 if sends_shallow && !server_caps.contains("shallow") {
208 return Err(Error::FetchShallowUnsupported);
209 }
210 for oid in local_shallow {
211 pkt_line::write_line_to_vec(req, &format!("shallow {}", oid.to_hex()))?;
212 }
213 if let Some((depth, relative)) = opts.wire_deepen() {
214 if relative && !server_caps.contains("deepen-relative") {
215 return Err(Error::FetchDeepenRelativeUnsupported);
216 }
217 pkt_line::write_line_to_vec(req, &format!("deepen {depth}"))?;
218 if relative {
219 pkt_line::write_line_to_vec(req, "deepen-relative")?;
220 }
221 }
222 if let Some(since) = opts
223 .deepen_since
224 .as_deref()
225 .filter(|s| !s.trim().is_empty())
226 {
227 if server_caps.contains("deepen-since") {
228 let value = crate::shallow::deepen_since_wire_value(since);
229 pkt_line::write_line_to_vec(req, &format!("deepen-since {value}"))?;
230 }
231 }
232 if server_caps.contains("deepen-not") {
233 for excl in &opts.deepen_not {
234 let excl = excl.trim();
235 if !excl.is_empty() {
236 pkt_line::write_line_to_vec(req, &format!("deepen-not {excl}"))?;
237 }
238 }
239 }
240 Ok(())
241}
242
243#[allow(clippy::too_many_arguments)]
257fn negotiate_pack(
258 local: &Repository,
259 conn: &mut dyn Connection,
260 wants: &[ObjectId],
261 opts: &mut FetchOptions,
262 local_shallow: &[ObjectId],
263 wire_filter: Option<&str>,
264 progress: &mut dyn Progress,
265 cancel: &dyn Cancel,
266 skip_local_haves: bool,
267) -> Result<(Option<std::path::PathBuf>, ShallowUpdate)> {
268 crate::trace::trace_span!("grit.fetch.negotiate");
269 let local_git_dir = local.git_dir();
270 let want_set: HashSet<ObjectId> = wants.iter().copied().collect();
271
272 let Some(first_want) = wants.first().copied() else {
273 return Ok((None, ShallowUpdate::default()));
274 };
275
276 let shallow_request = opts.has_deepen_request() || !local_shallow.is_empty();
281 let skip_haves = shallow_request || skip_local_haves;
282
283 let advertised: Vec<(String, ObjectId)> = conn.advertised_refs().to_vec();
288 let server_caps: String = conn.capabilities().join(" ");
289 validate_fetch_depth_server(opts, Some(&server_caps), None)?;
290
291 let mut caps = if opts.remote_progress {
293 String::from(" multi_ack_detailed side-band-64k thin-pack include-tag ofs-delta agent=grit")
294 } else {
295 String::from(
296 " multi_ack_detailed side-band-64k thin-pack no-progress include-tag ofs-delta agent=grit",
297 )
298 };
299 if opts.wire_deepen().is_some_and(|(_, relative)| relative) {
300 if !server_caps.contains("deepen-relative") {
301 return Err(Error::FetchDeepenRelativeUnsupported);
302 }
303 caps.push_str(" deepen-relative");
304 }
305 if wire_filter.is_some() {
306 caps.push_str(" filter");
307 }
308
309 let mut req: Vec<u8> = Vec::new();
310 let w0 = format!("want {}{}", first_want.to_hex(), caps);
311 pkt_line::write_line_to_vec(&mut req, &w0)?;
312 for w in wants.iter().skip(1) {
313 pkt_line::write_line_to_vec(&mut req, &format!("want {}", w.to_hex()))?;
314 }
315 if wants.len() == 1 && !skip_haves {
319 pkt_line::write_line_to_vec(&mut req, &format!("want {}", first_want.to_hex()))?;
320 }
321 if let Some(spec) = wire_filter {
322 append_filter_request_v0(&mut req, spec)?;
323 }
324 append_shallow_request_v0(&mut req, &server_caps, local_shallow, opts)?;
325 req.extend_from_slice(b"0000");
326 conn.writer().write_all(&req)?;
327 conn.writer().flush()?;
328
329 let mut negotiator = SkippingNegotiator::new(local);
333 if !skip_haves {
334 add_negotiation_tips(&mut negotiator, local_git_dir, &want_set)?;
335 let mut local_by_name: std::collections::HashMap<String, ObjectId> =
339 std::collections::HashMap::new();
340 for prefix in ["refs/heads/", "refs/tags/"] {
341 if let Ok(entries) = crate::refs::list_refs(local_git_dir, prefix) {
342 for (name, oid) in entries {
343 local_by_name.insert(name, oid);
344 }
345 }
346 }
347 for (name, oid) in &advertised {
348 if want_set.contains(oid) {
349 continue;
350 }
351 let Some(local_oid) = local_by_name.get(name) else {
352 continue;
353 };
354 if local_oid != oid {
355 continue;
356 }
357 if let Ok(Some(c)) = negotiator.repo().odb().peel_to_commit(*oid) {
358 negotiator.known_common(c)?;
359 }
360 }
361 }
362
363 let mut shallow_update = ShallowUpdate::default();
368 if shallow_request {
369 let (sh, unsh) = crate::shallow::read_shallow_info_section(&mut conn.reader())?;
370 shallow_update.shallow = sh;
371 shallow_update.unshallow = unsh;
372 } else if skip_local_haves && conn.stateless_rpc() {
373 read_ack_round(conn.reader(), &mut negotiator)?;
374 } else if conn.stateless_rpc() {
375 read_ack_round(conn.reader(), &mut negotiator)?;
378 }
379
380 let mut count: usize = 0;
382 let mut flush_at: usize = INITIAL_FLUSH;
383 let mut pending: Vec<u8> = Vec::new();
384 let mut flushes: i32 = 0;
385 if !skip_haves {
386 while let Some(oid) = negotiator.next_have()? {
387 poll_cancel(progress, cancel)?;
388 pkt_line::write_line_to_vec(&mut pending, &format!("have {}", oid.to_hex()))?;
389 count += 1;
390 if flush_at <= count {
391 pending.extend_from_slice(b"0000");
392 conn.writer().write_all(&pending)?;
393 conn.writer().flush()?;
394 pending.clear();
395 flush_at = next_flush_count(count);
396 flushes += 1;
397 if count == INITIAL_FLUSH && !conn.stateless_rpc() {
400 continue;
401 }
402 read_ack_round(conn.reader(), &mut negotiator)?;
403 flushes -= 1;
404 }
405 }
406 }
407 if !pending.is_empty() {
408 pending.extend_from_slice(b"0000");
409 conn.writer().write_all(&pending)?;
410 conn.writer().flush()?;
411 flushes += 1;
412 }
413 while flushes > 0 {
414 read_ack_round(conn.reader(), &mut negotiator)?;
415 flushes -= 1;
416 }
417
418 let mut tail = Vec::new();
420 pkt_line::write_line_to_vec(&mut tail, "done")?;
421 conn.writer().write_all(&tail)?;
422 conn.writer().flush()?;
423
424 if shallow_request && conn.stateless_rpc() {
426 let (sh, unsh) = crate::shallow::read_shallow_info_section(&mut conn.reader())?;
427 shallow_update.shallow.extend(sh);
428 shallow_update.unshallow.extend(unsh);
429 }
430
431 match pkt_line::read_packet(&mut conn.reader())? {
432 None => return Err(Error::Message("unexpected EOF after done".to_owned())),
433 Some(pkt_line::Packet::Flush) => {
434 return Err(Error::Message("unexpected flush after done".to_owned()))
435 }
436 Some(pkt_line::Packet::Data(ln)) => {
437 let ln = ln.trim_end();
438 if ln != "NAK" {
439 if let Some((ack_oid, kind)) = parse_ack(ln) {
440 if kind != AckKind::Bare {
441 let _ = negotiator.ack(ack_oid)?;
442 }
443 } else if let Some(msg) = ln.strip_prefix("ERR ") {
444 return Err(Error::Message(format!("remote error: {}", msg.trim_end())));
445 }
446 }
447 }
448 Some(_) => {}
449 }
450
451 let mut pack_receive = crate::pack_receive::TempPackReceive::create(local.odb().objects_dir())?;
452 pack_receive.read_sideband(conn.reader(), progress, cancel)?;
453 let pack_path = pack_receive.finish()?;
454 Ok((pack_path, shallow_update))
455}
456
457pub(crate) fn v2_object_format(server_caps: &[String], local_odb: &crate::odb::Odb) -> String {
472 for c in server_caps {
473 if let Some(fmt) = c.strip_prefix("object-format=") {
474 let f = fmt.trim();
475 if !f.is_empty() {
476 return f.to_ascii_lowercase();
477 }
478 }
479 }
480 local_odb.hash_algo().name().to_owned()
481}
482
483fn push_unique_prefix(out: &mut Vec<String>, value: &str) {
484 if !out.iter().any(|v| v == value) {
485 out.push(value.to_owned());
486 }
487}
488
489pub(crate) fn fetch_ref_list_prefixes(refspecs: &[String]) -> Vec<String> {
496 let mut out = vec!["refs/heads/".to_owned(), "refs/tags/".to_owned()];
497 for spec in refspecs {
498 if spec.starts_with('^') {
499 continue;
500 }
501 let raw = spec.strip_prefix('+').unwrap_or(spec.as_str());
502 let src = raw.split_once(':').map(|(s, _)| s).unwrap_or(raw).trim();
503 if src.is_empty() {
504 continue;
505 }
506 if src == "HEAD" {
507 push_unique_prefix(&mut out, "HEAD");
508 continue;
509 }
510 if let Some(star) = src.find('*') {
511 let prefix = &src[..star];
512 if prefix.is_empty() {
513 continue;
514 }
515 if prefix.starts_with("refs/") {
516 push_unique_prefix(&mut out, prefix);
517 } else {
518 push_unique_prefix(&mut out, &format!("refs/heads/{prefix}"));
519 }
520 continue;
521 }
522 if src.starts_with("refs/") {
523 push_unique_prefix(&mut out, src);
524 } else {
525 push_unique_prefix(&mut out, &format!("refs/heads/{src}"));
526 push_unique_prefix(&mut out, &format!("refs/{src}"));
528 }
529 }
530 out
531}
532
533#[must_use]
535pub(crate) fn parse_v2_ls_refs_unborn_head_symref(line: &str) -> Option<String> {
536 const PREFIX: &str = "unborn HEAD symref-target:";
537 let target = line.strip_prefix(PREFIX)?;
538 let target = target.split_whitespace().next()?.to_owned();
539 crate::refs::is_valid_advertised_symref_target(&target).then_some(target)
540}
541
542pub(crate) fn parse_ls_refs_v2_line(
549 line: &str,
550) -> Option<(String, ObjectId, Option<String>, Option<ObjectId>)> {
551 const SYM: &str = " symref-target:";
552 const PEEL: &str = " peeled:";
553 let (oid_hex, after_oid) = line.split_once(' ')?;
554 let oid = ObjectId::from_hex(oid_hex).ok()?;
555
556 let sym_at = after_oid.find(SYM);
558 let peel_at = after_oid.find(PEEL);
559 let name_end = match (sym_at, peel_at) {
560 (Some(a), Some(b)) => a.min(b),
561 (Some(a), None) => a,
562 (None, Some(b)) => b,
563 (None, None) => after_oid.len(),
564 };
565 let name = after_oid[..name_end].trim().to_owned();
566 if name.is_empty() {
567 return None;
568 }
569 let symref_target = sym_at.map(|pos| {
570 let tail = &after_oid[pos + SYM.len()..];
571 let end = tail.find(' ').unwrap_or(tail.len());
572 tail[..end].to_owned()
573 });
574 let advertised_peel = peel_at.and_then(|pos| {
575 let tail = &after_oid[pos + PEEL.len()..];
576 let end = tail.find(' ').unwrap_or(tail.len());
577 ObjectId::from_hex(tail[..end].trim()).ok()
578 });
579 Some((name, oid, symref_target, advertised_peel))
580}
581
582fn v2_ls_refs(
591 conn: &mut dyn Connection,
592 server_caps: &[String],
593 local_odb: &crate::odb::Odb,
594 tags: crate::transfer::TagMode,
595 refspecs: &[String],
596) -> Result<LsRefsAdvertised> {
597 let req = build_v2_ls_refs_request(server_caps, local_odb, tags, refspecs)?;
598 conn.writer().write_all(&req)?;
599 conn.writer().flush()?;
600 parse_v2_ls_refs_response(conn.reader())
601}
602
603pub(crate) fn build_v2_ls_refs_request(
612 server_caps: &[String],
613 local_odb: &crate::odb::Odb,
614 tags: crate::transfer::TagMode,
615 refspecs: &[String],
616) -> Result<Vec<u8>> {
617 let object_format = v2_object_format(server_caps, local_odb);
618 let cap_echo = protocol_v2::cap_lines_for_command_request(server_caps);
619
620 let mut req: Vec<u8> = Vec::new();
621 pkt_line::write_line(&mut req, "command=ls-refs")?;
622 if cap_echo.iter().any(|c| c.starts_with("object-format=")) {
625 for line in &cap_echo {
626 pkt_line::write_line(&mut req, line)?;
627 }
628 } else {
629 for line in &cap_echo {
630 pkt_line::write_line(&mut req, line)?;
631 }
632 pkt_line::write_line(&mut req, &format!("object-format={object_format}"))?;
633 }
634 pkt_line::write_delim(&mut req)?;
635 pkt_line::write_line(&mut req, "symrefs")?;
636 if crate::protocol_v2::server_advertises_ls_refs_unborn(server_caps) {
637 pkt_line::write_line(&mut req, "unborn")?;
638 }
639 pkt_line::write_line(&mut req, "peel")?;
640
641 pkt_line::write_line(&mut req, "ref-prefix HEAD")?;
645 let mut prefixes = fetch_ref_list_prefixes(refspecs);
646 if tags != crate::transfer::TagMode::None && !prefixes.iter().any(|p| p == "refs/tags/") {
647 prefixes.push("refs/tags/".to_owned());
650 }
651 for p in &prefixes {
652 pkt_line::write_line(&mut req, &format!("ref-prefix {p}"))?;
653 }
654 pkt_line::write_flush(&mut req)?;
655 Ok(req)
656}
657
658pub(crate) fn parse_v2_ls_refs_response(reader: &mut dyn Read) -> Result<LsRefsAdvertised> {
664 let mut advertised: Vec<(String, ObjectId)> = Vec::new();
666 let mut head_symref: Option<String> = None;
667 let mut head_oid: Option<ObjectId> = None;
668 let mut advertised_peel: std::collections::HashMap<String, ObjectId> =
669 std::collections::HashMap::new();
670 let mut reader = reader;
671 loop {
672 match pkt_line::read_packet(&mut reader)? {
673 None | Some(pkt_line::Packet::Flush) | Some(pkt_line::Packet::Delim) => break,
674 Some(pkt_line::Packet::ResponseEnd) => break,
675 Some(pkt_line::Packet::Data(line)) => {
676 let line = line.trim_end_matches('\n');
677 if let Some(msg) = line.strip_prefix("ERR ") {
678 return Err(Error::Message(format!("remote error: {}", msg.trim_end())));
679 }
680 if let Some(t) = parse_v2_ls_refs_unborn_head_symref(line) {
681 head_symref = Some(t);
682 continue;
683 }
684 let Some((name, oid, symref_target, peel)) = parse_ls_refs_v2_line(line) else {
685 continue;
686 };
687 if name.contains("^{") || name.ends_with("^{}") {
688 continue;
689 }
690 if name == "HEAD" {
691 head_oid = Some(oid);
692 if let Some(t) =
693 symref_target.filter(|t| crate::refs::is_valid_advertised_symref_target(t))
694 {
695 head_symref = Some(t);
696 }
697 continue;
699 }
700 if crate::refs::is_valid_fetch_advertised_ref(&name) {
701 if let Some(p) = peel {
702 advertised_peel.insert(name.clone(), p);
703 }
704 advertised.push((name, oid));
705 }
706 }
707 }
708 }
709 Ok((advertised, head_symref, advertised_peel, head_oid))
710}
711
712pub(crate) struct V2LocalHaves<'r> {
726 negotiator: SkippingNegotiator<'r>,
727 taken: Vec<ObjectId>,
728 exhausted: bool,
729}
730
731impl V2LocalHaves<'_> {
732 fn first(&mut self, n: usize) -> Result<&[ObjectId]> {
734 while self.taken.len() < n && !self.exhausted {
735 match self.negotiator.next_have()? {
736 Some(oid) => self.taken.push(oid),
737 None => self.exhausted = true,
738 }
739 }
740 Ok(&self.taken[..self.taken.len().min(n)])
741 }
742
743 #[cfg(test)]
745 fn all(&mut self) -> Result<&[ObjectId]> {
746 self.first(usize::MAX)
747 }
748}
749
750pub(crate) fn v2_local_haves<'r>(
751 local: &'r Repository,
752 wants: &[ObjectId],
753) -> Result<V2LocalHaves<'r>> {
754 let want_set: HashSet<ObjectId> = wants.iter().copied().collect();
755 let mut negotiator = SkippingNegotiator::new(local);
756 add_negotiation_tips(&mut negotiator, local.git_dir(), &want_set)?;
757 Ok(V2LocalHaves {
758 negotiator,
759 taken: Vec::new(),
760 exhausted: false,
761 })
762}
763
764fn add_negotiation_tips(
769 negotiator: &mut SkippingNegotiator<'_>,
770 local_git_dir: &Path,
771 wants: &HashSet<ObjectId>,
772) -> Result<()> {
773 let mut tips: Vec<ObjectId> = Vec::new();
774 let mut seen_tip: HashSet<ObjectId> = HashSet::new();
775 let mut push_tip = |negotiator: &SkippingNegotiator<'_>, oid: ObjectId| {
776 if let Ok(Some(c)) = negotiator.repo().odb().peel_to_commit(oid) {
777 if !wants.contains(&c) && seen_tip.insert(c) {
778 tips.push(c);
779 }
780 }
781 };
782 for prefix in ["refs/heads/", "refs/remotes/", "refs/tags/"] {
783 if let Ok(entries) = crate::refs::list_refs(local_git_dir, prefix) {
784 for (_, oid) in entries {
785 push_tip(negotiator, oid);
786 }
787 }
788 }
789 if let Ok(h) = crate::refs::resolve_ref(local_git_dir, "HEAD") {
790 push_tip(negotiator, h);
791 }
792 tips.sort_unstable();
793 for t in tips {
794 negotiator.add_tip(t)?;
795 }
796 Ok(())
797}
798
799#[allow(clippy::too_many_arguments)]
814fn negotiate_pack_v2(
815 local: &Repository,
816 conn: &mut dyn Connection,
817 server_caps: &[String],
818 wants: &[ObjectId],
819 deepen: &V2DeepenArgs,
820 wire_filter: Option<&str>,
821 progress: &mut dyn Progress,
822 cancel: &dyn Cancel,
823 skip_local_haves: bool,
824) -> Result<(Option<std::path::PathBuf>, ShallowUpdate)> {
825 if wants.is_empty() {
826 return Ok((None, ShallowUpdate::default()));
827 }
828 let local_odb = local.odb();
829 let object_format = v2_object_format(server_caps, local_odb);
830 let cap_echo = protocol_v2::cap_lines_for_command_request(server_caps);
831 let sideband_all = protocol_v2::fetch_supports_sideband_all(server_caps);
832
833 let shallow_request = deepen.is_shallow_request();
837
838 let mut haves = if shallow_request || skip_local_haves {
842 None
843 } else {
844 Some(v2_local_haves(local, wants)?)
845 };
846
847 let mut pack_receive = crate::pack_receive::TempPackReceive::create(local_odb.objects_dir())?;
848 let mut shallow_update = ShallowUpdate::default();
849 let mut common: Vec<ObjectId> = Vec::new();
850 let mut common_set: HashSet<ObjectId> = HashSet::new();
851 let mut batch = INITIAL_FLUSH;
852 let mut in_vain: usize = 0;
853 let mut seen_ack = false;
854 let mut round_haves: Vec<ObjectId> = Vec::new();
855 loop {
856 round_haves.clear();
857 round_haves.extend_from_slice(&common);
858 let added = match haves.as_mut() {
859 Some(haves) => {
860 poll_cancel(progress, cancel)?;
861 let previous = haves.taken.len();
862 haves.first(previous.saturating_add(batch))?;
863 round_haves.extend_from_slice(&haves.taken[previous..]);
864 haves.taken.len() - previous
865 }
866 None => 0,
867 };
868 in_vain += added;
869 let done = added == 0 || (seen_ack && in_vain >= MAX_IN_VAIN);
870 write_v2_fetch_request(
871 conn.writer(),
872 &object_format,
873 &cap_echo,
874 wants,
875 &round_haves,
876 sideband_all,
877 deepen,
878 wire_filter,
879 done,
880 )?;
881 if done {
882 break;
883 }
884 match read_v2_acknowledgments(conn.reader(), sideband_all)? {
885 Some(round) if !round.ready => {
889 for oid in round.common {
890 if let Some(haves) = haves.as_mut() {
891 haves.negotiator.ack(oid)?;
892 }
893 if common_set.insert(oid) {
894 common.push(oid);
895 in_vain = 0;
896 }
897 seen_ack = true;
898 }
899 batch = next_v2_flush_count(batch);
900 }
901 _ => break,
904 }
905 }
906 read_v2_fetch_pack_response(
907 conn.reader(),
908 &mut Vec::new(),
909 Some(&mut pack_receive),
910 &mut shallow_update,
911 progress,
912 cancel,
913 sideband_all,
914 )?;
915 Ok((pack_receive.finish()?, shallow_update))
916}
917
918#[derive(Clone, Default)]
923pub(crate) struct V2DeepenArgs {
924 pub(crate) local_shallow: Vec<ObjectId>,
926 pub(crate) depth: Option<u32>,
928 pub(crate) deepen_relative: bool,
930 pub(crate) deepen_since: Option<String>,
932 pub(crate) deepen_not: Vec<String>,
934 pub(crate) remote_progress: bool,
936}
937
938impl V2DeepenArgs {
939 pub(crate) fn from_opts(opts: &FetchOptions, local_shallow: &[ObjectId]) -> Self {
942 let (depth, deepen_relative) = opts
943 .wire_deepen()
944 .map(|(d, r)| (Some(d), r))
945 .unwrap_or((None, false));
946 Self {
947 local_shallow: local_shallow.to_vec(),
948 depth,
949 deepen_relative,
950 deepen_since: opts
951 .deepen_since
952 .as_deref()
953 .filter(|s| !s.trim().is_empty())
954 .map(crate::shallow::deepen_since_wire_value),
955 deepen_not: opts
956 .deepen_not
957 .iter()
958 .map(|s| s.trim().to_owned())
959 .filter(|s| !s.is_empty())
960 .collect(),
961 remote_progress: opts.remote_progress,
962 }
963 }
964
965 pub(crate) fn is_shallow_request(&self) -> bool {
968 self.depth.is_some()
969 || self.deepen_relative
970 || self.deepen_since.is_some()
971 || !self.deepen_not.is_empty()
972 || !self.local_shallow.is_empty()
973 }
974}
975#[expect(clippy::too_many_arguments)]
976pub(crate) fn write_v2_fetch_request(
982 w: &mut dyn Write,
983 object_format: &str,
984 cap_echo: &[String],
985 wants: &[ObjectId],
986 haves: &[ObjectId],
987 sideband_all: bool,
988 deepen: &V2DeepenArgs,
989 wire_filter: Option<&str>,
990 send_done: bool,
991) -> Result<()> {
992 let mut req: Vec<u8> = Vec::new();
993 pkt_line::write_line(&mut req, "command=fetch")?;
994 if cap_echo.iter().any(|c| c.starts_with("object-format=")) {
995 for line in cap_echo {
996 pkt_line::write_line(&mut req, line)?;
997 }
998 } else {
999 for line in cap_echo {
1000 pkt_line::write_line(&mut req, line)?;
1001 }
1002 pkt_line::write_line(&mut req, &format!("object-format={object_format}"))?;
1003 }
1004 pkt_line::write_delim(&mut req)?;
1005
1006 pkt_line::write_line(&mut req, "thin-pack")?;
1007 if !deepen.remote_progress {
1008 pkt_line::write_line(&mut req, "no-progress")?;
1009 }
1010 pkt_line::write_line(&mut req, "ofs-delta")?;
1011 if sideband_all {
1012 pkt_line::write_line(&mut req, "sideband-all")?;
1013 }
1014 pkt_line::write_line(&mut req, "include-tag")?;
1017
1018 for oid in &deepen.local_shallow {
1020 pkt_line::write_line(&mut req, &format!("shallow {}", oid.to_hex()))?;
1021 }
1022 if let Some(depth) = deepen.depth {
1023 pkt_line::write_line(&mut req, &format!("deepen {depth}"))?;
1024 }
1025 if deepen.deepen_relative {
1026 pkt_line::write_line(&mut req, "deepen-relative")?;
1027 }
1028 if let Some(since) = &deepen.deepen_since {
1029 pkt_line::write_line(&mut req, &format!("deepen-since {since}"))?;
1030 }
1031 for excl in &deepen.deepen_not {
1032 pkt_line::write_line(&mut req, &format!("deepen-not {excl}"))?;
1033 }
1034 if let Some(spec) = wire_filter {
1035 pkt_line::write_line(&mut req, &format!("filter {spec}"))?;
1036 }
1037
1038 for want in wants {
1039 pkt_line::write_line(&mut req, &format!("want {}", want.to_hex()))?;
1040 }
1041 for have in haves {
1042 pkt_line::write_line(&mut req, &format!("have {}", have.to_hex()))?;
1043 }
1044 if send_done {
1045 pkt_line::write_line(&mut req, "done")?;
1046 }
1047 pkt_line::write_flush(&mut req)?;
1048 w.write_all(&req)?;
1049 w.flush()?;
1050 Ok(())
1051}
1052
1053pub(crate) struct V2AckRound {
1055 pub(crate) ready: bool,
1058 pub(crate) common: Vec<ObjectId>,
1060}
1061
1062pub(crate) fn read_v2_acknowledgments(
1070 reader: &mut dyn Read,
1071 sideband_all: bool,
1072) -> Result<Option<V2AckRound>> {
1073 let mut reader = reader;
1074 let hdr = match crate::shallow::read_v2_fetch_primary_line(&mut reader, sideband_all)? {
1075 Some(s) => s,
1076 None => {
1077 return Ok(Some(V2AckRound {
1078 ready: false,
1079 common: Vec::new(),
1080 }))
1081 }
1082 };
1083 let hdr = hdr.as_str();
1084 if let Some(msg) = hdr.strip_prefix("ERR ") {
1085 return Err(Error::Message(format!("remote error: {}", msg.trim_end())));
1086 }
1087 if hdr != "acknowledgments" {
1088 return Err(Error::Message(format!(
1098 "unexpected v2 fetch section before acknowledgments: {hdr}"
1099 )));
1100 }
1101 let mut ready = false;
1102 let mut common = Vec::new();
1103 loop {
1104 match pkt_line::read_packet(&mut reader)? {
1105 Some(pkt_line::Packet::Delim) | Some(pkt_line::Packet::Flush) | None => break,
1106 Some(pkt_line::Packet::ResponseEnd) => break,
1107 Some(pkt_line::Packet::Data(payload)) => {
1108 let Some(ln) = crate::shallow::v2_primary_line_from_pkt(payload, sideband_all)?
1109 else {
1110 continue;
1111 };
1112 if ln == "NAK" {
1113 continue;
1114 }
1115 if let Some(hex) = ln.strip_prefix("ACK ") {
1116 let oid = ObjectId::from_hex(hex.trim()).map_err(|_| {
1117 Error::Message(format!("unexpected acknowledgment line: '{ln}'"))
1118 })?;
1119 common.push(oid);
1120 continue;
1121 }
1122 if ln == "ready" {
1123 ready = true;
1124 continue;
1125 }
1126 return Err(Error::Message(format!(
1127 "unexpected acknowledgment line: '{ln}'"
1128 )));
1129 }
1130 }
1131 }
1132 Ok(Some(V2AckRound { ready, common }))
1133}
1134
1135fn v2_wanted_ref_ad_line(line: &str) -> bool {
1138 let line = line.trim_end();
1139 let Some((oid_hex, rest)) = line.split_once(' ') else {
1140 return false;
1141 };
1142 if ObjectId::from_hex(oid_hex.trim()).is_err() {
1143 return false;
1144 }
1145 !rest.trim().is_empty()
1146}
1147
1148pub(crate) fn read_v2_fetch_pack_response(
1154 reader: &mut dyn Read,
1155 out: &mut Vec<u8>,
1156 pack_sink: Option<&mut crate::pack_receive::TempPackReceive>,
1157 shallow_out: &mut ShallowUpdate,
1158 progress: &mut dyn Progress,
1159 cancel: &dyn Cancel,
1160 sideband_all: bool,
1161) -> Result<()> {
1162 loop {
1163 let hdr = loop {
1164 match pkt_line::read_packet(&mut &mut *reader)? {
1165 Some(pkt_line::Packet::Data(payload)) => {
1166 if let Some(line) =
1167 crate::shallow::v2_primary_line_from_pkt(payload, sideband_all)?
1168 {
1169 break line;
1170 }
1171 }
1172 Some(pkt_line::Packet::Flush) | None => return Ok(()),
1173 Some(pkt_line::Packet::Delim) => continue,
1174 Some(other) => {
1175 return Err(Error::Message(format!(
1176 "unexpected v2 fetch response: {other:?}"
1177 )))
1178 }
1179 }
1180 };
1181 let hdr = hdr.as_str();
1182 if let Some(msg) = hdr.strip_prefix("ERR ") {
1183 return Err(Error::Message(format!("remote error: {}", msg.trim_end())));
1184 }
1185 match hdr {
1186 h if h == crate::protocol_v2::FetchResponseSection::ShallowInfo.header() => {
1187 let (sh, unsh) =
1191 crate::shallow::read_shallow_info_section_sideband(&mut *reader, sideband_all)?;
1192 shallow_out.shallow.extend(sh);
1193 shallow_out.unshallow.extend(unsh);
1194 }
1195 h if h == crate::protocol_v2::FetchResponseSection::Acknowledgments.header()
1196 || h == crate::protocol_v2::FetchResponseSection::WantedRefs.header()
1197 || h == crate::protocol_v2::FetchResponseSection::PackfileUris.header() =>
1198 {
1199 skip_v2_section_until_boundary(&mut *reader, sideband_all)?;
1200 }
1201 h if h == crate::protocol_v2::FetchResponseSection::Packfile.header() => {
1202 if let Some(sink) = pack_sink {
1205 sink.read_sideband(&mut *reader, progress, cancel)?;
1206 } else {
1207 let mut target = crate::pack_receive::PackReceiveTarget::Memory(out);
1208 read_sideband_pack(&mut *reader, &mut target, progress, cancel)?;
1209 }
1210 return Ok(());
1211 }
1212 other if v2_wanted_ref_ad_line(other) => {
1213 }
1215 other => {
1216 return Err(Error::Message(format!(
1217 "unexpected v2 fetch section: {other}"
1218 )))
1219 }
1220 }
1221 }
1222}
1223
1224fn skip_v2_section_until_boundary(reader: &mut dyn Read, sideband_all: bool) -> Result<()> {
1226 loop {
1227 match pkt_line::read_packet(&mut &mut *reader)? {
1228 None | Some(pkt_line::Packet::Flush) | Some(pkt_line::Packet::Delim) => return Ok(()),
1229 Some(pkt_line::Packet::ResponseEnd) => return Ok(()),
1230 Some(pkt_line::Packet::Data(payload)) => {
1231 let _ = crate::shallow::v2_primary_line_from_pkt(payload, sideband_all)?;
1232 }
1233 }
1234 }
1235}
1236
1237pub fn fetch_remote(
1259 local_git_dir: &Path,
1260 conn: &mut dyn Connection,
1261 opts: &mut FetchOptions,
1262 progress: &mut dyn Progress,
1263 cancel: &dyn Cancel,
1264) -> Result<FetchOutcome> {
1265 let local = open_fetch_target(local_git_dir, opts)?;
1266 fetch_remote_into(&local, conn, opts, progress, cancel)
1267}
1268
1269pub(crate) fn open_fetch_target(local_git_dir: &Path, opts: &FetchOptions) -> Result<Repository> {
1271 Repository::open(
1272 local_git_dir,
1273 &RepositoryOptions::with_environment(opts.environment.clone()),
1274 )
1275}
1276
1277pub(crate) fn fetch_remote_into(
1279 local: &Repository,
1280 conn: &mut dyn Connection,
1281 opts: &mut FetchOptions,
1282 progress: &mut dyn Progress,
1283 cancel: &dyn Cancel,
1284) -> Result<FetchOutcome> {
1285 use crate::net_trace::net_trace;
1286 let local_git_dir = local.git_dir();
1287 let t_begin = std::time::Instant::now();
1290 net_trace!(
1291 opts.network_trace,
1292 opts.diagnostics.as_ref(),
1293 "fetch_remote: begin — protocol v{}, {} refspec(s), tags={:?}, depth={:?}",
1294 conn.protocol_version(),
1295 opts.refspecs.len(),
1296 opts.tags,
1297 opts.fetch_depth
1298 );
1299 validate_fetch_depth_local(local_git_dir, opts)?;
1300 let local_cfg = crate::config::ConfigSet::load(&opts.environment, Some(local_git_dir), true)?;
1301 if opts.filter.is_none() {
1302 if let Some(remote) = opts.remote_name.as_deref() {
1303 opts.filter = crate::partial_clone::stored_remote_filter(&local_cfg, remote);
1304 }
1305 }
1306 if !opts.object_wants.is_empty() {
1307 return fetch_remote_object_wants(local, conn, opts, progress, cancel);
1308 }
1309 let local_odb_for_ls = local.odb();
1310
1311 let (remote_refs, default_branch, v2_caps, advertised_peel, remote_head_oid) = if conn
1318 .protocol_version()
1319 >= 2
1320 {
1321 let caps: Vec<String> = conn.capabilities().to_vec();
1322 let (refs, head_symref, peel, head_oid) =
1323 v2_ls_refs(conn, &caps, local_odb_for_ls, opts.tags, &opts.refspecs)?;
1324 let default_branch =
1325 head_symref.map(|t| t.strip_prefix("refs/heads/").unwrap_or(&t).to_owned());
1326 (refs, default_branch, Some(caps), peel, head_oid)
1327 } else {
1328 let default_branch = conn
1329 .head_symref()
1330 .map(|t| t.strip_prefix("refs/heads/").unwrap_or(t).to_owned());
1331 let advertised_peel = conn.advertised_tag_peel().cloned().unwrap_or_default();
1332 let remote_head_oid =
1333 crate::transfer::remote_head_oid_from_advertised(conn.advertised_refs());
1334 let remote_refs: Vec<(String, ObjectId)> = conn
1335 .advertised_refs()
1336 .iter()
1337 .filter(|(n, _)| {
1338 n != "HEAD" && !n.ends_with("^{}") && crate::refs::is_valid_fetch_advertised_ref(n)
1339 })
1340 .cloned()
1341 .collect();
1342 (
1343 remote_refs,
1344 default_branch,
1345 None,
1346 advertised_peel,
1347 remote_head_oid,
1348 )
1349 };
1350 net_trace!(
1351 opts.network_trace,
1352 opts.diagnostics.as_ref(),
1353 "fetch_remote: remote advertised {} ref(s){}",
1354 remote_refs.len(),
1355 v2_caps
1356 .as_ref()
1357 .map(|_| " (via v2 ls-refs)")
1358 .unwrap_or(" (v0/v1 advertisement)")
1359 );
1360
1361 if opts.initial_remote_fetch
1362 && remote_refs.is_empty()
1363 && !crate::transport::connection_handshake_complete(conn)
1364 {
1365 return Err(Error::RemoteUnreadable {
1366 url: None,
1367 reason: crate::error::RemoteUnreadableReason::NoHandshake,
1368 });
1369 }
1370
1371 let conn_caps: Vec<String> = conn.capabilities().to_vec();
1372 let remote_algo = crate::transfer::remote_hash_algo_from_advertisement(
1373 &[v2_caps.as_deref().unwrap_or(&[]), conn_caps.as_slice()],
1374 &remote_refs,
1375 );
1376 let reopened =
1377 crate::transfer::sync_empty_destination_object_format(local_git_dir, remote_algo, opts)?
1378 .then(|| crate::transfer::reopen_after_format_change(local))
1379 .transpose()?;
1380 let local = reopened.as_ref().unwrap_or(local);
1381 let local_git_dir = local.git_dir();
1382 let local_odb = local.odb();
1383
1384 let crate::transfer::FetchMatchSet {
1385 mut matched,
1386 positive,
1387 negatives,
1388 explicit_refspec_fetch,
1389 } = crate::transfer::build_fetch_matched_set(opts, &remote_refs, remote_head_oid)?;
1390 let mut matched_oids: HashSet<ObjectId> = matched.iter().map(|m| m.oid).collect();
1391 let mut seen_remote_ref: HashSet<String> =
1392 matched.iter().map(|m| m.remote_ref.clone()).collect();
1393
1394 let tags_present = if opts.tags == crate::transfer::TagMode::Following {
1408 tags_already_local(local.refs(), &remote_refs)
1409 } else {
1410 HashSet::new()
1411 };
1412 let following_only = add_wire_tags(
1413 opts.tags,
1414 &remote_refs,
1415 &advertised_peel,
1416 &negatives,
1417 &tags_present,
1418 &mut matched,
1419 &mut matched_oids,
1420 &mut seen_remote_ref,
1421 );
1422 let fetch_commit_oids: HashSet<ObjectId> = matched
1423 .iter()
1424 .filter(|m| !m.is_tag)
1425 .map(|m| m.oid)
1426 .collect();
1427 let mut following_tag_wants =
1428 following_tag_wants_with_local_peel(&matched, &following_only, local_odb)?;
1429
1430 let local_shallow = crate::shallow::load_shallow_oids(local_git_dir)?;
1433 let shallow_request = opts.has_deepen_request() || !local_shallow.is_empty();
1434 if shallow_request {
1435 let local_shallow_set: HashSet<ObjectId> = local_shallow.iter().copied().collect();
1436 let peel_set = following_peel_commit_set_for_shallow(
1437 local_odb,
1438 &fetch_commit_oids,
1439 opts.absolute_depth_for_tags(),
1440 &local_shallow_set,
1441 )?;
1442 following_tag_wants.extend(following_tag_wants_peel_among_fetched_commits(
1443 &matched,
1444 &following_only,
1445 &peel_set,
1446 ));
1447 }
1448
1449 let wants: Vec<ObjectId> = if shallow_request {
1454 matched_oids
1455 .iter()
1456 .copied()
1457 .filter(|oid| !following_only.contains(oid) || following_tag_wants.contains(oid))
1458 .collect()
1459 } else {
1460 matched_oids
1461 .iter()
1462 .copied()
1463 .filter_map(|oid| {
1464 if following_only.contains(&oid) {
1465 return following_tag_wants.contains(&oid).then_some(Ok(oid));
1466 }
1467 match local_odb.contains(&oid) {
1468 Ok(true) => None,
1469 Ok(false) => Some(Ok(oid)),
1470 Err(err) => Some(Err(err)),
1471 }
1472 })
1473 .collect::<Result<_>>()?
1474 };
1475
1476 let mut shallow_update = ShallowUpdate::default();
1479 let mut pack_oids: HashSet<ObjectId> = HashSet::new();
1480 let mut transfer = crate::transfer::TransferStats::default();
1481
1482 net_trace!(
1483 opts.network_trace,
1484 opts.diagnostics.as_ref(),
1485 "fetch_remote: {} matched ref(s), want {} object(s){} [+{:?} since begin]",
1486 matched.len(),
1487 wants.len(),
1488 if shallow_request {
1489 " (shallow request)"
1490 } else {
1491 ""
1492 },
1493 t_begin.elapsed()
1494 );
1495
1496 let mut dry_run_quarantine: Option<crate::receive_quarantine::ReceiveQuarantine> = None;
1497
1498 let v0_caps = if v2_caps.is_none() {
1499 Some(conn.capabilities().join(" "))
1500 } else {
1501 None
1502 };
1503 let (wire_filter, _filter_sent) = crate::partial_clone::wire_filter_for_capabilities(
1504 opts.filter.as_ref(),
1505 v2_caps.as_deref(),
1506 v0_caps.as_deref(),
1507 );
1508 opts.mark_received_pack_promisor = opts.filter.is_some();
1511 let wire_filter = wire_filter.as_deref();
1512
1513 if wants.is_empty() {
1514 crate::transport::send_client_flush(conn)?;
1515 } else {
1516 net_trace!(
1517 opts.network_trace,
1518 opts.diagnostics.as_ref(),
1519 "fetch_remote: negotiating + fetching pack…"
1520 );
1521 let (pack_path, su) = if let Some(caps) = v2_caps.as_ref() {
1522 validate_fetch_depth_server(opts, None, Some(caps))?;
1523 let deepen = V2DeepenArgs::from_opts(opts, &local_shallow);
1524 negotiate_pack_v2(
1525 local,
1526 conn,
1527 caps,
1528 &wants,
1529 &deepen,
1530 wire_filter,
1531 progress,
1532 cancel,
1533 false,
1534 )?
1535 } else {
1536 negotiate_pack(
1537 local,
1538 conn,
1539 &wants,
1540 opts,
1541 &local_shallow,
1542 wire_filter,
1543 progress,
1544 cancel,
1545 false,
1546 )?
1547 };
1548 shallow_update = su;
1549 net_trace!(
1550 opts.network_trace,
1551 opts.diagnostics.as_ref(),
1552 "fetch_remote: received pack, ingesting via index-pack…"
1553 );
1554 if let Some(path) = pack_path {
1555 if opts.pack_spool_only {
1556 net_trace!(
1557 opts.network_trace,
1558 opts.diagnostics.as_ref(),
1559 "fetch_remote: pack spooled to {} (pack_spool_only)",
1560 path.display()
1561 );
1562 return Ok(FetchOutcome {
1563 updates: Vec::new(),
1564 default_branch: None,
1565 new_shallow: crate::shallow::dedupe_shallow_list(shallow_update.shallow),
1566 new_unshallow: shallow_update.unshallow,
1567 transfer: crate::transfer::TransferStats::default(),
1568 });
1569 }
1570 let pack_config =
1571 crate::config::ConfigSet::load(&opts.environment, Some(local_git_dir), true)?;
1572 let index_threads = crate::parallel::index_pack_threads_for_ingest(
1573 opts.thread_policy,
1574 &pack_config,
1575 opts.index_pack_threads,
1576 )?;
1577 if opts.dry_run && dry_run_quarantine.is_none() {
1578 dry_run_quarantine = Some(crate::receive_quarantine::ReceiveQuarantine::create(
1579 local_odb,
1580 local_git_dir,
1581 )?);
1582 }
1583 let ingested = if let Some(q) = dry_run_quarantine.as_ref() {
1584 let q_odb = q.odb();
1585 crate::index_pack::ingest_received_pack_path_with_progress(
1586 path,
1587 &q_odb,
1588 &crate::index_pack::IngestPackOptions {
1589 fix_thin: true,
1590 skip_post_index_verify: true,
1591 threads: index_threads,
1592 merged_config: Some(pack_config),
1593 environment: opts.environment.clone(),
1594 mark_promisor_pack: opts.mark_received_pack_promisor,
1595 trusted_index_rows: None,
1596 },
1597 Some(progress),
1598 Some(cancel),
1599 )?
1600 } else {
1601 crate::index_pack::ingest_received_pack_path_with_progress(
1602 path,
1603 local_odb,
1604 &crate::index_pack::IngestPackOptions {
1605 fix_thin: true,
1606 skip_post_index_verify: true,
1607 threads: index_threads,
1608 merged_config: Some(pack_config),
1609 environment: opts.environment.clone(),
1610 mark_promisor_pack: opts.mark_received_pack_promisor,
1611 trusted_index_rows: None,
1612 },
1613 Some(progress),
1614 Some(cancel),
1615 )?
1616 };
1617 transfer = crate::transfer::TransferStats::from_ingested_pack(&ingested);
1618 pack_oids = ingested.object_ids;
1619 }
1620 }
1621
1622 if !opts.dry_run {
1625 crate::shallow::apply_shallow_updates(
1626 local_git_dir,
1627 &shallow_update.shallow,
1628 &shallow_update.unshallow,
1629 )?;
1630 }
1631
1632 if v2_caps.is_some() && !wants.is_empty() {
1638 conn.finish_send();
1639 }
1640
1641 if opts.tags == crate::transfer::TagMode::Following {
1645 let t = std::time::Instant::now();
1646 let tag_odb = dry_run_quarantine
1647 .as_ref()
1648 .map(|q| q.odb())
1649 .unwrap_or_else(|| local_odb.clone());
1650 retain_following_tags(
1651 &tag_odb,
1652 &mut matched,
1653 &pack_oids,
1654 &crate::shallow::load_shallow_boundaries(local_git_dir),
1655 )?;
1656 net_trace!(
1657 opts.network_trace,
1658 opts.diagnostics.as_ref(),
1659 "fetch_remote: retain_following_tags {:?}",
1660 t.elapsed()
1661 );
1662 }
1663
1664 let t_apply = std::time::Instant::now();
1666 let classify_odb = dry_run_quarantine.as_ref().map(|q| q.odb());
1667 let classify_repo = (!opts.dry_run).then_some(local);
1668 let d_repo_open = t_apply.elapsed();
1669
1670 let mut updates: Vec<RefUpdate> = Vec::new();
1671
1672 let mut prune_batch: Vec<crate::refs::store::RefUpdate> = Vec::new();
1673 let t_prune = std::time::Instant::now();
1674 if opts.prune {
1675 prune_tracking_refs(
1676 local_git_dir,
1677 &positive,
1678 &remote_refs,
1679 opts.dry_run,
1680 &mut updates,
1681 if opts.dry_run {
1682 None
1683 } else {
1684 Some(&mut prune_batch)
1685 },
1686 )?;
1687 }
1688 if !opts.dry_run && !prune_batch.is_empty() {
1689 crate::refs::commit_ref_store_batch(local_git_dir, &prune_batch)?;
1690 }
1691 let d_prune = t_prune.elapsed();
1692
1693 let mut store_batch: Vec<crate::refs::store::RefUpdate> = Vec::new();
1694
1695 let t_packed = std::time::Instant::now();
1701 let packed = crate::refs::PackedRefs::load(local_git_dir)?;
1702 let d_packed = t_packed.elapsed();
1703
1704 let mut d_resolve = std::time::Duration::ZERO;
1707 let mut d_classify = std::time::Duration::ZERO;
1708 let mut d_write = std::time::Duration::ZERO;
1709 let mut n_written = 0usize;
1710 let mut n_ancestry = 0usize; for m in &matched {
1713 let Some(local_ref) = &m.local_ref else {
1714 updates.push(RefUpdate {
1715 remote_ref: m.remote_ref.clone(),
1716 local_ref: None,
1717 old_oid: None,
1718 new_oid: Some(m.oid),
1719 mode: UpdateMode::NoChangeNeeded,
1720 note: Some("not stored (empty destination)".to_owned()),
1721 });
1722 continue;
1723 };
1724
1725 let t = std::time::Instant::now();
1726 let old = crate::refs::resolve_ref_cached(local_git_dir, local_ref, &packed)
1727 .ok()
1728 .flatten();
1729 d_resolve += t.elapsed();
1730
1731 if old.as_ref().is_some_and(|o| o != &m.oid) {
1732 n_ancestry += 1;
1733 }
1734
1735 let t = std::time::Instant::now();
1736 let mode = classify_update(
1737 old.as_ref(),
1738 &m.oid,
1739 m.force,
1740 m.dest_is_tag,
1741 classify_repo,
1742 classify_odb.as_ref(),
1743 );
1744 d_classify += t.elapsed();
1745
1746 let write = matches!(
1747 mode,
1748 UpdateMode::New | UpdateMode::FastForward | UpdateMode::Forced
1749 );
1750 crate::transfer::enforce_fetch_ref_update(
1751 local_ref,
1752 old,
1753 m.oid,
1754 mode,
1755 opts,
1756 explicit_refspec_fetch,
1757 classify_repo,
1758 )?;
1759 if write && !opts.dry_run {
1760 if !crate::refs::is_valid_storable_ref_name(local_ref) {
1761 updates.push(RefUpdate {
1762 remote_ref: m.remote_ref.clone(),
1763 local_ref: Some(local_ref.clone()),
1764 old_oid: old,
1765 new_oid: Some(m.oid),
1766 mode,
1767 note: Some("skipped (invalid ref name)".to_owned()),
1768 });
1769 continue;
1770 }
1771 store_batch.push(crate::refs::store::RefUpdate {
1772 name: local_ref.clone(),
1773 new_value: Some(crate::refs::store::RawRef::Direct(m.oid)),
1774 expected: crate::refs::store::Expected::Any,
1775 reflog: None,
1776 flags: Default::default(),
1777 });
1778 n_written += 1;
1779 }
1780
1781 updates.push(RefUpdate {
1782 remote_ref: m.remote_ref.clone(),
1783 local_ref: Some(local_ref.clone()),
1784 old_oid: old,
1785 new_oid: Some(m.oid),
1786 mode,
1787 note: None,
1788 });
1789 }
1790
1791 if !opts.dry_run && !store_batch.is_empty() {
1792 let t = std::time::Instant::now();
1793 crate::transfer::apply_fetch_ref_updates(
1794 local_git_dir,
1795 &store_batch,
1796 &mut updates,
1797 crate::transfer::FetchRefRejectHint {
1798 prune: opts.prune,
1799 positive: &positive,
1800 remote_refs: &remote_refs,
1801 remote_name: opts.remote_name.as_deref(),
1802 },
1803 )?;
1804 d_write += t.elapsed();
1805 }
1806
1807 net_trace!(
1808 opts.network_trace,
1809 opts.diagnostics.as_ref(),
1810 "fetch_remote: apply {:?} — repo_open {:?}, prune {:?}, packed_load {:?}, \
1811 resolve {:?}, classify {:?} ({} ancestry-checked), write {:?} ({} written)",
1812 t_apply.elapsed(),
1813 d_repo_open,
1814 d_prune,
1815 d_packed,
1816 d_resolve,
1817 d_classify,
1818 n_ancestry,
1819 d_write,
1820 n_written
1821 );
1822
1823 net_trace!(
1824 opts.network_trace,
1825 opts.diagnostics.as_ref(),
1826 "fetch_remote: done — {} ref update(s){} [+{:?} total]",
1827 updates.len(),
1828 default_branch
1829 .as_deref()
1830 .map(|b| format!(", default branch '{b}'"))
1831 .unwrap_or_default(),
1832 t_begin.elapsed()
1833 );
1834 finish_initial_remote_fetch_layout(local_git_dir, opts, default_branch.as_deref())?;
1835 if let Some(remote) = opts.remote_name.as_deref() {
1836 ensure_remote_tracking_head_symref(
1837 local_git_dir,
1838 remote,
1839 default_branch.as_deref(),
1840 opts.dry_run,
1841 )?;
1842 }
1843 crate::transfer::finish_anonymous_url_fetch(
1844 local_git_dir,
1845 opts,
1846 explicit_refspec_fetch,
1847 default_branch.as_deref(),
1848 &remote_refs,
1849 &matched,
1850 )?;
1851 Ok(FetchOutcome {
1852 updates,
1853 default_branch,
1854 new_shallow: crate::shallow::dedupe_shallow_list(shallow_update.shallow),
1855 new_unshallow: shallow_update.unshallow,
1856 transfer,
1857 })
1858}
1859
1860fn fetch_remote_object_wants(
1862 local: &Repository,
1863 conn: &mut dyn Connection,
1864 opts: &mut FetchOptions,
1865 progress: &mut dyn Progress,
1866 cancel: &dyn Cancel,
1867) -> Result<FetchOutcome> {
1868 use crate::net_trace::net_trace;
1869
1870 let t_begin = std::time::Instant::now();
1871 let local_git_dir = local.git_dir();
1872 let local_odb = local.odb();
1873 let wants = local_odb.missing_stored_oids(opts.object_wants.iter().copied())?;
1874 if wants.is_empty() {
1875 return Ok(FetchOutcome {
1876 updates: Vec::new(),
1877 default_branch: None,
1878 new_shallow: Vec::new(),
1879 new_unshallow: Vec::new(),
1880 transfer: crate::transfer::TransferStats::default(),
1881 });
1882 }
1883
1884 let v2_caps = if conn.protocol_version() >= 2 {
1885 Some(conn.capabilities().to_vec())
1886 } else {
1887 None
1888 };
1889 let v0_caps = if v2_caps.is_none() {
1890 Some(conn.capabilities().join(" "))
1891 } else {
1892 None
1893 };
1894 let (wire_filter, _filter_sent) = crate::partial_clone::wire_filter_for_capabilities(
1895 opts.filter.as_ref(),
1896 v2_caps.as_deref(),
1897 v0_caps.as_deref(),
1898 );
1899 if opts.promisor_object_fetch {
1900 opts.mark_received_pack_promisor = true;
1901 } else {
1902 opts.mark_received_pack_promisor = opts.filter.is_some();
1903 }
1904 let wire_filter = wire_filter.as_deref();
1905 let local_shallow = crate::shallow::load_shallow_oids(local_git_dir)?;
1906 let mut transfer = crate::transfer::TransferStats::default();
1907
1908 net_trace!(
1909 opts.network_trace,
1910 opts.diagnostics.as_ref(),
1911 "fetch_remote_object_wants: {} object(s) [+{:?} since begin]",
1912 wants.len(),
1913 t_begin.elapsed()
1914 );
1915
1916 let (pack_path, shallow_update) = if let Some(caps) = v2_caps.as_ref() {
1917 validate_fetch_depth_server(opts, None, Some(caps))?;
1918 let deepen = V2DeepenArgs::from_opts(opts, &local_shallow);
1919 negotiate_pack_v2(
1920 local,
1921 conn,
1922 caps,
1923 &wants,
1924 &deepen,
1925 wire_filter,
1926 progress,
1927 cancel,
1928 true,
1929 )?
1930 } else {
1931 negotiate_pack(
1932 local,
1933 conn,
1934 &wants,
1935 opts,
1936 &local_shallow,
1937 wire_filter,
1938 progress,
1939 cancel,
1940 true,
1941 )?
1942 };
1943 if let Some(path) = pack_path {
1944 let pack_config =
1945 crate::config::ConfigSet::load(&opts.environment, Some(local_git_dir), true)?;
1946 let index_threads = crate::parallel::index_pack_threads_for_ingest(
1947 opts.thread_policy,
1948 &pack_config,
1949 opts.index_pack_threads,
1950 )?;
1951 let ingested = crate::index_pack::ingest_received_pack_path_with_progress(
1952 path,
1953 local_odb,
1954 &crate::index_pack::IngestPackOptions {
1955 fix_thin: true,
1956 skip_post_index_verify: true,
1957 threads: index_threads,
1958 merged_config: Some(pack_config),
1959 environment: opts.environment.clone(),
1960 mark_promisor_pack: opts.mark_received_pack_promisor,
1961 trusted_index_rows: None,
1962 },
1963 Some(progress),
1964 Some(cancel),
1965 )?;
1966 transfer = crate::transfer::TransferStats::from_ingested_pack(&ingested);
1967 }
1968
1969 if v2_caps.is_some() && !wants.is_empty() {
1970 conn.finish_send();
1971 }
1972
1973 if !opts.dry_run {
1974 crate::shallow::apply_shallow_updates(
1975 local_git_dir,
1976 &shallow_update.shallow,
1977 &shallow_update.unshallow,
1978 )?;
1979 }
1980
1981 Ok(FetchOutcome {
1982 updates: Vec::new(),
1983 default_branch: None,
1984 new_shallow: crate::shallow::dedupe_shallow_list(shallow_update.shallow),
1985 new_unshallow: shallow_update.unshallow,
1986 transfer,
1987 })
1988}
1989
1990pub fn fetch_operation_identity(
1992 env: &crate::environment::Environment,
1993 git_dir: &Path,
1994) -> Result<crate::ident::Signature> {
1995 use crate::ident_config::ident_default_name;
1996 let cfg = crate::config::ConfigSet::load(env, Some(git_dir), true)?;
1997 let name = ident_default_name(&cfg, env);
1998 let email = cfg
1999 .get("user.email")
2000 .filter(|e| !e.trim().is_empty())
2001 .unwrap_or_else(|| "unknown".to_owned());
2002 let now = time::OffsetDateTime::now_utc();
2003 crate::ident::Signature::new(name, email, now).map_err(crate::error::Error::InvalidSignature)
2004}
2005
2006pub(crate) fn ensure_remote_tracking_head_symref(
2008 git_dir: &Path,
2009 remote: &str,
2010 default_branch: Option<&str>,
2011 dry_run: bool,
2012) -> Result<()> {
2013 if dry_run {
2014 return Ok(());
2015 }
2016 let Some(branch) = default_branch else {
2017 return Ok(());
2018 };
2019 let head_ref = format!("refs/remotes/{remote}/HEAD");
2020 if crate::refs::resolve_ref(git_dir, &head_ref).is_ok() {
2021 return Ok(());
2022 }
2023 let target = format!("refs/remotes/{remote}/{branch}");
2024 if crate::refs::resolve_ref(git_dir, &target).is_err() {
2025 return Ok(());
2026 }
2027 crate::refs::write_symbolic_ref(git_dir, &head_ref, &target)
2028}
2029
2030pub(crate) fn finish_initial_remote_fetch_layout(
2031 git_dir: &Path,
2032 opts: &FetchOptions,
2033 default_branch: Option<&str>,
2034) -> Result<()> {
2035 if opts.dry_run || !opts.initial_remote_fetch {
2036 return Ok(());
2037 }
2038 let Some(remote) = opts.remote_name.as_deref() else {
2039 return Ok(());
2040 };
2041 crate::refs::pack_remote_tracking_refs_for_clone(git_dir, remote)?;
2042 if let Some(branch) = default_branch {
2043 let target = format!("refs/remotes/{remote}/{branch}");
2044 let head_ref = format!("refs/remotes/{remote}/HEAD");
2045 if crate::refs::resolve_ref(git_dir, &target).is_ok() {
2046 crate::refs::write_symbolic_ref(git_dir, &head_ref, &target)?;
2047 if let Some(ref log) = opts.clone_reflog {
2048 let null = ObjectId::null(open_odb(git_dir).hash_algo());
2049 let tip = crate::refs::resolve_ref(git_dir, &target).unwrap_or(null);
2050 crate::refs::append_reflog_with_signature(
2051 git_dir,
2052 &head_ref,
2053 &null,
2054 &tip,
2055 &log.identity,
2056 &log.message,
2057 true,
2058 )?;
2059 }
2060 }
2061 }
2062 Ok(())
2063}
2064
2065#[allow(clippy::too_many_arguments)]
2082pub(crate) fn add_wire_tags(
2083 mode: crate::transfer::TagMode,
2084 remote_refs: &[(String, ObjectId)],
2085 advertised_peel: &std::collections::HashMap<String, ObjectId>,
2086 negatives: &[RefspecItem],
2087 already_local: &HashSet<String>,
2088 matched: &mut Vec<crate::transfer::MatchedRef>,
2089 matched_oids: &mut HashSet<ObjectId>,
2090 seen_remote_ref: &mut HashSet<String>,
2091) -> HashSet<ObjectId> {
2092 let mut following_only: HashSet<ObjectId> = HashSet::new();
2093 if mode == crate::transfer::TagMode::None {
2094 return following_only;
2095 }
2096 for (name, oid) in remote_refs {
2097 if !name.starts_with("refs/tags/") {
2098 continue;
2099 }
2100 if seen_remote_ref.contains(name) || ref_excluded(name, negatives) {
2101 continue;
2102 }
2103 if mode == crate::transfer::TagMode::Following && already_local.contains(name) {
2104 continue;
2105 }
2106 seen_remote_ref.insert(name.clone());
2107 if matched_oids.insert(*oid) && mode == crate::transfer::TagMode::Following {
2111 following_only.insert(*oid);
2112 }
2113 matched.push(crate::transfer::MatchedRef {
2114 remote_ref: name.clone(),
2115 local_ref: Some(name.clone()),
2116 oid: *oid,
2117 force: false,
2118 is_tag: true,
2119 dest_is_tag: true,
2120 advertised_peel: advertised_peel.get(name).copied(),
2121 fetch_head: crate::fetch_request::FetchHeadPolicy::ForMerge,
2122 });
2123 }
2124 following_only
2125}
2126
2127pub(crate) fn following_tag_wants_with_local_peel(
2130 matched: &[crate::transfer::MatchedRef],
2131 following_only: &HashSet<ObjectId>,
2132 local_odb: &crate::odb::Odb,
2133) -> Result<HashSet<ObjectId>> {
2134 let mut wants = HashSet::new();
2135 for m in matched {
2136 if !m.is_tag || !following_only.contains(&m.oid) {
2137 continue;
2138 }
2139 let Some(peel) = m.advertised_peel else {
2140 continue;
2141 };
2142 if local_odb.contains(&peel)? && !local_odb.contains(&m.oid)? {
2143 wants.insert(m.oid);
2144 }
2145 }
2146 Ok(wants)
2147}
2148
2149pub(crate) fn shallow_fetched_commit_oids(
2153 odb: &crate::odb::Odb,
2154 roots: &[ObjectId],
2155 depth: u32,
2156 shallow_boundaries: &HashSet<ObjectId>,
2157) -> Result<HashSet<ObjectId>> {
2158 use crate::objects::{parse_commit, ObjectKind};
2159 use std::collections::{HashMap, VecDeque};
2160
2161 if depth == 0 {
2162 return Ok(HashSet::new());
2163 }
2164 let max_depth = usize::try_from(depth).unwrap_or(usize::MAX);
2165 let mut best_depth: HashMap<ObjectId, usize> = HashMap::new();
2166 let mut q: VecDeque<(ObjectId, usize)> = VecDeque::new();
2167 for &root in roots {
2168 best_depth.insert(root, 1);
2169 q.push_back((root, 1));
2170 }
2171 let mut out = HashSet::new();
2172 while let Some((oid, d)) = q.pop_front() {
2173 if d > max_depth {
2174 continue;
2175 }
2176 if best_depth.get(&oid).copied() != Some(d) {
2177 continue;
2178 }
2179 out.insert(oid);
2180 if d == max_depth {
2181 continue;
2182 }
2183 if shallow_boundaries.contains(&oid) {
2184 continue;
2185 }
2186 if !odb.contains(&oid)? {
2187 continue;
2188 }
2189 let Ok(obj) = odb.read(&oid) else {
2190 continue;
2191 };
2192 if obj.kind != ObjectKind::Commit {
2193 continue;
2194 }
2195 let Ok(c) = parse_commit(&obj.data) else {
2196 continue;
2197 };
2198 for p in c.parents {
2199 let nd = d.saturating_add(1);
2200 if nd > max_depth {
2201 continue;
2202 }
2203 let prev = best_depth.get(&p).copied().unwrap_or(usize::MAX);
2204 if nd < prev {
2205 best_depth.insert(p, nd);
2206 q.push_back((p, nd));
2207 }
2208 }
2209 }
2210 Ok(out)
2211}
2212
2213pub(crate) fn following_peel_commit_set_for_shallow(
2216 odb: &crate::odb::Odb,
2217 commit_tips: &HashSet<ObjectId>,
2218 depth: Option<u32>,
2219 shallow_boundaries: &HashSet<ObjectId>,
2220) -> Result<HashSet<ObjectId>> {
2221 let roots: Vec<ObjectId> = commit_tips.iter().copied().collect();
2222 match depth.filter(|d| *d > 0) {
2223 Some(d) => shallow_fetched_commit_oids(odb, &roots, d, shallow_boundaries),
2224 None => Ok(commit_tips.clone()),
2225 }
2226}
2227
2228pub(crate) fn following_tag_wants_peel_among_fetched_commits(
2232 matched: &[crate::transfer::MatchedRef],
2233 following_only: &HashSet<ObjectId>,
2234 fetch_commit_oids: &HashSet<ObjectId>,
2235) -> HashSet<ObjectId> {
2236 let mut wants = HashSet::new();
2237 for m in matched {
2238 if !m.is_tag || !following_only.contains(&m.oid) {
2239 continue;
2240 }
2241 let Some(peel) = m.advertised_peel else {
2242 continue;
2243 };
2244 if peel != m.oid && fetch_commit_oids.contains(&peel) {
2245 wants.insert(m.oid);
2246 }
2247 }
2248 wants
2249}
2250
2251pub(crate) fn retain_following_tags(
2268 local_odb: &crate::odb::Odb,
2269 matched: &mut Vec<crate::transfer::MatchedRef>,
2270 pack_oids: &HashSet<ObjectId>,
2271 shallow_boundaries: &HashSet<ObjectId>,
2272) -> Result<()> {
2273 if !matched.iter().any(|m| m.is_tag) {
2274 return Ok(());
2275 }
2276
2277 let roots: Vec<ObjectId> = matched
2278 .iter()
2279 .filter(|m| !m.is_tag)
2280 .map(|m| m.oid)
2281 .collect();
2282 let peeled_by_tag: std::collections::HashMap<ObjectId, ObjectId> = matched
2283 .iter()
2284 .filter(|m| m.is_tag)
2285 .map(|m| {
2286 let peeled = m.advertised_peel.unwrap_or_else(|| {
2287 local_odb
2288 .peel_stored_tag_target(m.oid)
2289 .ok()
2290 .flatten()
2291 .unwrap_or(m.oid)
2292 });
2293 (m.oid, peeled)
2294 })
2295 .collect();
2296 let mut local_commits: HashSet<ObjectId> = HashSet::new();
2297 let mut local_trees_and_blobs: HashSet<ObjectId> = HashSet::new();
2298 for &oid in peeled_by_tag.values() {
2299 if pack_oids.contains(&oid) || !local_odb.contains(&oid)? {
2302 continue;
2303 }
2304 match local_odb.read_info(&oid)?.kind {
2305 crate::objects::ObjectKind::Tree | crate::objects::ObjectKind::Blob => {
2306 local_trees_and_blobs.insert(oid);
2307 }
2308 crate::objects::ObjectKind::Commit => {
2309 local_commits.insert(oid);
2310 }
2311 crate::objects::ObjectKind::Tag => {}
2313 }
2314 }
2315 let local_reach = reachable_commits(local_odb, &roots, &local_commits, shallow_boundaries)?;
2316 let mut keep = Vec::with_capacity(matched.len());
2317 for m in matched.iter() {
2318 if !m.is_tag {
2319 keep.push(true);
2320 continue;
2321 }
2322 let peeled = peeled_by_tag.get(&m.oid).copied().unwrap_or(m.oid);
2323 let reachable = pack_oids.contains(&peeled)
2324 || local_trees_and_blobs.contains(&peeled)
2325 || local_reach.contains(&peeled);
2326 keep.push(
2329 reachable
2330 && (m.oid == peeled || pack_oids.contains(&m.oid) || local_odb.contains(&m.oid)?),
2331 );
2332 }
2333 let mut keep = keep.into_iter();
2334 matched.retain(|_| keep.next().unwrap_or(false));
2335 Ok(())
2336}
2337
2338pub(crate) fn tags_already_local(
2356 local_refs: &dyn crate::refs::store::RefStore,
2357 remote_refs: &[(String, ObjectId)],
2358) -> HashSet<String> {
2359 remote_refs
2360 .iter()
2361 .filter(|(name, oid)| {
2362 name.starts_with("refs/tags/") && local_refs.resolve(name).ok() == Some(*oid)
2363 })
2364 .map(|(name, _)| name.clone())
2365 .collect()
2366}
2367
2368pub(crate) fn reachable_commits(
2379 odb: &crate::odb::Odb,
2380 roots: &[ObjectId],
2381 targets: &HashSet<ObjectId>,
2382 shallow_boundaries: &HashSet<ObjectId>,
2383) -> Result<HashSet<ObjectId>> {
2384 use crate::objects::{parse_commit, parse_tag, ObjectKind};
2385
2386 let mut seen: HashSet<ObjectId> = HashSet::new();
2387 let mut remaining: HashSet<ObjectId> = targets.clone();
2388 if remaining.is_empty() {
2389 return Ok(seen);
2390 }
2391
2392 let graph = crate::commit_graph_file::CommitGraphChain::load(odb.objects_dir());
2397
2398 let mut stack: Vec<ObjectId> = roots.to_vec();
2399 while let Some(oid) = stack.pop() {
2400 if remaining.is_empty() {
2401 break;
2402 }
2403 if seen.contains(&oid) {
2404 continue;
2405 }
2406 if shallow_boundaries.contains(&oid) {
2407 if odb.contains(&oid)? {
2408 seen.insert(oid);
2409 remaining.remove(&oid);
2410 }
2411 continue;
2412 }
2413 if let Some(graph) = graph.as_ref() {
2414 if let Some((parents, _time)) = graph.graph_commit(&oid) {
2415 seen.insert(oid);
2416 remaining.remove(&oid);
2417 stack.extend(parents);
2418 continue;
2419 }
2420 }
2421 let Ok(obj) = odb.read(&oid) else {
2422 continue;
2423 };
2424 seen.insert(oid);
2425 remaining.remove(&oid);
2426 match obj.kind {
2427 ObjectKind::Commit => {
2428 if let Ok(c) = parse_commit(&obj.data) {
2429 for p in c.parents {
2430 stack.push(p);
2431 }
2432 }
2433 }
2434 ObjectKind::Tag => {
2435 if let Ok(t) = parse_tag(&obj.data) {
2436 stack.push(t.object);
2437 }
2438 }
2439 ObjectKind::Tree | ObjectKind::Blob => {}
2441 }
2442 }
2443 Ok(seen)
2444}
2445
2446#[cfg(test)]
2447mod fetch_advertised_ref_tests {
2448 use super::*;
2449 use crate::objects::ObjectId;
2450 use std::io::Cursor;
2451
2452 fn linear_history(len: usize) -> (tempfile::TempDir, Vec<ObjectId>) {
2453 use crate::objects::ObjectKind;
2454 let dir = tempfile::tempdir().unwrap();
2455 let repo = crate::repo::init_repository(
2456 dir.path(),
2457 true,
2458 "main",
2459 None,
2460 crate::RefStorageFormat::Files,
2461 )
2462 .unwrap();
2463 let odb = repo.odb();
2464 let tree = odb.write(ObjectKind::Tree, b"").unwrap();
2465 let mut commits: Vec<ObjectId> = Vec::new();
2466 for i in 0..len {
2467 let mut body = format!("tree {tree}\n");
2468 if let Some(parent) = commits.last() {
2469 body.push_str(&format!("parent {parent}\n"));
2470 }
2471 let time = 1_000_000 + i;
2472 body.push_str(&format!(
2473 "author T <t@e.com> {time} +0000\ncommitter T <t@e.com> {time} +0000\n\nc{i}\n"
2474 ));
2475 commits.push(odb.write(ObjectKind::Commit, body.as_bytes()).unwrap());
2476 }
2477 let tip = commits.last().copied().unwrap();
2478 crate::refs::write_ref(repo.git_dir(), "refs/heads/main", &tip).unwrap();
2479 (dir, commits)
2480 }
2481
2482 #[test]
2483 fn v2_local_haves_are_produced_only_as_far_as_asked() {
2484 let (dir, commits) = linear_history(400);
2485 let unrelated_want = ObjectId::from_bytes(&[9u8; 20]).unwrap();
2486 let local = Repository::open(dir.path(), &RepositoryOptions::empty()).unwrap();
2487
2488 let mut drained = v2_local_haves(&local, &[unrelated_want]).unwrap();
2489 let all = drained.all().unwrap().to_vec();
2490 assert!(drained.exhausted);
2491 assert_eq!(all[0], *commits.last().unwrap());
2492 assert!(
2493 all.len() > 4,
2494 "skipping still offers several haves: {}",
2495 all.len()
2496 );
2497
2498 let mut lazy = v2_local_haves(&local, &[unrelated_want]).unwrap();
2499 let first = lazy.first(4).unwrap().to_vec();
2500 assert_eq!(first.as_slice(), &all[..4]);
2501 assert_eq!(lazy.taken.len(), 4, "only the requested haves are walked");
2502 assert!(!lazy.exhausted);
2503 assert_eq!(lazy.all().unwrap(), all.as_slice());
2504 }
2505
2506 #[test]
2507 fn v2_local_haves_first_stops_at_a_short_history() {
2508 let (dir, _) = linear_history(3);
2509 let want = ObjectId::from_bytes(&[9u8; 20]).unwrap();
2510 let local = Repository::open(dir.path(), &RepositoryOptions::empty()).unwrap();
2511 let all = v2_local_haves(&local, &[want])
2512 .unwrap()
2513 .all()
2514 .unwrap()
2515 .to_vec();
2516 let mut haves = v2_local_haves(&local, &[want]).unwrap();
2517 assert_eq!(haves.first(INITIAL_FLUSH).unwrap(), all.as_slice());
2518 assert!(haves.exhausted);
2519 }
2520
2521 #[test]
2522 fn fetch_ref_list_prefixes_includes_refs_dwim_for_pull() {
2523 let specs = vec!["pull/123/head:pr-123".to_owned()];
2524 let prefixes = fetch_ref_list_prefixes(&specs);
2525 assert!(prefixes.iter().any(|p| p == "refs/pull/123/head"));
2526 assert!(prefixes.iter().any(|p| p == "refs/heads/"));
2527 }
2528
2529 #[test]
2530 fn build_v2_ls_refs_request_includes_unborn_when_advertised() {
2531 use crate::odb::Odb;
2532 use crate::transfer::TagMode;
2533
2534 let caps = vec!["ls-refs=unborn".to_owned()];
2535 let odb = Odb::new(std::path::Path::new("/dev/null"));
2536 let req = build_v2_ls_refs_request(&caps, &odb, TagMode::None, &[]).unwrap();
2537 let text = String::from_utf8_lossy(&req);
2538 assert!(
2539 text.contains("unborn"),
2540 "request must ask for unborn HEAD:\n{text}"
2541 );
2542 }
2543
2544 #[test]
2545 fn v2_acknowledgments_collect_acked_commits_and_ready() {
2546 let a = ObjectId::from_hex("aabbccddeeff00112233445566778899aabbccdd").unwrap();
2547 let b = ObjectId::from_hex("00112233445566778899aabbccddeeff00112233").unwrap();
2548 let mut body = Vec::new();
2549 crate::pkt_line::write_line(&mut body, "acknowledgments").unwrap();
2550 crate::pkt_line::write_line(&mut body, &format!("ACK {a}")).unwrap();
2551 crate::pkt_line::write_line(&mut body, &format!("ACK {b}")).unwrap();
2552 crate::pkt_line::write_line(&mut body, "ready").unwrap();
2553 crate::pkt_line::write_delim(&mut body).unwrap();
2554
2555 let round = read_v2_acknowledgments(&mut Cursor::new(body), false)
2556 .unwrap()
2557 .unwrap();
2558 assert!(round.ready);
2559 assert_eq!(round.common, vec![a, b]);
2560 }
2561
2562 #[test]
2563 fn v2_acknowledgments_nak_has_no_common() {
2564 let mut body = Vec::new();
2565 crate::pkt_line::write_line(&mut body, "acknowledgments").unwrap();
2566 crate::pkt_line::write_line(&mut body, "NAK").unwrap();
2567 crate::pkt_line::write_flush(&mut body).unwrap();
2568
2569 let round = read_v2_acknowledgments(&mut Cursor::new(body), false)
2570 .unwrap()
2571 .unwrap();
2572 assert!(!round.ready);
2573 assert!(round.common.is_empty());
2574 }
2575
2576 #[test]
2577 fn v2_acknowledgments_reject_malformed_ack() {
2578 let mut body = Vec::new();
2579 crate::pkt_line::write_line(&mut body, "acknowledgments").unwrap();
2580 crate::pkt_line::write_line(&mut body, "ACK not-an-oid").unwrap();
2581 crate::pkt_line::write_flush(&mut body).unwrap();
2582
2583 assert!(read_v2_acknowledgments(&mut Cursor::new(body), false).is_err());
2584 }
2585
2586 #[test]
2587 fn v2_flush_schedule_doubles_then_grows_by_a_tenth() {
2588 let mut sizes = vec![INITIAL_FLUSH];
2589 while sizes.len() < 6 {
2590 let last = *sizes.last().unwrap();
2591 sizes.push(next_v2_flush_count(last));
2592 }
2593 assert_eq!(sizes, vec![16, 32, 64, 128, 256, 512]);
2594 assert_eq!(
2595 next_v2_flush_count(LARGE_FLUSH),
2596 LARGE_FLUSH + LARGE_FLUSH / 10
2597 );
2598 }
2599
2600 #[test]
2601 fn parse_v2_ls_refs_unborn_head_symref_line() {
2602 let mut body = Vec::new();
2603 crate::pkt_line::write_line(&mut body, "unborn HEAD symref-target:refs/heads/trunk")
2604 .unwrap();
2605 crate::pkt_line::write_flush(&mut body).unwrap();
2606
2607 let (_, head, _, _) = parse_v2_ls_refs_response(&mut Cursor::new(body)).unwrap();
2608 assert_eq!(head.as_deref(), Some("refs/heads/trunk"));
2609 }
2610
2611 #[test]
2612 fn parse_v2_ls_refs_drops_traversal_ref() {
2613 let oid = ObjectId::from_hex("aabbccddeeff00112233445566778899aabbccdd").unwrap();
2614 let line = format!("{oid} refs/heads/../../../config");
2615 let mut body = Vec::new();
2616 crate::pkt_line::write_line(&mut body, &line).unwrap();
2617 crate::pkt_line::write_flush(&mut body).unwrap();
2618
2619 let (refs, _, _, _) = parse_v2_ls_refs_response(&mut Cursor::new(body)).unwrap();
2620 assert!(refs.is_empty());
2621 }
2622
2623 #[test]
2624 fn refspec_maps_traversal_to_non_storable_local_name() {
2625 use crate::refs::is_valid_storable_ref_name;
2626 use crate::refspec::parse_fetch_refspec;
2627 use crate::transfer::match_positive;
2628
2629 let spec = parse_fetch_refspec("+refs/heads/*:refs/remotes/origin/*").unwrap();
2630 let positive = vec![spec];
2631 let local = match_positive("refs/heads/../../../config", &positive)
2632 .flatten()
2633 .expect("wildcard matches malicious remote name");
2634 assert!(!is_valid_storable_ref_name(&local));
2635 }
2636
2637 #[test]
2638 fn add_wire_tags_following_skips_following_only_when_oid_already_matched() {
2639 use crate::transfer::TagMode;
2640 use std::collections::{HashMap, HashSet};
2641
2642 let shared = ObjectId::from_hex("4eda3c62a0f0a0a0a0a0a0a0a0a0a0a0a0a0a0a0").unwrap();
2643 let unreachable_tag =
2644 ObjectId::from_hex("1111111111111111111111111111111111111111").unwrap();
2645 let remote_refs = vec![
2646 ("refs/heads/rel".to_owned(), shared),
2647 ("refs/tags/light".to_owned(), shared),
2648 ("refs/tags/orphan".to_owned(), unreachable_tag),
2649 ];
2650 let advertised_peel = HashMap::new();
2651 let negatives: Vec<crate::refspec::RefspecItem> = Vec::new();
2652 let mut matched = Vec::new();
2653 let mut matched_oids = HashSet::from([shared]);
2654 let mut seen = HashSet::from(["refs/heads/rel".to_owned()]);
2655 matched.push(crate::transfer::MatchedRef {
2656 remote_ref: "refs/heads/rel".to_owned(),
2657 local_ref: Some("refs/remotes/origin/rel".to_owned()),
2658 oid: shared,
2659 force: false,
2660 is_tag: false,
2661 dest_is_tag: false,
2662 advertised_peel: None,
2663 fetch_head: crate::fetch_request::FetchHeadPolicy::ForMerge,
2664 });
2665
2666 let following_only = add_wire_tags(
2667 TagMode::Following,
2668 &remote_refs,
2669 &advertised_peel,
2670 &negatives,
2671 &HashSet::new(),
2672 &mut matched,
2673 &mut matched_oids,
2674 &mut seen,
2675 );
2676
2677 assert!(
2678 !following_only.contains(&shared),
2679 "shared branch/tag tip must stay wanted"
2680 );
2681 assert!(
2682 following_only.contains(&unreachable_tag),
2683 "tag-only oid stays following-only (no upfront want)"
2684 );
2685 assert_eq!(matched.iter().filter(|m| m.is_tag).count(), 2);
2686 }
2687
2688 #[test]
2689 fn following_tag_wants_includes_annotated_tag_when_peel_is_local() {
2690 use std::collections::HashSet;
2691
2692 let tag_obj = ObjectId::from_hex("bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb").unwrap();
2693 let tmp = tempfile::tempdir().expect("tempdir");
2694 let git_dir = tmp.path().join(".git");
2695 std::fs::create_dir_all(git_dir.join("objects")).expect("objects");
2696 let odb = crate::odb::Odb::new(git_dir.join("objects").as_path());
2697 let commit_bytes = b"tree 0000000000000000000000000000000000000000\nauthor T <t@example.com> 0 +0000\ncommitter T <t@example.com> 0 +0000\n\nx\n";
2699 let commit = odb
2700 .write_loose_materialize(crate::objects::ObjectKind::Commit, commit_bytes)
2701 .expect("write commit");
2702
2703 let following_only = HashSet::from([tag_obj]);
2704 let matched = vec![crate::transfer::MatchedRef {
2705 remote_ref: "refs/tags/v".to_owned(),
2706 local_ref: Some("refs/tags/v".to_owned()),
2707 oid: tag_obj,
2708 force: false,
2709 is_tag: true,
2710 dest_is_tag: true,
2711 advertised_peel: Some(commit),
2712 fetch_head: crate::fetch_request::FetchHeadPolicy::ForMerge,
2713 }];
2714 let wants = following_tag_wants_with_local_peel(&matched, &following_only, &odb).unwrap();
2715 assert!(wants.contains(&tag_obj));
2716 }
2717
2718 #[test]
2719 fn following_tag_wants_peel_among_fetched_commits_before_peel_is_local() {
2720 use std::collections::HashSet;
2721
2722 let tag_obj = ObjectId::from_hex("bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb").unwrap();
2723 let tip = ObjectId::from_hex("cccccccccccccccccccccccccccccccccccccccc").unwrap();
2724 let following_only = HashSet::from([tag_obj]);
2725 let fetch_commits = HashSet::from([tip]);
2726 let matched = vec![crate::transfer::MatchedRef {
2727 remote_ref: "refs/tags/v".to_owned(),
2728 local_ref: Some("refs/tags/v".to_owned()),
2729 oid: tag_obj,
2730 force: false,
2731 is_tag: true,
2732 dest_is_tag: true,
2733 advertised_peel: Some(tip),
2734 fetch_head: crate::fetch_request::FetchHeadPolicy::ForMerge,
2735 }];
2736 let wants = following_tag_wants_peel_among_fetched_commits(
2737 &matched,
2738 &following_only,
2739 &fetch_commits,
2740 );
2741 assert!(wants.contains(&tag_obj));
2742 }
2743
2744 #[test]
2745 fn shallow_peel_set_includes_parent_at_depth_two_not_tip_only() {
2746 use std::collections::HashSet;
2747
2748 let tmp = tempfile::tempdir().expect("tempdir");
2749 let objects = tmp.path().join("objects");
2750 std::fs::create_dir_all(&objects).expect("objects");
2751 let odb = crate::odb::Odb::new(objects.as_path());
2752 let chain = write_commit_chain(&odb, 3);
2753 let tip = *chain.last().expect("tip");
2754 let parent = chain[1];
2755 let tips = HashSet::from([tip]);
2756 let tips_only =
2757 following_peel_commit_set_for_shallow(&odb, &tips, Some(1), &HashSet::new()).unwrap();
2758 assert!(!tips_only.contains(&parent));
2759 let depth_two =
2760 following_peel_commit_set_for_shallow(&odb, &tips, Some(2), &HashSet::new()).unwrap();
2761 assert!(depth_two.contains(&parent));
2762 }
2763
2764 fn write_commit_chain(odb: &crate::odb::Odb, n: usize) -> Vec<ObjectId> {
2766 let mut chain: Vec<ObjectId> = Vec::new();
2767 for i in 0..n {
2768 let parent = chain
2769 .last()
2770 .map(|p| format!("parent {}\n", p.to_hex()))
2771 .unwrap_or_default();
2772 let body = format!(
2773 "tree 4b825dc642cb6eb9a060e54bf8d69288fbee4904\n{parent}\
2774 author T <t@example.com> {i} +0000\ncommitter T <t@example.com> {i} +0000\n\nc{i}\n"
2775 );
2776 let oid = odb
2777 .write_loose_materialize(crate::objects::ObjectKind::Commit, body.as_bytes())
2778 .expect("write commit");
2779 chain.push(oid);
2780 }
2781 chain
2782 }
2783
2784 fn matched_ref(
2785 name: &str,
2786 oid: ObjectId,
2787 peel: Option<ObjectId>,
2788 ) -> crate::transfer::MatchedRef {
2789 let is_tag = name.starts_with("refs/tags/");
2790 crate::transfer::MatchedRef {
2791 remote_ref: name.to_owned(),
2792 local_ref: Some(name.to_owned()),
2793 oid,
2794 force: false,
2795 is_tag,
2796 dest_is_tag: is_tag,
2797 advertised_peel: peel,
2798 fetch_head: crate::fetch_request::FetchHeadPolicy::ForMerge,
2799 }
2800 }
2801
2802 fn kept_tags(matched: &[crate::transfer::MatchedRef]) -> Vec<&str> {
2803 let mut names: Vec<&str> = matched
2804 .iter()
2805 .filter(|m| m.is_tag)
2806 .map(|m| m.remote_ref.as_str())
2807 .collect();
2808 names.sort_unstable();
2809 names
2810 }
2811
2812 #[test]
2813 fn retain_following_tags_keeps_delivered_targets_without_reading_objects() {
2814 use std::collections::HashSet;
2815
2816 let tmp = tempfile::tempdir().expect("tempdir");
2819 let objects = tmp.path().join("objects");
2820 std::fs::create_dir_all(&objects).expect("objects");
2821 let odb = crate::odb::Odb::new(&objects);
2822 let oid = |c: char| ObjectId::from_hex(&c.to_string().repeat(40)).expect("oid");
2823 let (tip, tree, blob, tag_obj, missing_tag_obj, absent) =
2824 (oid('1'), oid('2'), oid('3'), oid('4'), oid('5'), oid('6'));
2825 let pack_oids = HashSet::from([tip, tree, blob, tag_obj]);
2826 let mut matched = vec![
2827 matched_ref("refs/heads/main", tip, None),
2828 matched_ref("refs/tags/light", tip, None),
2829 matched_ref("refs/tags/blob", blob, None),
2830 matched_ref("refs/tags/annotated-tree", tag_obj, Some(tree)),
2831 matched_ref("refs/tags/no-tag-object", missing_tag_obj, Some(tip)),
2832 matched_ref("refs/tags/undelivered", absent, None),
2833 ];
2834
2835 retain_following_tags(&odb, &mut matched, &pack_oids, &HashSet::new())
2836 .expect("retain following tags");
2837
2838 assert_eq!(
2839 kept_tags(&matched),
2840 [
2841 "refs/tags/annotated-tree",
2842 "refs/tags/blob",
2843 "refs/tags/light"
2844 ]
2845 );
2846 assert!(matched.iter().any(|m| m.remote_ref == "refs/heads/main"));
2847 }
2848
2849 #[test]
2850 fn retain_following_tags_walks_commits_only_for_targets_already_local() {
2851 use std::collections::HashSet;
2852
2853 let tmp = tempfile::tempdir().expect("tempdir");
2854 let objects = tmp.path().join("objects");
2855 std::fs::create_dir_all(&objects).expect("objects");
2856 let odb = crate::odb::Odb::new(&objects);
2857 let chain = write_commit_chain(&odb, 4);
2858 let tip = chain[3];
2859 let unrelated = odb
2861 .write_loose_materialize(
2862 crate::objects::ObjectKind::Commit,
2863 b"tree 4b825dc642cb6eb9a060e54bf8d69288fbee4904\n\
2864 author T <t@example.com> 9 +0000\ncommitter T <t@example.com> 9 +0000\n\nu\n",
2865 )
2866 .expect("write commit");
2867 let blob = odb
2868 .write_loose_materialize(crate::objects::ObjectKind::Blob, b"local blob\n")
2869 .expect("write blob");
2870 let mut matched = vec![
2871 matched_ref("refs/heads/main", tip, None),
2872 matched_ref("refs/tags/old", chain[0], None),
2873 matched_ref("refs/tags/unrelated", unrelated, None),
2874 matched_ref("refs/tags/local-blob", blob, None),
2875 ];
2876
2877 retain_following_tags(&odb, &mut matched, &HashSet::new(), &HashSet::new())
2878 .expect("retain following tags");
2879 assert_eq!(
2880 kept_tags(&matched),
2881 ["refs/tags/local-blob", "refs/tags/old"]
2882 );
2883
2884 let mut matched = vec![
2886 matched_ref("refs/heads/main", tip, None),
2887 matched_ref("refs/tags/at-boundary", chain[1], None),
2888 matched_ref("refs/tags/behind-boundary", chain[0], None),
2889 ];
2890 retain_following_tags(
2891 &odb,
2892 &mut matched,
2893 &HashSet::new(),
2894 &HashSet::from([chain[1]]),
2895 )
2896 .expect("retain following tags");
2897 assert_eq!(kept_tags(&matched), ["refs/tags/at-boundary"]);
2898 }
2899
2900 #[test]
2901 fn retain_following_tags_never_lazy_fetches_in_a_partial_clone() {
2902 use std::collections::HashSet;
2903 use std::sync::atomic::{AtomicUsize, Ordering};
2904 use std::sync::Arc;
2905
2906 struct CountingFetcher(Arc<AtomicUsize>);
2907 impl crate::promisor_fetch::PromisorFetcher for CountingFetcher {
2908 fn fetch_missing(
2909 &self,
2910 _repo: &crate::repo::Repository,
2911 _oids: &[ObjectId],
2912 ) -> Result<()> {
2913 self.0.fetch_add(1, Ordering::SeqCst);
2914 Ok(())
2915 }
2916 }
2917
2918 let tmp = tempfile::tempdir().expect("tempdir");
2919 let git_dir = tmp.path().join(".git");
2920 std::fs::create_dir_all(git_dir.join("objects/pack")).expect("objects");
2921 std::fs::create_dir_all(git_dir.join("refs/heads")).expect("refs");
2922 std::fs::write(git_dir.join("HEAD"), "ref: refs/heads/main\n").expect("HEAD");
2923 std::fs::write(
2924 git_dir.join("config"),
2925 "[core]\n\trepositoryformatversion = 1\n\tbare = true\n\
2926 [extensions]\n\tpartialclone = origin\n\
2927 [remote \"origin\"]\n\turl = /nonexistent\n\tpromisor = true\n",
2928 )
2929 .expect("config");
2930 let calls = Arc::new(AtomicUsize::new(0));
2931 let repo = crate::repo::Repository::open(
2932 &git_dir,
2933 &RepositoryOptions::empty()
2934 .with_promisor_fetcher(Some(Arc::new(CountingFetcher(Arc::clone(&calls))))),
2935 )
2936 .expect("open");
2937 let odb = repo.odb();
2938 let tip = write_commit_chain(odb, 1)[0];
2939 let oid = |c: char| ObjectId::from_hex(&c.to_string().repeat(40)).expect("oid");
2940 let (promised_blob, missing_tag_obj) = (oid('7'), oid('8'));
2942 let write_tag = |target: ObjectId, kind: &str| {
2943 let body = format!(
2944 "object {}\ntype {kind}\ntag t\ntagger T <t@example.com> 0 +0000\n\nt\n",
2945 target.to_hex()
2946 );
2947 odb.write_loose_materialize(crate::objects::ObjectKind::Tag, body.as_bytes())
2948 .expect("write tag")
2949 };
2950 let local_tag_on_promised_blob = write_tag(promised_blob, "blob");
2952 let local_tag_on_missing_tag = write_tag(missing_tag_obj, "tag");
2953 let mut matched = vec![
2954 matched_ref("refs/heads/main", tip, None),
2955 matched_ref("refs/tags/light", tip, None),
2956 matched_ref("refs/tags/promised-blob", promised_blob, None),
2957 matched_ref("refs/tags/unpeeled", missing_tag_obj, None),
2958 matched_ref(
2959 "refs/tags/local-on-promised",
2960 local_tag_on_promised_blob,
2961 None,
2962 ),
2963 matched_ref(
2964 "refs/tags/local-on-missing-tag",
2965 local_tag_on_missing_tag,
2966 None,
2967 ),
2968 ];
2969
2970 retain_following_tags(odb, &mut matched, &HashSet::new(), &HashSet::new())
2971 .expect("retain following tags");
2972
2973 assert_eq!(
2974 calls.load(Ordering::SeqCst),
2975 0,
2976 "tag following must not lazy-fetch missing objects"
2977 );
2978 assert_eq!(kept_tags(&matched), ["refs/tags/light"]);
2979 }
2980
2981 #[test]
2982 fn reachable_commits_stops_once_every_target_is_found() {
2983 use std::collections::HashSet;
2984
2985 let tmp = tempfile::tempdir().expect("tempdir");
2986 let objects = tmp.path().join("objects");
2987 std::fs::create_dir_all(&objects).expect("objects");
2988 let odb = crate::odb::Odb::new(&objects);
2989 let chain = write_commit_chain(&odb, 5);
2990 let tip = chain[4];
2991 let none = HashSet::new();
2992
2993 let near = HashSet::from([chain[3]]);
2994 let seen = reachable_commits(&odb, &[tip], &near, &none).unwrap();
2995 assert!(seen.contains(&chain[3]), "reachable target is reported");
2996 assert!(
2997 !seen.contains(&chain[0]),
2998 "walk must stop before the root once every target is found"
2999 );
3000
3001 let unreachable =
3002 HashSet::from([
3003 ObjectId::from_hex("1111111111111111111111111111111111111111").unwrap(),
3004 ]);
3005 let seen = reachable_commits(&odb, &[tip], &unreachable, &none).unwrap();
3006 assert_eq!(
3007 seen,
3008 chain.iter().copied().collect::<HashSet<_>>(),
3009 "an unreachable target forces the full closure"
3010 );
3011
3012 assert!(
3013 reachable_commits(&odb, &[tip], &HashSet::new(), &none)
3014 .unwrap()
3015 .is_empty(),
3016 "no targets means no walk"
3017 );
3018 }
3019
3020 #[test]
3021 fn tags_already_local_matches_name_and_oid_across_loose_and_packed() {
3022 let tmp = tempfile::tempdir().expect("tempdir");
3023 let git_dir = tmp.path().join(".git");
3024 std::fs::create_dir_all(git_dir.join("objects")).expect("objects");
3025 std::fs::create_dir_all(git_dir.join("refs/tags")).expect("refs/tags");
3026 std::fs::write(git_dir.join("HEAD"), "ref: refs/heads/main\n").expect("HEAD");
3027
3028 let a = ObjectId::from_hex("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa").unwrap();
3029 let b = ObjectId::from_hex("bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb").unwrap();
3030 let c = ObjectId::from_hex("cccccccccccccccccccccccccccccccccccccccc").unwrap();
3031 std::fs::write(git_dir.join("refs/tags/loose"), format!("{}\n", a.to_hex()))
3032 .expect("loose tag");
3033 std::fs::write(
3034 crate::refs::store::paths::packed_refs_path(&git_dir),
3035 format!(
3036 "# pack-refs with: peeled fully-peeled sorted \n{} refs/tags/moved\n{} refs/tags/packed\n",
3037 a.to_hex(),
3038 b.to_hex()
3039 ),
3040 )
3041 .expect("packed-refs");
3042
3043 let remote_refs = vec![
3044 ("refs/heads/main".to_owned(), a),
3045 ("refs/tags/loose".to_owned(), a),
3046 ("refs/tags/packed".to_owned(), b),
3047 ("refs/tags/moved".to_owned(), c),
3048 ("refs/tags/new".to_owned(), c),
3049 ];
3050 let store = crate::refs::store::open_ref_store(&git_dir).unwrap();
3051 let present = tags_already_local(store.as_ref(), &remote_refs);
3052 assert_eq!(
3053 present,
3054 HashSet::from(["refs/tags/loose".to_owned(), "refs/tags/packed".to_owned()]),
3055 "only tags stored locally at the advertised oid count as present"
3056 );
3057 assert!(tags_already_local(store.as_ref(), &remote_refs[..1]).is_empty());
3058 }
3059
3060 #[test]
3061 fn add_wire_tags_following_skips_tags_already_local() {
3062 use crate::transfer::TagMode;
3063 use std::collections::HashMap;
3064
3065 let tip = ObjectId::from_hex("2222222222222222222222222222222222222222").unwrap();
3066 let old_tag = ObjectId::from_hex("3333333333333333333333333333333333333333").unwrap();
3067 let new_tag = ObjectId::from_hex("4444444444444444444444444444444444444444").unwrap();
3068 let remote_refs = vec![
3069 ("refs/heads/main".to_owned(), tip),
3070 ("refs/tags/old".to_owned(), old_tag),
3071 ("refs/tags/new".to_owned(), new_tag),
3072 ];
3073 let already_local = HashSet::from(["refs/tags/old".to_owned()]);
3074
3075 for (mode, expect) in [
3076 (TagMode::Following, vec!["refs/tags/new"]),
3077 (TagMode::All, vec!["refs/tags/old", "refs/tags/new"]),
3078 ] {
3079 let mut matched = Vec::new();
3080 let mut matched_oids = HashSet::new();
3081 let mut seen = HashSet::new();
3082 add_wire_tags(
3083 mode,
3084 &remote_refs,
3085 &HashMap::new(),
3086 &[],
3087 &already_local,
3088 &mut matched,
3089 &mut matched_oids,
3090 &mut seen,
3091 );
3092 let names: Vec<&str> = matched.iter().map(|m| m.remote_ref.as_str()).collect();
3093 assert_eq!(names, expect, "{mode:?}");
3094 }
3095 }
3096}
3097
3098#[cfg(test)]
3099mod v2_negotiation_tests {
3100 use super::*;
3101 use crate::objects::ObjectKind;
3102 use crate::progress::NeverCancel;
3103 use crate::repo::{InitOptions, Repository};
3104 use std::io::Cursor;
3105
3106 struct AckingNeverReadyServer {
3109 caps: Vec<String>,
3110 pending: Vec<u8>,
3111 response: Cursor<Vec<u8>>,
3112 acked: Option<ObjectId>,
3113 requests: Vec<Vec<String>>,
3114 }
3115
3116 impl AckingNeverReadyServer {
3117 fn answer(&mut self) -> std::io::Result<()> {
3118 let mut lines = Vec::new();
3119 let mut cursor = Cursor::new(std::mem::take(&mut self.pending));
3120 while let Some(pkt) = pkt_line::read_packet(&mut cursor)? {
3121 if let pkt_line::Packet::Data(line) = pkt {
3122 lines.push(line.trim_end().to_owned());
3123 }
3124 }
3125 let haves: Vec<ObjectId> = lines
3126 .iter()
3127 .filter_map(|l| l.strip_prefix("have "))
3128 .filter_map(|h| ObjectId::from_hex(h).ok())
3129 .collect();
3130 let done = lines.iter().any(|l| l == "done");
3131 self.requests.push(lines);
3132 let mut out = Vec::new();
3133 if !done {
3134 let acked = *self.acked.get_or_insert(haves[0]);
3135 pkt_line::write_line(&mut out, "acknowledgments")?;
3136 if haves.contains(&acked) {
3137 pkt_line::write_line(&mut out, &format!("ACK {acked}"))?;
3138 } else {
3139 pkt_line::write_line(&mut out, "NAK")?;
3140 }
3141 pkt_line::write_flush(&mut out)?;
3142 }
3143 self.response = Cursor::new(out);
3144 Ok(())
3145 }
3146 }
3147
3148 impl Write for AckingNeverReadyServer {
3149 fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
3150 self.pending.extend_from_slice(buf);
3151 Ok(buf.len())
3152 }
3153
3154 fn flush(&mut self) -> std::io::Result<()> {
3155 if self.pending.is_empty() {
3156 return Ok(());
3157 }
3158 self.answer()
3159 }
3160 }
3161
3162 impl Connection for AckingNeverReadyServer {
3163 fn reader(&mut self) -> &mut dyn Read {
3164 &mut self.response
3165 }
3166 fn writer(&mut self) -> &mut dyn Write {
3167 self
3168 }
3169 fn advertised_refs(&self) -> &[(String, ObjectId)] {
3170 &[]
3171 }
3172 fn capabilities(&self) -> &[String] {
3173 &self.caps
3174 }
3175 fn head_symref(&self) -> Option<&str> {
3176 None
3177 }
3178 fn protocol_version(&self) -> u8 {
3179 2
3180 }
3181 fn stateless_rpc(&self) -> bool {
3182 true
3183 }
3184 }
3185
3186 #[test]
3187 fn replayed_acks_do_not_postpone_the_in_vain_cutoff() {
3188 const TIPS: usize = 2000;
3189 let tmp = tempfile::tempdir().unwrap();
3190 let repo = Repository::init(tmp.path(), &InitOptions::default()).unwrap();
3191 let git_dir = repo.git_dir().to_path_buf();
3192 let tree = repo.odb().write(ObjectKind::Tree, &[]).unwrap();
3193 for i in 0..TIPS {
3196 let when = 1_600_000_000 + i as i64;
3197 let body = format!(
3198 "tree {tree}\nauthor T <t@example.com> {when} +0000\n\
3199 committer T <t@example.com> {when} +0000\n\nroot {i}\n"
3200 );
3201 let oid = repo
3202 .odb()
3203 .write(ObjectKind::Commit, body.as_bytes())
3204 .unwrap();
3205 crate::refs::write_ref(&git_dir, &format!("refs/heads/b{i:04}"), &oid).unwrap();
3206 }
3207
3208 let mut server = AckingNeverReadyServer {
3209 caps: vec!["fetch".to_owned()],
3210 pending: Vec::new(),
3211 response: Cursor::new(Vec::new()),
3212 acked: None,
3213 requests: Vec::new(),
3214 };
3215 let want = ObjectId::from_hex("1111111111111111111111111111111111111111").unwrap();
3216 let _ = negotiate_pack_v2(
3218 &repo,
3219 &mut server,
3220 &[],
3221 &[want],
3222 &V2DeepenArgs::default(),
3223 None,
3224 &mut NoProgress,
3225 &NeverCancel,
3226 false,
3227 );
3228
3229 let last = server.requests.last().unwrap();
3230 assert!(
3231 last.iter().any(|l| l == "done"),
3232 "negotiation must end with done"
3233 );
3234 let distinct: HashSet<&str> = server
3235 .requests
3236 .iter()
3237 .flatten()
3238 .filter_map(|l| l.strip_prefix("have "))
3239 .collect();
3240 assert_eq!(distinct.len(), 16 + 32 + 64 + 128 + 256);
3244 assert!(distinct.len() < TIPS, "cutoff never fired");
3245 }
3246}