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