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