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