Skip to main content

grit_lib/
fetch.rs

1//! Wire-protocol fetch orchestration over a live transport connection.
2//!
3//! [`fetch_remote`] drives `git-upload-pack` negotiation: match refspecs against
4//! advertised refs, run `want`/`have`/`done`, demultiplex the side-band pack,
5//! ingest it, and classify ref updates into a shared fetch outcome. Protocol v2
6//! over streaming transports runs `ls-refs` before `fetch` when the connection
7//! advertises no refs on connect.
8
9use std::collections::HashSet;
10use std::io::{Read, Write};
11use std::path::Path;
12
13use crate::environment::RepositoryOptions;
14use crate::error::{Error, Result};
15use crate::fetch_negotiator::SkippingNegotiator;
16use crate::objects::ObjectId;
17use crate::pkt_line;
18use crate::protocol_v2;
19use crate::refspec::RefspecItem;
20use crate::repo::Repository;
21
22type LsRefsAdvertised = (
23    Vec<(String, ObjectId)>,
24    Option<String>,
25    std::collections::HashMap<String, ObjectId>,
26    Option<ObjectId>,
27);
28use crate::progress::{poll_cancel, Cancel, ProgressSink};
29use crate::transfer::{
30    classify_update, open_odb, prune_tracking_refs, ref_excluded, validate_fetch_depth_local,
31    validate_fetch_depth_server, FetchOptions, FetchOutcome, RefUpdate, UpdateMode,
32};
33use crate::transport::Connection;
34
35/// Sink for wire transport progress: typed phases via [`ProgressSink`] plus
36/// raw side-band channel 2 bytes from the remote.
37///
38/// Implementations receive the server's progress bytes (typically `\r`-delimited
39/// counter lines) through [`Progress::message`]. Typed
40/// [`progress::ProgressPhase`](crate::progress::ProgressPhase) updates use the
41/// [`crate::progress::ProgressSink`] methods inherited from this supertrait.
42pub trait Progress: ProgressSink {
43    /// Receive a chunk of progress bytes from side-band channel 2.
44    fn message(&mut self, _bytes: &[u8]) {}
45
46    /// When true, fetch omits `no-progress` so the remote may emit channel-2 progress.
47    fn remote_sideband_progress(&self) -> bool {
48        false
49    }
50
51    /// When true, a pack being received is also counted in objects and
52    /// reported through [`Progress::received_objects`].
53    ///
54    /// Counting inflates every entry once more on a background thread (the
55    /// receive itself never waits on it), so leave this off unless the counts
56    /// are shown.
57    fn wants_received_objects(&self) -> bool {
58        false
59    }
60
61    /// Objects received so far in the pack being streamed in, reported during
62    /// the [`ReceivingObjects`](crate::progress::ProgressPhase::ReceivingObjects)
63    /// phase when [`Progress::wants_received_objects`] is true. Counts may lag
64    /// the bytes reported through [`ProgressSink::inc`] slightly.
65    fn received_objects(&mut self, _counts: crate::progress::ReceivedObjects) {}
66}
67
68/// A [`Progress`] / [`ProgressSink`] that discards everything.
69pub struct NoProgress;
70
71impl Progress for NoProgress {}
72
73impl ProgressSink for NoProgress {}
74
75// --- Negotiation flush schedule (mirrors fetch-pack.c) --------------------
76
77const INITIAL_FLUSH: usize = 16;
78const PIPESAFE_FLUSH: usize = 32;
79
80fn next_flush_count(count: usize) -> usize {
81    if count < PIPESAFE_FLUSH {
82        count * 2
83    } else {
84        count + PIPESAFE_FLUSH
85    }
86}
87
88/// Batch size at which protocol v2 rounds stop doubling and grow by 10%.
89const LARGE_FLUSH: usize = 16384;
90
91/// Haves sent since the last new `ACK` after which a v2 fetch gives up and
92/// sends `done` (only once the server has acknowledged something).
93const MAX_IN_VAIN: usize = 256;
94
95/// Size of the next protocol v2 have batch. Every v2 `fetch` request is
96/// stateless, so rounds grow geometrically to bound the number of round trips.
97fn next_v2_flush_count(count: usize) -> usize {
98    if count < LARGE_FLUSH {
99        count * 2
100    } else {
101        count + count / 10
102    }
103}
104
105#[derive(Clone, Copy, PartialEq, Eq)]
106enum AckKind {
107    /// `ACK <oid>` with no status suffix (post-`done` or legacy).
108    Bare,
109    Common,
110    Continue,
111    Ready,
112}
113
114fn parse_ack(line: &str) -> Option<(ObjectId, AckKind)> {
115    if line == "NAK" {
116        return None;
117    }
118    let rest = line.strip_prefix("ACK ")?;
119    let hex = rest.split_whitespace().next()?;
120    let oid = ObjectId::from_hex(hex).ok()?;
121    let tail = rest.strip_prefix(hex).unwrap_or("").trim();
122    let kind = if tail.contains("continue") {
123        AckKind::Continue
124    } else if tail.contains("common") {
125        AckKind::Common
126    } else if tail.contains("ready") {
127        AckKind::Ready
128    } else {
129        AckKind::Bare
130    };
131    Some((oid, kind))
132}
133
134/// Read one ACK round, feeding `common`/`continue`/`ready` acks to the
135/// negotiator. Lifted from `read_ack_round_with_negotiator`.
136fn read_ack_round(reader: &mut dyn Read, negotiator: &mut SkippingNegotiator<'_>) -> Result<()> {
137    let mut reader = reader;
138    while let Some(pkt) = pkt_line::read_packet(&mut reader)? {
139        match pkt {
140            pkt_line::Packet::Flush => break,
141            pkt_line::Packet::Data(ln) => {
142                let ln = ln.trim_end();
143                if ln == "NAK" {
144                    // `upload-pack` sends `NAK` as the last line of a round with no trailing
145                    // flush; waiting for another packet would block forever.
146                    break;
147                }
148                let Some((ack_oid, kind)) = parse_ack(ln) else {
149                    break;
150                };
151                if kind == AckKind::Bare {
152                    break;
153                }
154                let _ = negotiator.ack(ack_oid)?;
155            }
156            _ => {}
157        }
158    }
159    Ok(())
160}
161
162/// Demultiplex the side-band-64k stream after `done`: collect channel-1 pack
163/// bytes into `out` (scanning for the `PACK` magic, which may span chunk
164/// boundaries), and forward channel-2 progress to `progress`. Channel 3 is a
165/// fatal error. Lifted from `read_sideband_pack_until_done`.
166fn read_sideband_pack(
167    r: &mut dyn Read,
168    target: &mut crate::pack_receive::PackReceiveTarget<'_>,
169    progress: &mut dyn Progress,
170    cancel: &dyn Cancel,
171) -> Result<()> {
172    crate::pack_receive::read_sideband_pack_to(r, target, progress, cancel)
173}
174
175/// New shallow boundaries the server reported during a fetch, captured from the
176/// `shallow-info` section so [`fetch_remote`] (and the HTTP fetch paths) can
177/// update the local `shallow` file and surface them in [`FetchOutcome`].
178#[derive(Default)]
179pub(crate) struct ShallowUpdate {
180    pub(crate) shallow: Vec<ObjectId>,
181    pub(crate) unshallow: Vec<ObjectId>,
182}
183
184/// Append the v0/v1 shallow/deepen request lines (after the `want`s, before the
185/// terminating flush): the client's current `shallow <oid>` grafts and any
186/// `deepen` / `deepen-since` / `deepen-not` the caller requested. Gated on the
187/// matching server capability where one exists. Mirrors the CLI's
188/// `append_fetch_request_extensions_v0_v1`.
189fn append_filter_request_v0(req: &mut Vec<u8>, wire_filter: &str) -> Result<()> {
190    pkt_line::write_line_to_vec(req, &format!("filter {wire_filter}"))?;
191    Ok(())
192}
193
194fn append_shallow_request_v0(
195    req: &mut Vec<u8>,
196    server_caps: &str,
197    local_shallow: &[ObjectId],
198    opts: &FetchOptions,
199) -> Result<()> {
200    let sends_shallow = !local_shallow.is_empty()
201        || opts.wire_deepen().is_some()
202        || opts
203            .deepen_since
204            .as_deref()
205            .is_some_and(|s| !s.trim().is_empty())
206        || opts.deepen_not.iter().any(|s| !s.trim().is_empty());
207    if sends_shallow && !server_caps.contains("shallow") {
208        return Err(Error::FetchShallowUnsupported);
209    }
210    for oid in local_shallow {
211        pkt_line::write_line_to_vec(req, &format!("shallow {}", oid.to_hex()))?;
212    }
213    if let Some((depth, relative)) = opts.wire_deepen() {
214        if relative && !server_caps.contains("deepen-relative") {
215            return Err(Error::FetchDeepenRelativeUnsupported);
216        }
217        pkt_line::write_line_to_vec(req, &format!("deepen {depth}"))?;
218        if relative {
219            pkt_line::write_line_to_vec(req, "deepen-relative")?;
220        }
221    }
222    if let Some(since) = opts
223        .deepen_since
224        .as_deref()
225        .filter(|s| !s.trim().is_empty())
226    {
227        if server_caps.contains("deepen-since") {
228            let value = crate::shallow::deepen_since_wire_value(since);
229            pkt_line::write_line_to_vec(req, &format!("deepen-since {value}"))?;
230        }
231    }
232    if server_caps.contains("deepen-not") {
233        for excl in &opts.deepen_not {
234            let excl = excl.trim();
235            if !excl.is_empty() {
236                pkt_line::write_line_to_vec(req, &format!("deepen-not {excl}"))?;
237            }
238        }
239    }
240    Ok(())
241}
242
243/// Negotiate with `git-upload-pack` over the connection and return the raw
244/// packfile bytes for the requested `wants`, plus any shallow-boundary updates
245/// the server reported (`shallow`/`unshallow`).
246///
247/// Drives the [`SkippingNegotiator`] over the connection: sends `want` lines
248/// (with v0/v1 capabilities) and the advertised refs as `known_common`, batches
249/// local `have`s with flushes (reading interleaved ACK rounds), sends `done`,
250/// consumes the final ACK/NAK, then demuxes the side-band pack.
251///
252/// When `opts` requests a deepen (or the repo is already shallow), the `want`
253/// block carries the client's `shallow <oid>` grafts and the `deepen*` args, and
254/// the server precedes the pack with a `shallow-info` section that this reads
255/// into the returned [`ShallowUpdate`].
256#[allow(clippy::too_many_arguments)]
257fn negotiate_pack(
258    local: &Repository,
259    conn: &mut dyn Connection,
260    wants: &[ObjectId],
261    opts: &mut FetchOptions,
262    local_shallow: &[ObjectId],
263    wire_filter: Option<&str>,
264    progress: &mut dyn Progress,
265    cancel: &dyn Cancel,
266    skip_local_haves: bool,
267) -> Result<(Option<std::path::PathBuf>, ShallowUpdate)> {
268    crate::trace::trace_span!("grit.fetch.negotiate");
269    let local_git_dir = local.git_dir();
270    let want_set: HashSet<ObjectId> = wants.iter().copied().collect();
271
272    let Some(first_want) = wants.first().copied() else {
273        return Ok((None, ShallowUpdate::default()));
274    };
275
276    // A deepen/shallow request changes the negotiation: the server precedes the
277    // pack with a `shallow-info` section, and the client's local history is not a
278    // usable negotiation base (its objects bottom out at grafts), so we skip
279    // offering `have`s. Mirrors `fetch-pack.c`'s shallow handling.
280    let shallow_request = opts.has_deepen_request() || !local_shallow.is_empty();
281    let skip_haves = shallow_request || skip_local_haves;
282
283    // Capture the advertised refs before borrowing the writer (avoids aliasing
284    // the connection's reader/writer with its accessors). v0/v1 shallow servers
285    // append `shallow <oid>` trailer lines to the advertisement; the capability
286    // string we read from the advertisement drives `deepen-since`/`deepen-not`.
287    let advertised: Vec<(String, ObjectId)> = conn.advertised_refs().to_vec();
288    let server_caps: String = conn.capabilities().join(" ");
289    validate_fetch_depth_server(opts, Some(&server_caps), None)?;
290
291    // Capability set matching `git fetch-pack`'s first `want` line for v0/v1.
292    let mut caps = if opts.remote_progress {
293        String::from(" multi_ack_detailed side-band-64k thin-pack include-tag ofs-delta agent=grit")
294    } else {
295        String::from(
296            " multi_ack_detailed side-band-64k thin-pack no-progress include-tag ofs-delta agent=grit",
297        )
298    };
299    if opts.wire_deepen().is_some_and(|(_, relative)| relative) {
300        if !server_caps.contains("deepen-relative") {
301            return Err(Error::FetchDeepenRelativeUnsupported);
302        }
303        caps.push_str(" deepen-relative");
304    }
305    if wire_filter.is_some() {
306        caps.push_str(" filter");
307    }
308
309    let mut req: Vec<u8> = Vec::new();
310    let w0 = format!("want {}{}", first_want.to_hex(), caps);
311    pkt_line::write_line_to_vec(&mut req, &w0)?;
312    for w in wants.iter().skip(1) {
313        pkt_line::write_line_to_vec(&mut req, &format!("want {}", w.to_hex()))?;
314    }
315    // Match `git fetch-pack`: with a single unique OID, repeat the bare want.
316    // git-daemon expects this. (Not done for shallow requests, which append
317    // shallow/deepen lines instead.)
318    if wants.len() == 1 && !skip_haves {
319        pkt_line::write_line_to_vec(&mut req, &format!("want {}", first_want.to_hex()))?;
320    }
321    if let Some(spec) = wire_filter {
322        append_filter_request_v0(&mut req, spec)?;
323    }
324    append_shallow_request_v0(&mut req, &server_caps, local_shallow, opts)?;
325    req.extend_from_slice(b"0000");
326    conn.writer().write_all(&req)?;
327    conn.writer().flush()?;
328
329    // Build the negotiator from local ref tips (heads, tags, HEAD), peeled to
330    // commits, excluding the wants. Advertised tips we already have become
331    // `known_common`.
332    let mut negotiator = SkippingNegotiator::new(local);
333    if !skip_haves {
334        add_negotiation_tips(&mut negotiator, local_git_dir, &want_set)?;
335        // Seed `known_common` only for advertised refs we already track at the same
336        // peeled commit. Unchanged refs avoid walking parent chains for every server
337        // tip during incremental fetch; moved refs are discovered via `have`s.
338        let mut local_by_name: std::collections::HashMap<String, ObjectId> =
339            std::collections::HashMap::new();
340        for prefix in ["refs/heads/", "refs/tags/"] {
341            if let Ok(entries) = crate::refs::list_refs(local_git_dir, prefix) {
342                for (name, oid) in entries {
343                    local_by_name.insert(name, oid);
344                }
345            }
346        }
347        for (name, oid) in &advertised {
348            if want_set.contains(oid) {
349                continue;
350            }
351            let Some(local_oid) = local_by_name.get(name) else {
352                continue;
353            };
354            if local_oid != oid {
355                continue;
356            }
357            if let Ok(Some(c)) = negotiator.repo().odb().peel_to_commit(*oid) {
358                negotiator.known_common(c)?;
359            }
360        }
361    }
362
363    // Shallow-info section: for a deepen/shallow request the v0/v1 server emits
364    // its `shallow`/`unshallow` lines (flush-terminated) immediately after the
365    // wants block, before any ACK round. Read it now so the subsequent ACK/NAK
366    // and pack reads line up.
367    let mut shallow_update = ShallowUpdate::default();
368    if shallow_request {
369        let (sh, unsh) = crate::shallow::read_shallow_info_section(&mut conn.reader())?;
370        shallow_update.shallow = sh;
371        shallow_update.unshallow = unsh;
372    } else if skip_local_haves && conn.stateless_rpc() {
373        read_ack_round(conn.reader(), &mut negotiator)?;
374    } else if conn.stateless_rpc() {
375        // Stateless HTTP completes a POST for the want block; consume its response
376        // before the first have round posts again (otherwise the next POST drains it).
377        read_ack_round(conn.reader(), &mut negotiator)?;
378    }
379
380    // Have/ACK exchange: batch haves, flush, read interleaved ACK rounds.
381    let mut count: usize = 0;
382    let mut flush_at: usize = INITIAL_FLUSH;
383    let mut pending: Vec<u8> = Vec::new();
384    let mut flushes: i32 = 0;
385    if !skip_haves {
386        while let Some(oid) = negotiator.next_have()? {
387            poll_cancel(progress, cancel)?;
388            pkt_line::write_line_to_vec(&mut pending, &format!("have {}", oid.to_hex()))?;
389            count += 1;
390            if flush_at <= count {
391                pending.extend_from_slice(b"0000");
392                conn.writer().write_all(&pending)?;
393                conn.writer().flush()?;
394                pending.clear();
395                flush_at = next_flush_count(count);
396                flushes += 1;
397                // Duplex: keep one window ahead. Stateless: every POST must be read
398                // before the next POST replaces the response.
399                if count == INITIAL_FLUSH && !conn.stateless_rpc() {
400                    continue;
401                }
402                read_ack_round(conn.reader(), &mut negotiator)?;
403                flushes -= 1;
404            }
405        }
406    }
407    if !pending.is_empty() {
408        pending.extend_from_slice(b"0000");
409        conn.writer().write_all(&pending)?;
410        conn.writer().flush()?;
411        flushes += 1;
412    }
413    while flushes > 0 {
414        read_ack_round(conn.reader(), &mut negotiator)?;
415        flushes -= 1;
416    }
417
418    // Send `done` (single pkt-line, no trailing flush) and read the ACK/NAK.
419    let mut tail = Vec::new();
420    pkt_line::write_line_to_vec(&mut tail, "done")?;
421    conn.writer().write_all(&tail)?;
422    conn.writer().flush()?;
423
424    // Stateless v0 repeats shallow/unshallow lines after `done` before NAK/pack.
425    if shallow_request && conn.stateless_rpc() {
426        let (sh, unsh) = crate::shallow::read_shallow_info_section(&mut conn.reader())?;
427        shallow_update.shallow.extend(sh);
428        shallow_update.unshallow.extend(unsh);
429    }
430
431    match pkt_line::read_packet(&mut conn.reader())? {
432        None => return Err(Error::Message("unexpected EOF after done".to_owned())),
433        Some(pkt_line::Packet::Flush) => {
434            return Err(Error::Message("unexpected flush after done".to_owned()))
435        }
436        Some(pkt_line::Packet::Data(ln)) => {
437            let ln = ln.trim_end();
438            if ln != "NAK" {
439                if let Some((ack_oid, kind)) = parse_ack(ln) {
440                    if kind != AckKind::Bare {
441                        let _ = negotiator.ack(ack_oid)?;
442                    }
443                } else if let Some(msg) = ln.strip_prefix("ERR ") {
444                    return Err(Error::Message(format!("remote error: {}", msg.trim_end())));
445                }
446            }
447        }
448        Some(_) => {}
449    }
450
451    let mut pack_receive = crate::pack_receive::TempPackReceive::create(local.odb().objects_dir())?;
452    pack_receive.read_sideband(conn.reader(), progress, cancel)?;
453    let pack_path = pack_receive.finish()?;
454    Ok((pack_path, shallow_update))
455}
456
457// ===========================================================================
458// Protocol v2 (streaming transports: git://, ssh)
459// ===========================================================================
460//
461// A v2 connection advertises no refs on connect (only the capability block).
462// `v2_ls_refs` recovers the ref map with a `command=ls-refs`; `negotiate_pack_v2`
463// runs the `command=fetch` negotiation and returns the demuxed pack. Both lift
464// the exact pkt-line shapes from the CLI's `file_upload_pack_v2` /
465// `fetch_transport` v2 paths and reuse `protocol_v2` cap helpers, the shared
466// `SkippingNegotiator`, and `read_sideband_pack`.
467
468/// The `object-format=` value to put on the wire for a v2 request: echo the
469/// server's advertised object-format when present, else fall back to the local
470/// odb's hash algorithm (sha1/sha256). Keeps the negotiation hash-algo-aware.
471pub(crate) fn v2_object_format(server_caps: &[String], local_odb: &crate::odb::Odb) -> String {
472    for c in server_caps {
473        if let Some(fmt) = c.strip_prefix("object-format=") {
474            let f = fmt.trim();
475            if !f.is_empty() {
476                return f.to_ascii_lowercase();
477            }
478        }
479    }
480    local_odb.hash_algo().name().to_owned()
481}
482
483fn push_unique_prefix(out: &mut Vec<String>, value: &str) {
484    if !out.iter().any(|v| v == value) {
485        out.push(value.to_owned());
486    }
487}
488
489/// Ref namespace prefixes to enumerate when advertising refs for fetch (local dir,
490/// v2 `ls-refs`, and opportunistic remote-tracking updates).
491///
492/// Always includes `refs/heads/` and `refs/tags/`. Explicit refspec sources also
493/// add literal prefixes (e.g. `refs/heads/main`, `refs/pull/123/head`) so DWIM can
494/// resolve `pull/123/head` under `refs/`.
495pub(crate) fn fetch_ref_list_prefixes(refspecs: &[String]) -> Vec<String> {
496    let mut out = vec!["refs/heads/".to_owned(), "refs/tags/".to_owned()];
497    for spec in refspecs {
498        if spec.starts_with('^') {
499            continue;
500        }
501        let raw = spec.strip_prefix('+').unwrap_or(spec.as_str());
502        let src = raw.split_once(':').map(|(s, _)| s).unwrap_or(raw).trim();
503        if src.is_empty() {
504            continue;
505        }
506        if src == "HEAD" {
507            push_unique_prefix(&mut out, "HEAD");
508            continue;
509        }
510        if let Some(star) = src.find('*') {
511            let prefix = &src[..star];
512            if prefix.is_empty() {
513                continue;
514            }
515            if prefix.starts_with("refs/") {
516                push_unique_prefix(&mut out, prefix);
517            } else {
518                push_unique_prefix(&mut out, &format!("refs/heads/{prefix}"));
519            }
520            continue;
521        }
522        if src.starts_with("refs/") {
523            push_unique_prefix(&mut out, src);
524        } else {
525            push_unique_prefix(&mut out, &format!("refs/heads/{src}"));
526            // Git DWIM: bare names may live directly under `refs/` (pull refs, etc.).
527            push_unique_prefix(&mut out, &format!("refs/{src}"));
528        }
529    }
530    out
531}
532
533/// Parse an `unborn HEAD symref-target:…` line from a v2 `ls-refs` response.
534#[must_use]
535pub(crate) fn parse_v2_ls_refs_unborn_head_symref(line: &str) -> Option<String> {
536    const PREFIX: &str = "unborn HEAD symref-target:";
537    let target = line.strip_prefix(PREFIX)?;
538    let target = target.split_whitespace().next()?.to_owned();
539    crate::refs::is_valid_advertised_symref_target(&target).then_some(target)
540}
541
542/// Parse one v2 `ls-refs` advertisement line into `(refname, oid, symref_target)`.
543///
544/// Lines look like `<oid> <refname>[ symref-target:<t>][ peeled:<oid>]`. Lib-side
545/// port of the CLI's `parse_ls_refs_v2_line` (the order of the optional suffixes
546/// is whichever the server emits; we scan for both tokens). Returns `None` for a
547/// malformed line.
548pub(crate) fn parse_ls_refs_v2_line(
549    line: &str,
550) -> Option<(String, ObjectId, Option<String>, Option<ObjectId>)> {
551    const SYM: &str = " symref-target:";
552    const PEEL: &str = " peeled:";
553    let (oid_hex, after_oid) = line.split_once(' ')?;
554    let oid = ObjectId::from_hex(oid_hex).ok()?;
555
556    // The refname ends at the first ` symref-target:` or ` peeled:` token.
557    let sym_at = after_oid.find(SYM);
558    let peel_at = after_oid.find(PEEL);
559    let name_end = match (sym_at, peel_at) {
560        (Some(a), Some(b)) => a.min(b),
561        (Some(a), None) => a,
562        (None, Some(b)) => b,
563        (None, None) => after_oid.len(),
564    };
565    let name = after_oid[..name_end].trim().to_owned();
566    if name.is_empty() {
567        return None;
568    }
569    let symref_target = sym_at.map(|pos| {
570        let tail = &after_oid[pos + SYM.len()..];
571        let end = tail.find(' ').unwrap_or(tail.len());
572        tail[..end].to_owned()
573    });
574    let advertised_peel = peel_at.and_then(|pos| {
575        let tail = &after_oid[pos + PEEL.len()..];
576        let end = tail.find(' ').unwrap_or(tail.len());
577        ObjectId::from_hex(tail[..end].trim()).ok()
578    });
579    Some((name, oid, symref_target, advertised_peel))
580}
581
582/// Issue `command=ls-refs` over a v2 connection and parse the ref map.
583///
584/// Sends the capability echo (agent/object-format via
585/// [`protocol_v2::cap_lines_for_command_request`]), the `0001` delimiter, then
586/// `symrefs`, `peel`, and `ref-prefix <p>` lines derived from `refspecs` (plus
587/// `refs/tags/` when `tags != None`), then flush. Returns the advertised
588/// `refs/heads/*` and `refs/tags/*` refs (peeled `^{}` carrier lines dropped) and
589/// the `HEAD` symref target. Lifted from the CLI's `v2_ls_refs_for_fetch`.
590fn v2_ls_refs(
591    conn: &mut dyn Connection,
592    server_caps: &[String],
593    local_odb: &crate::odb::Odb,
594    tags: crate::transfer::TagMode,
595    refspecs: &[String],
596) -> Result<LsRefsAdvertised> {
597    let req = build_v2_ls_refs_request(server_caps, local_odb, tags, refspecs)?;
598    conn.writer().write_all(&req)?;
599    conn.writer().flush()?;
600    parse_v2_ls_refs_response(conn.reader())
601}
602
603/// Build the `command=ls-refs` request body (capability echo + `0001` + the
604/// `symrefs`/`peel`/`ref-prefix` argument lines + flush) for a v2 fetch.
605///
606/// Factored out of [`v2_ls_refs`] so the streaming transports (which write it to
607/// a duplex socket) and the stateless smart-HTTP transport (which POSTs it as a
608/// request body) share one request builder. `HEAD` is always requested so the
609/// server advertises its `symref-target`; `refs/tags/` is added under `--tags` /
610/// tag-following even when the refspecs name only heads.
611pub(crate) fn build_v2_ls_refs_request(
612    server_caps: &[String],
613    local_odb: &crate::odb::Odb,
614    tags: crate::transfer::TagMode,
615    refspecs: &[String],
616) -> Result<Vec<u8>> {
617    let object_format = v2_object_format(server_caps, local_odb);
618    let cap_echo = protocol_v2::cap_lines_for_command_request(server_caps);
619
620    let mut req: Vec<u8> = Vec::new();
621    pkt_line::write_line(&mut req, "command=ls-refs")?;
622    // Echo agent/object-format; if the server advertised neither (rare), still
623    // pin the object-format so a sha256 server agrees on hash width.
624    if cap_echo.iter().any(|c| c.starts_with("object-format=")) {
625        for line in &cap_echo {
626            pkt_line::write_line(&mut req, line)?;
627        }
628    } else {
629        for line in &cap_echo {
630            pkt_line::write_line(&mut req, line)?;
631        }
632        pkt_line::write_line(&mut req, &format!("object-format={object_format}"))?;
633    }
634    pkt_line::write_delim(&mut req)?;
635    pkt_line::write_line(&mut req, "symrefs")?;
636    if crate::protocol_v2::server_advertises_ls_refs_unborn(server_caps) {
637        pkt_line::write_line(&mut req, "unborn")?;
638    }
639    pkt_line::write_line(&mut req, "peel")?;
640
641    // Always request `HEAD` so the server advertises its `symref-target`, which
642    // drives `FetchOutcome::default_branch` (the wire equivalent of the v0/v1
643    // `symref=HEAD:` capability). `HEAD` is dropped from the fetchable ref set.
644    pkt_line::write_line(&mut req, "ref-prefix HEAD")?;
645    let mut prefixes = fetch_ref_list_prefixes(refspecs);
646    if tags != crate::transfer::TagMode::None && !prefixes.iter().any(|p| p == "refs/tags/") {
647        // Tag-following / `--tags` wants the tag namespace advertised so we can
648        // add tags from the ls-refs result, even if the refspecs only name heads.
649        prefixes.push("refs/tags/".to_owned());
650    }
651    for p in &prefixes {
652        pkt_line::write_line(&mut req, &format!("ref-prefix {p}"))?;
653    }
654    pkt_line::write_flush(&mut req)?;
655    Ok(req)
656}
657
658/// Parse a `command=ls-refs` response into `(advertised refs, HEAD symref)`.
659///
660/// Reads `<oid> <refname>[ symref-target:…][ peeled:…]` lines up to the
661/// terminating flush, dropping peeled `^{}` carriers and recording the `HEAD`
662/// symref target. Shared by the streaming and stateless-HTTP v2 paths.
663pub(crate) fn parse_v2_ls_refs_response(reader: &mut dyn Read) -> Result<LsRefsAdvertised> {
664    // Response: `<oid> <refname>[ symref-target:…][ peeled:…]` lines, flush-terminated.
665    let mut advertised: Vec<(String, ObjectId)> = Vec::new();
666    let mut head_symref: Option<String> = None;
667    let mut head_oid: Option<ObjectId> = None;
668    let mut advertised_peel: std::collections::HashMap<String, ObjectId> =
669        std::collections::HashMap::new();
670    let mut reader = reader;
671    loop {
672        match pkt_line::read_packet(&mut reader)? {
673            None | Some(pkt_line::Packet::Flush) | Some(pkt_line::Packet::Delim) => break,
674            Some(pkt_line::Packet::ResponseEnd) => break,
675            Some(pkt_line::Packet::Data(line)) => {
676                let line = line.trim_end_matches('\n');
677                if let Some(msg) = line.strip_prefix("ERR ") {
678                    return Err(Error::Message(format!("remote error: {}", msg.trim_end())));
679                }
680                if let Some(t) = parse_v2_ls_refs_unborn_head_symref(line) {
681                    head_symref = Some(t);
682                    continue;
683                }
684                let Some((name, oid, symref_target, peel)) = parse_ls_refs_v2_line(line) else {
685                    continue;
686                };
687                if name.contains("^{") || name.ends_with("^{}") {
688                    continue;
689                }
690                if name == "HEAD" {
691                    head_oid = Some(oid);
692                    if let Some(t) =
693                        symref_target.filter(|t| crate::refs::is_valid_advertised_symref_target(t))
694                    {
695                        head_symref = Some(t);
696                    }
697                    // HEAD itself is not a fetchable ref here; refspecs target heads/tags.
698                    continue;
699                }
700                if crate::refs::is_valid_fetch_advertised_ref(&name) {
701                    if let Some(p) = peel {
702                        advertised_peel.insert(name.clone(), p);
703                    }
704                    advertised.push((name, oid));
705                }
706            }
707        }
708    }
709    Ok((advertised, head_symref, advertised_peel, head_oid))
710}
711
712/// Build the ordered `have` candidate list for a v2 fetch from the local ref
713/// tips, peeled to commits and excluding the wants, driven
714/// through the [`SkippingNegotiator`]'s skipping schedule.
715///
716/// Shared by the streaming (`negotiate_pack_v2`) and stateless-HTTP v2 fetch
717/// paths so both offer the server the same `have`s in the same order. The wire
718/// rounds (how many haves per request, when to send `done`) are batched by the
719/// caller, which differs between a duplex socket and stateless POSTs.
720/// The ordered local `have` list for a v2 fetch, produced on demand.
721///
722/// Draining the skipping negotiator walks the whole local history, so only as many haves are
723/// taken as the rounds actually send: usually the first batch is enough for the server to be
724/// `ready`.
725pub(crate) struct V2LocalHaves<'r> {
726    negotiator: SkippingNegotiator<'r>,
727    taken: Vec<ObjectId>,
728    exhausted: bool,
729}
730
731impl V2LocalHaves<'_> {
732    /// The first `n` haves (fewer when the local history has fewer).
733    fn first(&mut self, n: usize) -> Result<&[ObjectId]> {
734        while self.taken.len() < n && !self.exhausted {
735            match self.negotiator.next_have()? {
736                Some(oid) => self.taken.push(oid),
737                None => self.exhausted = true,
738            }
739        }
740        Ok(&self.taken[..self.taken.len().min(n)])
741    }
742
743    /// Every have, in the same order as [`Self::first`].
744    #[cfg(test)]
745    fn all(&mut self) -> Result<&[ObjectId]> {
746        self.first(usize::MAX)
747    }
748}
749
750pub(crate) fn v2_local_haves<'r>(
751    local: &'r Repository,
752    wants: &[ObjectId],
753) -> Result<V2LocalHaves<'r>> {
754    let want_set: HashSet<ObjectId> = wants.iter().copied().collect();
755    let mut negotiator = SkippingNegotiator::new(local);
756    add_negotiation_tips(&mut negotiator, local.git_dir(), &want_set)?;
757    Ok(V2LocalHaves {
758        negotiator,
759        taken: Vec::new(),
760        exhausted: false,
761    })
762}
763
764/// Seed `negotiator` with the local ref tips (heads, remote-tracking refs, tags,
765/// HEAD), peeled to commits and excluding `wants`, in a deterministic (hex)
766/// order. Remote-tracking tips matter: they usually sit right below what the
767/// server is about to send, so offering them keeps the pack minimal.
768fn add_negotiation_tips(
769    negotiator: &mut SkippingNegotiator<'_>,
770    local_git_dir: &Path,
771    wants: &HashSet<ObjectId>,
772) -> Result<()> {
773    let mut tips: Vec<ObjectId> = Vec::new();
774    let mut seen_tip: HashSet<ObjectId> = HashSet::new();
775    let mut push_tip = |negotiator: &SkippingNegotiator<'_>, oid: ObjectId| {
776        if let Ok(Some(c)) = negotiator.repo().odb().peel_to_commit(oid) {
777            if !wants.contains(&c) && seen_tip.insert(c) {
778                tips.push(c);
779            }
780        }
781    };
782    for prefix in ["refs/heads/", "refs/remotes/", "refs/tags/"] {
783        if let Ok(entries) = crate::refs::list_refs(local_git_dir, prefix) {
784            for (_, oid) in entries {
785                push_tip(negotiator, oid);
786            }
787        }
788    }
789    if let Ok(h) = crate::refs::resolve_ref(local_git_dir, "HEAD") {
790        push_tip(negotiator, h);
791    }
792    tips.sort_unstable();
793    for t in tips {
794        negotiator.add_tip(t)?;
795    }
796    Ok(())
797}
798
799/// Run a v2 `command=fetch` negotiation over the connection and return the raw
800/// pack bytes for `wants`.
801///
802/// Drives the [`SkippingNegotiator`] lazily, one round per request, framed as
803/// v2 (`command=fetch`, capability echo, `0001`, then
804/// `thin-pack`/`no-progress`/`ofs-delta`, `want <oid>` lines, `have <oid>` lines,
805/// and `done`). v2 servers keep no state between requests, so every round
806/// resends the commits the server has acknowledged so far, followed by the next
807/// batch of haves (16, then doubling). The walk stops as soon as the server
808/// answers `ready`, the negotiator runs out of haves, or [`MAX_IN_VAIN`] haves
809/// go unacknowledged after a first `ACK`; the final request carries `done`.
810/// Then reads the response sections (`acknowledgments`, optional
811/// `shallow-info`, then `wanted-refs`, then `packfile`) and demuxes the
812/// side-band-64k pack.
813#[allow(clippy::too_many_arguments)]
814fn negotiate_pack_v2(
815    local: &Repository,
816    conn: &mut dyn Connection,
817    server_caps: &[String],
818    wants: &[ObjectId],
819    deepen: &V2DeepenArgs,
820    wire_filter: Option<&str>,
821    progress: &mut dyn Progress,
822    cancel: &dyn Cancel,
823    skip_local_haves: bool,
824) -> Result<(Option<std::path::PathBuf>, ShallowUpdate)> {
825    if wants.is_empty() {
826        return Ok((None, ShallowUpdate::default()));
827    }
828    let local_odb = local.odb();
829    let object_format = v2_object_format(server_caps, local_odb);
830    let cap_echo = protocol_v2::cap_lines_for_command_request(server_caps);
831    let sideband_all = protocol_v2::fetch_supports_sideband_all(server_caps);
832
833    // A deepen/shallow request does not offer local haves (the local objects
834    // bottom out at grafts and are not a usable negotiation base), forcing the
835    // single-round path so the server sends a `shallow-info` section + pack.
836    let shallow_request = deepen.is_shallow_request();
837
838    // The ordered `have` list is produced lazily from local ref tips. Empty for
839    // a shallow request or promisor object-want fetch (Git
840    // `negotiationAlgorithm=noop`).
841    let mut haves = if shallow_request || skip_local_haves {
842        None
843    } else {
844        Some(v2_local_haves(local, wants)?)
845    };
846
847    let mut pack_receive = crate::pack_receive::TempPackReceive::create(local_odb.objects_dir())?;
848    let mut shallow_update = ShallowUpdate::default();
849    let mut common: Vec<ObjectId> = Vec::new();
850    let mut common_set: HashSet<ObjectId> = HashSet::new();
851    let mut batch = INITIAL_FLUSH;
852    let mut in_vain: usize = 0;
853    let mut seen_ack = false;
854    let mut round_haves: Vec<ObjectId> = Vec::new();
855    loop {
856        round_haves.clear();
857        round_haves.extend_from_slice(&common);
858        let added = match haves.as_mut() {
859            Some(haves) => {
860                poll_cancel(progress, cancel)?;
861                let previous = haves.taken.len();
862                haves.first(previous.saturating_add(batch))?;
863                round_haves.extend_from_slice(&haves.taken[previous..]);
864                haves.taken.len() - previous
865            }
866            None => 0,
867        };
868        in_vain += added;
869        let done = added == 0 || (seen_ack && in_vain >= MAX_IN_VAIN);
870        write_v2_fetch_request(
871            conn.writer(),
872            &object_format,
873            &cap_echo,
874            wants,
875            &round_haves,
876            sideband_all,
877            deepen,
878            wire_filter,
879            done,
880        )?;
881        if done {
882            break;
883        }
884        match read_v2_acknowledgments(conn.reader(), sideband_all)? {
885            // Not ready: record the new common commits and send the next round.
886            // The server re-ACKs every replayed common commit, so only a newly
887            // common commit counts as progress toward the in-vain cutoff.
888            Some(round) if !round.ready => {
889                for oid in round.common {
890                    if let Some(haves) = haves.as_mut() {
891                        haves.negotiator.ack(oid)?;
892                    }
893                    if common_set.insert(oid) {
894                        common.push(oid);
895                        in_vain = 0;
896                    }
897                    seen_ack = true;
898                }
899                batch = next_v2_flush_count(batch);
900            }
901            // `ready` (or no acknowledgments section): the pack follows in
902            // this same response after a delimiter.
903            _ => break,
904        }
905    }
906    read_v2_fetch_pack_response(
907        conn.reader(),
908        &mut Vec::new(),
909        Some(&mut pack_receive),
910        &mut shallow_update,
911        progress,
912        cancel,
913        sideband_all,
914    )?;
915    Ok((pack_receive.finish()?, shallow_update))
916}
917
918/// The shallow/deepen arguments for a v2 `command=fetch` request, derived from
919/// [`FetchOptions`] plus the local `shallow` file. Built once by the fetch driver
920/// and passed to [`write_v2_fetch_request`] on each round (every stateless POST
921/// must resend them).
922#[derive(Clone, Default)]
923pub(crate) struct V2DeepenArgs {
924    /// The client's current shallow grafts (`shallow <oid>` lines).
925    pub(crate) local_shallow: Vec<ObjectId>,
926    /// `deepen <n>` (absolute depth, or `INFINITE_DEPTH` for `--unshallow`).
927    pub(crate) depth: Option<u32>,
928    /// When true, send v0/v1/v2 `deepen-relative` after `deepen`.
929    pub(crate) deepen_relative: bool,
930    /// `deepen-since <unix-ts>`.
931    pub(crate) deepen_since: Option<String>,
932    /// `deepen-not <ref>` exclusions.
933    pub(crate) deepen_not: Vec<String>,
934    /// When true, omit the v2 `no-progress` fetch argument.
935    pub(crate) remote_progress: bool,
936}
937
938impl V2DeepenArgs {
939    /// Build the v2 deepen args from the fetch options and the local shallow file,
940    /// translating `--unshallow` into the `INFINITE_DEPTH` deepen Git uses.
941    pub(crate) fn from_opts(opts: &FetchOptions, local_shallow: &[ObjectId]) -> Self {
942        let (depth, deepen_relative) = opts
943            .wire_deepen()
944            .map(|(d, r)| (Some(d), r))
945            .unwrap_or((None, false));
946        Self {
947            local_shallow: local_shallow.to_vec(),
948            depth,
949            deepen_relative,
950            deepen_since: opts
951                .deepen_since
952                .as_deref()
953                .filter(|s| !s.trim().is_empty())
954                .map(crate::shallow::deepen_since_wire_value),
955            deepen_not: opts
956                .deepen_not
957                .iter()
958                .map(|s| s.trim().to_owned())
959                .filter(|s| !s.is_empty())
960                .collect(),
961            remote_progress: opts.remote_progress,
962        }
963    }
964
965    /// Whether any deepen/shallow argument is present (drives `shallow-info`
966    /// handling and the "skip offering haves" decision).
967    pub(crate) fn is_shallow_request(&self) -> bool {
968        self.depth.is_some()
969            || self.deepen_relative
970            || self.deepen_since.is_some()
971            || !self.deepen_not.is_empty()
972            || !self.local_shallow.is_empty()
973    }
974}
975#[expect(clippy::too_many_arguments)]
976/// Write a v2 `command=fetch` request: capability echo, `0001`, the standard
977/// `thin-pack`/`no-progress`/`ofs-delta` (+ `sideband-all`/`include-tag`)
978/// arguments, the shallow/deepen arguments, the `want <oid>` lines, the
979/// `have <oid>` lines, and `done` when `send_done`, terminated by flush. Lifted
980/// from the CLI's `write_v2_fetch_request` (streaming-fetch subset).
981pub(crate) fn write_v2_fetch_request(
982    w: &mut dyn Write,
983    object_format: &str,
984    cap_echo: &[String],
985    wants: &[ObjectId],
986    haves: &[ObjectId],
987    sideband_all: bool,
988    deepen: &V2DeepenArgs,
989    wire_filter: Option<&str>,
990    send_done: bool,
991) -> Result<()> {
992    let mut req: Vec<u8> = Vec::new();
993    pkt_line::write_line(&mut req, "command=fetch")?;
994    if cap_echo.iter().any(|c| c.starts_with("object-format=")) {
995        for line in cap_echo {
996            pkt_line::write_line(&mut req, line)?;
997        }
998    } else {
999        for line in cap_echo {
1000            pkt_line::write_line(&mut req, line)?;
1001        }
1002        pkt_line::write_line(&mut req, &format!("object-format={object_format}"))?;
1003    }
1004    pkt_line::write_delim(&mut req)?;
1005
1006    pkt_line::write_line(&mut req, "thin-pack")?;
1007    if !deepen.remote_progress {
1008        pkt_line::write_line(&mut req, "no-progress")?;
1009    }
1010    pkt_line::write_line(&mut req, "ofs-delta")?;
1011    if sideband_all {
1012        pkt_line::write_line(&mut req, "sideband-all")?;
1013    }
1014    // Ask the server to bundle tag objects pointing at fetched history; the
1015    // TagMode plumbing in `fetch_remote` decides which tag refs to write.
1016    pkt_line::write_line(&mut req, "include-tag")?;
1017
1018    // Shallow/deepen arguments (the `fetch` v2 command's `shallow`/`deepen*` args).
1019    for oid in &deepen.local_shallow {
1020        pkt_line::write_line(&mut req, &format!("shallow {}", oid.to_hex()))?;
1021    }
1022    if let Some(depth) = deepen.depth {
1023        pkt_line::write_line(&mut req, &format!("deepen {depth}"))?;
1024    }
1025    if deepen.deepen_relative {
1026        pkt_line::write_line(&mut req, "deepen-relative")?;
1027    }
1028    if let Some(since) = &deepen.deepen_since {
1029        pkt_line::write_line(&mut req, &format!("deepen-since {since}"))?;
1030    }
1031    for excl in &deepen.deepen_not {
1032        pkt_line::write_line(&mut req, &format!("deepen-not {excl}"))?;
1033    }
1034    if let Some(spec) = wire_filter {
1035        pkt_line::write_line(&mut req, &format!("filter {spec}"))?;
1036    }
1037
1038    for want in wants {
1039        pkt_line::write_line(&mut req, &format!("want {}", want.to_hex()))?;
1040    }
1041    for have in haves {
1042        pkt_line::write_line(&mut req, &format!("have {}", have.to_hex()))?;
1043    }
1044    if send_done {
1045        pkt_line::write_line(&mut req, "done")?;
1046    }
1047    pkt_line::write_flush(&mut req)?;
1048    w.write_all(&req)?;
1049    w.flush()?;
1050    Ok(())
1051}
1052
1053/// Outcome of reading one v2 `acknowledgments` section.
1054pub(crate) struct V2AckRound {
1055    /// Server emitted `ready`: the packfile follows in the same response after a
1056    /// delimiter — the caller reads the pack now without sending more.
1057    pub(crate) ready: bool,
1058    /// Commits the server acknowledged as common (`ACK <oid>`), in wire order.
1059    pub(crate) common: Vec<ObjectId>,
1060}
1061
1062/// Read a v2 `acknowledgments` section header and its `ACK`/`NAK`/`ready` lines.
1063///
1064/// Returns `Some(round)` for an `acknowledgments` section (with `ready` set when
1065/// the server is ready to send the pack), or `None` when the server skipped the
1066/// section and started a different one (e.g. went straight to `packfile`) — in
1067/// which case the header has been consumed and the caller proceeds to read the
1068/// pack response directly. Lifted from the CLI's `read_v2_acknowledgments`.
1069pub(crate) fn read_v2_acknowledgments(
1070    reader: &mut dyn Read,
1071    sideband_all: bool,
1072) -> Result<Option<V2AckRound>> {
1073    let mut reader = reader;
1074    let hdr = match crate::shallow::read_v2_fetch_primary_line(&mut reader, sideband_all)? {
1075        Some(s) => s,
1076        None => {
1077            return Ok(Some(V2AckRound {
1078                ready: false,
1079                common: Vec::new(),
1080            }))
1081        }
1082    };
1083    let hdr = hdr.as_str();
1084    if let Some(msg) = hdr.strip_prefix("ERR ") {
1085        return Err(Error::Message(format!("remote error: {}", msg.trim_end())));
1086    }
1087    if hdr != "acknowledgments" {
1088        // The server started a non-acknowledgments section; the pack reader,
1089        // called next, re-dispatches on this header. We cannot push it back, so
1090        // signal `None` only when we know the pack reader will see the same
1091        // header — which it will, because the next read picks up where we left
1092        // off. To make that work, the caller treats `None` as "read the pack".
1093        // The header we just consumed (`shallow-info`/`wanted-refs`/`packfile`)
1094        // would be lost; for the streaming fetch we only reach here after a
1095        // first round of haves, where servers always emit `acknowledgments`
1096        // first. Reaching a different header is therefore unexpected.
1097        return Err(Error::Message(format!(
1098            "unexpected v2 fetch section before acknowledgments: {hdr}"
1099        )));
1100    }
1101    let mut ready = false;
1102    let mut common = Vec::new();
1103    loop {
1104        match pkt_line::read_packet(&mut reader)? {
1105            Some(pkt_line::Packet::Delim) | Some(pkt_line::Packet::Flush) | None => break,
1106            Some(pkt_line::Packet::ResponseEnd) => break,
1107            Some(pkt_line::Packet::Data(payload)) => {
1108                let Some(ln) = crate::shallow::v2_primary_line_from_pkt(payload, sideband_all)?
1109                else {
1110                    continue;
1111                };
1112                if ln == "NAK" {
1113                    continue;
1114                }
1115                if let Some(hex) = ln.strip_prefix("ACK ") {
1116                    let oid = ObjectId::from_hex(hex.trim()).map_err(|_| {
1117                        Error::Message(format!("unexpected acknowledgment line: '{ln}'"))
1118                    })?;
1119                    common.push(oid);
1120                    continue;
1121                }
1122                if ln == "ready" {
1123                    ready = true;
1124                    continue;
1125                }
1126                return Err(Error::Message(format!(
1127                    "unexpected acknowledgment line: '{ln}'"
1128                )));
1129            }
1130        }
1131    }
1132    Ok(Some(V2AckRound { ready, common }))
1133}
1134
1135/// Whether `line` is a v2 `wanted-refs` (or ls-refs) advertisement row:
1136/// `<oid> <refname>[ …attributes…]`.
1137fn v2_wanted_ref_ad_line(line: &str) -> bool {
1138    let line = line.trim_end();
1139    let Some((oid_hex, rest)) = line.split_once(' ') else {
1140        return false;
1141    };
1142    if ObjectId::from_hex(oid_hex.trim()).is_err() {
1143        return false;
1144    }
1145    !rest.trim().is_empty()
1146}
1147
1148/// Read a v2 `command=fetch` response: capture the `shallow-info` section's
1149/// `shallow`/`unshallow` lines into `shallow_out`, skip the other non-pack
1150/// sections (`acknowledgments`/`shallow-info`/`wanted-refs`/`packfile-uris`), and demux the
1151/// side-band-64k pack from the `packfile` section into `out`. Lifted from the
1152/// CLI's `read_v2_fetch_pack_response`, extended to surface shallow updates.
1153pub(crate) fn read_v2_fetch_pack_response(
1154    reader: &mut dyn Read,
1155    out: &mut Vec<u8>,
1156    pack_sink: Option<&mut crate::pack_receive::TempPackReceive>,
1157    shallow_out: &mut ShallowUpdate,
1158    progress: &mut dyn Progress,
1159    cancel: &dyn Cancel,
1160    sideband_all: bool,
1161) -> Result<()> {
1162    loop {
1163        let hdr = loop {
1164            match pkt_line::read_packet(&mut &mut *reader)? {
1165                Some(pkt_line::Packet::Data(payload)) => {
1166                    if let Some(line) =
1167                        crate::shallow::v2_primary_line_from_pkt(payload, sideband_all)?
1168                    {
1169                        break line;
1170                    }
1171                }
1172                Some(pkt_line::Packet::Flush) | None => return Ok(()),
1173                Some(pkt_line::Packet::Delim) => continue,
1174                Some(other) => {
1175                    return Err(Error::Message(format!(
1176                        "unexpected v2 fetch response: {other:?}"
1177                    )))
1178                }
1179            }
1180        };
1181        let hdr = hdr.as_str();
1182        if let Some(msg) = hdr.strip_prefix("ERR ") {
1183            return Err(Error::Message(format!("remote error: {}", msg.trim_end())));
1184        }
1185        match hdr {
1186            h if h == crate::protocol_v2::FetchResponseSection::ShallowInfo.header() => {
1187                // Capture the shallow/unshallow boundary updates. The section is
1188                // delim-terminated (before the `packfile` header), which
1189                // `read_shallow_info_section` stops at, leaving the header intact.
1190                let (sh, unsh) =
1191                    crate::shallow::read_shallow_info_section_sideband(&mut *reader, sideband_all)?;
1192                shallow_out.shallow.extend(sh);
1193                shallow_out.unshallow.extend(unsh);
1194            }
1195            h if h == crate::protocol_v2::FetchResponseSection::Acknowledgments.header()
1196                || h == crate::protocol_v2::FetchResponseSection::WantedRefs.header()
1197                || h == crate::protocol_v2::FetchResponseSection::PackfileUris.header() =>
1198            {
1199                skip_v2_section_until_boundary(&mut *reader, sideband_all)?;
1200            }
1201            h if h == crate::protocol_v2::FetchResponseSection::Packfile.header() => {
1202                // The `packfile` section body is side-band-64k framed; reuse the
1203                // shared demuxer (channel 1 = pack, channel 2 = progress, 3 = err).
1204                if let Some(sink) = pack_sink {
1205                    sink.read_sideband(&mut *reader, progress, cancel)?;
1206                } else {
1207                    let mut target = crate::pack_receive::PackReceiveTarget::Memory(out);
1208                    read_sideband_pack(&mut *reader, &mut target, progress, cancel)?;
1209                }
1210                return Ok(());
1211            }
1212            other if v2_wanted_ref_ad_line(other) => {
1213                // `wanted-refs` payload line (some servers omit the section header).
1214            }
1215            other => {
1216                return Err(Error::Message(format!(
1217                    "unexpected v2 fetch section: {other}"
1218                )))
1219            }
1220        }
1221    }
1222}
1223
1224/// Skip a v2 response section up to its terminating flush/delim.
1225fn skip_v2_section_until_boundary(reader: &mut dyn Read, sideband_all: bool) -> Result<()> {
1226    loop {
1227        match pkt_line::read_packet(&mut &mut *reader)? {
1228            None | Some(pkt_line::Packet::Flush) | Some(pkt_line::Packet::Delim) => return Ok(()),
1229            Some(pkt_line::Packet::ResponseEnd) => return Ok(()),
1230            Some(pkt_line::Packet::Data(payload)) => {
1231                let _ = crate::shallow::v2_primary_line_from_pkt(payload, sideband_all)?;
1232            }
1233        }
1234    }
1235}
1236
1237/// Fetch from a remote over a live [`Connection`], driving the upload-pack
1238/// negotiation and writing the resulting tracking-ref updates into
1239/// `local_git_dir`.
1240///
1241/// The flow mirrors local fetch, but the remote ref list comes from the
1242/// connection's advertisement, objects arrive over the wire as a negotiated pack,
1243/// and the local repo is opened to classify ancestry. Reuses refspec matching,
1244/// tag-mode, prune, and classification helpers shared with other fetch paths.
1245///
1246/// Handles protocol v0, v1, and v2. For a v2 connection the ref map is recovered
1247/// via a `command=ls-refs` round (no refs are advertised on connect) and the
1248/// pack is negotiated with `command=fetch`; v0/v1 use the connect-time
1249/// advertisement and the classic `want`/`have`/`done` exchange.
1250///
1251/// # Errors
1252///
1253/// The local repository is opened with [`FetchOptions::environment`], so objects land in its
1254/// `GIT_OBJECT_DIRECTORY` when that is set.
1255///
1256/// Returns an error if `local_git_dir` is not a repository, if a refspec is invalid, if the
1257/// negotiation or pack ingest fails, or on ref/odb I/O failure.
1258pub fn fetch_remote(
1259    local_git_dir: &Path,
1260    conn: &mut dyn Connection,
1261    opts: &mut FetchOptions,
1262    progress: &mut dyn Progress,
1263    cancel: &dyn Cancel,
1264) -> Result<FetchOutcome> {
1265    let local = open_fetch_target(local_git_dir, opts)?;
1266    fetch_remote_into(&local, conn, opts, progress, cancel)
1267}
1268
1269/// Open the repository a git-dir based fetch writes into, honouring the fetch's environment.
1270pub(crate) fn open_fetch_target(local_git_dir: &Path, opts: &FetchOptions) -> Result<Repository> {
1271    Repository::open(
1272        local_git_dir,
1273        &RepositoryOptions::with_environment(opts.environment.clone()),
1274    )
1275}
1276
1277/// Like [`fetch_remote`], reading and writing objects through `local`'s object store.
1278pub(crate) fn fetch_remote_into(
1279    local: &Repository,
1280    conn: &mut dyn Connection,
1281    opts: &mut FetchOptions,
1282    progress: &mut dyn Progress,
1283    cancel: &dyn Cancel,
1284) -> Result<FetchOutcome> {
1285    use crate::net_trace::net_trace;
1286    let local_git_dir = local.git_dir();
1287    // Wall-clock since fetch_remote entry, included in phase-boundary traces so
1288    // `GRIT_NET_DEBUG=1` shows where time goes (see the "matched"/"done" lines).
1289    let t_begin = std::time::Instant::now();
1290    net_trace!(
1291        opts.network_trace,
1292        opts.diagnostics.as_ref(),
1293        "fetch_remote: begin — protocol v{}, {} refspec(s), tags={:?}, depth={:?}",
1294        conn.protocol_version(),
1295        opts.refspecs.len(),
1296        opts.tags,
1297        opts.fetch_depth
1298    );
1299    validate_fetch_depth_local(local_git_dir, opts)?;
1300    let local_cfg = crate::config::ConfigSet::load(&opts.environment, Some(local_git_dir), true)?;
1301    if opts.filter.is_none() {
1302        if let Some(remote) = opts.remote_name.as_deref() {
1303            opts.filter = crate::partial_clone::stored_remote_filter(&local_cfg, remote);
1304        }
1305    }
1306    if !opts.object_wants.is_empty() {
1307        return fetch_remote_object_wants(local, conn, opts, progress, cancel);
1308    }
1309    let local_odb_for_ls = local.odb();
1310
1311    // 1. Remote refs + default branch.
1312    //
1313    // For protocol v2 the connect-time advertisement carries no refs (only the
1314    // capability block); we obtain them now with an `ls-refs` command, derived
1315    // from the fetch refspecs. For v0/v1 they come from the connect-time
1316    // advertisement directly.
1317    let (remote_refs, default_branch, v2_caps, advertised_peel, remote_head_oid) = if conn
1318        .protocol_version()
1319        >= 2
1320    {
1321        let caps: Vec<String> = conn.capabilities().to_vec();
1322        let (refs, head_symref, peel, head_oid) =
1323            v2_ls_refs(conn, &caps, local_odb_for_ls, opts.tags, &opts.refspecs)?;
1324        let default_branch =
1325            head_symref.map(|t| t.strip_prefix("refs/heads/").unwrap_or(&t).to_owned());
1326        (refs, default_branch, Some(caps), peel, head_oid)
1327    } else {
1328        let default_branch = conn
1329            .head_symref()
1330            .map(|t| t.strip_prefix("refs/heads/").unwrap_or(t).to_owned());
1331        let advertised_peel = conn.advertised_tag_peel().cloned().unwrap_or_default();
1332        let remote_head_oid =
1333            crate::transfer::remote_head_oid_from_advertised(conn.advertised_refs());
1334        let remote_refs: Vec<(String, ObjectId)> = conn
1335            .advertised_refs()
1336            .iter()
1337            .filter(|(n, _)| {
1338                n != "HEAD" && !n.ends_with("^{}") && crate::refs::is_valid_fetch_advertised_ref(n)
1339            })
1340            .cloned()
1341            .collect();
1342        (
1343            remote_refs,
1344            default_branch,
1345            None,
1346            advertised_peel,
1347            remote_head_oid,
1348        )
1349    };
1350    net_trace!(
1351        opts.network_trace,
1352        opts.diagnostics.as_ref(),
1353        "fetch_remote: remote advertised {} ref(s){}",
1354        remote_refs.len(),
1355        v2_caps
1356            .as_ref()
1357            .map(|_| " (via v2 ls-refs)")
1358            .unwrap_or(" (v0/v1 advertisement)")
1359    );
1360
1361    if opts.initial_remote_fetch
1362        && remote_refs.is_empty()
1363        && !crate::transport::connection_handshake_complete(conn)
1364    {
1365        return Err(Error::RemoteUnreadable {
1366            url: None,
1367            reason: crate::error::RemoteUnreadableReason::NoHandshake,
1368        });
1369    }
1370
1371    let conn_caps: Vec<String> = conn.capabilities().to_vec();
1372    let remote_algo = crate::transfer::remote_hash_algo_from_advertisement(
1373        &[v2_caps.as_deref().unwrap_or(&[]), conn_caps.as_slice()],
1374        &remote_refs,
1375    );
1376    let reopened =
1377        crate::transfer::sync_empty_destination_object_format(local_git_dir, remote_algo, opts)?
1378            .then(|| crate::transfer::reopen_after_format_change(local))
1379            .transpose()?;
1380    let local = reopened.as_ref().unwrap_or(local);
1381    let local_git_dir = local.git_dir();
1382    let local_odb = local.odb();
1383
1384    let crate::transfer::FetchMatchSet {
1385        mut matched,
1386        positive,
1387        negatives,
1388        explicit_refspec_fetch,
1389    } = crate::transfer::build_fetch_matched_set(opts, &remote_refs, remote_head_oid)?;
1390    let mut matched_oids: HashSet<ObjectId> = matched.iter().map(|m| m.oid).collect();
1391    let mut seen_remote_ref: HashSet<String> =
1392        matched.iter().map(|m| m.remote_ref.clone()).collect();
1393
1394    // TagMode: add tags. Tag-following needs the closure of fetched objects,
1395    // which we cannot compute remotely; the wire `include-tag` capability makes
1396    // the server send tag objects with the pack, so we add advertised tags by
1397    // mode here and let classification proceed once the pack lands. For
1398    // `Following` we approximate using the advertised remote odb if present
1399    // (it is not, over the wire), so we add following tags whose oid is among
1400    // the matched set after the fact — handled below using the local odb.
1401    //
1402    // `following_only` collects the oids of tags added *provisionally* under
1403    // `Following`. These must NOT be `want`ed up front: git's tag-following only
1404    // keeps a tag whose target is already reachable from the fetched heads, so
1405    // wanting the tag itself would drag down its (otherwise unreachable) target
1406    // and incorrectly keep the tag. They are pruned by `retain_following_tags`.
1407    let tags_present = if opts.tags == crate::transfer::TagMode::Following {
1408        tags_already_local(local.refs(), &remote_refs)
1409    } else {
1410        HashSet::new()
1411    };
1412    let following_only = add_wire_tags(
1413        opts.tags,
1414        &remote_refs,
1415        &advertised_peel,
1416        &negatives,
1417        &tags_present,
1418        &mut matched,
1419        &mut matched_oids,
1420        &mut seen_remote_ref,
1421    );
1422    let fetch_commit_oids: HashSet<ObjectId> = matched
1423        .iter()
1424        .filter(|m| !m.is_tag)
1425        .map(|m| m.oid)
1426        .collect();
1427    let mut following_tag_wants =
1428        following_tag_wants_with_local_peel(&matched, &following_only, local_odb)?;
1429
1430    // The client's current shallow grafts (drives the wire `shallow <oid>` lines
1431    // and the "this is a shallow request" decisions in the negotiators).
1432    let local_shallow = crate::shallow::load_shallow_oids(local_git_dir)?;
1433    let shallow_request = opts.has_deepen_request() || !local_shallow.is_empty();
1434    if shallow_request {
1435        let local_shallow_set: HashSet<ObjectId> = local_shallow.iter().copied().collect();
1436        let peel_set = following_peel_commit_set_for_shallow(
1437            local_odb,
1438            &fetch_commit_oids,
1439            opts.absolute_depth_for_tags(),
1440            &local_shallow_set,
1441        )?;
1442        following_tag_wants.extend(following_tag_wants_peel_among_fetched_commits(
1443            &matched,
1444            &following_only,
1445            &peel_set,
1446        ));
1447    }
1448
1449    // 4. Wants. Normally the matched oids that are absent locally. For a
1450    // deepen/`--unshallow` request the wanted tips may already be present (a prior
1451    // shallow fetch landed them); we must still `want` them so the server fills in
1452    // the now-reachable ancestors past the old boundary.
1453    let wants: Vec<ObjectId> = if shallow_request {
1454        matched_oids
1455            .iter()
1456            .copied()
1457            .filter(|oid| !following_only.contains(oid) || following_tag_wants.contains(oid))
1458            .collect()
1459    } else {
1460        matched_oids
1461            .iter()
1462            .copied()
1463            .filter_map(|oid| {
1464                if following_only.contains(&oid) {
1465                    return following_tag_wants.contains(&oid).then_some(Ok(oid));
1466                }
1467                match local_odb.contains(&oid) {
1468                    Ok(true) => None,
1469                    Ok(false) => Some(Ok(oid)),
1470                    Err(err) => Some(Err(err)),
1471                }
1472            })
1473            .collect::<Result<_>>()?
1474    };
1475
1476    // Shallow-boundary updates the server reports (`shallow`/`unshallow`), applied
1477    // to the local `shallow` file and surfaced in the outcome.
1478    let mut shallow_update = ShallowUpdate::default();
1479    let mut pack_oids: HashSet<ObjectId> = HashSet::new();
1480    let mut transfer = crate::transfer::TransferStats::default();
1481
1482    net_trace!(
1483        opts.network_trace,
1484        opts.diagnostics.as_ref(),
1485        "fetch_remote: {} matched ref(s), want {} object(s){} [+{:?} since begin]",
1486        matched.len(),
1487        wants.len(),
1488        if shallow_request {
1489            " (shallow request)"
1490        } else {
1491            ""
1492        },
1493        t_begin.elapsed()
1494    );
1495
1496    let mut dry_run_quarantine: Option<crate::receive_quarantine::ReceiveQuarantine> = None;
1497
1498    let v0_caps = if v2_caps.is_none() {
1499        Some(conn.capabilities().join(" "))
1500    } else {
1501        None
1502    };
1503    let (wire_filter, _filter_sent) = crate::partial_clone::wire_filter_for_capabilities(
1504        opts.filter.as_ref(),
1505        v2_caps.as_deref(),
1506        v0_caps.as_deref(),
1507    );
1508    // Match Git: a requested/stored partial-clone filter marks the received pack
1509    // promisor even when the server lacks `filter` and fetches unfiltered.
1510    opts.mark_received_pack_promisor = opts.filter.is_some();
1511    let wire_filter = wire_filter.as_deref();
1512
1513    if wants.is_empty() {
1514        crate::transport::send_client_flush(conn)?;
1515    } else {
1516        net_trace!(
1517            opts.network_trace,
1518            opts.diagnostics.as_ref(),
1519            "fetch_remote: negotiating + fetching pack…"
1520        );
1521        let (pack_path, su) = if let Some(caps) = v2_caps.as_ref() {
1522            validate_fetch_depth_server(opts, None, Some(caps))?;
1523            let deepen = V2DeepenArgs::from_opts(opts, &local_shallow);
1524            negotiate_pack_v2(
1525                local,
1526                conn,
1527                caps,
1528                &wants,
1529                &deepen,
1530                wire_filter,
1531                progress,
1532                cancel,
1533                false,
1534            )?
1535        } else {
1536            negotiate_pack(
1537                local,
1538                conn,
1539                &wants,
1540                opts,
1541                &local_shallow,
1542                wire_filter,
1543                progress,
1544                cancel,
1545                false,
1546            )?
1547        };
1548        shallow_update = su;
1549        net_trace!(
1550            opts.network_trace,
1551            opts.diagnostics.as_ref(),
1552            "fetch_remote: received pack, ingesting via index-pack…"
1553        );
1554        if let Some(path) = pack_path {
1555            if opts.pack_spool_only {
1556                net_trace!(
1557                    opts.network_trace,
1558                    opts.diagnostics.as_ref(),
1559                    "fetch_remote: pack spooled to {} (pack_spool_only)",
1560                    path.display()
1561                );
1562                return Ok(FetchOutcome {
1563                    updates: Vec::new(),
1564                    default_branch: None,
1565                    new_shallow: crate::shallow::dedupe_shallow_list(shallow_update.shallow),
1566                    new_unshallow: shallow_update.unshallow,
1567                    transfer: crate::transfer::TransferStats::default(),
1568                });
1569            }
1570            let pack_config =
1571                crate::config::ConfigSet::load(&opts.environment, Some(local_git_dir), true)?;
1572            let index_threads = crate::parallel::index_pack_threads_for_ingest(
1573                opts.thread_policy,
1574                &pack_config,
1575                opts.index_pack_threads,
1576            )?;
1577            if opts.dry_run && dry_run_quarantine.is_none() {
1578                dry_run_quarantine = Some(crate::receive_quarantine::ReceiveQuarantine::create(
1579                    local_odb,
1580                    local_git_dir,
1581                )?);
1582            }
1583            let ingested = if let Some(q) = dry_run_quarantine.as_ref() {
1584                let q_odb = q.odb();
1585                crate::index_pack::ingest_received_pack_path_with_progress(
1586                    path,
1587                    &q_odb,
1588                    &crate::index_pack::IngestPackOptions {
1589                        fix_thin: true,
1590                        skip_post_index_verify: true,
1591                        threads: index_threads,
1592                        merged_config: Some(pack_config),
1593                        environment: opts.environment.clone(),
1594                        mark_promisor_pack: opts.mark_received_pack_promisor,
1595                        trusted_index_rows: None,
1596                    },
1597                    Some(progress),
1598                    Some(cancel),
1599                )?
1600            } else {
1601                crate::index_pack::ingest_received_pack_path_with_progress(
1602                    path,
1603                    local_odb,
1604                    &crate::index_pack::IngestPackOptions {
1605                        fix_thin: true,
1606                        skip_post_index_verify: true,
1607                        threads: index_threads,
1608                        merged_config: Some(pack_config),
1609                        environment: opts.environment.clone(),
1610                        mark_promisor_pack: opts.mark_received_pack_promisor,
1611                        trusted_index_rows: None,
1612                    },
1613                    Some(progress),
1614                    Some(cancel),
1615                )?
1616            };
1617            transfer = crate::transfer::TransferStats::from_ingested_pack(&ingested);
1618            pack_oids = ingested.object_ids;
1619        }
1620    }
1621
1622    // Apply the shallow/unshallow boundary updates to the on-disk `shallow` file
1623    // before classifying refs (so connectivity reflects the new graft set).
1624    if !opts.dry_run {
1625        crate::shallow::apply_shallow_updates(
1626            local_git_dir,
1627            &shallow_update.shallow,
1628            &shallow_update.unshallow,
1629        )?;
1630    }
1631
1632    // Close the write side once the v2 conversation is done so the server's
1633    // persistent `serve_loop` sees EOF and exits — even when we sent only an
1634    // `ls-refs` (no wants) and skipped `command=fetch`. Without this a streaming
1635    // transport (ssh subprocess, daemon socket) hangs at teardown. No-op for
1636    // v0/v1, where the server closes after its single response.
1637    if v2_caps.is_some() && !wants.is_empty() {
1638        conn.finish_send();
1639    }
1640
1641    // For TagMode::Following, prune tags whose target did not arrive in the
1642    // pack (now resolvable against the local odb, which holds the fetched
1643    // objects). All/None already handled; Following kept only when reachable.
1644    if opts.tags == crate::transfer::TagMode::Following {
1645        let t = std::time::Instant::now();
1646        let tag_odb = dry_run_quarantine
1647            .as_ref()
1648            .map(|q| q.odb())
1649            .unwrap_or_else(|| local_odb.clone());
1650        retain_following_tags(
1651            &tag_odb,
1652            &mut matched,
1653            &pack_oids,
1654            &crate::shallow::load_shallow_boundaries(local_git_dir),
1655        )?;
1656        net_trace!(
1657            opts.network_trace,
1658            opts.diagnostics.as_ref(),
1659            "fetch_remote: retain_following_tags {:?}",
1660            t.elapsed()
1661        );
1662    }
1663
1664    // 5. Classify + apply ref updates (ancestry via the now-populated local repo).
1665    let t_apply = std::time::Instant::now();
1666    let classify_odb = dry_run_quarantine.as_ref().map(|q| q.odb());
1667    let classify_repo = (!opts.dry_run).then_some(local);
1668    let d_repo_open = t_apply.elapsed();
1669
1670    let mut updates: Vec<RefUpdate> = Vec::new();
1671
1672    let mut prune_batch: Vec<crate::refs::store::RefUpdate> = Vec::new();
1673    let t_prune = std::time::Instant::now();
1674    if opts.prune {
1675        prune_tracking_refs(
1676            local_git_dir,
1677            &positive,
1678            &remote_refs,
1679            opts.dry_run,
1680            &mut updates,
1681            if opts.dry_run {
1682                None
1683            } else {
1684                Some(&mut prune_batch)
1685            },
1686        )?;
1687    }
1688    if !opts.dry_run && !prune_batch.is_empty() {
1689        crate::refs::commit_ref_store_batch(local_git_dir, &prune_batch)?;
1690    }
1691    let d_prune = t_prune.elapsed();
1692
1693    let mut store_batch: Vec<crate::refs::store::RefUpdate> = Vec::new();
1694
1695    // Load `packed-refs` once for the whole apply. Resolving each ref's previous
1696    // value (and the per-write namespace-conflict check) would otherwise re-read
1697    // and re-scan the entire file per ref — O(refs × packed-refs size), which
1698    // dominates fetches on repositories with tens of thousands of packed refs
1699    // even when no objects are transferred.
1700    let t_packed = std::time::Instant::now();
1701    let packed = crate::refs::PackedRefs::load(local_git_dir)?;
1702    let d_packed = t_packed.elapsed();
1703
1704    // Per-sub-step timing accumulators for the apply loop, surfaced via the
1705    // env-gated "done" trace so the dominant cost is visible without a profiler.
1706    let mut d_resolve = std::time::Duration::ZERO;
1707    let mut d_classify = std::time::Duration::ZERO;
1708    let mut d_write = std::time::Duration::ZERO;
1709    let mut n_written = 0usize;
1710    let mut n_ancestry = 0usize; // refs whose old != new → classify walks ancestry
1711
1712    for m in &matched {
1713        let Some(local_ref) = &m.local_ref else {
1714            updates.push(RefUpdate {
1715                remote_ref: m.remote_ref.clone(),
1716                local_ref: None,
1717                old_oid: None,
1718                new_oid: Some(m.oid),
1719                mode: UpdateMode::NoChangeNeeded,
1720                note: Some("not stored (empty destination)".to_owned()),
1721            });
1722            continue;
1723        };
1724
1725        let t = std::time::Instant::now();
1726        let old = crate::refs::resolve_ref_cached(local_git_dir, local_ref, &packed)
1727            .ok()
1728            .flatten();
1729        d_resolve += t.elapsed();
1730
1731        if old.as_ref().is_some_and(|o| o != &m.oid) {
1732            n_ancestry += 1;
1733        }
1734
1735        let t = std::time::Instant::now();
1736        let mode = classify_update(
1737            old.as_ref(),
1738            &m.oid,
1739            m.force,
1740            m.dest_is_tag,
1741            classify_repo,
1742            classify_odb.as_ref(),
1743        );
1744        d_classify += t.elapsed();
1745
1746        let write = matches!(
1747            mode,
1748            UpdateMode::New | UpdateMode::FastForward | UpdateMode::Forced
1749        );
1750        crate::transfer::enforce_fetch_ref_update(
1751            local_ref,
1752            old,
1753            m.oid,
1754            mode,
1755            opts,
1756            explicit_refspec_fetch,
1757            classify_repo,
1758        )?;
1759        if write && !opts.dry_run {
1760            if !crate::refs::is_valid_storable_ref_name(local_ref) {
1761                updates.push(RefUpdate {
1762                    remote_ref: m.remote_ref.clone(),
1763                    local_ref: Some(local_ref.clone()),
1764                    old_oid: old,
1765                    new_oid: Some(m.oid),
1766                    mode,
1767                    note: Some("skipped (invalid ref name)".to_owned()),
1768                });
1769                continue;
1770            }
1771            store_batch.push(crate::refs::store::RefUpdate {
1772                name: local_ref.clone(),
1773                new_value: Some(crate::refs::store::RawRef::Direct(m.oid)),
1774                expected: crate::refs::store::Expected::Any,
1775                reflog: None,
1776                flags: Default::default(),
1777            });
1778            n_written += 1;
1779        }
1780
1781        updates.push(RefUpdate {
1782            remote_ref: m.remote_ref.clone(),
1783            local_ref: Some(local_ref.clone()),
1784            old_oid: old,
1785            new_oid: Some(m.oid),
1786            mode,
1787            note: None,
1788        });
1789    }
1790
1791    if !opts.dry_run && !store_batch.is_empty() {
1792        let t = std::time::Instant::now();
1793        crate::transfer::apply_fetch_ref_updates(
1794            local_git_dir,
1795            &store_batch,
1796            &mut updates,
1797            crate::transfer::FetchRefRejectHint {
1798                prune: opts.prune,
1799                positive: &positive,
1800                remote_refs: &remote_refs,
1801                remote_name: opts.remote_name.as_deref(),
1802            },
1803        )?;
1804        d_write += t.elapsed();
1805    }
1806
1807    net_trace!(
1808        opts.network_trace,
1809        opts.diagnostics.as_ref(),
1810        "fetch_remote: apply {:?} — repo_open {:?}, prune {:?}, packed_load {:?}, \
1811         resolve {:?}, classify {:?} ({} ancestry-checked), write {:?} ({} written)",
1812        t_apply.elapsed(),
1813        d_repo_open,
1814        d_prune,
1815        d_packed,
1816        d_resolve,
1817        d_classify,
1818        n_ancestry,
1819        d_write,
1820        n_written
1821    );
1822
1823    net_trace!(
1824        opts.network_trace,
1825        opts.diagnostics.as_ref(),
1826        "fetch_remote: done — {} ref update(s){} [+{:?} total]",
1827        updates.len(),
1828        default_branch
1829            .as_deref()
1830            .map(|b| format!(", default branch '{b}'"))
1831            .unwrap_or_default(),
1832        t_begin.elapsed()
1833    );
1834    finish_initial_remote_fetch_layout(local_git_dir, opts, default_branch.as_deref())?;
1835    if let Some(remote) = opts.remote_name.as_deref() {
1836        ensure_remote_tracking_head_symref(
1837            local_git_dir,
1838            remote,
1839            default_branch.as_deref(),
1840            opts.dry_run,
1841        )?;
1842    }
1843    crate::transfer::finish_anonymous_url_fetch(
1844        local_git_dir,
1845        opts,
1846        explicit_refspec_fetch,
1847        default_branch.as_deref(),
1848        &remote_refs,
1849        &matched,
1850    )?;
1851    Ok(FetchOutcome {
1852        updates,
1853        default_branch,
1854        new_shallow: crate::shallow::dedupe_shallow_list(shallow_update.shallow),
1855        new_unshallow: shallow_update.unshallow,
1856        transfer,
1857    })
1858}
1859
1860/// Promisor on-demand fetch: negotiate a pack for [`FetchOptions::object_wants`] only.
1861fn fetch_remote_object_wants(
1862    local: &Repository,
1863    conn: &mut dyn Connection,
1864    opts: &mut FetchOptions,
1865    progress: &mut dyn Progress,
1866    cancel: &dyn Cancel,
1867) -> Result<FetchOutcome> {
1868    use crate::net_trace::net_trace;
1869
1870    let t_begin = std::time::Instant::now();
1871    let local_git_dir = local.git_dir();
1872    let local_odb = local.odb();
1873    let wants = local_odb.missing_stored_oids(opts.object_wants.iter().copied())?;
1874    if wants.is_empty() {
1875        return Ok(FetchOutcome {
1876            updates: Vec::new(),
1877            default_branch: None,
1878            new_shallow: Vec::new(),
1879            new_unshallow: Vec::new(),
1880            transfer: crate::transfer::TransferStats::default(),
1881        });
1882    }
1883
1884    let v2_caps = if conn.protocol_version() >= 2 {
1885        Some(conn.capabilities().to_vec())
1886    } else {
1887        None
1888    };
1889    let v0_caps = if v2_caps.is_none() {
1890        Some(conn.capabilities().join(" "))
1891    } else {
1892        None
1893    };
1894    let (wire_filter, _filter_sent) = crate::partial_clone::wire_filter_for_capabilities(
1895        opts.filter.as_ref(),
1896        v2_caps.as_deref(),
1897        v0_caps.as_deref(),
1898    );
1899    if opts.promisor_object_fetch {
1900        opts.mark_received_pack_promisor = true;
1901    } else {
1902        opts.mark_received_pack_promisor = opts.filter.is_some();
1903    }
1904    let wire_filter = wire_filter.as_deref();
1905    let local_shallow = crate::shallow::load_shallow_oids(local_git_dir)?;
1906    let mut transfer = crate::transfer::TransferStats::default();
1907
1908    net_trace!(
1909        opts.network_trace,
1910        opts.diagnostics.as_ref(),
1911        "fetch_remote_object_wants: {} object(s) [+{:?} since begin]",
1912        wants.len(),
1913        t_begin.elapsed()
1914    );
1915
1916    let (pack_path, shallow_update) = if let Some(caps) = v2_caps.as_ref() {
1917        validate_fetch_depth_server(opts, None, Some(caps))?;
1918        let deepen = V2DeepenArgs::from_opts(opts, &local_shallow);
1919        negotiate_pack_v2(
1920            local,
1921            conn,
1922            caps,
1923            &wants,
1924            &deepen,
1925            wire_filter,
1926            progress,
1927            cancel,
1928            true,
1929        )?
1930    } else {
1931        negotiate_pack(
1932            local,
1933            conn,
1934            &wants,
1935            opts,
1936            &local_shallow,
1937            wire_filter,
1938            progress,
1939            cancel,
1940            true,
1941        )?
1942    };
1943    if let Some(path) = pack_path {
1944        let pack_config =
1945            crate::config::ConfigSet::load(&opts.environment, Some(local_git_dir), true)?;
1946        let index_threads = crate::parallel::index_pack_threads_for_ingest(
1947            opts.thread_policy,
1948            &pack_config,
1949            opts.index_pack_threads,
1950        )?;
1951        let ingested = crate::index_pack::ingest_received_pack_path_with_progress(
1952            path,
1953            local_odb,
1954            &crate::index_pack::IngestPackOptions {
1955                fix_thin: true,
1956                skip_post_index_verify: true,
1957                threads: index_threads,
1958                merged_config: Some(pack_config),
1959                environment: opts.environment.clone(),
1960                mark_promisor_pack: opts.mark_received_pack_promisor,
1961                trusted_index_rows: None,
1962            },
1963            Some(progress),
1964            Some(cancel),
1965        )?;
1966        transfer = crate::transfer::TransferStats::from_ingested_pack(&ingested);
1967    }
1968
1969    if v2_caps.is_some() && !wants.is_empty() {
1970        conn.finish_send();
1971    }
1972
1973    if !opts.dry_run {
1974        crate::shallow::apply_shallow_updates(
1975            local_git_dir,
1976            &shallow_update.shallow,
1977            &shallow_update.unshallow,
1978        )?;
1979    }
1980
1981    Ok(FetchOutcome {
1982        updates: Vec::new(),
1983        default_branch: None,
1984        new_shallow: crate::shallow::dedupe_shallow_list(shallow_update.shallow),
1985        new_unshallow: shallow_update.unshallow,
1986        transfer,
1987    })
1988}
1989
1990/// Committer identity for fetch/clone reflog entries.
1991pub fn fetch_operation_identity(
1992    env: &crate::environment::Environment,
1993    git_dir: &Path,
1994) -> Result<crate::ident::Signature> {
1995    use crate::ident_config::ident_default_name;
1996    let cfg = crate::config::ConfigSet::load(env, Some(git_dir), true)?;
1997    let name = ident_default_name(&cfg, env);
1998    let email = cfg
1999        .get("user.email")
2000        .filter(|e| !e.trim().is_empty())
2001        .unwrap_or_else(|| "unknown".to_owned());
2002    let now = time::OffsetDateTime::now_utc();
2003    crate::ident::Signature::new(name, email, now).map_err(crate::error::Error::InvalidSignature)
2004}
2005
2006/// Ensure `refs/remotes/<remote>/HEAD` exists when the remote default branch is known.
2007pub(crate) fn ensure_remote_tracking_head_symref(
2008    git_dir: &Path,
2009    remote: &str,
2010    default_branch: Option<&str>,
2011    dry_run: bool,
2012) -> Result<()> {
2013    if dry_run {
2014        return Ok(());
2015    }
2016    let Some(branch) = default_branch else {
2017        return Ok(());
2018    };
2019    let head_ref = format!("refs/remotes/{remote}/HEAD");
2020    if crate::refs::resolve_ref(git_dir, &head_ref).is_ok() {
2021        return Ok(());
2022    }
2023    let target = format!("refs/remotes/{remote}/{branch}");
2024    if crate::refs::resolve_ref(git_dir, &target).is_err() {
2025        return Ok(());
2026    }
2027    crate::refs::write_symbolic_ref(git_dir, &head_ref, &target)
2028}
2029
2030pub(crate) fn finish_initial_remote_fetch_layout(
2031    git_dir: &Path,
2032    opts: &FetchOptions,
2033    default_branch: Option<&str>,
2034) -> Result<()> {
2035    if opts.dry_run || !opts.initial_remote_fetch {
2036        return Ok(());
2037    }
2038    let Some(remote) = opts.remote_name.as_deref() else {
2039        return Ok(());
2040    };
2041    crate::refs::pack_remote_tracking_refs_for_clone(git_dir, remote)?;
2042    if let Some(branch) = default_branch {
2043        let target = format!("refs/remotes/{remote}/{branch}");
2044        let head_ref = format!("refs/remotes/{remote}/HEAD");
2045        if crate::refs::resolve_ref(git_dir, &target).is_ok() {
2046            crate::refs::write_symbolic_ref(git_dir, &head_ref, &target)?;
2047            if let Some(ref log) = opts.clone_reflog {
2048                let null = ObjectId::null(open_odb(git_dir).hash_algo());
2049                let tip = crate::refs::resolve_ref(git_dir, &target).unwrap_or(null);
2050                crate::refs::append_reflog_with_signature(
2051                    git_dir,
2052                    &head_ref,
2053                    &null,
2054                    &tip,
2055                    &log.identity,
2056                    &log.message,
2057                    true,
2058                )?;
2059            }
2060        }
2061    }
2062    Ok(())
2063}
2064
2065/// Add advertised tags to the matched set per [`crate::transfer::TagMode`].
2066///
2067/// Over the wire we cannot peel remote tags before the pack arrives, so:
2068/// * `All` adds every advertised tag (and `want`s it unconditionally).
2069/// * `Following` provisionally adds every advertised tag here; unreachable ones
2070///   are dropped by [`retain_following_tags`] after the pack is ingested.
2071/// * `None` adds nothing.
2072///
2073/// Returns the oids of tags added under `Following` — the caller must keep these
2074/// out of the default `want` list so an unreachable tag does not drag its target
2075/// into the pack (which would make it look reachable and survive the prune).
2076/// Annotated tags whose peeled commit is already local are still wanted via
2077/// [`following_tag_wants_with_local_peel`].
2078///
2079/// Under `Following`, tags named in `already_local` (see [`tags_already_local`])
2080/// are skipped: the local repository already has them at the advertised value.
2081#[allow(clippy::too_many_arguments)]
2082pub(crate) fn add_wire_tags(
2083    mode: crate::transfer::TagMode,
2084    remote_refs: &[(String, ObjectId)],
2085    advertised_peel: &std::collections::HashMap<String, ObjectId>,
2086    negatives: &[RefspecItem],
2087    already_local: &HashSet<String>,
2088    matched: &mut Vec<crate::transfer::MatchedRef>,
2089    matched_oids: &mut HashSet<ObjectId>,
2090    seen_remote_ref: &mut HashSet<String>,
2091) -> HashSet<ObjectId> {
2092    let mut following_only: HashSet<ObjectId> = HashSet::new();
2093    if mode == crate::transfer::TagMode::None {
2094        return following_only;
2095    }
2096    for (name, oid) in remote_refs {
2097        if !name.starts_with("refs/tags/") {
2098            continue;
2099        }
2100        if seen_remote_ref.contains(name) || ref_excluded(name, negatives) {
2101            continue;
2102        }
2103        if mode == crate::transfer::TagMode::Following && already_local.contains(name) {
2104            continue;
2105        }
2106        seen_remote_ref.insert(name.clone());
2107        // Only tag oids newly added here belong in `following_only`. A lightweight
2108        // tag at the same commit as an already-matched branch tip must not suppress
2109        // the branch's `want` when building the pack request.
2110        if matched_oids.insert(*oid) && mode == crate::transfer::TagMode::Following {
2111            following_only.insert(*oid);
2112        }
2113        matched.push(crate::transfer::MatchedRef {
2114            remote_ref: name.clone(),
2115            local_ref: Some(name.clone()),
2116            oid: *oid,
2117            force: false,
2118            is_tag: true,
2119            dest_is_tag: true,
2120            advertised_peel: advertised_peel.get(name).copied(),
2121            fetch_head: crate::fetch_request::FetchHeadPolicy::ForMerge,
2122        });
2123    }
2124    following_only
2125}
2126
2127/// Under `TagMode::Following`, tag OIDs in `following_only` that should still
2128/// be `want`ed: annotated tags pointing at a commit already present locally.
2129pub(crate) fn following_tag_wants_with_local_peel(
2130    matched: &[crate::transfer::MatchedRef],
2131    following_only: &HashSet<ObjectId>,
2132    local_odb: &crate::odb::Odb,
2133) -> Result<HashSet<ObjectId>> {
2134    let mut wants = HashSet::new();
2135    for m in matched {
2136        if !m.is_tag || !following_only.contains(&m.oid) {
2137            continue;
2138        }
2139        let Some(peel) = m.advertised_peel else {
2140            continue;
2141        };
2142        if local_odb.contains(&peel)? && !local_odb.contains(&m.oid)? {
2143            wants.insert(m.oid);
2144        }
2145    }
2146    Ok(wants)
2147}
2148
2149/// Commit OIDs reachable within an absolute shallow `depth` from each root,
2150/// walking all parents (upload-pack `deepen` shape), stopping at
2151/// `shallow_boundaries`.
2152pub(crate) fn shallow_fetched_commit_oids(
2153    odb: &crate::odb::Odb,
2154    roots: &[ObjectId],
2155    depth: u32,
2156    shallow_boundaries: &HashSet<ObjectId>,
2157) -> Result<HashSet<ObjectId>> {
2158    use crate::objects::{parse_commit, ObjectKind};
2159    use std::collections::{HashMap, VecDeque};
2160
2161    if depth == 0 {
2162        return Ok(HashSet::new());
2163    }
2164    let max_depth = usize::try_from(depth).unwrap_or(usize::MAX);
2165    let mut best_depth: HashMap<ObjectId, usize> = HashMap::new();
2166    let mut q: VecDeque<(ObjectId, usize)> = VecDeque::new();
2167    for &root in roots {
2168        best_depth.insert(root, 1);
2169        q.push_back((root, 1));
2170    }
2171    let mut out = HashSet::new();
2172    while let Some((oid, d)) = q.pop_front() {
2173        if d > max_depth {
2174            continue;
2175        }
2176        if best_depth.get(&oid).copied() != Some(d) {
2177            continue;
2178        }
2179        out.insert(oid);
2180        if d == max_depth {
2181            continue;
2182        }
2183        if shallow_boundaries.contains(&oid) {
2184            continue;
2185        }
2186        if !odb.contains(&oid)? {
2187            continue;
2188        }
2189        let Ok(obj) = odb.read(&oid) else {
2190            continue;
2191        };
2192        if obj.kind != ObjectKind::Commit {
2193            continue;
2194        }
2195        let Ok(c) = parse_commit(&obj.data) else {
2196            continue;
2197        };
2198        for p in c.parents {
2199            let nd = d.saturating_add(1);
2200            if nd > max_depth {
2201                continue;
2202            }
2203            let prev = best_depth.get(&p).copied().unwrap_or(usize::MAX);
2204            if nd < prev {
2205                best_depth.insert(p, nd);
2206                q.push_back((p, nd));
2207            }
2208        }
2209    }
2210    Ok(out)
2211}
2212
2213/// Commits whose peels count as "fetched" for shallow tag-following: branch tips
2214/// when there is no absolute depth, otherwise the depth-limited closure.
2215pub(crate) fn following_peel_commit_set_for_shallow(
2216    odb: &crate::odb::Odb,
2217    commit_tips: &HashSet<ObjectId>,
2218    depth: Option<u32>,
2219    shallow_boundaries: &HashSet<ObjectId>,
2220) -> Result<HashSet<ObjectId>> {
2221    let roots: Vec<ObjectId> = commit_tips.iter().copied().collect();
2222    match depth.filter(|d| *d > 0) {
2223        Some(d) => shallow_fetched_commit_oids(odb, &roots, d, shallow_boundaries),
2224        None => Ok(commit_tips.clone()),
2225    }
2226}
2227
2228/// On a shallow fetch, annotated tags whose peel is a branch tip being fetched
2229/// must be copied even when the peel is not local yet (matches upload-pack
2230/// include-tag under depth).
2231pub(crate) fn following_tag_wants_peel_among_fetched_commits(
2232    matched: &[crate::transfer::MatchedRef],
2233    following_only: &HashSet<ObjectId>,
2234    fetch_commit_oids: &HashSet<ObjectId>,
2235) -> HashSet<ObjectId> {
2236    let mut wants = HashSet::new();
2237    for m in matched {
2238        if !m.is_tag || !following_only.contains(&m.oid) {
2239            continue;
2240        }
2241        let Some(peel) = m.advertised_peel else {
2242            continue;
2243        };
2244        if peel != m.oid && fetch_commit_oids.contains(&peel) {
2245            wants.insert(m.oid);
2246        }
2247    }
2248    wants
2249}
2250
2251/// Drop provisional `Following` tags that do not point into the fetched
2252/// history, i.e. whose peeled target is not reachable from the matched non-tag
2253/// refs. Matches `git fetch`'s default tag-following.
2254///
2255/// `pack_oids` holds the objects this fetch delivered. Every caller only
2256/// receives objects reachable from what it wanted (wire fetch), or has already
2257/// limited the provisional tags to reachable ones (local and bundle fetch), so
2258/// a target inside `pack_oids` is kept without reading any object. Commit
2259/// targets that were already local are checked with a commit walk, which stops
2260/// as soon as all of them are found. Tree and blob targets that were already
2261/// local are kept, as Git does: proving their reachability would mean walking
2262/// every tree of the local history. Targets that are neither delivered nor
2263/// local cannot be in the fetched history and are dropped. An annotated tag is
2264/// kept only when its tag object is delivered or already local. Objects that
2265/// are not stored locally are never read, so a partial clone does not
2266/// lazy-fetch anything while following tags.
2267pub(crate) fn retain_following_tags(
2268    local_odb: &crate::odb::Odb,
2269    matched: &mut Vec<crate::transfer::MatchedRef>,
2270    pack_oids: &HashSet<ObjectId>,
2271    shallow_boundaries: &HashSet<ObjectId>,
2272) -> Result<()> {
2273    if !matched.iter().any(|m| m.is_tag) {
2274        return Ok(());
2275    }
2276
2277    let roots: Vec<ObjectId> = matched
2278        .iter()
2279        .filter(|m| !m.is_tag)
2280        .map(|m| m.oid)
2281        .collect();
2282    let peeled_by_tag: std::collections::HashMap<ObjectId, ObjectId> = matched
2283        .iter()
2284        .filter(|m| m.is_tag)
2285        .map(|m| {
2286            let peeled = m.advertised_peel.unwrap_or_else(|| {
2287                local_odb
2288                    .peel_stored_tag_target(m.oid)
2289                    .ok()
2290                    .flatten()
2291                    .unwrap_or(m.oid)
2292            });
2293            (m.oid, peeled)
2294        })
2295        .collect();
2296    let mut local_commits: HashSet<ObjectId> = HashSet::new();
2297    let mut local_trees_and_blobs: HashSet<ObjectId> = HashSet::new();
2298    for &oid in peeled_by_tag.values() {
2299        // `contains` never lazy-fetches; reading a missing object from a
2300        // partial clone's ODB would pull it from the promisor remote.
2301        if pack_oids.contains(&oid) || !local_odb.contains(&oid)? {
2302            continue;
2303        }
2304        match local_odb.read_info(&oid)?.kind {
2305            crate::objects::ObjectKind::Tree | crate::objects::ObjectKind::Blob => {
2306                local_trees_and_blobs.insert(oid);
2307            }
2308            crate::objects::ObjectKind::Commit => {
2309                local_commits.insert(oid);
2310            }
2311            // A tag here could not be peeled from local objects: its chain is broken.
2312            crate::objects::ObjectKind::Tag => {}
2313        }
2314    }
2315    let local_reach = reachable_commits(local_odb, &roots, &local_commits, shallow_boundaries)?;
2316    let mut keep = Vec::with_capacity(matched.len());
2317    for m in matched.iter() {
2318        if !m.is_tag {
2319            keep.push(true);
2320            continue;
2321        }
2322        let peeled = peeled_by_tag.get(&m.oid).copied().unwrap_or(m.oid);
2323        let reachable = pack_oids.contains(&peeled)
2324            || local_trees_and_blobs.contains(&peeled)
2325            || local_reach.contains(&peeled);
2326        // Lightweight tags store the peeled commit directly; annotated tags need their
2327        // tag object (new in this fetch or already local).
2328        keep.push(
2329            reachable
2330                && (m.oid == peeled || pack_oids.contains(&m.oid) || local_odb.contains(&m.oid)?),
2331        );
2332    }
2333    let mut keep = keep.into_iter();
2334    matched.retain(|_| keep.next().unwrap_or(false));
2335    Ok(())
2336}
2337
2338/// Names of the remote `refs/tags/*` entries in `remote_refs` that the local
2339/// repository already stores under the same name at the same object id.
2340///
2341/// Tag-following never needs to consider these: Git's auto-follow skips tags
2342/// that already exist locally, and there is nothing to fetch or write for them.
2343/// Excluding them up front keeps the reachability walk off the whole history
2344/// when the client already has every advertised tag (the common incremental
2345/// fetch).
2346///
2347/// # Parameters
2348///
2349/// * `local_refs` — the fetching repository's existing ref store.
2350/// * `remote_refs` — advertised `(name, oid)` pairs (peeled `^{}` entries excluded).
2351///
2352/// Unreadable local refs count as absent, so they still go through the regular
2353/// tag-following path. Resolution retains the backend's loose/symbolic ref rules
2354/// and cache freshness checks without reopening the store for each tag.
2355pub(crate) fn tags_already_local(
2356    local_refs: &dyn crate::refs::store::RefStore,
2357    remote_refs: &[(String, ObjectId)],
2358) -> HashSet<String> {
2359    remote_refs
2360        .iter()
2361        .filter(|(name, oid)| {
2362            name.starts_with("refs/tags/") && local_refs.resolve(name).ok() == Some(*oid)
2363        })
2364        .map(|(name, _)| name.clone())
2365        .collect()
2366}
2367
2368/// Commit-level reachability from `roots`: the set of commit OIDs reachable by
2369/// following commit parents (peeling annotated tags to their target). Unlike
2370/// [`reachable_closure`] it never descends into trees or blobs, so it stays cheap
2371/// across thousands of heads. Sufficient for tag-following retention, whose only
2372/// question is whether a tag's target commit is reachable from the fetched heads.
2373///
2374/// The walk stops as soon as every oid in `targets` has been reached, so the
2375/// returned set is only guaranteed complete for membership tests of `targets`
2376/// (and is the full closure whenever some target is unreachable). An empty
2377/// `targets` set returns immediately with an empty result.
2378pub(crate) fn reachable_commits(
2379    odb: &crate::odb::Odb,
2380    roots: &[ObjectId],
2381    targets: &HashSet<ObjectId>,
2382    shallow_boundaries: &HashSet<ObjectId>,
2383) -> Result<HashSet<ObjectId>> {
2384    use crate::objects::{parse_commit, parse_tag, ObjectKind};
2385
2386    let mut seen: HashSet<ObjectId> = HashSet::new();
2387    let mut remaining: HashSet<ObjectId> = targets.clone();
2388    if remaining.is_empty() {
2389        return Ok(seen);
2390    }
2391
2392    // Read commit parents from the commit-graph file when present — an mmap-style
2393    // lookup that avoids decompressing/parsing every commit object (the dominant
2394    // cost on large repos). Commits absent from the graph (recently fetched) and
2395    // octopus merges fall back to the object read below.
2396    let graph = crate::commit_graph_file::CommitGraphChain::load(odb.objects_dir());
2397
2398    let mut stack: Vec<ObjectId> = roots.to_vec();
2399    while let Some(oid) = stack.pop() {
2400        if remaining.is_empty() {
2401            break;
2402        }
2403        if seen.contains(&oid) {
2404            continue;
2405        }
2406        if shallow_boundaries.contains(&oid) {
2407            if odb.contains(&oid)? {
2408                seen.insert(oid);
2409                remaining.remove(&oid);
2410            }
2411            continue;
2412        }
2413        if let Some(graph) = graph.as_ref() {
2414            if let Some((parents, _time)) = graph.graph_commit(&oid) {
2415                seen.insert(oid);
2416                remaining.remove(&oid);
2417                stack.extend(parents);
2418                continue;
2419            }
2420        }
2421        let Ok(obj) = odb.read(&oid) else {
2422            continue;
2423        };
2424        seen.insert(oid);
2425        remaining.remove(&oid);
2426        match obj.kind {
2427            ObjectKind::Commit => {
2428                if let Ok(c) = parse_commit(&obj.data) {
2429                    for p in c.parents {
2430                        stack.push(p);
2431                    }
2432                }
2433            }
2434            ObjectKind::Tag => {
2435                if let Ok(t) = parse_tag(&obj.data) {
2436                    stack.push(t.object);
2437                }
2438            }
2439            // Tree/blob reachability is irrelevant to tag-following; don't descend.
2440            ObjectKind::Tree | ObjectKind::Blob => {}
2441        }
2442    }
2443    Ok(seen)
2444}
2445
2446#[cfg(test)]
2447mod fetch_advertised_ref_tests {
2448    use super::*;
2449    use crate::objects::ObjectId;
2450    use std::io::Cursor;
2451
2452    fn linear_history(len: usize) -> (tempfile::TempDir, Vec<ObjectId>) {
2453        use crate::objects::ObjectKind;
2454        let dir = tempfile::tempdir().unwrap();
2455        let repo = crate::repo::init_repository(
2456            dir.path(),
2457            true,
2458            "main",
2459            None,
2460            crate::RefStorageFormat::Files,
2461        )
2462        .unwrap();
2463        let odb = repo.odb();
2464        let tree = odb.write(ObjectKind::Tree, b"").unwrap();
2465        let mut commits: Vec<ObjectId> = Vec::new();
2466        for i in 0..len {
2467            let mut body = format!("tree {tree}\n");
2468            if let Some(parent) = commits.last() {
2469                body.push_str(&format!("parent {parent}\n"));
2470            }
2471            let time = 1_000_000 + i;
2472            body.push_str(&format!(
2473                "author T <t@e.com> {time} +0000\ncommitter T <t@e.com> {time} +0000\n\nc{i}\n"
2474            ));
2475            commits.push(odb.write(ObjectKind::Commit, body.as_bytes()).unwrap());
2476        }
2477        let tip = commits.last().copied().unwrap();
2478        crate::refs::write_ref(repo.git_dir(), "refs/heads/main", &tip).unwrap();
2479        (dir, commits)
2480    }
2481
2482    #[test]
2483    fn v2_local_haves_are_produced_only_as_far_as_asked() {
2484        let (dir, commits) = linear_history(400);
2485        let unrelated_want = ObjectId::from_bytes(&[9u8; 20]).unwrap();
2486        let local = Repository::open(dir.path(), &RepositoryOptions::empty()).unwrap();
2487
2488        let mut drained = v2_local_haves(&local, &[unrelated_want]).unwrap();
2489        let all = drained.all().unwrap().to_vec();
2490        assert!(drained.exhausted);
2491        assert_eq!(all[0], *commits.last().unwrap());
2492        assert!(
2493            all.len() > 4,
2494            "skipping still offers several haves: {}",
2495            all.len()
2496        );
2497
2498        let mut lazy = v2_local_haves(&local, &[unrelated_want]).unwrap();
2499        let first = lazy.first(4).unwrap().to_vec();
2500        assert_eq!(first.as_slice(), &all[..4]);
2501        assert_eq!(lazy.taken.len(), 4, "only the requested haves are walked");
2502        assert!(!lazy.exhausted);
2503        assert_eq!(lazy.all().unwrap(), all.as_slice());
2504    }
2505
2506    #[test]
2507    fn v2_local_haves_first_stops_at_a_short_history() {
2508        let (dir, _) = linear_history(3);
2509        let want = ObjectId::from_bytes(&[9u8; 20]).unwrap();
2510        let local = Repository::open(dir.path(), &RepositoryOptions::empty()).unwrap();
2511        let all = v2_local_haves(&local, &[want])
2512            .unwrap()
2513            .all()
2514            .unwrap()
2515            .to_vec();
2516        let mut haves = v2_local_haves(&local, &[want]).unwrap();
2517        assert_eq!(haves.first(INITIAL_FLUSH).unwrap(), all.as_slice());
2518        assert!(haves.exhausted);
2519    }
2520
2521    #[test]
2522    fn fetch_ref_list_prefixes_includes_refs_dwim_for_pull() {
2523        let specs = vec!["pull/123/head:pr-123".to_owned()];
2524        let prefixes = fetch_ref_list_prefixes(&specs);
2525        assert!(prefixes.iter().any(|p| p == "refs/pull/123/head"));
2526        assert!(prefixes.iter().any(|p| p == "refs/heads/"));
2527    }
2528
2529    #[test]
2530    fn build_v2_ls_refs_request_includes_unborn_when_advertised() {
2531        use crate::odb::Odb;
2532        use crate::transfer::TagMode;
2533
2534        let caps = vec!["ls-refs=unborn".to_owned()];
2535        let odb = Odb::new(std::path::Path::new("/dev/null"));
2536        let req = build_v2_ls_refs_request(&caps, &odb, TagMode::None, &[]).unwrap();
2537        let text = String::from_utf8_lossy(&req);
2538        assert!(
2539            text.contains("unborn"),
2540            "request must ask for unborn HEAD:\n{text}"
2541        );
2542    }
2543
2544    #[test]
2545    fn v2_acknowledgments_collect_acked_commits_and_ready() {
2546        let a = ObjectId::from_hex("aabbccddeeff00112233445566778899aabbccdd").unwrap();
2547        let b = ObjectId::from_hex("00112233445566778899aabbccddeeff00112233").unwrap();
2548        let mut body = Vec::new();
2549        crate::pkt_line::write_line(&mut body, "acknowledgments").unwrap();
2550        crate::pkt_line::write_line(&mut body, &format!("ACK {a}")).unwrap();
2551        crate::pkt_line::write_line(&mut body, &format!("ACK {b}")).unwrap();
2552        crate::pkt_line::write_line(&mut body, "ready").unwrap();
2553        crate::pkt_line::write_delim(&mut body).unwrap();
2554
2555        let round = read_v2_acknowledgments(&mut Cursor::new(body), false)
2556            .unwrap()
2557            .unwrap();
2558        assert!(round.ready);
2559        assert_eq!(round.common, vec![a, b]);
2560    }
2561
2562    #[test]
2563    fn v2_acknowledgments_nak_has_no_common() {
2564        let mut body = Vec::new();
2565        crate::pkt_line::write_line(&mut body, "acknowledgments").unwrap();
2566        crate::pkt_line::write_line(&mut body, "NAK").unwrap();
2567        crate::pkt_line::write_flush(&mut body).unwrap();
2568
2569        let round = read_v2_acknowledgments(&mut Cursor::new(body), false)
2570            .unwrap()
2571            .unwrap();
2572        assert!(!round.ready);
2573        assert!(round.common.is_empty());
2574    }
2575
2576    #[test]
2577    fn v2_acknowledgments_reject_malformed_ack() {
2578        let mut body = Vec::new();
2579        crate::pkt_line::write_line(&mut body, "acknowledgments").unwrap();
2580        crate::pkt_line::write_line(&mut body, "ACK not-an-oid").unwrap();
2581        crate::pkt_line::write_flush(&mut body).unwrap();
2582
2583        assert!(read_v2_acknowledgments(&mut Cursor::new(body), false).is_err());
2584    }
2585
2586    #[test]
2587    fn v2_flush_schedule_doubles_then_grows_by_a_tenth() {
2588        let mut sizes = vec![INITIAL_FLUSH];
2589        while sizes.len() < 6 {
2590            let last = *sizes.last().unwrap();
2591            sizes.push(next_v2_flush_count(last));
2592        }
2593        assert_eq!(sizes, vec![16, 32, 64, 128, 256, 512]);
2594        assert_eq!(
2595            next_v2_flush_count(LARGE_FLUSH),
2596            LARGE_FLUSH + LARGE_FLUSH / 10
2597        );
2598    }
2599
2600    #[test]
2601    fn parse_v2_ls_refs_unborn_head_symref_line() {
2602        let mut body = Vec::new();
2603        crate::pkt_line::write_line(&mut body, "unborn HEAD symref-target:refs/heads/trunk")
2604            .unwrap();
2605        crate::pkt_line::write_flush(&mut body).unwrap();
2606
2607        let (_, head, _, _) = parse_v2_ls_refs_response(&mut Cursor::new(body)).unwrap();
2608        assert_eq!(head.as_deref(), Some("refs/heads/trunk"));
2609    }
2610
2611    #[test]
2612    fn parse_v2_ls_refs_drops_traversal_ref() {
2613        let oid = ObjectId::from_hex("aabbccddeeff00112233445566778899aabbccdd").unwrap();
2614        let line = format!("{oid} refs/heads/../../../config");
2615        let mut body = Vec::new();
2616        crate::pkt_line::write_line(&mut body, &line).unwrap();
2617        crate::pkt_line::write_flush(&mut body).unwrap();
2618
2619        let (refs, _, _, _) = parse_v2_ls_refs_response(&mut Cursor::new(body)).unwrap();
2620        assert!(refs.is_empty());
2621    }
2622
2623    #[test]
2624    fn refspec_maps_traversal_to_non_storable_local_name() {
2625        use crate::refs::is_valid_storable_ref_name;
2626        use crate::refspec::parse_fetch_refspec;
2627        use crate::transfer::match_positive;
2628
2629        let spec = parse_fetch_refspec("+refs/heads/*:refs/remotes/origin/*").unwrap();
2630        let positive = vec![spec];
2631        let local = match_positive("refs/heads/../../../config", &positive)
2632            .flatten()
2633            .expect("wildcard matches malicious remote name");
2634        assert!(!is_valid_storable_ref_name(&local));
2635    }
2636
2637    #[test]
2638    fn add_wire_tags_following_skips_following_only_when_oid_already_matched() {
2639        use crate::transfer::TagMode;
2640        use std::collections::{HashMap, HashSet};
2641
2642        let shared = ObjectId::from_hex("4eda3c62a0f0a0a0a0a0a0a0a0a0a0a0a0a0a0a0").unwrap();
2643        let unreachable_tag =
2644            ObjectId::from_hex("1111111111111111111111111111111111111111").unwrap();
2645        let remote_refs = vec![
2646            ("refs/heads/rel".to_owned(), shared),
2647            ("refs/tags/light".to_owned(), shared),
2648            ("refs/tags/orphan".to_owned(), unreachable_tag),
2649        ];
2650        let advertised_peel = HashMap::new();
2651        let negatives: Vec<crate::refspec::RefspecItem> = Vec::new();
2652        let mut matched = Vec::new();
2653        let mut matched_oids = HashSet::from([shared]);
2654        let mut seen = HashSet::from(["refs/heads/rel".to_owned()]);
2655        matched.push(crate::transfer::MatchedRef {
2656            remote_ref: "refs/heads/rel".to_owned(),
2657            local_ref: Some("refs/remotes/origin/rel".to_owned()),
2658            oid: shared,
2659            force: false,
2660            is_tag: false,
2661            dest_is_tag: false,
2662            advertised_peel: None,
2663            fetch_head: crate::fetch_request::FetchHeadPolicy::ForMerge,
2664        });
2665
2666        let following_only = add_wire_tags(
2667            TagMode::Following,
2668            &remote_refs,
2669            &advertised_peel,
2670            &negatives,
2671            &HashSet::new(),
2672            &mut matched,
2673            &mut matched_oids,
2674            &mut seen,
2675        );
2676
2677        assert!(
2678            !following_only.contains(&shared),
2679            "shared branch/tag tip must stay wanted"
2680        );
2681        assert!(
2682            following_only.contains(&unreachable_tag),
2683            "tag-only oid stays following-only (no upfront want)"
2684        );
2685        assert_eq!(matched.iter().filter(|m| m.is_tag).count(), 2);
2686    }
2687
2688    #[test]
2689    fn following_tag_wants_includes_annotated_tag_when_peel_is_local() {
2690        use std::collections::HashSet;
2691
2692        let tag_obj = ObjectId::from_hex("bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb").unwrap();
2693        let tmp = tempfile::tempdir().expect("tempdir");
2694        let git_dir = tmp.path().join(".git");
2695        std::fs::create_dir_all(git_dir.join("objects")).expect("objects");
2696        let odb = crate::odb::Odb::new(git_dir.join("objects").as_path());
2697        // Seed only the peeled commit (tag object absent).
2698        let commit_bytes = b"tree 0000000000000000000000000000000000000000\nauthor T <t@example.com> 0 +0000\ncommitter T <t@example.com> 0 +0000\n\nx\n";
2699        let commit = odb
2700            .write_loose_materialize(crate::objects::ObjectKind::Commit, commit_bytes)
2701            .expect("write commit");
2702
2703        let following_only = HashSet::from([tag_obj]);
2704        let matched = vec![crate::transfer::MatchedRef {
2705            remote_ref: "refs/tags/v".to_owned(),
2706            local_ref: Some("refs/tags/v".to_owned()),
2707            oid: tag_obj,
2708            force: false,
2709            is_tag: true,
2710            dest_is_tag: true,
2711            advertised_peel: Some(commit),
2712            fetch_head: crate::fetch_request::FetchHeadPolicy::ForMerge,
2713        }];
2714        let wants = following_tag_wants_with_local_peel(&matched, &following_only, &odb).unwrap();
2715        assert!(wants.contains(&tag_obj));
2716    }
2717
2718    #[test]
2719    fn following_tag_wants_peel_among_fetched_commits_before_peel_is_local() {
2720        use std::collections::HashSet;
2721
2722        let tag_obj = ObjectId::from_hex("bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb").unwrap();
2723        let tip = ObjectId::from_hex("cccccccccccccccccccccccccccccccccccccccc").unwrap();
2724        let following_only = HashSet::from([tag_obj]);
2725        let fetch_commits = HashSet::from([tip]);
2726        let matched = vec![crate::transfer::MatchedRef {
2727            remote_ref: "refs/tags/v".to_owned(),
2728            local_ref: Some("refs/tags/v".to_owned()),
2729            oid: tag_obj,
2730            force: false,
2731            is_tag: true,
2732            dest_is_tag: true,
2733            advertised_peel: Some(tip),
2734            fetch_head: crate::fetch_request::FetchHeadPolicy::ForMerge,
2735        }];
2736        let wants = following_tag_wants_peel_among_fetched_commits(
2737            &matched,
2738            &following_only,
2739            &fetch_commits,
2740        );
2741        assert!(wants.contains(&tag_obj));
2742    }
2743
2744    #[test]
2745    fn shallow_peel_set_includes_parent_at_depth_two_not_tip_only() {
2746        use std::collections::HashSet;
2747
2748        let tmp = tempfile::tempdir().expect("tempdir");
2749        let objects = tmp.path().join("objects");
2750        std::fs::create_dir_all(&objects).expect("objects");
2751        let odb = crate::odb::Odb::new(objects.as_path());
2752        let chain = write_commit_chain(&odb, 3);
2753        let tip = *chain.last().expect("tip");
2754        let parent = chain[1];
2755        let tips = HashSet::from([tip]);
2756        let tips_only =
2757            following_peel_commit_set_for_shallow(&odb, &tips, Some(1), &HashSet::new()).unwrap();
2758        assert!(!tips_only.contains(&parent));
2759        let depth_two =
2760            following_peel_commit_set_for_shallow(&odb, &tips, Some(2), &HashSet::new()).unwrap();
2761        assert!(depth_two.contains(&parent));
2762    }
2763
2764    /// Writes a linear chain of `n` commits (oldest first) into `odb`.
2765    fn write_commit_chain(odb: &crate::odb::Odb, n: usize) -> Vec<ObjectId> {
2766        let mut chain: Vec<ObjectId> = Vec::new();
2767        for i in 0..n {
2768            let parent = chain
2769                .last()
2770                .map(|p| format!("parent {}\n", p.to_hex()))
2771                .unwrap_or_default();
2772            let body = format!(
2773                "tree 4b825dc642cb6eb9a060e54bf8d69288fbee4904\n{parent}\
2774                 author T <t@example.com> {i} +0000\ncommitter T <t@example.com> {i} +0000\n\nc{i}\n"
2775            );
2776            let oid = odb
2777                .write_loose_materialize(crate::objects::ObjectKind::Commit, body.as_bytes())
2778                .expect("write commit");
2779            chain.push(oid);
2780        }
2781        chain
2782    }
2783
2784    fn matched_ref(
2785        name: &str,
2786        oid: ObjectId,
2787        peel: Option<ObjectId>,
2788    ) -> crate::transfer::MatchedRef {
2789        let is_tag = name.starts_with("refs/tags/");
2790        crate::transfer::MatchedRef {
2791            remote_ref: name.to_owned(),
2792            local_ref: Some(name.to_owned()),
2793            oid,
2794            force: false,
2795            is_tag,
2796            dest_is_tag: is_tag,
2797            advertised_peel: peel,
2798            fetch_head: crate::fetch_request::FetchHeadPolicy::ForMerge,
2799        }
2800    }
2801
2802    fn kept_tags(matched: &[crate::transfer::MatchedRef]) -> Vec<&str> {
2803        let mut names: Vec<&str> = matched
2804            .iter()
2805            .filter(|m| m.is_tag)
2806            .map(|m| m.remote_ref.as_str())
2807            .collect();
2808        names.sort_unstable();
2809        names
2810    }
2811
2812    #[test]
2813    fn retain_following_tags_keeps_delivered_targets_without_reading_objects() {
2814        use std::collections::HashSet;
2815
2816        // The object database is empty: deciding on delivered targets must not
2817        // read (inflate) any commit, tree or blob.
2818        let tmp = tempfile::tempdir().expect("tempdir");
2819        let objects = tmp.path().join("objects");
2820        std::fs::create_dir_all(&objects).expect("objects");
2821        let odb = crate::odb::Odb::new(&objects);
2822        let oid = |c: char| ObjectId::from_hex(&c.to_string().repeat(40)).expect("oid");
2823        let (tip, tree, blob, tag_obj, missing_tag_obj, absent) =
2824            (oid('1'), oid('2'), oid('3'), oid('4'), oid('5'), oid('6'));
2825        let pack_oids = HashSet::from([tip, tree, blob, tag_obj]);
2826        let mut matched = vec![
2827            matched_ref("refs/heads/main", tip, None),
2828            matched_ref("refs/tags/light", tip, None),
2829            matched_ref("refs/tags/blob", blob, None),
2830            matched_ref("refs/tags/annotated-tree", tag_obj, Some(tree)),
2831            matched_ref("refs/tags/no-tag-object", missing_tag_obj, Some(tip)),
2832            matched_ref("refs/tags/undelivered", absent, None),
2833        ];
2834
2835        retain_following_tags(&odb, &mut matched, &pack_oids, &HashSet::new())
2836            .expect("retain following tags");
2837
2838        assert_eq!(
2839            kept_tags(&matched),
2840            [
2841                "refs/tags/annotated-tree",
2842                "refs/tags/blob",
2843                "refs/tags/light"
2844            ]
2845        );
2846        assert!(matched.iter().any(|m| m.remote_ref == "refs/heads/main"));
2847    }
2848
2849    #[test]
2850    fn retain_following_tags_walks_commits_only_for_targets_already_local() {
2851        use std::collections::HashSet;
2852
2853        let tmp = tempfile::tempdir().expect("tempdir");
2854        let objects = tmp.path().join("objects");
2855        std::fs::create_dir_all(&objects).expect("objects");
2856        let odb = crate::odb::Odb::new(&objects);
2857        let chain = write_commit_chain(&odb, 4);
2858        let tip = chain[3];
2859        // A second local history that the fetched tip does not reach.
2860        let unrelated = odb
2861            .write_loose_materialize(
2862                crate::objects::ObjectKind::Commit,
2863                b"tree 4b825dc642cb6eb9a060e54bf8d69288fbee4904\n\
2864                  author T <t@example.com> 9 +0000\ncommitter T <t@example.com> 9 +0000\n\nu\n",
2865            )
2866            .expect("write commit");
2867        let blob = odb
2868            .write_loose_materialize(crate::objects::ObjectKind::Blob, b"local blob\n")
2869            .expect("write blob");
2870        let mut matched = vec![
2871            matched_ref("refs/heads/main", tip, None),
2872            matched_ref("refs/tags/old", chain[0], None),
2873            matched_ref("refs/tags/unrelated", unrelated, None),
2874            matched_ref("refs/tags/local-blob", blob, None),
2875        ];
2876
2877        retain_following_tags(&odb, &mut matched, &HashSet::new(), &HashSet::new())
2878            .expect("retain following tags");
2879        assert_eq!(
2880            kept_tags(&matched),
2881            ["refs/tags/local-blob", "refs/tags/old"]
2882        );
2883
2884        // A shallow boundary stops the walk: history behind it is not fetched.
2885        let mut matched = vec![
2886            matched_ref("refs/heads/main", tip, None),
2887            matched_ref("refs/tags/at-boundary", chain[1], None),
2888            matched_ref("refs/tags/behind-boundary", chain[0], None),
2889        ];
2890        retain_following_tags(
2891            &odb,
2892            &mut matched,
2893            &HashSet::new(),
2894            &HashSet::from([chain[1]]),
2895        )
2896        .expect("retain following tags");
2897        assert_eq!(kept_tags(&matched), ["refs/tags/at-boundary"]);
2898    }
2899
2900    #[test]
2901    fn retain_following_tags_never_lazy_fetches_in_a_partial_clone() {
2902        use std::collections::HashSet;
2903        use std::sync::atomic::{AtomicUsize, Ordering};
2904        use std::sync::Arc;
2905
2906        struct CountingFetcher(Arc<AtomicUsize>);
2907        impl crate::promisor_fetch::PromisorFetcher for CountingFetcher {
2908            fn fetch_missing(
2909                &self,
2910                _repo: &crate::repo::Repository,
2911                _oids: &[ObjectId],
2912            ) -> Result<()> {
2913                self.0.fetch_add(1, Ordering::SeqCst);
2914                Ok(())
2915            }
2916        }
2917
2918        let tmp = tempfile::tempdir().expect("tempdir");
2919        let git_dir = tmp.path().join(".git");
2920        std::fs::create_dir_all(git_dir.join("objects/pack")).expect("objects");
2921        std::fs::create_dir_all(git_dir.join("refs/heads")).expect("refs");
2922        std::fs::write(git_dir.join("HEAD"), "ref: refs/heads/main\n").expect("HEAD");
2923        std::fs::write(
2924            git_dir.join("config"),
2925            "[core]\n\trepositoryformatversion = 1\n\tbare = true\n\
2926             [extensions]\n\tpartialclone = origin\n\
2927             [remote \"origin\"]\n\turl = /nonexistent\n\tpromisor = true\n",
2928        )
2929        .expect("config");
2930        let calls = Arc::new(AtomicUsize::new(0));
2931        let repo = crate::repo::Repository::open(
2932            &git_dir,
2933            &RepositoryOptions::empty()
2934                .with_promisor_fetcher(Some(Arc::new(CountingFetcher(Arc::clone(&calls))))),
2935        )
2936        .expect("open");
2937        let odb = repo.odb();
2938        let tip = write_commit_chain(odb, 1)[0];
2939        let oid = |c: char| ObjectId::from_hex(&c.to_string().repeat(40)).expect("oid");
2940        // Neither the promised blob nor the annotated tag object is stored locally.
2941        let (promised_blob, missing_tag_obj) = (oid('7'), oid('8'));
2942        let write_tag = |target: ObjectId, kind: &str| {
2943            let body = format!(
2944                "object {}\ntype {kind}\ntag t\ntagger T <t@example.com> 0 +0000\n\nt\n",
2945                target.to_hex()
2946            );
2947            odb.write_loose_materialize(crate::objects::ObjectKind::Tag, body.as_bytes())
2948                .expect("write tag")
2949        };
2950        // Stored tags whose final target, or an intermediate tag, is missing.
2951        let local_tag_on_promised_blob = write_tag(promised_blob, "blob");
2952        let local_tag_on_missing_tag = write_tag(missing_tag_obj, "tag");
2953        let mut matched = vec![
2954            matched_ref("refs/heads/main", tip, None),
2955            matched_ref("refs/tags/light", tip, None),
2956            matched_ref("refs/tags/promised-blob", promised_blob, None),
2957            matched_ref("refs/tags/unpeeled", missing_tag_obj, None),
2958            matched_ref(
2959                "refs/tags/local-on-promised",
2960                local_tag_on_promised_blob,
2961                None,
2962            ),
2963            matched_ref(
2964                "refs/tags/local-on-missing-tag",
2965                local_tag_on_missing_tag,
2966                None,
2967            ),
2968        ];
2969
2970        retain_following_tags(odb, &mut matched, &HashSet::new(), &HashSet::new())
2971            .expect("retain following tags");
2972
2973        assert_eq!(
2974            calls.load(Ordering::SeqCst),
2975            0,
2976            "tag following must not lazy-fetch missing objects"
2977        );
2978        assert_eq!(kept_tags(&matched), ["refs/tags/light"]);
2979    }
2980
2981    #[test]
2982    fn reachable_commits_stops_once_every_target_is_found() {
2983        use std::collections::HashSet;
2984
2985        let tmp = tempfile::tempdir().expect("tempdir");
2986        let objects = tmp.path().join("objects");
2987        std::fs::create_dir_all(&objects).expect("objects");
2988        let odb = crate::odb::Odb::new(&objects);
2989        let chain = write_commit_chain(&odb, 5);
2990        let tip = chain[4];
2991        let none = HashSet::new();
2992
2993        let near = HashSet::from([chain[3]]);
2994        let seen = reachable_commits(&odb, &[tip], &near, &none).unwrap();
2995        assert!(seen.contains(&chain[3]), "reachable target is reported");
2996        assert!(
2997            !seen.contains(&chain[0]),
2998            "walk must stop before the root once every target is found"
2999        );
3000
3001        let unreachable =
3002            HashSet::from([
3003                ObjectId::from_hex("1111111111111111111111111111111111111111").unwrap(),
3004            ]);
3005        let seen = reachable_commits(&odb, &[tip], &unreachable, &none).unwrap();
3006        assert_eq!(
3007            seen,
3008            chain.iter().copied().collect::<HashSet<_>>(),
3009            "an unreachable target forces the full closure"
3010        );
3011
3012        assert!(
3013            reachable_commits(&odb, &[tip], &HashSet::new(), &none)
3014                .unwrap()
3015                .is_empty(),
3016            "no targets means no walk"
3017        );
3018    }
3019
3020    #[test]
3021    fn tags_already_local_matches_name_and_oid_across_loose_and_packed() {
3022        let tmp = tempfile::tempdir().expect("tempdir");
3023        let git_dir = tmp.path().join(".git");
3024        std::fs::create_dir_all(git_dir.join("objects")).expect("objects");
3025        std::fs::create_dir_all(git_dir.join("refs/tags")).expect("refs/tags");
3026        std::fs::write(git_dir.join("HEAD"), "ref: refs/heads/main\n").expect("HEAD");
3027
3028        let a = ObjectId::from_hex("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa").unwrap();
3029        let b = ObjectId::from_hex("bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb").unwrap();
3030        let c = ObjectId::from_hex("cccccccccccccccccccccccccccccccccccccccc").unwrap();
3031        std::fs::write(git_dir.join("refs/tags/loose"), format!("{}\n", a.to_hex()))
3032            .expect("loose tag");
3033        std::fs::write(
3034            crate::refs::store::paths::packed_refs_path(&git_dir),
3035            format!(
3036                "# pack-refs with: peeled fully-peeled sorted \n{} refs/tags/moved\n{} refs/tags/packed\n",
3037                a.to_hex(),
3038                b.to_hex()
3039            ),
3040        )
3041        .expect("packed-refs");
3042
3043        let remote_refs = vec![
3044            ("refs/heads/main".to_owned(), a),
3045            ("refs/tags/loose".to_owned(), a),
3046            ("refs/tags/packed".to_owned(), b),
3047            ("refs/tags/moved".to_owned(), c),
3048            ("refs/tags/new".to_owned(), c),
3049        ];
3050        let store = crate::refs::store::open_ref_store(&git_dir).unwrap();
3051        let present = tags_already_local(store.as_ref(), &remote_refs);
3052        assert_eq!(
3053            present,
3054            HashSet::from(["refs/tags/loose".to_owned(), "refs/tags/packed".to_owned()]),
3055            "only tags stored locally at the advertised oid count as present"
3056        );
3057        assert!(tags_already_local(store.as_ref(), &remote_refs[..1]).is_empty());
3058    }
3059
3060    #[test]
3061    fn add_wire_tags_following_skips_tags_already_local() {
3062        use crate::transfer::TagMode;
3063        use std::collections::HashMap;
3064
3065        let tip = ObjectId::from_hex("2222222222222222222222222222222222222222").unwrap();
3066        let old_tag = ObjectId::from_hex("3333333333333333333333333333333333333333").unwrap();
3067        let new_tag = ObjectId::from_hex("4444444444444444444444444444444444444444").unwrap();
3068        let remote_refs = vec![
3069            ("refs/heads/main".to_owned(), tip),
3070            ("refs/tags/old".to_owned(), old_tag),
3071            ("refs/tags/new".to_owned(), new_tag),
3072        ];
3073        let already_local = HashSet::from(["refs/tags/old".to_owned()]);
3074
3075        for (mode, expect) in [
3076            (TagMode::Following, vec!["refs/tags/new"]),
3077            (TagMode::All, vec!["refs/tags/old", "refs/tags/new"]),
3078        ] {
3079            let mut matched = Vec::new();
3080            let mut matched_oids = HashSet::new();
3081            let mut seen = HashSet::new();
3082            add_wire_tags(
3083                mode,
3084                &remote_refs,
3085                &HashMap::new(),
3086                &[],
3087                &already_local,
3088                &mut matched,
3089                &mut matched_oids,
3090                &mut seen,
3091            );
3092            let names: Vec<&str> = matched.iter().map(|m| m.remote_ref.as_str()).collect();
3093            assert_eq!(names, expect, "{mode:?}");
3094        }
3095    }
3096}
3097
3098#[cfg(test)]
3099mod v2_negotiation_tests {
3100    use super::*;
3101    use crate::objects::ObjectKind;
3102    use crate::progress::NeverCancel;
3103    use crate::repo::{InitOptions, Repository};
3104    use std::io::Cursor;
3105
3106    /// A stateless v2 upload-pack stand-in that ACKs the first `have` it ever
3107    /// sees, re-ACKs it whenever a request repeats it, and never says `ready`.
3108    struct AckingNeverReadyServer {
3109        caps: Vec<String>,
3110        pending: Vec<u8>,
3111        response: Cursor<Vec<u8>>,
3112        acked: Option<ObjectId>,
3113        requests: Vec<Vec<String>>,
3114    }
3115
3116    impl AckingNeverReadyServer {
3117        fn answer(&mut self) -> std::io::Result<()> {
3118            let mut lines = Vec::new();
3119            let mut cursor = Cursor::new(std::mem::take(&mut self.pending));
3120            while let Some(pkt) = pkt_line::read_packet(&mut cursor)? {
3121                if let pkt_line::Packet::Data(line) = pkt {
3122                    lines.push(line.trim_end().to_owned());
3123                }
3124            }
3125            let haves: Vec<ObjectId> = lines
3126                .iter()
3127                .filter_map(|l| l.strip_prefix("have "))
3128                .filter_map(|h| ObjectId::from_hex(h).ok())
3129                .collect();
3130            let done = lines.iter().any(|l| l == "done");
3131            self.requests.push(lines);
3132            let mut out = Vec::new();
3133            if !done {
3134                let acked = *self.acked.get_or_insert(haves[0]);
3135                pkt_line::write_line(&mut out, "acknowledgments")?;
3136                if haves.contains(&acked) {
3137                    pkt_line::write_line(&mut out, &format!("ACK {acked}"))?;
3138                } else {
3139                    pkt_line::write_line(&mut out, "NAK")?;
3140                }
3141                pkt_line::write_flush(&mut out)?;
3142            }
3143            self.response = Cursor::new(out);
3144            Ok(())
3145        }
3146    }
3147
3148    impl Write for AckingNeverReadyServer {
3149        fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
3150            self.pending.extend_from_slice(buf);
3151            Ok(buf.len())
3152        }
3153
3154        fn flush(&mut self) -> std::io::Result<()> {
3155            if self.pending.is_empty() {
3156                return Ok(());
3157            }
3158            self.answer()
3159        }
3160    }
3161
3162    impl Connection for AckingNeverReadyServer {
3163        fn reader(&mut self) -> &mut dyn Read {
3164            &mut self.response
3165        }
3166        fn writer(&mut self) -> &mut dyn Write {
3167            self
3168        }
3169        fn advertised_refs(&self) -> &[(String, ObjectId)] {
3170            &[]
3171        }
3172        fn capabilities(&self) -> &[String] {
3173            &self.caps
3174        }
3175        fn head_symref(&self) -> Option<&str> {
3176            None
3177        }
3178        fn protocol_version(&self) -> u8 {
3179            2
3180        }
3181        fn stateless_rpc(&self) -> bool {
3182            true
3183        }
3184    }
3185
3186    #[test]
3187    fn replayed_acks_do_not_postpone_the_in_vain_cutoff() {
3188        const TIPS: usize = 2000;
3189        let tmp = tempfile::tempdir().unwrap();
3190        let repo = Repository::init(tmp.path(), &InitOptions::default()).unwrap();
3191        let git_dir = repo.git_dir().to_path_buf();
3192        let tree = repo.odb().write(ObjectKind::Tree, &[]).unwrap();
3193        // Unrelated root commits: each one is a have of its own, so the
3194        // negotiator never runs dry before the cutoff.
3195        for i in 0..TIPS {
3196            let when = 1_600_000_000 + i as i64;
3197            let body = format!(
3198                "tree {tree}\nauthor T <t@example.com> {when} +0000\n\
3199                 committer T <t@example.com> {when} +0000\n\nroot {i}\n"
3200            );
3201            let oid = repo
3202                .odb()
3203                .write(ObjectKind::Commit, body.as_bytes())
3204                .unwrap();
3205            crate::refs::write_ref(&git_dir, &format!("refs/heads/b{i:04}"), &oid).unwrap();
3206        }
3207
3208        let mut server = AckingNeverReadyServer {
3209            caps: vec!["fetch".to_owned()],
3210            pending: Vec::new(),
3211            response: Cursor::new(Vec::new()),
3212            acked: None,
3213            requests: Vec::new(),
3214        };
3215        let want = ObjectId::from_hex("1111111111111111111111111111111111111111").unwrap();
3216        // The scripted server sends no pack after `done`; only the requests matter.
3217        let _ = negotiate_pack_v2(
3218            &repo,
3219            &mut server,
3220            &[],
3221            &[want],
3222            &V2DeepenArgs::default(),
3223            None,
3224            &mut NoProgress,
3225            &NeverCancel,
3226            false,
3227        );
3228
3229        let last = server.requests.last().unwrap();
3230        assert!(
3231            last.iter().any(|l| l == "done"),
3232            "negotiation must end with done"
3233        );
3234        let distinct: HashSet<&str> = server
3235            .requests
3236            .iter()
3237            .flatten()
3238            .filter_map(|l| l.strip_prefix("have "))
3239            .collect();
3240        // 16 haves before the first ACK, then rounds of 32, 64, 128 and 256
3241        // haves the server never acknowledges (it only re-ACKs the replayed
3242        // common commit): 480 vain haves, so the cutoff fires with that round.
3243        assert_eq!(distinct.len(), 16 + 32 + 64 + 128 + 256);
3244        assert!(distinct.len() < TIPS, "cutoff never fired");
3245    }
3246}