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 if let Some(msg) = ln.strip_prefix("ERR ") {
448 return Err(Error::Message(format!("remote error: {}", msg.trim_end())));
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 if let Some(msg) = line.strip_prefix("ERR ") {
733 return Err(Error::Message(format!("remote error: {}", msg.trim_end())));
734 }
735 if let Some(t) = parse_v2_ls_refs_unborn_head_symref(line) {
736 head_symref = Some(t);
737 continue;
738 }
739 let Some((name, oid, symref_target, peel)) = parse_ls_refs_v2_line(line) else {
740 continue;
741 };
742 if name.contains("^{") || name.ends_with("^{}") {
743 continue;
744 }
745 if name == "HEAD" {
746 head_oid = Some(oid);
747 if let Some(t) =
748 symref_target.filter(|t| crate::refs::is_valid_advertised_symref_target(t))
749 {
750 head_symref = Some(t);
751 }
752 continue;
754 }
755 if crate::refs::is_valid_fetch_advertised_ref(&name) {
756 if let Some(p) = peel {
757 advertised_peel.insert(name.clone(), p);
758 }
759 advertised.push((name, oid));
760 }
761 }
762 }
763 }
764 Ok((advertised, head_symref, advertised_peel, head_oid))
765}
766
767pub(crate) struct V2LocalHaves<'r> {
773 negotiator: SkippingNegotiator<'r>,
774 taken: Vec<ObjectId>,
775 exhausted: bool,
776}
777
778impl V2LocalHaves<'_> {
779 fn first(&mut self, n: usize) -> Result<&[ObjectId]> {
781 while self.taken.len() < n && !self.exhausted {
782 match self.negotiator.next_have()? {
783 Some(oid) => self.taken.push(oid),
784 None => self.exhausted = true,
785 }
786 }
787 Ok(&self.taken[..self.taken.len().min(n)])
788 }
789
790 #[cfg(test)]
792 fn all(&mut self) -> Result<&[ObjectId]> {
793 self.first(usize::MAX)
794 }
795}
796
797pub(crate) fn v2_local_haves<'r>(
806 local: &'r Repository,
807 exclude: &[ObjectId],
808) -> Result<V2LocalHaves<'r>> {
809 let exclude: HashSet<ObjectId> = exclude.iter().copied().collect();
810 let mut negotiator = SkippingNegotiator::new(local);
811 add_negotiation_tips(&mut negotiator, &exclude)?;
812 Ok(V2LocalHaves {
813 negotiator,
814 taken: Vec::new(),
815 exhausted: false,
816 })
817}
818
819fn add_negotiation_tips(
824 negotiator: &mut SkippingNegotiator<'_>,
825 wants: &HashSet<ObjectId>,
826) -> Result<()> {
827 let local = negotiator.repo();
828 let mut tips: Vec<ObjectId> = Vec::new();
829 let mut seen_tip: HashSet<ObjectId> = HashSet::new();
830 let mut push_tip = |negotiator: &SkippingNegotiator<'_>, oid: ObjectId| {
831 if let Ok(Some(c)) = negotiator.repo().odb().peel_to_commit(oid) {
832 if !wants.contains(&c) && seen_tip.insert(c) {
833 tips.push(c);
834 }
835 }
836 };
837 for prefix in [
838 "refs/heads/",
839 "refs/remotes/",
840 "refs/tags/",
841 crate::bundle_uri::BUNDLE_REFS_PREFIX,
842 ] {
843 if let Ok(entries) = crate::refs::list_refs_in(local, prefix) {
844 for (_, oid) in entries {
845 push_tip(negotiator, oid);
846 }
847 }
848 }
849 if let Ok(h) = crate::refs::resolve_ref_in(local, "HEAD") {
850 push_tip(negotiator, h);
851 }
852 tips.sort_unstable();
853 for t in tips {
854 negotiator.add_tip(t)?;
855 }
856 Ok(())
857}
858
859#[derive(Clone, Copy)]
861enum V2FetchMode<'a> {
862 Refs {
865 promisor_reply: Option<&'a str>,
867 },
868 ObjectWants,
871}
872
873fn command_cap_echo(server_caps: &[String], promisor_reply: Option<&str>) -> Vec<String> {
877 let mut echo = protocol_v2::cap_lines_for_command_request(server_caps);
878 if let Some(reply) = promisor_reply {
879 echo.push(format!(
880 "{}={reply}",
881 protocol_v2::PROMISOR_REMOTE_CAPABILITY
882 ));
883 }
884 echo
885}
886
887fn v2_promisor_handshake(
898 local: &Repository,
899 local_cfg: &crate::config::ConfigSet,
900 server_caps: &[String],
901 opts: &mut FetchOptions,
902) -> Result<Option<crate::promisor_remote::PromisorHandshake>> {
903 use crate::net_trace::net_trace;
904 use crate::promisor_remote::{PromisorAdvertisement, PromisorHandshake};
905 let Some(value) = protocol_v2::server_promisor_remote_info(server_caps) else {
906 return Ok(None);
907 };
908 let advertisement = PromisorAdvertisement::parse(value);
909 if advertisement.remotes.is_empty() {
910 return Ok(None);
911 }
912 let auto_filter = opts.filter.is_none() && opts.auto_filter;
913 let partial_fetch = opts.filter.is_some()
914 || auto_filter
915 || opts
916 .remote_name
917 .as_deref()
918 .is_some_and(|name| crate::promisor_remote::is_promisor_remote(local_cfg, name));
919 let mut handshake =
920 PromisorHandshake::evaluate(local_cfg, &advertisement, partial_fetch, auto_filter);
921 if !opts.dry_run {
922 handshake.store_fields(local.git_dir(), local_cfg)?;
923 }
924 if auto_filter {
925 opts.filter = handshake.resolved_filter;
926 }
927 net_trace!(
928 opts.network_trace,
929 opts.diagnostics.as_ref(),
930 "fetch_remote: server advertised {} promisor remote(s), accepted [{}]",
931 advertisement.remotes.len(),
932 handshake.decision.accepted_names().join(", ")
933 );
934 Ok(Some(handshake))
935}
936
937#[allow(clippy::too_many_arguments)]
952fn negotiate_pack_v2(
953 local: &Repository,
954 conn: &mut dyn Connection,
955 server_caps: &[String],
956 wants: &[ObjectId],
957 deepen: &V2DeepenArgs,
958 wire_filter: Option<&str>,
959 progress: &mut dyn Progress,
960 cancel: &dyn Cancel,
961 mode: V2FetchMode<'_>,
962) -> Result<(Option<std::path::PathBuf>, ShallowUpdate)> {
963 if wants.is_empty() {
964 return Ok((None, ShallowUpdate::default()));
965 }
966 let (skip_local_haves, promisor_reply) = match mode {
967 V2FetchMode::Refs { promisor_reply } => (false, promisor_reply),
968 V2FetchMode::ObjectWants => (true, None),
969 };
970 let local_odb = local.odb();
971 let object_format = v2_object_format(server_caps, local_odb);
972 let cap_echo = command_cap_echo(server_caps, promisor_reply);
973 let sideband_all = protocol_v2::fetch_supports_sideband_all(server_caps);
974
975 let mut haves = if skip_local_haves {
982 None
983 } else {
984 let exclude: &[ObjectId] = if deepen.is_shallow_request() {
985 &[]
986 } else {
987 wants
988 };
989 Some(v2_local_haves(local, exclude)?)
990 };
991
992 let mut pack_receive = crate::pack_receive::TempPackReceive::create(local_odb.objects_dir())?;
993 let mut shallow_update = ShallowUpdate::default();
994 let mut common: Vec<ObjectId> = Vec::new();
995 let mut common_set: HashSet<ObjectId> = HashSet::new();
996 let mut batch = INITIAL_FLUSH;
997 let mut in_vain: usize = 0;
998 let mut seen_ack = false;
999 let mut round_haves: Vec<ObjectId> = Vec::new();
1000 loop {
1001 round_haves.clear();
1002 round_haves.extend_from_slice(&common);
1003 let added = match haves.as_mut() {
1004 Some(haves) => {
1005 poll_cancel(progress, cancel)?;
1006 let previous = haves.taken.len();
1007 haves.first(previous.saturating_add(batch))?;
1008 round_haves.extend_from_slice(&haves.taken[previous..]);
1009 haves.taken.len() - previous
1010 }
1011 None => 0,
1012 };
1013 in_vain += added;
1014 let done = added == 0 || (seen_ack && in_vain >= MAX_IN_VAIN);
1015 write_v2_fetch_request(
1016 conn.writer(),
1017 &object_format,
1018 &cap_echo,
1019 wants,
1020 &round_haves,
1021 sideband_all,
1022 deepen,
1023 wire_filter,
1024 done,
1025 )?;
1026 if done {
1027 break;
1028 }
1029 match read_v2_acknowledgments(conn.reader(), sideband_all)? {
1030 Some(round) if !round.ready => {
1034 for oid in round.common {
1035 if let Some(haves) = haves.as_mut() {
1036 haves.negotiator.ack(oid)?;
1037 }
1038 if common_set.insert(oid) {
1039 common.push(oid);
1040 in_vain = 0;
1041 }
1042 seen_ack = true;
1043 }
1044 batch = next_v2_flush_count(batch);
1045 }
1046 _ => break,
1049 }
1050 }
1051 read_v2_fetch_pack_response(
1052 conn.reader(),
1053 &mut Vec::new(),
1054 Some(&mut pack_receive),
1055 &mut shallow_update,
1056 progress,
1057 cancel,
1058 sideband_all,
1059 )?;
1060 Ok((pack_receive.finish()?, shallow_update))
1061}
1062
1063#[derive(Clone, Default)]
1068pub(crate) struct V2DeepenArgs {
1069 pub(crate) local_shallow: Vec<ObjectId>,
1071 pub(crate) depth: Option<u32>,
1073 pub(crate) deepen_relative: bool,
1075 pub(crate) deepen_since: Option<String>,
1077 pub(crate) deepen_not: Vec<String>,
1079 pub(crate) remote_progress: bool,
1081}
1082
1083impl V2DeepenArgs {
1084 pub(crate) fn from_opts(opts: &FetchOptions, local_shallow: &[ObjectId]) -> Self {
1087 let (depth, deepen_relative) = opts
1088 .wire_deepen()
1089 .map(|(d, r)| (Some(d), r))
1090 .unwrap_or((None, false));
1091 Self {
1092 local_shallow: local_shallow.to_vec(),
1093 depth,
1094 deepen_relative,
1095 deepen_since: opts
1096 .deepen_since
1097 .as_deref()
1098 .filter(|s| !s.trim().is_empty())
1099 .map(crate::shallow::deepen_since_wire_value),
1100 deepen_not: opts
1101 .deepen_not
1102 .iter()
1103 .map(|s| s.trim().to_owned())
1104 .filter(|s| !s.is_empty())
1105 .collect(),
1106 remote_progress: opts.remote_progress,
1107 }
1108 }
1109
1110 pub(crate) fn is_shallow_request(&self) -> bool {
1113 self.depth.is_some()
1114 || self.deepen_relative
1115 || self.deepen_since.is_some()
1116 || !self.deepen_not.is_empty()
1117 || !self.local_shallow.is_empty()
1118 }
1119}
1120#[expect(clippy::too_many_arguments)]
1121pub(crate) fn write_v2_fetch_request(
1127 w: &mut dyn Write,
1128 object_format: &str,
1129 cap_echo: &[String],
1130 wants: &[ObjectId],
1131 haves: &[ObjectId],
1132 sideband_all: bool,
1133 deepen: &V2DeepenArgs,
1134 wire_filter: Option<&str>,
1135 send_done: bool,
1136) -> Result<()> {
1137 let mut req: Vec<u8> = Vec::new();
1138 pkt_line::write_line(&mut req, "command=fetch")?;
1139 if cap_echo.iter().any(|c| c.starts_with("object-format=")) {
1140 for line in cap_echo {
1141 pkt_line::write_line(&mut req, line)?;
1142 }
1143 } else {
1144 for line in cap_echo {
1145 pkt_line::write_line(&mut req, line)?;
1146 }
1147 pkt_line::write_line(&mut req, &format!("object-format={object_format}"))?;
1148 }
1149 pkt_line::write_delim(&mut req)?;
1150
1151 pkt_line::write_line(&mut req, "thin-pack")?;
1152 if !deepen.remote_progress {
1153 pkt_line::write_line(&mut req, "no-progress")?;
1154 }
1155 pkt_line::write_line(&mut req, "ofs-delta")?;
1156 if sideband_all {
1157 pkt_line::write_line(&mut req, "sideband-all")?;
1158 }
1159 pkt_line::write_line(&mut req, "include-tag")?;
1162
1163 for oid in &deepen.local_shallow {
1165 pkt_line::write_line(&mut req, &format!("shallow {}", oid.to_hex()))?;
1166 }
1167 if let Some(depth) = deepen.depth {
1168 pkt_line::write_line(&mut req, &format!("deepen {depth}"))?;
1169 }
1170 if deepen.deepen_relative {
1171 pkt_line::write_line(&mut req, "deepen-relative")?;
1172 }
1173 if let Some(since) = &deepen.deepen_since {
1174 pkt_line::write_line(&mut req, &format!("deepen-since {since}"))?;
1175 }
1176 for excl in &deepen.deepen_not {
1177 pkt_line::write_line(&mut req, &format!("deepen-not {excl}"))?;
1178 }
1179 if let Some(spec) = wire_filter {
1180 pkt_line::write_line(&mut req, &format!("filter {spec}"))?;
1181 }
1182
1183 for want in wants {
1184 pkt_line::write_line(&mut req, &format!("want {}", want.to_hex()))?;
1185 }
1186 for have in haves {
1187 pkt_line::write_line(&mut req, &format!("have {}", have.to_hex()))?;
1188 }
1189 if send_done {
1190 pkt_line::write_line(&mut req, "done")?;
1191 }
1192 pkt_line::write_flush(&mut req)?;
1193 w.write_all(&req)?;
1194 w.flush()?;
1195 Ok(())
1196}
1197
1198pub(crate) struct V2AckRound {
1200 pub(crate) ready: bool,
1203 pub(crate) common: Vec<ObjectId>,
1205}
1206
1207pub(crate) fn read_v2_acknowledgments(
1215 reader: &mut dyn Read,
1216 sideband_all: bool,
1217) -> Result<Option<V2AckRound>> {
1218 let mut reader = reader;
1219 let hdr = match crate::shallow::read_v2_fetch_primary_line(&mut reader, sideband_all)? {
1220 Some(s) => s,
1221 None => {
1222 return Ok(Some(V2AckRound {
1223 ready: false,
1224 common: Vec::new(),
1225 }))
1226 }
1227 };
1228 let hdr = hdr.as_str();
1229 if let Some(msg) = hdr.strip_prefix("ERR ") {
1230 return Err(Error::Message(format!("remote error: {}", msg.trim_end())));
1231 }
1232 if hdr != "acknowledgments" {
1233 return Err(Error::Message(format!(
1243 "unexpected v2 fetch section before acknowledgments: {hdr}"
1244 )));
1245 }
1246 let mut ready = false;
1247 let mut common = Vec::new();
1248 loop {
1249 match pkt_line::read_packet(&mut reader)? {
1250 Some(pkt_line::Packet::Delim) | Some(pkt_line::Packet::Flush) | None => break,
1251 Some(pkt_line::Packet::ResponseEnd) => break,
1252 Some(pkt_line::Packet::Data(payload)) => {
1253 let Some(ln) = crate::shallow::v2_primary_line_from_pkt(payload, sideband_all)?
1254 else {
1255 continue;
1256 };
1257 if ln == "NAK" {
1258 continue;
1259 }
1260 if let Some(hex) = ln.strip_prefix("ACK ") {
1261 let oid = ObjectId::from_hex(hex.trim()).map_err(|_| {
1262 Error::Message(format!("unexpected acknowledgment line: '{ln}'"))
1263 })?;
1264 common.push(oid);
1265 continue;
1266 }
1267 if ln == "ready" {
1268 ready = true;
1269 continue;
1270 }
1271 return Err(Error::Message(format!(
1272 "unexpected acknowledgment line: '{ln}'"
1273 )));
1274 }
1275 }
1276 }
1277 Ok(Some(V2AckRound { ready, common }))
1278}
1279
1280fn v2_wanted_ref_ad_line(line: &str) -> bool {
1283 let line = line.trim_end();
1284 let Some((oid_hex, rest)) = line.split_once(' ') else {
1285 return false;
1286 };
1287 if ObjectId::from_hex(oid_hex.trim()).is_err() {
1288 return false;
1289 }
1290 !rest.trim().is_empty()
1291}
1292
1293pub(crate) fn read_v2_fetch_pack_response(
1302 reader: &mut dyn Read,
1303 out: &mut Vec<u8>,
1304 pack_sink: Option<&mut crate::pack_receive::TempPackReceive>,
1305 shallow_out: &mut ShallowUpdate,
1306 progress: &mut dyn Progress,
1307 cancel: &dyn Cancel,
1308 sideband_all: bool,
1309) -> Result<()> {
1310 loop {
1311 let hdr = loop {
1312 match pkt_line::read_packet(&mut &mut *reader)? {
1313 Some(pkt_line::Packet::Data(payload)) => {
1314 if let Some(line) =
1315 crate::shallow::v2_primary_line_from_pkt(payload, sideband_all)?
1316 {
1317 break line;
1318 }
1319 }
1320 Some(pkt_line::Packet::Flush) => return Ok(()),
1321 None => {
1324 return Err(Error::RemoteUnreadable {
1325 url: None,
1326 reason: crate::error::RemoteUnreadableReason::ResponseTruncated,
1327 })
1328 }
1329 Some(pkt_line::Packet::Delim) => continue,
1330 Some(other) => {
1331 return Err(Error::Message(format!(
1332 "unexpected v2 fetch response: {other:?}"
1333 )))
1334 }
1335 }
1336 };
1337 let hdr = hdr.as_str();
1338 if let Some(msg) = hdr.strip_prefix("ERR ") {
1339 return Err(Error::Message(format!("remote error: {}", msg.trim_end())));
1340 }
1341 match hdr {
1342 h if h == crate::protocol_v2::FetchResponseSection::ShallowInfo.header() => {
1343 let (sh, unsh) =
1347 crate::shallow::read_shallow_info_section_sideband(&mut *reader, sideband_all)?;
1348 shallow_out.shallow.extend(sh);
1349 shallow_out.unshallow.extend(unsh);
1350 }
1351 h if h == crate::protocol_v2::FetchResponseSection::Acknowledgments.header()
1352 || h == crate::protocol_v2::FetchResponseSection::WantedRefs.header()
1353 || h == crate::protocol_v2::FetchResponseSection::PackfileUris.header() =>
1354 {
1355 skip_v2_section_until_boundary(&mut *reader, sideband_all)?;
1356 }
1357 h if h == crate::protocol_v2::FetchResponseSection::Packfile.header() => {
1358 if let Some(sink) = pack_sink {
1361 sink.read_sideband(&mut *reader, progress, cancel)?;
1362 } else {
1363 let mut target = crate::pack_receive::PackReceiveTarget::Memory(out);
1364 read_sideband_pack(&mut *reader, &mut target, progress, cancel)?;
1365 }
1366 return Ok(());
1367 }
1368 other if v2_wanted_ref_ad_line(other) => {
1369 }
1371 other => {
1372 return Err(Error::Message(format!(
1373 "unexpected v2 fetch section: {other}"
1374 )))
1375 }
1376 }
1377 }
1378}
1379
1380fn skip_v2_section_until_boundary(reader: &mut dyn Read, sideband_all: bool) -> Result<()> {
1382 loop {
1383 match pkt_line::read_packet(&mut &mut *reader)? {
1384 None | Some(pkt_line::Packet::Flush) | Some(pkt_line::Packet::Delim) => return Ok(()),
1385 Some(pkt_line::Packet::ResponseEnd) => return Ok(()),
1386 Some(pkt_line::Packet::Data(payload)) => {
1387 let _ = crate::shallow::v2_primary_line_from_pkt(payload, sideband_all)?;
1388 }
1389 }
1390 }
1391}
1392
1393#[allow(clippy::large_enum_variant)]
1395enum FetchDryRunIngest {
1396 Quarantine(crate::receive_quarantine::ReceiveQuarantine),
1397 Memory {
1398 objects_dir: std::path::PathBuf,
1399 pack: std::sync::Arc<crate::objects::odb::store::MemoryStore>,
1400 read: std::sync::Arc<dyn crate::objects::odb::store::ObjectStore>,
1401 },
1402}
1403
1404impl FetchDryRunIngest {
1405 fn create(local_odb: &crate::odb::Odb, git_dir: &Path) -> Result<Self> {
1406 use crate::objects::odb::store::MemoryStore;
1407 if local_odb.uses_files_primary() {
1408 Ok(Self::Quarantine(
1409 crate::receive_quarantine::ReceiveQuarantine::create(local_odb, git_dir)?,
1410 ))
1411 } else {
1412 let pack = std::sync::Arc::new(MemoryStore::new(local_odb.hash_algo()));
1413 Ok(Self::Memory {
1414 objects_dir: local_odb.objects_dir().to_path_buf(),
1415 pack: pack.clone(),
1416 read: local_odb.read_store(),
1417 })
1418 }
1419 }
1420
1421 fn odb(&self) -> crate::odb::Odb {
1422 match self {
1423 Self::Quarantine(q) => q.odb(),
1424 Self::Memory {
1425 objects_dir,
1426 pack,
1427 read,
1428 } => crate::odb::OdbBuilder::files(objects_dir)
1429 .primary(pack.clone())
1430 .push_read_source(read.clone())
1431 .alternates(false)
1432 .build(),
1433 }
1434 }
1435}
1436
1437pub fn fetch_remote(
1459 local_git_dir: &Path,
1460 conn: &mut dyn Connection,
1461 opts: &mut FetchOptions,
1462 progress: &mut dyn Progress,
1463 cancel: &dyn Cancel,
1464) -> Result<FetchOutcome> {
1465 let local = open_fetch_target(local_git_dir, opts)?;
1466 fetch_remote_into(&local, conn, opts, progress, cancel)
1467}
1468
1469pub(crate) fn open_fetch_target(local_git_dir: &Path, opts: &FetchOptions) -> Result<Repository> {
1471 Repository::open(
1472 local_git_dir,
1473 &RepositoryOptions::with_environment(opts.environment.clone()),
1474 )
1475}
1476
1477pub fn fetch_remote_into(
1485 local: &Repository,
1486 conn: &mut dyn Connection,
1487 opts: &mut FetchOptions,
1488 progress: &mut dyn Progress,
1489 cancel: &dyn Cancel,
1490) -> Result<FetchOutcome> {
1491 use crate::net_trace::net_trace;
1492 let local_git_dir = local.git_dir();
1493 let t_begin = std::time::Instant::now();
1496 net_trace!(
1497 opts.network_trace,
1498 opts.diagnostics.as_ref(),
1499 "fetch_remote: begin — protocol v{}, {} refspec(s), tags={:?}, depth={:?}",
1500 conn.protocol_version(),
1501 opts.refspecs.len(),
1502 opts.tags,
1503 opts.fetch_depth
1504 );
1505 validate_fetch_depth_local(local_git_dir, opts)?;
1506 let local_cfg = crate::config::ConfigSet::load(&opts.environment, Some(local_git_dir), true)?;
1507 if opts.filter.is_none() {
1508 if let Some(remote) = opts.remote_name.as_deref() {
1509 opts.filter = crate::partial_clone::stored_remote_filter(&local_cfg, remote);
1510 opts.auto_filter |=
1511 crate::partial_clone::stored_remote_filter_is_auto(&local_cfg, remote);
1512 }
1513 }
1514 if !opts.object_wants.is_empty() {
1515 return fetch_remote_object_wants(local, conn, opts, progress, cancel);
1516 }
1517 let mut promisor_handshake: Option<crate::promisor_remote::PromisorHandshake> = None;
1518 let local_odb_for_ls = local.odb();
1519
1520 let (remote_refs, default_branch, v2_caps, advertised_peel, remote_head_oid) = if conn
1527 .protocol_version()
1528 >= 2
1529 {
1530 let caps: Vec<String> = conn.capabilities().to_vec();
1531 promisor_handshake = v2_promisor_handshake(local, &local_cfg, &caps, opts)?;
1532 let promisor_reply = promisor_handshake
1533 .as_ref()
1534 .and_then(|h| h.decision.reply_value());
1535 let (refs, head_symref, peel, head_oid) = v2_ls_refs(
1536 conn,
1537 &caps,
1538 local_odb_for_ls,
1539 opts.tags,
1540 &opts.refspecs,
1541 promisor_reply.as_deref(),
1542 )?;
1543 let default_branch =
1544 head_symref.map(|t| t.strip_prefix("refs/heads/").unwrap_or(&t).to_owned());
1545 (refs, default_branch, Some(caps), peel, head_oid)
1546 } else {
1547 let default_branch = conn
1548 .head_symref()
1549 .map(|t| t.strip_prefix("refs/heads/").unwrap_or(t).to_owned());
1550 let advertised_peel = conn.advertised_tag_peel().cloned().unwrap_or_default();
1551 let remote_head_oid =
1552 crate::transfer::remote_head_oid_from_advertised(conn.advertised_refs());
1553 let remote_refs: Vec<(String, ObjectId)> = conn
1554 .advertised_refs()
1555 .iter()
1556 .filter(|(n, _)| {
1557 n != "HEAD" && !n.ends_with("^{}") && crate::refs::is_valid_fetch_advertised_ref(n)
1558 })
1559 .cloned()
1560 .collect();
1561 (
1562 remote_refs,
1563 default_branch,
1564 None,
1565 advertised_peel,
1566 remote_head_oid,
1567 )
1568 };
1569 net_trace!(
1570 opts.network_trace,
1571 opts.diagnostics.as_ref(),
1572 "fetch_remote: remote advertised {} ref(s){}",
1573 remote_refs.len(),
1574 v2_caps
1575 .as_ref()
1576 .map(|_| " (via v2 ls-refs)")
1577 .unwrap_or(" (v0/v1 advertisement)")
1578 );
1579
1580 if opts.initial_remote_fetch
1581 && remote_refs.is_empty()
1582 && !crate::transport::connection_handshake_complete(conn)
1583 {
1584 return Err(Error::RemoteUnreadable {
1585 url: None,
1586 reason: crate::error::RemoteUnreadableReason::NoHandshake,
1587 });
1588 }
1589
1590 let conn_caps: Vec<String> = conn.capabilities().to_vec();
1591 let remote_algo = crate::transfer::remote_hash_algo_from_advertisement(
1592 &[v2_caps.as_deref().unwrap_or(&[]), conn_caps.as_slice()],
1593 &remote_refs,
1594 );
1595 let reopened =
1596 crate::transfer::sync_empty_destination_object_format(local_git_dir, remote_algo, opts)?
1597 .then(|| crate::transfer::reopen_after_format_change(local))
1598 .transpose()?;
1599 let local = reopened.as_ref().unwrap_or(local);
1600 let local_git_dir = local.git_dir();
1601 let local_odb = local.odb();
1602 if opts.filter.is_some() && !local_odb.uses_files_primary() {
1603 return Err(Error::UnsupportedObjectStore {
1604 operation: "partial fetch",
1605 });
1606 }
1607
1608 let crate::transfer::FetchMatchSet {
1609 mut matched,
1610 positive,
1611 negatives,
1612 explicit_refspec_fetch,
1613 } = crate::transfer::build_fetch_matched_set(opts, &remote_refs, remote_head_oid)?;
1614 let mut matched_oids: HashSet<ObjectId> = matched.iter().map(|m| m.oid).collect();
1615 let mut seen_remote_ref: HashSet<String> =
1616 matched.iter().map(|m| m.remote_ref.clone()).collect();
1617
1618 let tags_present = if opts.tags == crate::transfer::TagMode::Following {
1632 tags_already_local(local.refs(), &remote_refs)
1633 } else {
1634 HashSet::new()
1635 };
1636 let following_only = add_wire_tags(
1637 opts.tags,
1638 &remote_refs,
1639 &advertised_peel,
1640 &negatives,
1641 &tags_present,
1642 opts.force,
1643 &mut matched,
1644 &mut matched_oids,
1645 &mut seen_remote_ref,
1646 );
1647 let fetch_commit_oids: HashSet<ObjectId> = matched
1648 .iter()
1649 .filter(|m| !m.is_tag)
1650 .map(|m| m.oid)
1651 .collect();
1652 let mut following_tag_wants =
1653 following_tag_wants_with_local_peel(&matched, &following_only, local_odb)?;
1654
1655 let local_shallow = crate::shallow::load_shallow_oids(local_git_dir)?;
1658 let shallow_request = opts.has_deepen_request() || !local_shallow.is_empty();
1659 if shallow_request {
1660 let local_shallow_set: HashSet<ObjectId> = local_shallow.iter().copied().collect();
1661 let peel_set = following_peel_commit_set_for_shallow(
1662 local_odb,
1663 &fetch_commit_oids,
1664 opts.absolute_depth_for_tags(),
1665 &local_shallow_set,
1666 )?;
1667 following_tag_wants.extend(following_tag_wants_peel_among_fetched_commits(
1668 &matched,
1669 &following_only,
1670 &peel_set,
1671 ));
1672 }
1673
1674 let wants: Vec<ObjectId> = if shallow_request {
1679 matched_oids
1680 .iter()
1681 .copied()
1682 .filter(|oid| !following_only.contains(oid) || following_tag_wants.contains(oid))
1683 .collect()
1684 } else {
1685 matched_oids
1686 .iter()
1687 .copied()
1688 .filter_map(|oid| {
1689 if following_only.contains(&oid) {
1690 return following_tag_wants.contains(&oid).then_some(Ok(oid));
1691 }
1692 match local_odb.contains(&oid) {
1693 Ok(true) => None,
1694 Ok(false) => Some(Ok(oid)),
1695 Err(err) => Some(Err(err)),
1696 }
1697 })
1698 .collect::<Result<_>>()?
1699 };
1700
1701 let mut shallow_update = ShallowUpdate::default();
1704 let mut pack_oids: HashSet<ObjectId> = HashSet::new();
1705 let mut transfer = crate::transfer::TransferStats::default();
1706
1707 net_trace!(
1708 opts.network_trace,
1709 opts.diagnostics.as_ref(),
1710 "fetch_remote: {} matched ref(s), want {} object(s){} [+{:?} since begin]",
1711 matched.len(),
1712 wants.len(),
1713 if shallow_request {
1714 " (shallow request)"
1715 } else {
1716 ""
1717 },
1718 t_begin.elapsed()
1719 );
1720
1721 let mut dry_run_ingest: Option<FetchDryRunIngest> = None;
1722
1723 let v0_caps = if v2_caps.is_none() {
1724 Some(conn.capabilities().join(" "))
1725 } else {
1726 None
1727 };
1728 let (wire_filter, filter_sent) = crate::partial_clone::wire_filter_for_capabilities(
1729 opts.filter.as_ref(),
1730 v2_caps.as_deref(),
1731 v0_caps.as_deref(),
1732 );
1733 let promisor_reply = promisor_handshake
1738 .as_ref()
1739 .and_then(|h| h.decision.reply_value());
1740 opts.mark_received_pack_promisor = opts.filter.is_some() || promisor_reply.is_some();
1741 let wire_filter = wire_filter.as_deref();
1742
1743 if wants.is_empty() {
1744 crate::transport::send_client_flush(conn)?;
1745 } else {
1746 net_trace!(
1747 opts.network_trace,
1748 opts.diagnostics.as_ref(),
1749 "fetch_remote: negotiating + fetching pack…"
1750 );
1751 let (pack_path, su) = if let Some(caps) = v2_caps.as_ref() {
1752 validate_fetch_depth_server(opts, None, Some(caps))?;
1753 let deepen = V2DeepenArgs::from_opts(opts, &local_shallow);
1754 negotiate_pack_v2(
1755 local,
1756 conn,
1757 caps,
1758 &wants,
1759 &deepen,
1760 wire_filter,
1761 progress,
1762 cancel,
1763 V2FetchMode::Refs {
1764 promisor_reply: promisor_reply.as_deref(),
1765 },
1766 )?
1767 } else {
1768 negotiate_pack(
1769 local,
1770 conn,
1771 &wants,
1772 opts,
1773 &local_shallow,
1774 wire_filter,
1775 progress,
1776 cancel,
1777 false,
1778 )?
1779 };
1780 shallow_update = su;
1781 net_trace!(
1782 opts.network_trace,
1783 opts.diagnostics.as_ref(),
1784 "fetch_remote: received pack, ingesting via index-pack…"
1785 );
1786 if let Some(path) = pack_path {
1787 if opts.pack_spool_only {
1788 net_trace!(
1789 opts.network_trace,
1790 opts.diagnostics.as_ref(),
1791 "fetch_remote: pack spooled to {} (pack_spool_only)",
1792 path.display()
1793 );
1794 return Ok(FetchOutcome {
1795 updates: Vec::new(),
1796 default_branch: None,
1797 new_shallow: crate::shallow::dedupe_shallow_list(shallow_update.shallow),
1798 new_unshallow: shallow_update.unshallow,
1799 transfer: crate::transfer::TransferStats::default(),
1800 filter_sent,
1801 promisor: promisor_handshake,
1802 });
1803 }
1804 let pack_config =
1805 crate::config::ConfigSet::load(&opts.environment, Some(local_git_dir), true)?;
1806 let index_threads = crate::parallel::index_pack_threads_for_ingest(
1807 opts.thread_policy,
1808 &pack_config,
1809 opts.index_pack_threads,
1810 )?;
1811 if opts.dry_run && dry_run_ingest.is_none() {
1812 dry_run_ingest = Some(FetchDryRunIngest::create(local_odb, local_git_dir)?);
1813 }
1814 let ingested = if let Some(target) = dry_run_ingest.as_ref() {
1815 let q_odb = target.odb();
1816 crate::index_pack::ingest_received_pack_path_with_progress(
1817 path,
1818 &q_odb,
1819 &crate::index_pack::IngestPackOptions {
1820 fix_thin: true,
1821 skip_post_index_verify: true,
1822 threads: index_threads,
1823 merged_config: Some(pack_config),
1824 environment: opts.environment.clone(),
1825 mark_promisor_pack: opts.mark_received_pack_promisor,
1826 keep_message: None,
1827 trusted_index_rows: None,
1828 },
1829 Some(progress),
1830 Some(cancel),
1831 )?
1832 } else {
1833 crate::index_pack::ingest_received_pack_path_with_progress(
1834 path,
1835 local_odb,
1836 &crate::index_pack::IngestPackOptions {
1837 fix_thin: true,
1838 skip_post_index_verify: true,
1839 threads: index_threads,
1840 merged_config: Some(pack_config),
1841 environment: opts.environment.clone(),
1842 mark_promisor_pack: opts.mark_received_pack_promisor,
1843 keep_message: None,
1844 trusted_index_rows: None,
1845 },
1846 Some(progress),
1847 Some(cancel),
1848 )?
1849 };
1850 transfer = crate::transfer::TransferStats::from_ingested_pack(&ingested);
1851 pack_oids = ingested.object_ids;
1852 }
1853 }
1854
1855 if !opts.dry_run {
1858 crate::shallow::apply_shallow_updates(
1859 local_git_dir,
1860 &shallow_update.shallow,
1861 &shallow_update.unshallow,
1862 )?;
1863 }
1864
1865 if v2_caps.is_some() && !wants.is_empty() {
1871 conn.finish_send();
1872 }
1873
1874 if opts.tags == crate::transfer::TagMode::Following {
1878 let t = std::time::Instant::now();
1879 let tag_odb = dry_run_ingest
1880 .as_ref()
1881 .map(FetchDryRunIngest::odb)
1882 .unwrap_or_else(|| local_odb.clone());
1883 retain_following_tags(
1884 &tag_odb,
1885 &mut matched,
1886 &pack_oids,
1887 &crate::shallow::load_shallow_boundaries(local_git_dir),
1888 )?;
1889 net_trace!(
1890 opts.network_trace,
1891 opts.diagnostics.as_ref(),
1892 "fetch_remote: retain_following_tags {:?}",
1893 t.elapsed()
1894 );
1895 }
1896
1897 let t_apply = std::time::Instant::now();
1899 let classify_odb = dry_run_ingest.as_ref().map(FetchDryRunIngest::odb);
1900 let classify_repo = (!opts.dry_run).then_some(local);
1901 let d_repo_open = t_apply.elapsed();
1902
1903 let mut updates: Vec<RefUpdate> = Vec::new();
1904
1905 let t_prune = std::time::Instant::now();
1906 if opts.prune {
1907 prune_tracking_refs(local, &positive, &remote_refs, opts.dry_run, &mut updates)?;
1908 }
1909 let d_prune = t_prune.elapsed();
1910
1911 let mut store_batch: Vec<(String, crate::refs::store::RawRef)> =
1912 Vec::with_capacity(matched.len());
1913
1914 let t_packed = std::time::Instant::now();
1921 let packed = if local.refs_on_disk() {
1922 Some(crate::refs::PackedRefs::load(local_git_dir)?)
1923 } else {
1924 None
1925 };
1926 let d_packed = t_packed.elapsed();
1927
1928 let mut d_resolve = std::time::Duration::ZERO;
1931 let mut d_classify = std::time::Duration::ZERO;
1932 let mut d_write = std::time::Duration::ZERO;
1933 let mut n_written = 0usize;
1934 let mut n_ancestry = 0usize; for m in &matched {
1937 let Some(local_ref) = &m.local_ref else {
1938 updates.push(RefUpdate {
1939 remote_ref: m.remote_ref.clone(),
1940 local_ref: None,
1941 old_oid: None,
1942 new_oid: Some(m.oid),
1943 mode: UpdateMode::NoChangeNeeded,
1944 note: Some("not stored (empty destination)".to_owned()),
1945 });
1946 continue;
1947 };
1948
1949 let t = std::time::Instant::now();
1950 let old = match &packed {
1951 Some(packed) => crate::refs::resolve_ref_cached(local_git_dir, local_ref, packed)
1952 .ok()
1953 .flatten(),
1954 None => local.resolve_ref_oid(local_ref).ok(),
1955 };
1956 d_resolve += t.elapsed();
1957
1958 if old.as_ref().is_some_and(|o| o != &m.oid) {
1959 n_ancestry += 1;
1960 }
1961
1962 let t = std::time::Instant::now();
1963 let mode = classify_update(
1964 old.as_ref(),
1965 &m.oid,
1966 m.force,
1967 m.dest_is_tag,
1968 classify_repo,
1969 classify_odb.as_ref(),
1970 );
1971 d_classify += t.elapsed();
1972
1973 let write = matches!(
1974 mode,
1975 UpdateMode::New | UpdateMode::FastForward | UpdateMode::Forced
1976 );
1977 crate::transfer::enforce_fetch_ref_update(
1978 local_ref,
1979 old,
1980 m.oid,
1981 mode,
1982 opts,
1983 explicit_refspec_fetch,
1984 classify_repo,
1985 )?;
1986 if write && !opts.dry_run {
1987 if !crate::refs::is_valid_storable_ref_name(local_ref) {
1988 updates.push(RefUpdate {
1989 remote_ref: m.remote_ref.clone(),
1990 local_ref: Some(local_ref.clone()),
1991 old_oid: old,
1992 new_oid: Some(m.oid),
1993 mode,
1994 note: Some("skipped (invalid ref name)".to_owned()),
1995 });
1996 continue;
1997 }
1998 store_batch.push((local_ref.clone(), crate::refs::store::RawRef::Direct(m.oid)));
1999 n_written += 1;
2000 }
2001
2002 updates.push(RefUpdate {
2003 remote_ref: m.remote_ref.clone(),
2004 local_ref: Some(local_ref.clone()),
2005 old_oid: old,
2006 new_oid: Some(m.oid),
2007 mode,
2008 note: None,
2009 });
2010 }
2011
2012 if !opts.dry_run && !store_batch.is_empty() {
2013 let t = std::time::Instant::now();
2014 crate::transfer::apply_fetch_ref_updates(
2015 local,
2016 store_batch,
2017 &mut updates,
2018 crate::transfer::FetchRefRejectHint {
2019 prune: opts.prune,
2020 positive: &positive,
2021 remote_refs: &remote_refs,
2022 remote_name: opts.remote_name.as_deref(),
2023 },
2024 opts.initial_remote_fetch,
2025 )?;
2026 d_write += t.elapsed();
2027 }
2028
2029 net_trace!(
2030 opts.network_trace,
2031 opts.diagnostics.as_ref(),
2032 "fetch_remote: apply {:?} — repo_open {:?}, prune {:?}, packed_load {:?}, \
2033 resolve {:?}, classify {:?} ({} ancestry-checked), write {:?} ({} written)",
2034 t_apply.elapsed(),
2035 d_repo_open,
2036 d_prune,
2037 d_packed,
2038 d_resolve,
2039 d_classify,
2040 n_ancestry,
2041 d_write,
2042 n_written
2043 );
2044
2045 net_trace!(
2046 opts.network_trace,
2047 opts.diagnostics.as_ref(),
2048 "fetch_remote: done — {} ref update(s){} [+{:?} total]",
2049 updates.len(),
2050 default_branch
2051 .as_deref()
2052 .map(|b| format!(", default branch '{b}'"))
2053 .unwrap_or_default(),
2054 t_begin.elapsed()
2055 );
2056 finish_initial_remote_fetch_layout(local, opts, default_branch.as_deref())?;
2057 if let Some(remote) = opts.remote_name.as_deref() {
2058 ensure_remote_tracking_head_symref(local, remote, default_branch.as_deref(), opts.dry_run)?;
2059 }
2060 crate::transfer::finish_anonymous_url_fetch(
2061 local_git_dir,
2062 opts,
2063 explicit_refspec_fetch,
2064 default_branch.as_deref(),
2065 &remote_refs,
2066 &matched,
2067 )?;
2068 Ok(FetchOutcome {
2069 updates,
2070 default_branch,
2071 new_shallow: crate::shallow::dedupe_shallow_list(shallow_update.shallow),
2072 new_unshallow: shallow_update.unshallow,
2073 transfer,
2074 filter_sent,
2075 promisor: promisor_handshake,
2076 })
2077}
2078
2079fn fetch_remote_object_wants(
2081 local: &Repository,
2082 conn: &mut dyn Connection,
2083 opts: &mut FetchOptions,
2084 progress: &mut dyn Progress,
2085 cancel: &dyn Cancel,
2086) -> Result<FetchOutcome> {
2087 use crate::net_trace::net_trace;
2088
2089 let t_begin = std::time::Instant::now();
2090 let local_git_dir = local.git_dir();
2091 let local_odb = local.odb();
2092 let wants = local_odb.missing_stored_oids(opts.object_wants.iter().copied())?;
2093 if wants.is_empty() {
2094 return Ok(FetchOutcome {
2095 updates: Vec::new(),
2096 default_branch: None,
2097 new_shallow: Vec::new(),
2098 new_unshallow: Vec::new(),
2099 transfer: crate::transfer::TransferStats::default(),
2100 filter_sent: false,
2101 promisor: None,
2102 });
2103 }
2104
2105 let v2_caps = if conn.protocol_version() >= 2 {
2106 Some(conn.capabilities().to_vec())
2107 } else {
2108 None
2109 };
2110 let v0_caps = if v2_caps.is_none() {
2111 Some(conn.capabilities().join(" "))
2112 } else {
2113 None
2114 };
2115 let (wire_filter, filter_sent) = crate::partial_clone::wire_filter_for_capabilities(
2116 opts.filter.as_ref(),
2117 v2_caps.as_deref(),
2118 v0_caps.as_deref(),
2119 );
2120 if opts.promisor_object_fetch {
2121 opts.mark_received_pack_promisor = true;
2122 } else {
2123 opts.mark_received_pack_promisor = opts.filter.is_some();
2124 }
2125 let wire_filter = wire_filter.as_deref();
2126 let local_shallow = crate::shallow::load_shallow_oids(local_git_dir)?;
2127 let mut transfer = crate::transfer::TransferStats::default();
2128
2129 net_trace!(
2130 opts.network_trace,
2131 opts.diagnostics.as_ref(),
2132 "fetch_remote_object_wants: {} object(s) [+{:?} since begin]",
2133 wants.len(),
2134 t_begin.elapsed()
2135 );
2136
2137 let (pack_path, shallow_update) = if let Some(caps) = v2_caps.as_ref() {
2138 validate_fetch_depth_server(opts, None, Some(caps))?;
2139 let deepen = V2DeepenArgs::from_opts(opts, &local_shallow);
2140 negotiate_pack_v2(
2141 local,
2142 conn,
2143 caps,
2144 &wants,
2145 &deepen,
2146 wire_filter,
2147 progress,
2148 cancel,
2149 V2FetchMode::ObjectWants,
2150 )?
2151 } else {
2152 negotiate_pack(
2153 local,
2154 conn,
2155 &wants,
2156 opts,
2157 &local_shallow,
2158 wire_filter,
2159 progress,
2160 cancel,
2161 true,
2162 )?
2163 };
2164 if let Some(path) = pack_path {
2165 let pack_config =
2166 crate::config::ConfigSet::load(&opts.environment, Some(local_git_dir), true)?;
2167 let index_threads = crate::parallel::index_pack_threads_for_ingest(
2168 opts.thread_policy,
2169 &pack_config,
2170 opts.index_pack_threads,
2171 )?;
2172 let ingested = crate::index_pack::ingest_received_pack_path_with_progress(
2173 path,
2174 local_odb,
2175 &crate::index_pack::IngestPackOptions {
2176 fix_thin: true,
2177 skip_post_index_verify: true,
2178 threads: index_threads,
2179 merged_config: Some(pack_config),
2180 environment: opts.environment.clone(),
2181 mark_promisor_pack: opts.mark_received_pack_promisor,
2182 keep_message: None,
2183 trusted_index_rows: None,
2184 },
2185 Some(progress),
2186 Some(cancel),
2187 )?;
2188 transfer = crate::transfer::TransferStats::from_ingested_pack(&ingested);
2189 }
2190
2191 if v2_caps.is_some() && !wants.is_empty() {
2192 conn.finish_send();
2193 }
2194
2195 if !opts.dry_run {
2196 crate::shallow::apply_shallow_updates(
2197 local_git_dir,
2198 &shallow_update.shallow,
2199 &shallow_update.unshallow,
2200 )?;
2201 }
2202
2203 Ok(FetchOutcome {
2204 updates: Vec::new(),
2205 default_branch: None,
2206 new_shallow: crate::shallow::dedupe_shallow_list(shallow_update.shallow),
2207 new_unshallow: shallow_update.unshallow,
2208 transfer,
2209 filter_sent,
2210 promisor: None,
2211 })
2212}
2213
2214pub fn fetch_operation_identity(
2216 env: &crate::environment::Environment,
2217 git_dir: &Path,
2218) -> Result<crate::ident::Signature> {
2219 use crate::ident_config::ident_default_name;
2220 let cfg = crate::config::ConfigSet::load(env, Some(git_dir), true)?;
2221 let name = ident_default_name(&cfg, env);
2222 let email = cfg
2223 .get("user.email")
2224 .filter(|e| !e.trim().is_empty())
2225 .unwrap_or_else(|| "unknown".to_owned());
2226 let now = time::OffsetDateTime::now_utc();
2227 crate::ident::Signature::new(name, email, now).map_err(crate::error::Error::InvalidSignature)
2228}
2229
2230pub(crate) fn ensure_remote_tracking_head_symref(
2233 local: &Repository,
2234 remote: &str,
2235 default_branch: Option<&str>,
2236 dry_run: bool,
2237) -> Result<()> {
2238 if dry_run {
2239 return Ok(());
2240 }
2241 let Some(branch) = default_branch else {
2242 return Ok(());
2243 };
2244 let head_ref = format!("refs/remotes/{remote}/HEAD");
2245 if crate::refs::resolve_ref_in(local, &head_ref).is_ok() {
2246 return Ok(());
2247 }
2248 let target = format!("refs/remotes/{remote}/{branch}");
2249 if crate::refs::resolve_ref_in(local, &target).is_err() {
2250 return Ok(());
2251 }
2252 crate::refs::write_symbolic_ref_on_store(local.refs(), local.git_dir(), &head_ref, &target)
2253}
2254
2255pub(crate) fn finish_initial_remote_fetch_layout(
2259 local: &Repository,
2260 opts: &FetchOptions,
2261 default_branch: Option<&str>,
2262) -> Result<()> {
2263 if opts.dry_run || !opts.initial_remote_fetch {
2264 return Ok(());
2265 }
2266 let git_dir = local.git_dir();
2267 if local.refs_on_disk() {
2268 crate::refs::pack_imported_refs_for_clone(git_dir, opts.remote_name.as_deref())?;
2269 }
2270 let Some(remote) = opts.remote_name.as_deref() else {
2271 return Ok(());
2272 };
2273 let Some(branch) = default_branch else {
2274 return Ok(());
2275 };
2276 let target = format!("refs/remotes/{remote}/{branch}");
2277 let head_ref = format!("refs/remotes/{remote}/HEAD");
2278 let Ok(tip) = crate::refs::resolve_ref_in(local, &target) else {
2279 return Ok(());
2280 };
2281 crate::refs::write_symbolic_ref_on_store(local.refs(), git_dir, &head_ref, &target)?;
2282 let Some(ref log) = opts.clone_reflog else {
2283 return Ok(());
2284 };
2285 let null = ObjectId::null(local.odb().hash_algo());
2286 if local.refs_on_disk() {
2287 return crate::refs::append_reflog_with_signature(
2288 git_dir,
2289 &head_ref,
2290 &null,
2291 &tip,
2292 &log.identity,
2293 &log.message,
2294 true,
2295 );
2296 }
2297 log.identity
2298 .validate()
2299 .map_err(crate::error::Error::InvalidSignature)?;
2300 let identity = crate::refs::store::format_reflog_identity(
2301 &log.identity.reflog_name_email(),
2302 log.identity.time,
2303 );
2304 let config = local.config()?;
2305 crate::refs::append_reflog_on_store(
2306 local.refs(),
2307 &head_ref,
2308 &null,
2309 &tip,
2310 &identity,
2311 &log.message,
2312 true,
2313 &config,
2314 )
2315}
2316
2317#[allow(clippy::too_many_arguments)]
2334pub(crate) fn add_wire_tags(
2335 mode: crate::transfer::TagMode,
2336 remote_refs: &[(String, ObjectId)],
2337 advertised_peel: &std::collections::HashMap<String, ObjectId>,
2338 negatives: &[RefspecItem],
2339 already_local: &HashSet<String>,
2340 force_all_tags: bool,
2341 matched: &mut Vec<crate::transfer::MatchedRef>,
2342 matched_oids: &mut HashSet<ObjectId>,
2343 seen_remote_ref: &mut HashSet<String>,
2344) -> HashSet<ObjectId> {
2345 let mut following_only: HashSet<ObjectId> = HashSet::new();
2346 if mode == crate::transfer::TagMode::None {
2347 return following_only;
2348 }
2349 for (name, oid) in remote_refs {
2350 if !name.starts_with("refs/tags/") {
2351 continue;
2352 }
2353 if seen_remote_ref.contains(name) || ref_excluded(name, negatives) {
2354 continue;
2355 }
2356 if mode == crate::transfer::TagMode::Following && already_local.contains(name) {
2357 continue;
2358 }
2359 seen_remote_ref.insert(name.clone());
2360 if matched_oids.insert(*oid) && mode == crate::transfer::TagMode::Following {
2364 following_only.insert(*oid);
2365 }
2366 matched.push(crate::transfer::MatchedRef {
2367 remote_ref: name.clone(),
2368 local_ref: Some(name.clone()),
2369 oid: *oid,
2370 force: force_all_tags && mode == crate::transfer::TagMode::All,
2371 is_tag: true,
2372 auto_followed: mode == crate::transfer::TagMode::Following,
2373 dest_is_tag: true,
2374 advertised_peel: advertised_peel.get(name).copied(),
2375 fetch_head: crate::fetch_request::FetchHeadPolicy::ForMerge,
2376 });
2377 }
2378 following_only
2379}
2380
2381pub(crate) fn following_tag_wants_with_local_peel(
2384 matched: &[crate::transfer::MatchedRef],
2385 following_only: &HashSet<ObjectId>,
2386 local_odb: &crate::odb::Odb,
2387) -> Result<HashSet<ObjectId>> {
2388 let mut wants = HashSet::new();
2389 for m in matched {
2390 if !m.is_tag || !following_only.contains(&m.oid) {
2391 continue;
2392 }
2393 let Some(peel) = m.advertised_peel else {
2394 continue;
2395 };
2396 if local_odb.contains(&peel)? && !local_odb.contains(&m.oid)? {
2397 wants.insert(m.oid);
2398 }
2399 }
2400 Ok(wants)
2401}
2402
2403pub(crate) fn shallow_fetched_commit_oids(
2407 odb: &crate::odb::Odb,
2408 roots: &[ObjectId],
2409 depth: u32,
2410 shallow_boundaries: &HashSet<ObjectId>,
2411) -> Result<HashSet<ObjectId>> {
2412 use crate::objects::{parse_commit, ObjectKind};
2413 use std::collections::{HashMap, VecDeque};
2414
2415 if depth == 0 {
2416 return Ok(HashSet::new());
2417 }
2418 let max_depth = usize::try_from(depth).unwrap_or(usize::MAX);
2419 let mut best_depth: HashMap<ObjectId, usize> = HashMap::new();
2420 let mut q: VecDeque<(ObjectId, usize)> = VecDeque::new();
2421 for &root in roots {
2422 best_depth.insert(root, 1);
2423 q.push_back((root, 1));
2424 }
2425 let mut out = HashSet::new();
2426 while let Some((oid, d)) = q.pop_front() {
2427 if d > max_depth {
2428 continue;
2429 }
2430 if best_depth.get(&oid).copied() != Some(d) {
2431 continue;
2432 }
2433 out.insert(oid);
2434 if d == max_depth {
2435 continue;
2436 }
2437 if shallow_boundaries.contains(&oid) {
2438 continue;
2439 }
2440 if !odb.contains(&oid)? {
2441 continue;
2442 }
2443 let Ok(obj) = odb.read(&oid) else {
2444 continue;
2445 };
2446 if obj.kind != ObjectKind::Commit {
2447 continue;
2448 }
2449 let Ok(c) = parse_commit(&obj.data) else {
2450 continue;
2451 };
2452 for p in c.parents {
2453 let nd = d.saturating_add(1);
2454 if nd > max_depth {
2455 continue;
2456 }
2457 let prev = best_depth.get(&p).copied().unwrap_or(usize::MAX);
2458 if nd < prev {
2459 best_depth.insert(p, nd);
2460 q.push_back((p, nd));
2461 }
2462 }
2463 }
2464 Ok(out)
2465}
2466
2467pub(crate) fn following_peel_commit_set_for_shallow(
2470 odb: &crate::odb::Odb,
2471 commit_tips: &HashSet<ObjectId>,
2472 depth: Option<u32>,
2473 shallow_boundaries: &HashSet<ObjectId>,
2474) -> Result<HashSet<ObjectId>> {
2475 let roots: Vec<ObjectId> = commit_tips.iter().copied().collect();
2476 match depth.filter(|d| *d > 0) {
2477 Some(d) => shallow_fetched_commit_oids(odb, &roots, d, shallow_boundaries),
2478 None => Ok(commit_tips.clone()),
2479 }
2480}
2481
2482pub(crate) fn following_tag_wants_peel_among_fetched_commits(
2486 matched: &[crate::transfer::MatchedRef],
2487 following_only: &HashSet<ObjectId>,
2488 fetch_commit_oids: &HashSet<ObjectId>,
2489) -> HashSet<ObjectId> {
2490 let mut wants = HashSet::new();
2491 for m in matched {
2492 if !m.is_tag || !following_only.contains(&m.oid) {
2493 continue;
2494 }
2495 let Some(peel) = m.advertised_peel else {
2496 continue;
2497 };
2498 if peel != m.oid && fetch_commit_oids.contains(&peel) {
2499 wants.insert(m.oid);
2500 }
2501 }
2502 wants
2503}
2504
2505pub(crate) fn retain_following_tags(
2526 local_odb: &crate::odb::Odb,
2527 matched: &mut Vec<crate::transfer::MatchedRef>,
2528 pack_oids: &HashSet<ObjectId>,
2529 shallow_boundaries: &HashSet<ObjectId>,
2530) -> Result<()> {
2531 let followed = |m: &crate::transfer::MatchedRef| m.is_tag && m.auto_followed;
2532 if !matched.iter().any(followed) {
2533 return Ok(());
2534 }
2535
2536 let roots: Vec<ObjectId> = matched
2537 .iter()
2538 .filter(|m| !m.is_tag)
2539 .map(|m| m.oid)
2540 .collect();
2541 let peeled_by_tag: std::collections::HashMap<ObjectId, ObjectId> = matched
2542 .iter()
2543 .filter(|m| followed(m))
2544 .map(|m| {
2545 let peeled = m.advertised_peel.unwrap_or_else(|| {
2546 local_odb
2547 .peel_stored_tag_target(m.oid)
2548 .ok()
2549 .flatten()
2550 .unwrap_or(m.oid)
2551 });
2552 (m.oid, peeled)
2553 })
2554 .collect();
2555 let mut local_commits: HashSet<ObjectId> = HashSet::new();
2556 let mut local_trees_and_blobs: HashSet<ObjectId> = HashSet::new();
2557 for &oid in peeled_by_tag.values() {
2558 if pack_oids.contains(&oid) || !local_odb.contains(&oid)? {
2561 continue;
2562 }
2563 match local_odb.read_info(&oid)?.kind {
2564 crate::objects::ObjectKind::Tree | crate::objects::ObjectKind::Blob => {
2565 local_trees_and_blobs.insert(oid);
2566 }
2567 crate::objects::ObjectKind::Commit => {
2568 local_commits.insert(oid);
2569 }
2570 crate::objects::ObjectKind::Tag => {}
2572 }
2573 }
2574 let local_reach = reachable_commits(local_odb, &roots, &local_commits, shallow_boundaries)?;
2575 let mut keep = Vec::with_capacity(matched.len());
2576 for m in matched.iter() {
2577 if !followed(m) {
2578 keep.push(true);
2579 continue;
2580 }
2581 let peeled = peeled_by_tag.get(&m.oid).copied().unwrap_or(m.oid);
2582 let reachable = pack_oids.contains(&peeled)
2583 || local_trees_and_blobs.contains(&peeled)
2584 || local_reach.contains(&peeled);
2585 keep.push(
2588 reachable
2589 && (m.oid == peeled || pack_oids.contains(&m.oid) || local_odb.contains(&m.oid)?),
2590 );
2591 }
2592 let mut keep = keep.into_iter();
2593 matched.retain(|_| keep.next().unwrap_or(false));
2594 Ok(())
2595}
2596
2597pub(crate) fn tags_already_local(
2615 local_refs: &dyn crate::refs::store::RefStore,
2616 remote_refs: &[(String, ObjectId)],
2617) -> HashSet<String> {
2618 remote_refs
2619 .iter()
2620 .filter(|(name, _)| name.starts_with("refs/tags/") && local_refs.resolve(name).is_ok())
2621 .map(|(name, _)| name.clone())
2622 .collect()
2623}
2624
2625pub(crate) fn reachable_commits(
2636 odb: &crate::odb::Odb,
2637 roots: &[ObjectId],
2638 targets: &HashSet<ObjectId>,
2639 shallow_boundaries: &HashSet<ObjectId>,
2640) -> Result<HashSet<ObjectId>> {
2641 use crate::objects::{parse_commit, parse_tag, ObjectKind};
2642
2643 let mut seen: HashSet<ObjectId> = HashSet::new();
2644 let mut remaining: HashSet<ObjectId> = targets.clone();
2645 if remaining.is_empty() {
2646 return Ok(seen);
2647 }
2648
2649 let graph = crate::commit_graph_file::CommitGraphChain::load(odb.objects_dir());
2654
2655 let mut stack: Vec<ObjectId> = roots.to_vec();
2656 while let Some(oid) = stack.pop() {
2657 if remaining.is_empty() {
2658 break;
2659 }
2660 if seen.contains(&oid) {
2661 continue;
2662 }
2663 if shallow_boundaries.contains(&oid) {
2664 if odb.contains(&oid)? {
2665 seen.insert(oid);
2666 remaining.remove(&oid);
2667 }
2668 continue;
2669 }
2670 if let Some(graph) = graph.as_ref() {
2671 if let Some((parents, _time)) = graph.graph_commit(&oid) {
2672 seen.insert(oid);
2673 remaining.remove(&oid);
2674 stack.extend(parents);
2675 continue;
2676 }
2677 }
2678 let Ok(obj) = odb.read(&oid) else {
2679 continue;
2680 };
2681 seen.insert(oid);
2682 remaining.remove(&oid);
2683 match obj.kind {
2684 ObjectKind::Commit => {
2685 if let Ok(c) = parse_commit(&obj.data) {
2686 for p in c.parents {
2687 stack.push(p);
2688 }
2689 }
2690 }
2691 ObjectKind::Tag => {
2692 if let Ok(t) = parse_tag(&obj.data) {
2693 stack.push(t.object);
2694 }
2695 }
2696 ObjectKind::Tree | ObjectKind::Blob => {}
2698 }
2699 }
2700 Ok(seen)
2701}
2702
2703#[cfg(test)]
2704mod fetch_advertised_ref_tests {
2705 use super::*;
2706 use crate::objects::ObjectId;
2707 use std::io::Cursor;
2708
2709 fn linear_history(len: usize) -> (tempfile::TempDir, Vec<ObjectId>) {
2710 use crate::objects::ObjectKind;
2711 let dir = tempfile::tempdir().unwrap();
2712 let repo = crate::repo::init_repository(
2713 dir.path(),
2714 true,
2715 "main",
2716 None,
2717 crate::RefStorageFormat::Files,
2718 )
2719 .unwrap();
2720 let odb = repo.odb();
2721 let tree = odb.write(ObjectKind::Tree, b"").unwrap();
2722 let mut commits: Vec<ObjectId> = Vec::new();
2723 for i in 0..len {
2724 let mut body = format!("tree {tree}\n");
2725 if let Some(parent) = commits.last() {
2726 body.push_str(&format!("parent {parent}\n"));
2727 }
2728 let time = 1_000_000 + i;
2729 body.push_str(&format!(
2730 "author T <t@e.com> {time} +0000\ncommitter T <t@e.com> {time} +0000\n\nc{i}\n"
2731 ));
2732 commits.push(odb.write(ObjectKind::Commit, body.as_bytes()).unwrap());
2733 }
2734 let tip = commits.last().copied().unwrap();
2735 crate::refs::write_ref(repo.git_dir(), "refs/heads/main", &tip).unwrap();
2736 (dir, commits)
2737 }
2738
2739 #[test]
2740 fn v2_local_haves_are_produced_only_as_far_as_asked() {
2741 let (dir, commits) = linear_history(400);
2742 let unrelated_want = ObjectId::from_bytes(&[9u8; 20]).unwrap();
2743 let local = Repository::open(dir.path(), &RepositoryOptions::empty()).unwrap();
2744
2745 let mut drained = v2_local_haves(&local, &[unrelated_want]).unwrap();
2746 let all = drained.all().unwrap().to_vec();
2747 assert!(drained.exhausted);
2748 assert_eq!(all[0], *commits.last().unwrap());
2749 assert!(
2750 all.len() > 4,
2751 "skipping still offers several haves: {}",
2752 all.len()
2753 );
2754
2755 let mut lazy = v2_local_haves(&local, &[unrelated_want]).unwrap();
2756 let first = lazy.first(4).unwrap().to_vec();
2757 assert_eq!(first.as_slice(), &all[..4]);
2758 assert_eq!(lazy.taken.len(), 4, "only the requested haves are walked");
2759 assert!(!lazy.exhausted);
2760 assert_eq!(lazy.all().unwrap(), all.as_slice());
2761 }
2762
2763 #[test]
2764 fn v2_local_haves_first_stops_at_a_short_history() {
2765 let (dir, _) = linear_history(3);
2766 let want = ObjectId::from_bytes(&[9u8; 20]).unwrap();
2767 let local = Repository::open(dir.path(), &RepositoryOptions::empty()).unwrap();
2768 let all = v2_local_haves(&local, &[want])
2769 .unwrap()
2770 .all()
2771 .unwrap()
2772 .to_vec();
2773 let mut haves = v2_local_haves(&local, &[want]).unwrap();
2774 assert_eq!(haves.first(INITIAL_FLUSH).unwrap(), all.as_slice());
2775 assert!(haves.exhausted);
2776 }
2777
2778 #[test]
2779 fn v2_ls_refs_prefixes_follow_refspecs_and_tag_mode() {
2780 use crate::transfer::TagMode;
2781
2782 let heads = vec!["+refs/heads/*:refs/remotes/origin/*".to_owned()];
2783 assert_eq!(v2_ls_refs_prefixes(&heads, TagMode::None), ["refs/heads/"]);
2784 assert_eq!(
2785 v2_ls_refs_prefixes(&heads, TagMode::Following),
2786 ["refs/heads/", "refs/tags/"]
2787 );
2788 let short = vec!["main".to_owned()];
2789 assert_eq!(
2790 v2_ls_refs_prefixes(&short, TagMode::None),
2791 [
2792 "main",
2793 "refs/main",
2794 "refs/tags/main",
2795 "refs/heads/main",
2796 "refs/remotes/main",
2797 "refs/remotes/main/HEAD",
2798 ]
2799 );
2800 let oid = vec![format!("{}:refs/heads/x", "a".repeat(40))];
2801 assert_eq!(
2802 v2_ls_refs_prefixes(&oid, TagMode::None),
2803 fetch_ref_list_prefixes(&oid)
2804 );
2805 }
2806
2807 #[test]
2808 fn fetch_ref_list_prefixes_includes_refs_dwim_for_pull() {
2809 let specs = vec!["pull/123/head:pr-123".to_owned()];
2810 let prefixes = fetch_ref_list_prefixes(&specs);
2811 assert!(prefixes.iter().any(|p| p == "refs/pull/123/head"));
2812 assert!(prefixes.iter().any(|p| p == "refs/heads/"));
2813 }
2814
2815 #[test]
2816 fn build_v2_ls_refs_request_includes_unborn_when_advertised() {
2817 use crate::odb::Odb;
2818 use crate::transfer::TagMode;
2819
2820 let caps = vec!["ls-refs=unborn".to_owned()];
2821 let odb = Odb::new(std::path::Path::new("/dev/null"));
2822 let req = build_v2_ls_refs_request(&caps, &odb, TagMode::None, &[], None).unwrap();
2823 let text = String::from_utf8_lossy(&req);
2824 assert!(
2825 text.contains("unborn"),
2826 "request must ask for unborn HEAD:\n{text}"
2827 );
2828 assert!(!text.contains("promisor-remote"), "{text}");
2829 }
2830
2831 #[test]
2832 fn v2_fetch_response_cut_before_the_pack_is_truncated() {
2833 let mut wire: Vec<u8> = Vec::new();
2835 wire.extend_from_slice(b"0014acknowledgments\n");
2836 wire.extend_from_slice(b"0008NAK\n");
2837 wire.extend_from_slice(b"0001");
2838 let err = super::read_v2_fetch_pack_response(
2839 &mut wire.as_slice(),
2840 &mut Vec::new(),
2841 None,
2842 &mut super::ShallowUpdate::default(),
2843 &mut crate::fetch::NoProgress,
2844 &crate::progress::NeverCancel,
2845 false,
2846 )
2847 .unwrap_err();
2848 assert!(
2849 matches!(
2850 err,
2851 Error::RemoteUnreadable {
2852 reason: crate::error::RemoteUnreadableReason::ResponseTruncated,
2853 ..
2854 }
2855 ),
2856 "{err:?}"
2857 );
2858 }
2859
2860 #[test]
2861 fn build_v2_ls_refs_request_carries_promisor_reply_before_args() {
2862 use crate::odb::Odb;
2863 use crate::transfer::TagMode;
2864
2865 let caps = vec!["agent=git/2.50".to_owned(), "object-format=sha1".to_owned()];
2866 let odb = Odb::new(std::path::Path::new("/dev/null"));
2867 let req = build_v2_ls_refs_request(&caps, &odb, TagMode::None, &[], Some("lop")).unwrap();
2868 let text = String::from_utf8_lossy(&req);
2869 let reply = text.find("promisor-remote=lop\n").expect("reply line");
2870 let delim = text.find("0001").expect("delim");
2871 assert!(reply < delim, "{text}");
2872 }
2873
2874 #[test]
2875 fn v2_acknowledgments_collect_acked_commits_and_ready() {
2876 let a = ObjectId::from_hex("aabbccddeeff00112233445566778899aabbccdd").unwrap();
2877 let b = ObjectId::from_hex("00112233445566778899aabbccddeeff00112233").unwrap();
2878 let mut body = Vec::new();
2879 crate::pkt_line::write_line(&mut body, "acknowledgments").unwrap();
2880 crate::pkt_line::write_line(&mut body, &format!("ACK {a}")).unwrap();
2881 crate::pkt_line::write_line(&mut body, &format!("ACK {b}")).unwrap();
2882 crate::pkt_line::write_line(&mut body, "ready").unwrap();
2883 crate::pkt_line::write_delim(&mut body).unwrap();
2884
2885 let round = read_v2_acknowledgments(&mut Cursor::new(body), false)
2886 .unwrap()
2887 .unwrap();
2888 assert!(round.ready);
2889 assert_eq!(round.common, vec![a, b]);
2890 }
2891
2892 #[test]
2893 fn v2_acknowledgments_nak_has_no_common() {
2894 let mut body = Vec::new();
2895 crate::pkt_line::write_line(&mut body, "acknowledgments").unwrap();
2896 crate::pkt_line::write_line(&mut body, "NAK").unwrap();
2897 crate::pkt_line::write_flush(&mut body).unwrap();
2898
2899 let round = read_v2_acknowledgments(&mut Cursor::new(body), false)
2900 .unwrap()
2901 .unwrap();
2902 assert!(!round.ready);
2903 assert!(round.common.is_empty());
2904 }
2905
2906 #[test]
2907 fn v2_acknowledgments_reject_malformed_ack() {
2908 let mut body = Vec::new();
2909 crate::pkt_line::write_line(&mut body, "acknowledgments").unwrap();
2910 crate::pkt_line::write_line(&mut body, "ACK not-an-oid").unwrap();
2911 crate::pkt_line::write_flush(&mut body).unwrap();
2912
2913 assert!(read_v2_acknowledgments(&mut Cursor::new(body), false).is_err());
2914 }
2915
2916 #[test]
2917 fn v2_flush_schedule_doubles_then_grows_by_a_tenth() {
2918 let mut sizes = vec![INITIAL_FLUSH];
2919 while sizes.len() < 6 {
2920 let last = *sizes.last().unwrap();
2921 sizes.push(next_v2_flush_count(last));
2922 }
2923 assert_eq!(sizes, vec![16, 32, 64, 128, 256, 512]);
2924 assert_eq!(
2925 next_v2_flush_count(LARGE_FLUSH),
2926 LARGE_FLUSH + LARGE_FLUSH / 10
2927 );
2928 }
2929
2930 #[test]
2931 fn parse_v2_ls_refs_unborn_head_symref_line() {
2932 let mut body = Vec::new();
2933 crate::pkt_line::write_line(&mut body, "unborn HEAD symref-target:refs/heads/trunk")
2934 .unwrap();
2935 crate::pkt_line::write_flush(&mut body).unwrap();
2936
2937 let (_, head, _, _) = parse_v2_ls_refs_response(&mut Cursor::new(body)).unwrap();
2938 assert_eq!(head.as_deref(), Some("refs/heads/trunk"));
2939 }
2940
2941 #[test]
2942 fn parse_v2_ls_refs_drops_traversal_ref() {
2943 let oid = ObjectId::from_hex("aabbccddeeff00112233445566778899aabbccdd").unwrap();
2944 let line = format!("{oid} refs/heads/../../../config");
2945 let mut body = Vec::new();
2946 crate::pkt_line::write_line(&mut body, &line).unwrap();
2947 crate::pkt_line::write_flush(&mut body).unwrap();
2948
2949 let (refs, _, _, _) = parse_v2_ls_refs_response(&mut Cursor::new(body)).unwrap();
2950 assert!(refs.is_empty());
2951 }
2952
2953 #[test]
2954 fn refspec_maps_traversal_to_non_storable_local_name() {
2955 use crate::refs::is_valid_storable_ref_name;
2956 use crate::refspec::parse_fetch_refspec;
2957 use crate::transfer::match_positive;
2958
2959 let spec = parse_fetch_refspec("+refs/heads/*:refs/remotes/origin/*").unwrap();
2960 let positive = vec![spec];
2961 let local = match_positive("refs/heads/../../../config", &positive)
2962 .flatten()
2963 .expect("wildcard matches malicious remote name");
2964 assert!(!is_valid_storable_ref_name(&local));
2965 }
2966
2967 #[test]
2968 fn add_wire_tags_following_skips_following_only_when_oid_already_matched() {
2969 use crate::transfer::TagMode;
2970 use std::collections::{HashMap, HashSet};
2971
2972 let shared = ObjectId::from_hex("4eda3c62a0f0a0a0a0a0a0a0a0a0a0a0a0a0a0a0").unwrap();
2973 let unreachable_tag =
2974 ObjectId::from_hex("1111111111111111111111111111111111111111").unwrap();
2975 let remote_refs = vec![
2976 ("refs/heads/rel".to_owned(), shared),
2977 ("refs/tags/light".to_owned(), shared),
2978 ("refs/tags/orphan".to_owned(), unreachable_tag),
2979 ];
2980 let advertised_peel = HashMap::new();
2981 let negatives: Vec<crate::refspec::RefspecItem> = Vec::new();
2982 let mut matched = Vec::new();
2983 let mut matched_oids = HashSet::from([shared]);
2984 let mut seen = HashSet::from(["refs/heads/rel".to_owned()]);
2985 matched.push(crate::transfer::MatchedRef {
2986 remote_ref: "refs/heads/rel".to_owned(),
2987 local_ref: Some("refs/remotes/origin/rel".to_owned()),
2988 oid: shared,
2989 force: false,
2990 is_tag: false,
2991 auto_followed: false,
2992 dest_is_tag: false,
2993 advertised_peel: None,
2994 fetch_head: crate::fetch_request::FetchHeadPolicy::ForMerge,
2995 });
2996
2997 let following_only = add_wire_tags(
2998 TagMode::Following,
2999 &remote_refs,
3000 &advertised_peel,
3001 &negatives,
3002 &HashSet::new(),
3003 false,
3004 &mut matched,
3005 &mut matched_oids,
3006 &mut seen,
3007 );
3008
3009 assert!(
3010 !following_only.contains(&shared),
3011 "shared branch/tag tip must stay wanted"
3012 );
3013 assert!(
3014 following_only.contains(&unreachable_tag),
3015 "tag-only oid stays following-only (no upfront want)"
3016 );
3017 assert_eq!(matched.iter().filter(|m| m.is_tag).count(), 2);
3018 }
3019
3020 #[test]
3021 fn following_tag_wants_includes_annotated_tag_when_peel_is_local() {
3022 use std::collections::HashSet;
3023
3024 let tag_obj = ObjectId::from_hex("bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb").unwrap();
3025 let tmp = tempfile::tempdir().expect("tempdir");
3026 let git_dir = tmp.path().join(".git");
3027 std::fs::create_dir_all(git_dir.join("objects")).expect("objects");
3028 let odb = crate::odb::Odb::new(git_dir.join("objects").as_path());
3029 let commit_bytes = b"tree 0000000000000000000000000000000000000000\nauthor T <t@example.com> 0 +0000\ncommitter T <t@example.com> 0 +0000\n\nx\n";
3031 let commit = odb
3032 .write_loose_materialize(crate::objects::ObjectKind::Commit, commit_bytes)
3033 .expect("write commit");
3034
3035 let following_only = HashSet::from([tag_obj]);
3036 let matched = vec![crate::transfer::MatchedRef {
3037 remote_ref: "refs/tags/v".to_owned(),
3038 local_ref: Some("refs/tags/v".to_owned()),
3039 oid: tag_obj,
3040 force: false,
3041 is_tag: true,
3042 auto_followed: true,
3043 dest_is_tag: true,
3044 advertised_peel: Some(commit),
3045 fetch_head: crate::fetch_request::FetchHeadPolicy::ForMerge,
3046 }];
3047 let wants = following_tag_wants_with_local_peel(&matched, &following_only, &odb).unwrap();
3048 assert!(wants.contains(&tag_obj));
3049 }
3050
3051 #[test]
3052 fn following_tag_wants_peel_among_fetched_commits_before_peel_is_local() {
3053 use std::collections::HashSet;
3054
3055 let tag_obj = ObjectId::from_hex("bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb").unwrap();
3056 let tip = ObjectId::from_hex("cccccccccccccccccccccccccccccccccccccccc").unwrap();
3057 let following_only = HashSet::from([tag_obj]);
3058 let fetch_commits = HashSet::from([tip]);
3059 let matched = vec![crate::transfer::MatchedRef {
3060 remote_ref: "refs/tags/v".to_owned(),
3061 local_ref: Some("refs/tags/v".to_owned()),
3062 oid: tag_obj,
3063 force: false,
3064 is_tag: true,
3065 auto_followed: true,
3066 dest_is_tag: true,
3067 advertised_peel: Some(tip),
3068 fetch_head: crate::fetch_request::FetchHeadPolicy::ForMerge,
3069 }];
3070 let wants = following_tag_wants_peel_among_fetched_commits(
3071 &matched,
3072 &following_only,
3073 &fetch_commits,
3074 );
3075 assert!(wants.contains(&tag_obj));
3076 }
3077
3078 #[test]
3079 fn shallow_peel_set_includes_parent_at_depth_two_not_tip_only() {
3080 use std::collections::HashSet;
3081
3082 let tmp = tempfile::tempdir().expect("tempdir");
3083 let objects = tmp.path().join("objects");
3084 std::fs::create_dir_all(&objects).expect("objects");
3085 let odb = crate::odb::Odb::new(objects.as_path());
3086 let chain = write_commit_chain(&odb, 3);
3087 let tip = *chain.last().expect("tip");
3088 let parent = chain[1];
3089 let tips = HashSet::from([tip]);
3090 let tips_only =
3091 following_peel_commit_set_for_shallow(&odb, &tips, Some(1), &HashSet::new()).unwrap();
3092 assert!(!tips_only.contains(&parent));
3093 let depth_two =
3094 following_peel_commit_set_for_shallow(&odb, &tips, Some(2), &HashSet::new()).unwrap();
3095 assert!(depth_two.contains(&parent));
3096 }
3097
3098 fn write_commit_chain(odb: &crate::odb::Odb, n: usize) -> Vec<ObjectId> {
3100 let mut chain: Vec<ObjectId> = Vec::new();
3101 for i in 0..n {
3102 let parent = chain
3103 .last()
3104 .map(|p| format!("parent {}\n", p.to_hex()))
3105 .unwrap_or_default();
3106 let body = format!(
3107 "tree 4b825dc642cb6eb9a060e54bf8d69288fbee4904\n{parent}\
3108 author T <t@example.com> {i} +0000\ncommitter T <t@example.com> {i} +0000\n\nc{i}\n"
3109 );
3110 let oid = odb
3111 .write_loose_materialize(crate::objects::ObjectKind::Commit, body.as_bytes())
3112 .expect("write commit");
3113 chain.push(oid);
3114 }
3115 chain
3116 }
3117
3118 fn matched_ref(
3119 name: &str,
3120 oid: ObjectId,
3121 peel: Option<ObjectId>,
3122 ) -> crate::transfer::MatchedRef {
3123 let is_tag = name.starts_with("refs/tags/");
3124 crate::transfer::MatchedRef {
3125 remote_ref: name.to_owned(),
3126 local_ref: Some(name.to_owned()),
3127 oid,
3128 force: false,
3129 is_tag,
3130 auto_followed: is_tag,
3131 dest_is_tag: is_tag,
3132 advertised_peel: peel,
3133 fetch_head: crate::fetch_request::FetchHeadPolicy::ForMerge,
3134 }
3135 }
3136
3137 fn kept_tags(matched: &[crate::transfer::MatchedRef]) -> Vec<&str> {
3138 let mut names: Vec<&str> = matched
3139 .iter()
3140 .filter(|m| m.is_tag)
3141 .map(|m| m.remote_ref.as_str())
3142 .collect();
3143 names.sort_unstable();
3144 names
3145 }
3146
3147 #[test]
3148 fn retain_following_tags_keeps_delivered_targets_without_reading_objects() {
3149 use std::collections::HashSet;
3150
3151 let tmp = tempfile::tempdir().expect("tempdir");
3154 let objects = tmp.path().join("objects");
3155 std::fs::create_dir_all(&objects).expect("objects");
3156 let odb = crate::odb::Odb::new(&objects);
3157 let oid = |c: char| ObjectId::from_hex(&c.to_string().repeat(40)).expect("oid");
3158 let (tip, tree, blob, tag_obj, missing_tag_obj, absent) =
3159 (oid('1'), oid('2'), oid('3'), oid('4'), oid('5'), oid('6'));
3160 let pack_oids = HashSet::from([tip, tree, blob, tag_obj]);
3161 let mut matched = vec![
3162 matched_ref("refs/heads/main", tip, None),
3163 matched_ref("refs/tags/light", tip, None),
3164 matched_ref("refs/tags/blob", blob, None),
3165 matched_ref("refs/tags/annotated-tree", tag_obj, Some(tree)),
3166 matched_ref("refs/tags/no-tag-object", missing_tag_obj, Some(tip)),
3167 matched_ref("refs/tags/undelivered", absent, None),
3168 ];
3169
3170 retain_following_tags(&odb, &mut matched, &pack_oids, &HashSet::new())
3171 .expect("retain following tags");
3172
3173 assert_eq!(
3174 kept_tags(&matched),
3175 [
3176 "refs/tags/annotated-tree",
3177 "refs/tags/blob",
3178 "refs/tags/light"
3179 ]
3180 );
3181 assert!(matched.iter().any(|m| m.remote_ref == "refs/heads/main"));
3182 }
3183
3184 #[test]
3185 fn retain_following_tags_keeps_refspec_selected_tags() {
3186 use std::collections::HashSet;
3187
3188 let tmp = tempfile::tempdir().expect("tempdir");
3189 let objects = tmp.path().join("objects");
3190 std::fs::create_dir_all(&objects).expect("objects");
3191 let odb = crate::odb::Odb::new(&objects);
3192 let peel = write_commit_chain(&odb, 1)[0];
3195 let tag_obj = ObjectId::from_hex(&"9".repeat(40)).expect("oid");
3196 let followed = matched_ref("refs/tags/followed", tag_obj, Some(peel));
3197 let mut selected = matched_ref("refs/tags/selected", tag_obj, Some(peel));
3198 selected.auto_followed = false;
3199 let mut matched = vec![followed, selected];
3200
3201 retain_following_tags(&odb, &mut matched, &HashSet::new(), &HashSet::new())
3202 .expect("retain following tags");
3203
3204 assert_eq!(kept_tags(&matched), ["refs/tags/selected"]);
3205 }
3206
3207 #[test]
3208 fn retain_following_tags_walks_commits_only_for_targets_already_local() {
3209 use std::collections::HashSet;
3210
3211 let tmp = tempfile::tempdir().expect("tempdir");
3212 let objects = tmp.path().join("objects");
3213 std::fs::create_dir_all(&objects).expect("objects");
3214 let odb = crate::odb::Odb::new(&objects);
3215 let chain = write_commit_chain(&odb, 4);
3216 let tip = chain[3];
3217 let unrelated = odb
3219 .write_loose_materialize(
3220 crate::objects::ObjectKind::Commit,
3221 b"tree 4b825dc642cb6eb9a060e54bf8d69288fbee4904\n\
3222 author T <t@example.com> 9 +0000\ncommitter T <t@example.com> 9 +0000\n\nu\n",
3223 )
3224 .expect("write commit");
3225 let blob = odb
3226 .write_loose_materialize(crate::objects::ObjectKind::Blob, b"local blob\n")
3227 .expect("write blob");
3228 let mut matched = vec![
3229 matched_ref("refs/heads/main", tip, None),
3230 matched_ref("refs/tags/old", chain[0], None),
3231 matched_ref("refs/tags/unrelated", unrelated, None),
3232 matched_ref("refs/tags/local-blob", blob, None),
3233 ];
3234
3235 retain_following_tags(&odb, &mut matched, &HashSet::new(), &HashSet::new())
3236 .expect("retain following tags");
3237 assert_eq!(
3238 kept_tags(&matched),
3239 ["refs/tags/local-blob", "refs/tags/old"]
3240 );
3241
3242 let mut matched = vec![
3244 matched_ref("refs/heads/main", tip, None),
3245 matched_ref("refs/tags/at-boundary", chain[1], None),
3246 matched_ref("refs/tags/behind-boundary", chain[0], None),
3247 ];
3248 retain_following_tags(
3249 &odb,
3250 &mut matched,
3251 &HashSet::new(),
3252 &HashSet::from([chain[1]]),
3253 )
3254 .expect("retain following tags");
3255 assert_eq!(kept_tags(&matched), ["refs/tags/at-boundary"]);
3256 }
3257
3258 #[test]
3259 fn retain_following_tags_never_lazy_fetches_in_a_partial_clone() {
3260 use std::collections::HashSet;
3261 use std::sync::atomic::{AtomicUsize, Ordering};
3262 use std::sync::Arc;
3263
3264 struct CountingFetcher(Arc<AtomicUsize>);
3265 impl crate::promisor_fetch::PromisorFetcher for CountingFetcher {
3266 fn fetch_missing(
3267 &self,
3268 _repo: &crate::repo::Repository,
3269 _oids: &[ObjectId],
3270 ) -> Result<()> {
3271 self.0.fetch_add(1, Ordering::SeqCst);
3272 Ok(())
3273 }
3274 }
3275
3276 let tmp = tempfile::tempdir().expect("tempdir");
3277 let git_dir = tmp.path().join(".git");
3278 std::fs::create_dir_all(git_dir.join("objects/pack")).expect("objects");
3279 std::fs::create_dir_all(git_dir.join("refs/heads")).expect("refs");
3280 std::fs::write(git_dir.join("HEAD"), "ref: refs/heads/main\n").expect("HEAD");
3281 std::fs::write(
3282 git_dir.join("config"),
3283 "[core]\n\trepositoryformatversion = 1\n\tbare = true\n\
3284 [extensions]\n\tpartialclone = origin\n\
3285 [remote \"origin\"]\n\turl = /nonexistent\n\tpromisor = true\n",
3286 )
3287 .expect("config");
3288 let calls = Arc::new(AtomicUsize::new(0));
3289 let repo = crate::repo::Repository::open(
3290 &git_dir,
3291 &RepositoryOptions::empty()
3292 .with_promisor_fetcher(Some(Arc::new(CountingFetcher(Arc::clone(&calls))))),
3293 )
3294 .expect("open");
3295 let odb = repo.odb();
3296 let tip = write_commit_chain(odb, 1)[0];
3297 let oid = |c: char| ObjectId::from_hex(&c.to_string().repeat(40)).expect("oid");
3298 let (promised_blob, missing_tag_obj) = (oid('7'), oid('8'));
3300 let write_tag = |target: ObjectId, kind: &str| {
3301 let body = format!(
3302 "object {}\ntype {kind}\ntag t\ntagger T <t@example.com> 0 +0000\n\nt\n",
3303 target.to_hex()
3304 );
3305 odb.write_loose_materialize(crate::objects::ObjectKind::Tag, body.as_bytes())
3306 .expect("write tag")
3307 };
3308 let local_tag_on_promised_blob = write_tag(promised_blob, "blob");
3310 let local_tag_on_missing_tag = write_tag(missing_tag_obj, "tag");
3311 let mut matched = vec![
3312 matched_ref("refs/heads/main", tip, None),
3313 matched_ref("refs/tags/light", tip, None),
3314 matched_ref("refs/tags/promised-blob", promised_blob, None),
3315 matched_ref("refs/tags/unpeeled", missing_tag_obj, None),
3316 matched_ref(
3317 "refs/tags/local-on-promised",
3318 local_tag_on_promised_blob,
3319 None,
3320 ),
3321 matched_ref(
3322 "refs/tags/local-on-missing-tag",
3323 local_tag_on_missing_tag,
3324 None,
3325 ),
3326 ];
3327
3328 retain_following_tags(odb, &mut matched, &HashSet::new(), &HashSet::new())
3329 .expect("retain following tags");
3330
3331 assert_eq!(
3332 calls.load(Ordering::SeqCst),
3333 0,
3334 "tag following must not lazy-fetch missing objects"
3335 );
3336 assert_eq!(kept_tags(&matched), ["refs/tags/light"]);
3337 }
3338
3339 #[test]
3340 fn reachable_commits_stops_once_every_target_is_found() {
3341 use std::collections::HashSet;
3342
3343 let tmp = tempfile::tempdir().expect("tempdir");
3344 let objects = tmp.path().join("objects");
3345 std::fs::create_dir_all(&objects).expect("objects");
3346 let odb = crate::odb::Odb::new(&objects);
3347 let chain = write_commit_chain(&odb, 5);
3348 let tip = chain[4];
3349 let none = HashSet::new();
3350
3351 let near = HashSet::from([chain[3]]);
3352 let seen = reachable_commits(&odb, &[tip], &near, &none).unwrap();
3353 assert!(seen.contains(&chain[3]), "reachable target is reported");
3354 assert!(
3355 !seen.contains(&chain[0]),
3356 "walk must stop before the root once every target is found"
3357 );
3358
3359 let unreachable =
3360 HashSet::from([
3361 ObjectId::from_hex("1111111111111111111111111111111111111111").unwrap(),
3362 ]);
3363 let seen = reachable_commits(&odb, &[tip], &unreachable, &none).unwrap();
3364 assert_eq!(
3365 seen,
3366 chain.iter().copied().collect::<HashSet<_>>(),
3367 "an unreachable target forces the full closure"
3368 );
3369
3370 assert!(
3371 reachable_commits(&odb, &[tip], &HashSet::new(), &none)
3372 .unwrap()
3373 .is_empty(),
3374 "no targets means no walk"
3375 );
3376 }
3377
3378 #[test]
3379 fn tags_already_local_matches_name_and_oid_across_loose_and_packed() {
3380 let tmp = tempfile::tempdir().expect("tempdir");
3381 let git_dir = tmp.path().join(".git");
3382 std::fs::create_dir_all(git_dir.join("objects")).expect("objects");
3383 std::fs::create_dir_all(git_dir.join("refs/tags")).expect("refs/tags");
3384 std::fs::write(git_dir.join("HEAD"), "ref: refs/heads/main\n").expect("HEAD");
3385
3386 let a = ObjectId::from_hex("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa").unwrap();
3387 let b = ObjectId::from_hex("bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb").unwrap();
3388 let c = ObjectId::from_hex("cccccccccccccccccccccccccccccccccccccccc").unwrap();
3389 std::fs::write(git_dir.join("refs/tags/loose"), format!("{}\n", a.to_hex()))
3390 .expect("loose tag");
3391 std::fs::write(
3392 crate::refs::store::paths::packed_refs_path(&git_dir),
3393 format!(
3394 "# pack-refs with: peeled fully-peeled sorted \n{} refs/tags/moved\n{} refs/tags/packed\n",
3395 a.to_hex(),
3396 b.to_hex()
3397 ),
3398 )
3399 .expect("packed-refs");
3400
3401 let remote_refs = vec![
3402 ("refs/heads/main".to_owned(), a),
3403 ("refs/tags/loose".to_owned(), a),
3404 ("refs/tags/packed".to_owned(), b),
3405 ("refs/tags/moved".to_owned(), c),
3406 ("refs/tags/new".to_owned(), c),
3407 ];
3408 let store = crate::refs::store::open_ref_store(&git_dir).unwrap();
3409 let present = tags_already_local(store.as_ref(), &remote_refs);
3410 assert_eq!(
3411 present,
3412 HashSet::from([
3413 "refs/tags/loose".to_owned(),
3414 "refs/tags/packed".to_owned(),
3415 "refs/tags/moved".to_owned(),
3416 ]),
3417 "every local tag name is skipped under Following"
3418 );
3419 assert!(tags_already_local(store.as_ref(), &remote_refs[..1]).is_empty());
3420 }
3421
3422 #[test]
3423 fn add_wire_tags_following_skips_tags_already_local() {
3424 use crate::transfer::TagMode;
3425 use std::collections::HashMap;
3426
3427 let tip = ObjectId::from_hex("2222222222222222222222222222222222222222").unwrap();
3428 let old_tag = ObjectId::from_hex("3333333333333333333333333333333333333333").unwrap();
3429 let new_tag = ObjectId::from_hex("4444444444444444444444444444444444444444").unwrap();
3430 let remote_refs = vec![
3431 ("refs/heads/main".to_owned(), tip),
3432 ("refs/tags/old".to_owned(), old_tag),
3433 ("refs/tags/new".to_owned(), new_tag),
3434 ];
3435 let already_local = HashSet::from(["refs/tags/old".to_owned()]);
3436
3437 for (mode, expect) in [
3438 (TagMode::Following, vec!["refs/tags/new"]),
3439 (TagMode::All, vec!["refs/tags/old", "refs/tags/new"]),
3440 ] {
3441 let mut matched = Vec::new();
3442 let mut matched_oids = HashSet::new();
3443 let mut seen = HashSet::new();
3444 add_wire_tags(
3445 mode,
3446 &remote_refs,
3447 &HashMap::new(),
3448 &[],
3449 &already_local,
3450 false,
3451 &mut matched,
3452 &mut matched_oids,
3453 &mut seen,
3454 );
3455 let names: Vec<&str> = matched.iter().map(|m| m.remote_ref.as_str()).collect();
3456 assert_eq!(names, expect, "{mode:?}");
3457 }
3458 }
3459}
3460
3461#[cfg(test)]
3462mod v2_negotiation_tests {
3463 use super::*;
3464 use crate::objects::ObjectKind;
3465 use crate::progress::NeverCancel;
3466 use crate::repo::{InitOptions, Repository};
3467 use std::io::Cursor;
3468
3469 struct AckingNeverReadyServer {
3472 caps: Vec<String>,
3473 pending: Vec<u8>,
3474 response: Cursor<Vec<u8>>,
3475 acked: Option<ObjectId>,
3476 requests: Vec<Vec<String>>,
3477 }
3478
3479 impl AckingNeverReadyServer {
3480 fn answer(&mut self) -> std::io::Result<()> {
3481 let mut lines = Vec::new();
3482 let mut cursor = Cursor::new(std::mem::take(&mut self.pending));
3483 while let Some(pkt) = pkt_line::read_packet(&mut cursor)? {
3484 if let pkt_line::Packet::Data(line) = pkt {
3485 lines.push(line.trim_end().to_owned());
3486 }
3487 }
3488 let haves: Vec<ObjectId> = lines
3489 .iter()
3490 .filter_map(|l| l.strip_prefix("have "))
3491 .filter_map(|h| ObjectId::from_hex(h).ok())
3492 .collect();
3493 let done = lines.iter().any(|l| l == "done");
3494 self.requests.push(lines);
3495 let mut out = Vec::new();
3496 if !done {
3497 let acked = *self.acked.get_or_insert(haves[0]);
3498 pkt_line::write_line(&mut out, "acknowledgments")?;
3499 if haves.contains(&acked) {
3500 pkt_line::write_line(&mut out, &format!("ACK {acked}"))?;
3501 } else {
3502 pkt_line::write_line(&mut out, "NAK")?;
3503 }
3504 pkt_line::write_flush(&mut out)?;
3505 }
3506 self.response = Cursor::new(out);
3507 Ok(())
3508 }
3509 }
3510
3511 impl Write for AckingNeverReadyServer {
3512 fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
3513 self.pending.extend_from_slice(buf);
3514 Ok(buf.len())
3515 }
3516
3517 fn flush(&mut self) -> std::io::Result<()> {
3518 if self.pending.is_empty() {
3519 return Ok(());
3520 }
3521 self.answer()
3522 }
3523 }
3524
3525 impl Connection for AckingNeverReadyServer {
3526 fn reader(&mut self) -> &mut dyn Read {
3527 &mut self.response
3528 }
3529 fn writer(&mut self) -> &mut dyn Write {
3530 self
3531 }
3532 fn advertised_refs(&self) -> &[(String, ObjectId)] {
3533 &[]
3534 }
3535 fn capabilities(&self) -> &[String] {
3536 &self.caps
3537 }
3538 fn head_symref(&self) -> Option<&str> {
3539 None
3540 }
3541 fn protocol_version(&self) -> u8 {
3542 2
3543 }
3544 fn stateless_rpc(&self) -> bool {
3545 true
3546 }
3547 }
3548
3549 #[test]
3550 fn replayed_acks_do_not_postpone_the_in_vain_cutoff() {
3551 const TIPS: usize = 2000;
3552 let tmp = tempfile::tempdir().unwrap();
3553 let repo = Repository::init(tmp.path(), &InitOptions::default()).unwrap();
3554 let git_dir = repo.git_dir().to_path_buf();
3555 let tree = repo.odb().write(ObjectKind::Tree, &[]).unwrap();
3556 for i in 0..TIPS {
3559 let when = 1_600_000_000 + i as i64;
3560 let body = format!(
3561 "tree {tree}\nauthor T <t@example.com> {when} +0000\n\
3562 committer T <t@example.com> {when} +0000\n\nroot {i}\n"
3563 );
3564 let oid = repo
3565 .odb()
3566 .write(ObjectKind::Commit, body.as_bytes())
3567 .unwrap();
3568 crate::refs::write_ref(&git_dir, &format!("refs/heads/b{i:04}"), &oid).unwrap();
3569 }
3570
3571 let mut server = AckingNeverReadyServer {
3572 caps: vec!["fetch".to_owned()],
3573 pending: Vec::new(),
3574 response: Cursor::new(Vec::new()),
3575 acked: None,
3576 requests: Vec::new(),
3577 };
3578 let want = ObjectId::from_hex("1111111111111111111111111111111111111111").unwrap();
3579 let _ = negotiate_pack_v2(
3581 &repo,
3582 &mut server,
3583 &[],
3584 &[want],
3585 &V2DeepenArgs::default(),
3586 None,
3587 &mut NoProgress,
3588 &NeverCancel,
3589 V2FetchMode::Refs {
3590 promisor_reply: None,
3591 },
3592 );
3593
3594 let last = server.requests.last().unwrap();
3595 assert!(
3596 last.iter().any(|l| l == "done"),
3597 "negotiation must end with done"
3598 );
3599 let distinct: HashSet<&str> = server
3600 .requests
3601 .iter()
3602 .flatten()
3603 .filter_map(|l| l.strip_prefix("have "))
3604 .collect();
3605 assert_eq!(distinct.len(), 16 + 32 + 64 + 128 + 256);
3609 assert!(distinct.len() < TIPS, "cutoff never fired");
3610 }
3611}