Skip to main content

grit_lib/
fetch.rs

1//! Wire-protocol fetch orchestration over a [`crate::transport::Connection`].
2//!
3//! [`fetch_remote`] is the wire counterpart to [`crate::transfer::fetch_local`]:
4//! instead of copying objects between two on-disk repositories, it drives a
5//! `git-upload-pack` negotiation over a live [`crate::transport::Connection`] —
6//! resolving wanted oids from the connection's advertised refs (via the same
7//! refspec matching `fetch_local` uses), running the
8//! [`crate::fetch_negotiator::SkippingNegotiator`] `want`/`have`/`done`
9//! exchange, demultiplexing the side-band pack, ingesting it with
10//! [`crate::unpack_objects`], and classifying ref updates into the shared
11//! [`crate::transfer::FetchOutcome`].
12//!
13//! This is the protocol-v0/v1 negotiation loop lifted from the CLI's
14//! `fetch_transport::fetch_upload_pack_negotiate_pack_bytes_with_streams`,
15//! generalized to run over the [`crate::transport::Connection`] reader/writer
16//! rather than subprocess pipes.
17//!
18//! Protocol v2 over the streaming transports (`git://`, ssh) is also handled
19//! here: a v2 [`crate::transport::Connection`] advertises no refs on connect, so
20//! [`fetch_remote`] first issues a `command=ls-refs` (deriving ref-prefixes from
21//! the fetch refspecs) to recover the ref map, then runs a `command=fetch`
22//! negotiation — multi-round `want`/`have`/`done` with the same
23//! [`crate::fetch_negotiator::SkippingNegotiator`] — and demuxes the
24//! side-band-64k pack from the `packfile` section. Both paths share the refspec
25//! matching, tag-mode, prune, classification, and pack-ingest plumbing. The v2
26//! request fragments are lifted from the CLI's `file_upload_pack_v2` /
27//! `fetch_transport` (`write_v2_fetch_request`, `read_v2_acknowledgments`,
28//! `read_v2_fetch_pack_response`, `v2_ls_refs_for_fetch`). Smart-HTTP stays on
29//! v0/v1 (its stateless multi-POST v2 flow is out of scope for this pass).
30
31use std::collections::HashSet;
32use std::io::{Read, Write};
33use std::path::Path;
34
35use crate::error::{Error, Result};
36use crate::fetch_negotiator::SkippingNegotiator;
37use crate::objects::ObjectId;
38use crate::pkt_line;
39use crate::protocol_v2;
40use crate::refspec::{parse_fetch_refspec, RefspecItem};
41use crate::transfer::{
42    classify_update, match_positive, open_odb, prune_tracking_refs, ref_excluded, refspecs_force,
43    FetchOptions, FetchOutcome, RefUpdate, UpdateMode,
44};
45use crate::transport::Connection;
46
47/// Sink for the remote's human-readable progress (side-band channel 2).
48///
49/// Implementations receive the raw progress bytes the server writes (typically
50/// `\r`-delimited counter lines). The default does nothing.
51pub trait Progress {
52    /// Receive a chunk of progress bytes from side-band channel 2.
53    fn message(&mut self, _bytes: &[u8]) {}
54}
55
56/// A [`Progress`] that discards everything.
57pub struct NoProgress;
58
59impl Progress for NoProgress {}
60
61// --- Negotiation flush schedule (mirrors fetch-pack.c) --------------------
62
63const INITIAL_FLUSH: usize = 16;
64const PIPESAFE_FLUSH: usize = 32;
65
66fn next_flush_count(count: usize) -> usize {
67    if count < PIPESAFE_FLUSH {
68        count * 2
69    } else {
70        count + PIPESAFE_FLUSH
71    }
72}
73
74#[derive(Clone, Copy, PartialEq, Eq)]
75enum AckKind {
76    /// `ACK <oid>` with no status suffix (post-`done` or legacy).
77    Bare,
78    Common,
79    Continue,
80    Ready,
81}
82
83fn parse_ack(line: &str) -> Option<(ObjectId, AckKind)> {
84    if line == "NAK" {
85        return None;
86    }
87    let rest = line.strip_prefix("ACK ")?;
88    let hex = rest.split_whitespace().next()?;
89    let oid = ObjectId::from_hex(hex).ok()?;
90    let tail = rest.strip_prefix(hex).unwrap_or("").trim();
91    let kind = if tail.contains("continue") {
92        AckKind::Continue
93    } else if tail.contains("common") {
94        AckKind::Common
95    } else if tail.contains("ready") {
96        AckKind::Ready
97    } else {
98        AckKind::Bare
99    };
100    Some((oid, kind))
101}
102
103/// Read one ACK round, feeding `common`/`continue`/`ready` acks to the
104/// negotiator. Lifted from `read_ack_round_with_negotiator`.
105fn read_ack_round(reader: &mut dyn Read, negotiator: &mut SkippingNegotiator) -> Result<()> {
106    let mut reader = reader;
107    loop {
108        let Some(pkt) = pkt_line::read_packet(&mut reader)? else {
109            break;
110        };
111        match pkt {
112            pkt_line::Packet::Flush => break,
113            pkt_line::Packet::Data(ln) => {
114                let ln = ln.trim_end();
115                if ln == "NAK" {
116                    // `upload-pack` sends `NAK` as the last line of a round with no trailing
117                    // flush; waiting for another packet would block forever.
118                    break;
119                }
120                let Some((ack_oid, kind)) = parse_ack(ln) else {
121                    break;
122                };
123                if kind == AckKind::Bare {
124                    break;
125                }
126                let _ = negotiator.ack(ack_oid)?;
127            }
128            _ => {}
129        }
130    }
131    Ok(())
132}
133
134/// Read a raw pkt-line payload (length-prefixed), returning `None` on
135/// flush/delim/response-end/EOF. Side-band readers stop at a flush.
136fn read_pkt_payload_raw(r: &mut dyn Read) -> std::io::Result<Option<Vec<u8>>> {
137    let mut len_buf = [0u8; 4];
138    match r.read_exact(&mut len_buf) {
139        Ok(()) => {}
140        Err(e) if e.kind() == std::io::ErrorKind::UnexpectedEof => return Ok(None),
141        Err(e) => return Err(e),
142    }
143    let len_str = std::str::from_utf8(&len_buf)
144        .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?;
145    let len = usize::from_str_radix(len_str, 16)
146        .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?;
147    match len {
148        0..=2 => Ok(None),
149        n if n <= 4 => Err(std::io::Error::new(
150            std::io::ErrorKind::InvalidData,
151            format!("invalid pkt-line length: {n}"),
152        )),
153        n => {
154            let payload_len = n - 4;
155            let mut buf = vec![0u8; payload_len];
156            r.read_exact(&mut buf)?;
157            Ok(Some(buf))
158        }
159    }
160}
161
162/// Demultiplex the side-band-64k stream after `done`: collect channel-1 pack
163/// bytes into `out` (scanning for the `PACK` magic, which may span chunk
164/// boundaries), and forward channel-2 progress to `progress`. Channel 3 is a
165/// fatal error. Lifted from `read_sideband_pack_until_done`.
166fn read_sideband_pack(
167    r: &mut dyn Read,
168    out: &mut Vec<u8>,
169    progress: &mut dyn Progress,
170) -> Result<()> {
171    let mut seen_pack = false;
172    let mut pending: Vec<u8> = Vec::new();
173    loop {
174        let Some(payload) = read_pkt_payload_raw(r)? else {
175            break;
176        };
177        if payload.is_empty() {
178            continue;
179        }
180        match payload[0] {
181            1 => {
182                let data = &payload[1..];
183                if seen_pack {
184                    out.extend_from_slice(data);
185                } else {
186                    pending.extend_from_slice(data);
187                    if let Some(pos) = pending.windows(4).position(|w| w == b"PACK") {
188                        seen_pack = true;
189                        out.extend_from_slice(&pending[pos..]);
190                        pending.clear();
191                    } else if pending.len() > 3 {
192                        let keep_from = pending.len() - 3;
193                        pending.drain(..keep_from);
194                    }
195                }
196            }
197            2 => progress.message(&payload[1..]),
198            3 => {
199                return Err(Error::Message(format!(
200                    "remote error: {}",
201                    String::from_utf8_lossy(&payload[1..]).trim_end()
202                )));
203            }
204            _ => {
205                // No side-band: raw pack bytes.
206                if !seen_pack && payload.starts_with(b"PACK") {
207                    seen_pack = true;
208                    out.extend_from_slice(&payload);
209                } else if seen_pack {
210                    out.extend_from_slice(&payload);
211                }
212            }
213        }
214    }
215    Ok(())
216}
217
218/// Peel `oid` to the commit usable as a negotiation tip; `None` if it is not a
219/// commit (or is missing). Mirrors the CLI's `peel_commit_oid_for_negotiation`
220/// but tolerates missing/non-commit objects by returning `None`.
221fn peel_to_commit(repo: &crate::repo::Repository, oid: ObjectId) -> Option<ObjectId> {
222    let mut current = oid;
223    for _ in 0..16 {
224        let obj = repo.odb.read(&current).ok()?;
225        match obj.kind {
226            crate::objects::ObjectKind::Commit => return Some(current),
227            crate::objects::ObjectKind::Tag => {
228                current = crate::objects::parse_tag(&obj.data).ok()?.object;
229            }
230            _ => return None,
231        }
232    }
233    None
234}
235
236/// New shallow boundaries the server reported during a fetch, captured from the
237/// `shallow-info` section so [`fetch_remote`] (and the HTTP fetch paths) can
238/// update the local `shallow` file and surface them in [`FetchOutcome`].
239#[derive(Default)]
240pub(crate) struct ShallowUpdate {
241    pub(crate) shallow: Vec<ObjectId>,
242    pub(crate) unshallow: Vec<ObjectId>,
243}
244
245/// Append the v0/v1 shallow/deepen request lines (after the `want`s, before the
246/// terminating flush): the client's current `shallow <oid>` grafts and any
247/// `deepen` / `deepen-since` / `deepen-not` the caller requested. Gated on the
248/// matching server capability where one exists. Mirrors the CLI's
249/// `append_fetch_request_extensions_v0_v1`.
250fn append_shallow_request_v0(
251    req: &mut Vec<u8>,
252    server_caps: &str,
253    local_shallow: &[ObjectId],
254    opts: &FetchOptions,
255) -> Result<()> {
256    for oid in local_shallow {
257        pkt_line::write_line_to_vec(req, &format!("shallow {}", oid.to_hex()))?;
258    }
259    if opts.unshallow {
260        pkt_line::write_line_to_vec(req, &format!("deepen {}", crate::shallow::INFINITE_DEPTH))?;
261    } else if let Some(depth) = opts.depth.filter(|d| *d > 0) {
262        pkt_line::write_line_to_vec(req, &format!("deepen {depth}"))?;
263    }
264    if let Some(since) = opts.deepen_since.as_deref().filter(|s| !s.trim().is_empty()) {
265        if server_caps.contains("deepen-since") {
266            let value = crate::shallow::deepen_since_wire_value(since);
267            pkt_line::write_line_to_vec(req, &format!("deepen-since {value}"))?;
268        }
269    }
270    if server_caps.contains("deepen-not") {
271        for excl in &opts.deepen_not {
272            let excl = excl.trim();
273            if !excl.is_empty() {
274                pkt_line::write_line_to_vec(req, &format!("deepen-not {excl}"))?;
275            }
276        }
277    }
278    Ok(())
279}
280
281/// Negotiate with `git-upload-pack` over the connection and return the raw
282/// packfile bytes for the requested `wants`, plus any shallow-boundary updates
283/// the server reported (`shallow`/`unshallow`).
284///
285/// Drives the [`SkippingNegotiator`] over the connection: sends `want` lines
286/// (with v0/v1 capabilities) and the advertised refs as `known_common`, batches
287/// local `have`s with flushes (reading interleaved ACK rounds), sends `done`,
288/// consumes the final ACK/NAK, then demuxes the side-band pack.
289///
290/// When `opts` requests a deepen (or the repo is already shallow), the `want`
291/// block carries the client's `shallow <oid>` grafts and the `deepen*` args, and
292/// the server precedes the pack with a `shallow-info` section that this reads
293/// into the returned [`ShallowUpdate`].
294fn negotiate_pack(
295    local_git_dir: &Path,
296    conn: &mut dyn Connection,
297    wants: &[ObjectId],
298    opts: &FetchOptions,
299    local_shallow: &[ObjectId],
300    progress: &mut dyn Progress,
301) -> Result<(Vec<u8>, ShallowUpdate)> {
302    let local_repo = crate::repo::Repository::open(local_git_dir, None)?;
303    let want_set: HashSet<ObjectId> = wants.iter().copied().collect();
304
305    let Some(first_want) = wants.first().copied() else {
306        return Ok((Vec::new(), ShallowUpdate::default()));
307    };
308
309    // A deepen/shallow request changes the negotiation: the server precedes the
310    // pack with a `shallow-info` section, and the client's local history is not a
311    // usable negotiation base (its objects bottom out at grafts), so we skip
312    // offering `have`s. Mirrors `fetch-pack.c`'s shallow handling.
313    let shallow_request = opts.has_deepen_request() || !local_shallow.is_empty();
314
315    // Capability set matching `git fetch-pack`'s first `want` line for v0/v1.
316    let caps = " multi_ack_detailed side-band-64k thin-pack no-progress include-tag ofs-delta agent=grit";
317
318    // Capture the advertised refs before borrowing the writer (avoids aliasing
319    // the connection's reader/writer with its accessors). v0/v1 shallow servers
320    // append `shallow <oid>` trailer lines to the advertisement; the capability
321    // string we read from the advertisement drives `deepen-since`/`deepen-not`.
322    let advertised: Vec<(String, ObjectId)> = conn.advertised_refs().to_vec();
323    let server_caps: String = conn.capabilities().join(" ");
324
325    let mut req: Vec<u8> = Vec::new();
326    let w0 = format!("want {}{}", first_want.to_hex(), caps);
327    pkt_line::write_line_to_vec(&mut req, &w0)?;
328    for w in wants.iter().skip(1) {
329        pkt_line::write_line_to_vec(&mut req, &format!("want {}", w.to_hex()))?;
330    }
331    // Match `git fetch-pack`: with a single unique OID, repeat the bare want.
332    // git-daemon expects this. (Not done for shallow requests, which append
333    // shallow/deepen lines instead.)
334    if wants.len() == 1 && !shallow_request {
335        pkt_line::write_line_to_vec(&mut req, &format!("want {}", first_want.to_hex()))?;
336    }
337    append_shallow_request_v0(&mut req, &server_caps, local_shallow, opts)?;
338    req.extend_from_slice(b"0000");
339    conn.writer().write_all(&req)?;
340    conn.writer().flush()?;
341
342    // Build the negotiator from local ref tips (heads, tags, HEAD), peeled to
343    // commits, excluding the wants. Advertised tips we already have become
344    // `known_common`.
345    let mut negotiator = SkippingNegotiator::new(local_repo);
346    let mut tips: Vec<ObjectId> = Vec::new();
347    let mut seen_tip: HashSet<ObjectId> = HashSet::new();
348    for prefix in ["refs/heads/", "refs/tags/"] {
349        if let Ok(entries) = crate::refs::list_refs(local_git_dir, prefix) {
350            for (_, oid) in entries {
351                if let Some(c) = peel_to_commit(negotiator.repo(), oid) {
352                    if !want_set.contains(&c) && seen_tip.insert(c) {
353                        tips.push(c);
354                    }
355                }
356            }
357        }
358    }
359    if let Ok(h) = crate::refs::resolve_ref(local_git_dir, "HEAD") {
360        if let Some(c) = peel_to_commit(negotiator.repo(), h) {
361            if !want_set.contains(&c) && seen_tip.insert(c) {
362                tips.push(c);
363            }
364        }
365    }
366    tips.sort_by_key(ObjectId::to_hex);
367    if !shallow_request {
368        for t in tips {
369            negotiator.add_tip(t)?;
370        }
371        for (_, oid) in &advertised {
372            if want_set.contains(oid) {
373                continue;
374            }
375            if let Some(c) = peel_to_commit(negotiator.repo(), *oid) {
376                negotiator.known_common(c)?;
377            }
378        }
379    }
380
381    // Shallow-info section: for a deepen/shallow request the v0/v1 server emits
382    // its `shallow`/`unshallow` lines (flush-terminated) immediately after the
383    // wants block, before any ACK round. Read it now so the subsequent ACK/NAK
384    // and pack reads line up.
385    let mut shallow_update = ShallowUpdate::default();
386    if shallow_request {
387        let (sh, unsh) = crate::shallow::read_shallow_info_section(&mut conn.reader())?;
388        shallow_update.shallow = sh;
389        shallow_update.unshallow = unsh;
390    }
391
392    // Have/ACK exchange: batch haves, flush, read interleaved ACK rounds.
393    let mut count: usize = 0;
394    let mut flush_at: usize = INITIAL_FLUSH;
395    let mut pending: Vec<u8> = Vec::new();
396    let mut flushes: i32 = 0;
397    while let Some(oid) = negotiator.next_have()? {
398        pkt_line::write_line_to_vec(&mut pending, &format!("have {}", oid.to_hex()))?;
399        count += 1;
400        if flush_at <= count {
401            pending.extend_from_slice(b"0000");
402            conn.writer().write_all(&pending)?;
403            conn.writer().flush()?;
404            pending.clear();
405            flush_at = next_flush_count(count);
406            flushes += 1;
407            // Keep one window ahead: skip reading ACKs after the first flush.
408            if count == INITIAL_FLUSH {
409                continue;
410            }
411            read_ack_round(conn.reader(), &mut negotiator)?;
412            flushes -= 1;
413        }
414    }
415    if !pending.is_empty() {
416        pending.extend_from_slice(b"0000");
417        conn.writer().write_all(&pending)?;
418        conn.writer().flush()?;
419        flushes += 1;
420    }
421    while flushes > 0 {
422        read_ack_round(conn.reader(), &mut negotiator)?;
423        flushes -= 1;
424    }
425
426    // Send `done` (single pkt-line, no trailing flush) and read the ACK/NAK.
427    let mut tail = Vec::new();
428    pkt_line::write_line_to_vec(&mut tail, "done")?;
429    conn.writer().write_all(&tail)?;
430    conn.writer().flush()?;
431
432    match pkt_line::read_packet(&mut conn.reader())? {
433        None => return Err(Error::Message("unexpected EOF after done".to_owned())),
434        Some(pkt_line::Packet::Flush) => {
435            return Err(Error::Message("unexpected flush after done".to_owned()))
436        }
437        Some(pkt_line::Packet::Data(ln)) => {
438            let ln = ln.trim_end();
439            if ln != "NAK" {
440                if let Some((ack_oid, kind)) = parse_ack(ln) {
441                    if kind != AckKind::Bare {
442                        let _ = negotiator.ack(ack_oid)?;
443                    }
444                } else if let Some(msg) = ln.strip_prefix("ERR ") {
445                    return Err(Error::Message(format!("remote error: {}", msg.trim_end())));
446                }
447            }
448        }
449        Some(_) => {}
450    }
451
452    let mut pack = Vec::new();
453    read_sideband_pack(conn.reader(), &mut pack, progress)?;
454    Ok((pack, shallow_update))
455}
456
457// ===========================================================================
458// Protocol v2 (streaming transports: git://, ssh)
459// ===========================================================================
460//
461// A v2 connection advertises no refs on connect (only the capability block).
462// `v2_ls_refs` recovers the ref map with a `command=ls-refs`; `negotiate_pack_v2`
463// runs the `command=fetch` negotiation and returns the demuxed pack. Both lift
464// the exact pkt-line shapes from the CLI's `file_upload_pack_v2` /
465// `fetch_transport` v2 paths and reuse `protocol_v2` cap helpers, the shared
466// `SkippingNegotiator`, and `read_sideband_pack`.
467
468/// The `object-format=` value to put on the wire for a v2 request: echo the
469/// server's advertised object-format when present, else fall back to the local
470/// odb's hash algorithm (sha1/sha256). Keeps the negotiation hash-algo-aware.
471pub(crate) fn v2_object_format(server_caps: &[String], local_odb: &crate::odb::Odb) -> String {
472    for c in server_caps {
473        if let Some(fmt) = c.strip_prefix("object-format=") {
474            let f = fmt.trim();
475            if !f.is_empty() {
476                return f.to_ascii_lowercase();
477            }
478        }
479    }
480    local_odb.hash_algo().name().to_owned()
481}
482
483/// Derive `ref-prefix` lines for `command=ls-refs` from the fetch refspecs, port
484/// of the CLI's `v2_ref_prefixes_from_refspecs`. A `refs/...` source maps to its
485/// literal directory prefix (up to the first `*`); a bare name maps under
486/// `refs/heads/`. `HEAD` is requested as a literal prefix.
487fn v2_ref_prefixes_from_refspecs(refspecs: &[String]) -> Vec<String> {
488    let mut out: Vec<String> = Vec::new();
489    let push_unique = |out: &mut Vec<String>, value: &str| {
490        if !out.iter().any(|v| v == value) {
491            out.push(value.to_owned());
492        }
493    };
494    for spec in refspecs {
495        if spec.starts_with('^') {
496            continue;
497        }
498        let raw = spec.strip_prefix('+').unwrap_or(spec.as_str());
499        let src = raw.split_once(':').map(|(s, _)| s).unwrap_or(raw).trim();
500        if src.is_empty() {
501            continue;
502        }
503        if src == "HEAD" {
504            push_unique(&mut out, "HEAD");
505            continue;
506        }
507        if let Some(star) = src.find('*') {
508            let prefix = &src[..star];
509            if prefix.is_empty() {
510                continue;
511            }
512            if prefix.starts_with("refs/") {
513                push_unique(&mut out, prefix);
514            } else {
515                push_unique(&mut out, &format!("refs/heads/{prefix}"));
516            }
517            continue;
518        }
519        if src.starts_with("refs/") {
520            push_unique(&mut out, src);
521        } else {
522            push_unique(&mut out, &format!("refs/heads/{src}"));
523        }
524    }
525    out
526}
527
528/// Parse one v2 `ls-refs` advertisement line into `(refname, oid, symref_target)`.
529///
530/// Lines look like `<oid> <refname>[ symref-target:<t>][ peeled:<oid>]`. Lib-side
531/// port of the CLI's `parse_ls_refs_v2_line` (the order of the optional suffixes
532/// is whichever the server emits; we scan for both tokens). Returns `None` for a
533/// malformed line.
534fn parse_ls_refs_v2_line(line: &str) -> Option<(String, ObjectId, Option<String>)> {
535    const SYM: &str = " symref-target:";
536    const PEEL: &str = " peeled:";
537    let (oid_hex, after_oid) = line.split_once(' ')?;
538    let oid = ObjectId::from_hex(oid_hex).ok()?;
539
540    // The refname ends at the first ` symref-target:` or ` peeled:` token.
541    let sym_at = after_oid.find(SYM);
542    let peel_at = after_oid.find(PEEL);
543    let name_end = match (sym_at, peel_at) {
544        (Some(a), Some(b)) => a.min(b),
545        (Some(a), None) => a,
546        (None, Some(b)) => b,
547        (None, None) => after_oid.len(),
548    };
549    let name = after_oid[..name_end].trim().to_owned();
550    if name.is_empty() {
551        return None;
552    }
553    let symref_target = sym_at.map(|pos| {
554        let tail = &after_oid[pos + SYM.len()..];
555        let end = tail.find(' ').unwrap_or(tail.len());
556        tail[..end].to_owned()
557    });
558    Some((name, oid, symref_target))
559}
560
561/// Issue `command=ls-refs` over a v2 connection and parse the ref map.
562///
563/// Sends the capability echo (agent/object-format via
564/// [`protocol_v2::cap_lines_for_command_request`]), the `0001` delimiter, then
565/// `symrefs`, `peel`, and `ref-prefix <p>` lines derived from `refspecs` (plus
566/// `refs/tags/` when `tags != None`), then flush. Returns the advertised
567/// `refs/heads/*` and `refs/tags/*` refs (peeled `^{}` carrier lines dropped) and
568/// the `HEAD` symref target. Lifted from the CLI's `v2_ls_refs_for_fetch`.
569fn v2_ls_refs(
570    conn: &mut dyn Connection,
571    server_caps: &[String],
572    local_odb: &crate::odb::Odb,
573    tags: crate::transfer::TagMode,
574    refspecs: &[String],
575) -> Result<(Vec<(String, ObjectId)>, Option<String>)> {
576    let req = build_v2_ls_refs_request(server_caps, local_odb, tags, refspecs)?;
577    conn.writer().write_all(&req)?;
578    conn.writer().flush()?;
579    parse_v2_ls_refs_response(conn.reader())
580}
581
582/// Build the `command=ls-refs` request body (capability echo + `0001` + the
583/// `symrefs`/`peel`/`ref-prefix` argument lines + flush) for a v2 fetch.
584///
585/// Factored out of [`v2_ls_refs`] so the streaming transports (which write it to
586/// a duplex socket) and the stateless smart-HTTP transport (which POSTs it as a
587/// request body) share one request builder. `HEAD` is always requested so the
588/// server advertises its `symref-target`; `refs/tags/` is added under `--tags` /
589/// tag-following even when the refspecs name only heads.
590pub(crate) fn build_v2_ls_refs_request(
591    server_caps: &[String],
592    local_odb: &crate::odb::Odb,
593    tags: crate::transfer::TagMode,
594    refspecs: &[String],
595) -> Result<Vec<u8>> {
596    let object_format = v2_object_format(server_caps, local_odb);
597    let cap_echo = protocol_v2::cap_lines_for_command_request(server_caps);
598
599    let mut req: Vec<u8> = Vec::new();
600    pkt_line::write_line(&mut req, "command=ls-refs")?;
601    // Echo agent/object-format; if the server advertised neither (rare), still
602    // pin the object-format so a sha256 server agrees on hash width.
603    if cap_echo.iter().any(|c| c.starts_with("object-format=")) {
604        for line in &cap_echo {
605            pkt_line::write_line(&mut req, line)?;
606        }
607    } else {
608        for line in &cap_echo {
609            pkt_line::write_line(&mut req, line)?;
610        }
611        pkt_line::write_line(&mut req, &format!("object-format={object_format}"))?;
612    }
613    pkt_line::write_delim(&mut req)?;
614    pkt_line::write_line(&mut req, "symrefs")?;
615    pkt_line::write_line(&mut req, "peel")?;
616
617    // Always request `HEAD` so the server advertises its `symref-target`, which
618    // drives `FetchOutcome::default_branch` (the wire equivalent of the v0/v1
619    // `symref=HEAD:` capability). `HEAD` is dropped from the fetchable ref set.
620    pkt_line::write_line(&mut req, "ref-prefix HEAD")?;
621    let mut prefixes = v2_ref_prefixes_from_refspecs(refspecs);
622    if prefixes.is_empty() {
623        prefixes.push("refs/heads/".to_owned());
624        prefixes.push("refs/tags/".to_owned());
625    } else if tags != crate::transfer::TagMode::None
626        && !prefixes.iter().any(|p| p == "refs/tags/")
627    {
628        // Tag-following / `--tags` wants the tag namespace advertised so we can
629        // add tags from the ls-refs result, even if the refspecs only name heads.
630        prefixes.push("refs/tags/".to_owned());
631    }
632    for p in &prefixes {
633        pkt_line::write_line(&mut req, &format!("ref-prefix {p}"))?;
634    }
635    pkt_line::write_flush(&mut req)?;
636    Ok(req)
637}
638
639/// Parse a `command=ls-refs` response into `(advertised refs, HEAD symref)`.
640///
641/// Reads `<oid> <refname>[ symref-target:…][ peeled:…]` lines up to the
642/// terminating flush, dropping peeled `^{}` carriers and recording the `HEAD`
643/// symref target. Shared by the streaming and stateless-HTTP v2 paths.
644pub(crate) fn parse_v2_ls_refs_response(
645    reader: &mut dyn Read,
646) -> Result<(Vec<(String, ObjectId)>, Option<String>)> {
647    // Response: `<oid> <refname>[ symref-target:…][ peeled:…]` lines, flush-terminated.
648    let mut advertised: Vec<(String, ObjectId)> = Vec::new();
649    let mut head_symref: Option<String> = None;
650    let mut reader = reader;
651    loop {
652        match pkt_line::read_packet(&mut reader)? {
653            None | Some(pkt_line::Packet::Flush) | Some(pkt_line::Packet::Delim) => break,
654            Some(pkt_line::Packet::ResponseEnd) => break,
655            Some(pkt_line::Packet::Data(line)) => {
656                let line = line.trim_end_matches('\n');
657                if let Some(msg) = line.strip_prefix("ERR ") {
658                    return Err(Error::Message(format!(
659                        "remote error: {}",
660                        msg.trim_end()
661                    )));
662                }
663                let Some((name, oid, symref_target)) = parse_ls_refs_v2_line(line) else {
664                    continue;
665                };
666                if name.contains("^{") || name.ends_with("^{}") {
667                    continue;
668                }
669                if name == "HEAD" {
670                    if let Some(t) = symref_target {
671                        head_symref = Some(t);
672                    }
673                    // HEAD itself is not a fetchable ref here; refspecs target heads/tags.
674                    continue;
675                }
676                if name.starts_with("refs/heads/")
677                    || name.starts_with("refs/tags/")
678                    || name.starts_with("refs/")
679                {
680                    advertised.push((name, oid));
681                }
682            }
683        }
684    }
685    Ok((advertised, head_symref))
686}
687
688/// Build the ordered `have` candidate list for a v2 fetch from the local ref
689/// tips (heads, tags, HEAD), peeled to commits and excluding the wants, driven
690/// through the [`SkippingNegotiator`]'s skipping schedule.
691///
692/// Shared by the streaming (`negotiate_pack_v2`) and stateless-HTTP v2 fetch
693/// paths so both offer the server the same `have`s in the same order. The wire
694/// rounds (how many haves per request, when to send `done`) are batched by the
695/// caller, which differs between a duplex socket and stateless POSTs.
696pub(crate) fn v2_local_haves(
697    local_git_dir: &Path,
698    wants: &[ObjectId],
699) -> Result<Vec<ObjectId>> {
700    let want_set: HashSet<ObjectId> = wants.iter().copied().collect();
701    let local_repo = crate::repo::Repository::open(local_git_dir, None)?;
702    let mut negotiator = SkippingNegotiator::new(local_repo);
703    let mut tips: Vec<ObjectId> = Vec::new();
704    let mut seen_tip: HashSet<ObjectId> = HashSet::new();
705    for prefix in ["refs/heads/", "refs/tags/"] {
706        if let Ok(entries) = crate::refs::list_refs(local_git_dir, prefix) {
707            for (_, oid) in entries {
708                if let Some(c) = peel_to_commit(negotiator.repo(), oid) {
709                    if !want_set.contains(&c) && seen_tip.insert(c) {
710                        tips.push(c);
711                    }
712                }
713            }
714        }
715    }
716    if let Ok(h) = crate::refs::resolve_ref(local_git_dir, "HEAD") {
717        if let Some(c) = peel_to_commit(negotiator.repo(), h) {
718            if !want_set.contains(&c) && seen_tip.insert(c) {
719                tips.push(c);
720            }
721        }
722    }
723    tips.sort_by_key(ObjectId::to_hex);
724    for t in tips {
725        negotiator.add_tip(t)?;
726    }
727    // Drain the negotiator into an ordered have list (it already applies the
728    // skipping schedule); the caller batches the wire rounds.
729    let mut haves: Vec<ObjectId> = Vec::new();
730    while let Some(oid) = negotiator.next_have()? {
731        haves.push(oid);
732    }
733    Ok(haves)
734}
735
736/// Run a v2 `command=fetch` negotiation over the connection and return the raw
737/// pack bytes for `wants`.
738///
739/// Drives the [`SkippingNegotiator`] exactly like the v0/v1 path, but frames the
740/// request as v2 (`command=fetch`, capability echo, `0001`, then
741/// `thin-pack`/`no-progress`/`ofs-delta`, `want <oid>` lines, `have <oid>` lines,
742/// and `done`). Multi-round: round 1 sends the first batch of haves *without*
743/// `done`, reads the `acknowledgments` section (looking for `ready`); if not yet
744/// ready it sends the remaining haves + `done`. Then reads the response sections
745/// (`acknowledgments`, optional `shallow-info`/`wanted-refs`, then `packfile`) and
746/// demuxes the side-band-64k pack. Lifted from `write_v2_fetch_request` +
747/// `read_v2_acknowledgments` / `read_v2_fetch_pack_response`.
748fn negotiate_pack_v2(
749    local_git_dir: &Path,
750    conn: &mut dyn Connection,
751    server_caps: &[String],
752    local_odb: &crate::odb::Odb,
753    wants: &[ObjectId],
754    deepen: &V2DeepenArgs,
755    progress: &mut dyn Progress,
756) -> Result<(Vec<u8>, ShallowUpdate)> {
757    if wants.is_empty() {
758        return Ok((Vec::new(), ShallowUpdate::default()));
759    }
760    let object_format = v2_object_format(server_caps, local_odb);
761    let cap_echo = protocol_v2::cap_lines_for_command_request(server_caps);
762    let sideband_all = protocol_v2::fetch_supports_sideband_all(server_caps);
763
764    // A deepen/shallow request does not offer local haves (the local objects
765    // bottom out at grafts and are not a usable negotiation base), forcing the
766    // single-round path so the server sends a `shallow-info` section + pack.
767    let shallow_request = deepen.is_shallow_request();
768
769    // The ordered `have` list, built from the local ref tips with the skipping
770    // negotiator (shared with the stateless-HTTP v2 path). Empty for a shallow
771    // request.
772    let haves = if shallow_request {
773        Vec::new()
774    } else {
775        v2_local_haves(local_git_dir, wants)?
776    };
777
778    let mut pack = Vec::new();
779    let mut shallow_update = ShallowUpdate::default();
780    if haves.is_empty() {
781        // No local history to offer: single round, wants + done, read the pack.
782        write_v2_fetch_request(
783            conn.writer(),
784            &object_format,
785            &cap_echo,
786            wants,
787            &[],
788            sideband_all,
789            deepen,
790            true,
791        )?;
792        read_v2_fetch_pack_response(conn.reader(), &mut pack, &mut shallow_update, progress)?;
793        return Ok((pack, shallow_update));
794    }
795
796    // Multi-round: round 1 sends the first batch of haves WITHOUT done.
797    let first_batch = haves.len().min(INITIAL_FLUSH);
798    write_v2_fetch_request(
799        conn.writer(),
800        &object_format,
801        &cap_echo,
802        wants,
803        &haves[..first_batch],
804        sideband_all,
805        deepen,
806        false,
807    )?;
808
809    let ack = read_v2_acknowledgments(conn.reader())?;
810    match ack {
811        // Server is `ready`: the pack follows in the SAME response after a delim.
812        Some(round) if round.ready => {
813            read_v2_fetch_pack_response(conn.reader(), &mut pack, &mut shallow_update, progress)?;
814        }
815        // Server skipped acknowledgments and went straight to the pack header
816        // (consumed inside the reader); read the pack now.
817        None => {
818            read_v2_fetch_pack_response(conn.reader(), &mut pack, &mut shallow_update, progress)?;
819        }
820        // Not ready yet: round 2 sends the remaining haves + `done`, then pack.
821        Some(_) => {
822            write_v2_fetch_request(
823                conn.writer(),
824                &object_format,
825                &cap_echo,
826                wants,
827                &haves[first_batch..],
828                sideband_all,
829                deepen,
830                true,
831            )?;
832            read_v2_fetch_pack_response(conn.reader(), &mut pack, &mut shallow_update, progress)?;
833        }
834    }
835    Ok((pack, shallow_update))
836}
837
838/// The shallow/deepen arguments for a v2 `command=fetch` request, derived from
839/// [`FetchOptions`] plus the local `shallow` file. Built once by the fetch driver
840/// and passed to [`write_v2_fetch_request`] on each round (every stateless POST
841/// must resend them).
842#[derive(Clone, Default)]
843pub(crate) struct V2DeepenArgs {
844    /// The client's current shallow grafts (`shallow <oid>` lines).
845    pub(crate) local_shallow: Vec<ObjectId>,
846    /// `deepen <n>` (absolute depth, or `INFINITE_DEPTH` for `--unshallow`).
847    pub(crate) depth: Option<u32>,
848    /// `deepen-since <unix-ts>`.
849    pub(crate) deepen_since: Option<String>,
850    /// `deepen-not <ref>` exclusions.
851    pub(crate) deepen_not: Vec<String>,
852}
853
854impl V2DeepenArgs {
855    /// Build the v2 deepen args from the fetch options and the local shallow file,
856    /// translating `--unshallow` into the `INFINITE_DEPTH` deepen Git uses.
857    pub(crate) fn from_opts(opts: &FetchOptions, local_shallow: &[ObjectId]) -> Self {
858        let depth = if opts.unshallow {
859            Some(crate::shallow::INFINITE_DEPTH)
860        } else {
861            opts.depth.filter(|d| *d > 0)
862        };
863        Self {
864            local_shallow: local_shallow.to_vec(),
865            depth,
866            deepen_since: opts
867                .deepen_since
868                .as_deref()
869                .filter(|s| !s.trim().is_empty())
870                .map(crate::shallow::deepen_since_wire_value),
871            deepen_not: opts
872                .deepen_not
873                .iter()
874                .map(|s| s.trim().to_owned())
875                .filter(|s| !s.is_empty())
876                .collect(),
877        }
878    }
879
880    /// Whether any deepen/shallow argument is present (drives `shallow-info`
881    /// handling and the "skip offering haves" decision).
882    pub(crate) fn is_shallow_request(&self) -> bool {
883        self.depth.is_some()
884            || self.deepen_since.is_some()
885            || !self.deepen_not.is_empty()
886            || !self.local_shallow.is_empty()
887    }
888}
889
890/// Write a v2 `command=fetch` request: capability echo, `0001`, the standard
891/// `thin-pack`/`no-progress`/`ofs-delta` (+ `sideband-all`/`include-tag`)
892/// arguments, the shallow/deepen arguments, the `want <oid>` lines, the
893/// `have <oid>` lines, and `done` when `send_done`, terminated by flush. Lifted
894/// from the CLI's `write_v2_fetch_request` (streaming-fetch subset).
895pub(crate) fn write_v2_fetch_request(
896    w: &mut dyn Write,
897    object_format: &str,
898    cap_echo: &[String],
899    wants: &[ObjectId],
900    haves: &[ObjectId],
901    sideband_all: bool,
902    deepen: &V2DeepenArgs,
903    send_done: bool,
904) -> Result<()> {
905    let mut req: Vec<u8> = Vec::new();
906    pkt_line::write_line(&mut req, "command=fetch")?;
907    if cap_echo.iter().any(|c| c.starts_with("object-format=")) {
908        for line in cap_echo {
909            pkt_line::write_line(&mut req, line)?;
910        }
911    } else {
912        for line in cap_echo {
913            pkt_line::write_line(&mut req, line)?;
914        }
915        pkt_line::write_line(&mut req, &format!("object-format={object_format}"))?;
916    }
917    pkt_line::write_delim(&mut req)?;
918
919    pkt_line::write_line(&mut req, "thin-pack")?;
920    pkt_line::write_line(&mut req, "no-progress")?;
921    pkt_line::write_line(&mut req, "ofs-delta")?;
922    if sideband_all {
923        pkt_line::write_line(&mut req, "sideband-all")?;
924    }
925    // Ask the server to bundle tag objects pointing at fetched history; the
926    // TagMode plumbing in `fetch_remote` decides which tag refs to write.
927    pkt_line::write_line(&mut req, "include-tag")?;
928
929    // Shallow/deepen arguments (the `fetch` v2 command's `shallow`/`deepen*` args).
930    for oid in &deepen.local_shallow {
931        pkt_line::write_line(&mut req, &format!("shallow {}", oid.to_hex()))?;
932    }
933    if let Some(depth) = deepen.depth {
934        pkt_line::write_line(&mut req, &format!("deepen {depth}"))?;
935    }
936    if let Some(since) = &deepen.deepen_since {
937        pkt_line::write_line(&mut req, &format!("deepen-since {since}"))?;
938    }
939    for excl in &deepen.deepen_not {
940        pkt_line::write_line(&mut req, &format!("deepen-not {excl}"))?;
941    }
942
943    for want in wants {
944        pkt_line::write_line(&mut req, &format!("want {}", want.to_hex()))?;
945    }
946    for have in haves {
947        pkt_line::write_line(&mut req, &format!("have {}", have.to_hex()))?;
948    }
949    if send_done {
950        pkt_line::write_line(&mut req, "done")?;
951    }
952    pkt_line::write_flush(&mut req)?;
953    w.write_all(&req)?;
954    w.flush()?;
955    Ok(())
956}
957
958/// Outcome of reading one v2 `acknowledgments` section.
959pub(crate) struct V2AckRound {
960    /// Server emitted `ready`: the packfile follows in the same response after a
961    /// delimiter — the caller reads the pack now without sending more.
962    pub(crate) ready: bool,
963}
964
965/// Read a v2 `acknowledgments` section header and its `ACK`/`NAK`/`ready` lines.
966///
967/// Returns `Some(round)` for an `acknowledgments` section (with `ready` set when
968/// the server is ready to send the pack), or `None` when the server skipped the
969/// section and started a different one (e.g. went straight to `packfile`) — in
970/// which case the header has been consumed and the caller proceeds to read the
971/// pack response directly. Lifted from the CLI's `read_v2_acknowledgments`.
972pub(crate) fn read_v2_acknowledgments(reader: &mut dyn Read) -> Result<Option<V2AckRound>> {
973    let mut reader = reader;
974    let hdr = match pkt_line::read_packet(&mut reader)? {
975        Some(pkt_line::Packet::Data(s)) => s,
976        Some(pkt_line::Packet::Flush) => return Ok(Some(V2AckRound { ready: false })),
977        None => return Ok(None),
978        Some(other) => {
979            return Err(Error::Message(format!(
980                "unexpected v2 fetch response: {other:?}"
981            )))
982        }
983    };
984    let hdr = hdr.trim_end();
985    if let Some(msg) = hdr.strip_prefix("ERR ") {
986        return Err(Error::Message(format!("remote error: {}", msg.trim_end())));
987    }
988    if hdr != "acknowledgments" {
989        // The server started a non-acknowledgments section; the pack reader,
990        // called next, re-dispatches on this header. We cannot push it back, so
991        // signal `None` only when we know the pack reader will see the same
992        // header — which it will, because the next read picks up where we left
993        // off. To make that work, the caller treats `None` as "read the pack".
994        // The header we just consumed (`shallow-info`/`wanted-refs`/`packfile`)
995        // would be lost; for the streaming fetch we only reach here after a
996        // first round of haves, where servers always emit `acknowledgments`
997        // first. Reaching a different header is therefore unexpected.
998        return Err(Error::Message(format!(
999            "unexpected v2 fetch section before acknowledgments: {hdr}"
1000        )));
1001    }
1002    let mut ready = false;
1003    loop {
1004        match pkt_line::read_packet(&mut reader)? {
1005            Some(pkt_line::Packet::Data(ln)) => {
1006                let ln = ln.trim_end();
1007                if ln == "NAK" || ln.starts_with("ACK ") {
1008                    continue;
1009                }
1010                if ln == "ready" {
1011                    ready = true;
1012                    continue;
1013                }
1014                return Err(Error::Message(format!(
1015                    "unexpected acknowledgment line: '{ln}'"
1016                )));
1017            }
1018            Some(pkt_line::Packet::Delim) | Some(pkt_line::Packet::Flush) | None => break,
1019            Some(other) => {
1020                return Err(Error::Message(format!(
1021                    "unexpected acknowledgments packet: {other:?}"
1022                )))
1023            }
1024        }
1025    }
1026    Ok(Some(V2AckRound { ready }))
1027}
1028
1029/// Read a v2 `command=fetch` response: capture the `shallow-info` section's
1030/// `shallow`/`unshallow` lines into `shallow_out`, skip the other non-pack
1031/// sections (`acknowledgments`/`wanted-refs`/`packfile-uris`), and demux the
1032/// side-band-64k pack from the `packfile` section into `out`. Lifted from the
1033/// CLI's `read_v2_fetch_pack_response`, extended to surface shallow updates.
1034pub(crate) fn read_v2_fetch_pack_response(
1035    reader: &mut dyn Read,
1036    out: &mut Vec<u8>,
1037    shallow_out: &mut ShallowUpdate,
1038    progress: &mut dyn Progress,
1039) -> Result<()> {
1040    loop {
1041        let hdr = match pkt_line::read_packet(&mut &mut *reader)? {
1042            Some(pkt_line::Packet::Data(s)) => s,
1043            Some(pkt_line::Packet::Flush) | None => return Ok(()),
1044            Some(pkt_line::Packet::Delim) => continue,
1045            Some(other) => {
1046                return Err(Error::Message(format!(
1047                    "unexpected v2 fetch response: {other:?}"
1048                )))
1049            }
1050        };
1051        let hdr = hdr.trim_end();
1052        if let Some(msg) = hdr.strip_prefix("ERR ") {
1053            return Err(Error::Message(format!("remote error: {}", msg.trim_end())));
1054        }
1055        match hdr {
1056            "shallow-info" => {
1057                // Capture the shallow/unshallow boundary updates. The section is
1058                // delim-terminated (before the `packfile` header), which
1059                // `read_shallow_info_section` stops at, leaving the header intact.
1060                let (sh, unsh) = crate::shallow::read_shallow_info_section(&mut *reader)?;
1061                shallow_out.shallow.extend(sh);
1062                shallow_out.unshallow.extend(unsh);
1063            }
1064            "acknowledgments" | "wanted-refs" | "packfile-uris" => {
1065                skip_v2_section_until_boundary(&mut *reader)?;
1066            }
1067            "packfile" => {
1068                // The `packfile` section body is side-band-64k framed; reuse the
1069                // shared demuxer (channel 1 = pack, channel 2 = progress, 3 = err).
1070                read_sideband_pack(&mut *reader, out, progress)?;
1071                return Ok(());
1072            }
1073            other => {
1074                return Err(Error::Message(format!(
1075                    "unexpected v2 fetch section: {other}"
1076                )))
1077            }
1078        }
1079    }
1080}
1081
1082/// Skip a v2 response section up to its terminating flush/delim.
1083fn skip_v2_section_until_boundary(reader: &mut dyn Read) -> Result<()> {
1084    loop {
1085        match pkt_line::read_packet(&mut &mut *reader)? {
1086            None | Some(pkt_line::Packet::Flush) | Some(pkt_line::Packet::Delim) => return Ok(()),
1087            Some(pkt_line::Packet::ResponseEnd) => return Ok(()),
1088            Some(pkt_line::Packet::Data(_)) => {}
1089        }
1090    }
1091}
1092
1093/// Fetch from a remote over a live [`Connection`], driving the upload-pack
1094/// negotiation and writing the resulting tracking-ref updates into
1095/// `local_git_dir`.
1096///
1097/// The flow mirrors [`crate::transfer::fetch_local`], but the remote ref list
1098/// comes from the connection's advertisement, the objects arrive over the wire
1099/// (negotiated pack -> [`crate::unpack_objects`]), and the local repo is opened
1100/// to classify ancestry. Reuses the refspec matching, tag-mode, prune, and
1101/// classification helpers from [`crate::transfer`].
1102///
1103/// Handles protocol v0, v1, and v2. For a v2 connection the ref map is recovered
1104/// via a `command=ls-refs` round (no refs are advertised on connect) and the
1105/// pack is negotiated with `command=fetch`; v0/v1 use the connect-time
1106/// advertisement and the classic `want`/`have`/`done` exchange.
1107///
1108/// # Errors
1109///
1110/// Returns an error if a refspec is invalid, if the negotiation or pack ingest
1111/// fails, or on ref/odb I/O failure.
1112pub fn fetch_remote(
1113    local_git_dir: &Path,
1114    conn: &mut dyn Connection,
1115    opts: &FetchOptions,
1116    progress: &mut dyn Progress,
1117) -> Result<FetchOutcome> {
1118    let local_odb = open_odb(local_git_dir);
1119
1120    // 1. Remote refs + default branch.
1121    //
1122    // For protocol v2 the connect-time advertisement carries no refs (only the
1123    // capability block); we obtain them now with an `ls-refs` command, derived
1124    // from the fetch refspecs. For v0/v1 they come from the connect-time
1125    // advertisement directly.
1126    let (remote_refs, default_branch, v2_caps): (
1127        Vec<(String, ObjectId)>,
1128        Option<String>,
1129        Option<Vec<String>>,
1130    ) = if conn.protocol_version() >= 2 {
1131        let caps: Vec<String> = conn.capabilities().to_vec();
1132        let (refs, head_symref) =
1133            v2_ls_refs(conn, &caps, &local_odb, opts.tags, &opts.refspecs)?;
1134        let default_branch = head_symref.map(|t| {
1135            t.strip_prefix("refs/heads/")
1136                .unwrap_or(&t)
1137                .to_owned()
1138        });
1139        (refs, default_branch, Some(caps))
1140    } else {
1141        let default_branch = conn.head_symref().map(|t| {
1142            t.strip_prefix("refs/heads/")
1143                .unwrap_or(t)
1144                .to_owned()
1145        });
1146        let remote_refs: Vec<(String, ObjectId)> = conn
1147            .advertised_refs()
1148            .iter()
1149            .filter(|(n, _)| n != "HEAD" && !n.ends_with("^{}"))
1150            .cloned()
1151            .collect();
1152        (remote_refs, default_branch, None)
1153    };
1154
1155    // 2. Parse refspecs.
1156    let mut positive: Vec<RefspecItem> = Vec::new();
1157    let mut negatives: Vec<RefspecItem> = Vec::new();
1158    for spec in &opts.refspecs {
1159        let item = parse_fetch_refspec(spec)
1160            .map_err(|e| Error::Message(format!("invalid refspec '{spec}': {e}")))?;
1161        if item.negative {
1162            negatives.push(item);
1163        } else {
1164            positive.push(item);
1165        }
1166    }
1167    for spec in &opts.negative_refspecs {
1168        let item = parse_fetch_refspec(spec)
1169            .map_err(|e| Error::Message(format!("invalid negative refspec '{spec}': {e}")))?;
1170        negatives.push(item);
1171    }
1172
1173    // 3. Match refs to refspecs (mirror transfer::fetch_local).
1174    let mut matched: Vec<crate::transfer::MatchedRef> = Vec::new();
1175    let mut matched_oids: HashSet<ObjectId> = HashSet::new();
1176    let mut seen_remote_ref: HashSet<String> = HashSet::new();
1177    for (name, oid) in &remote_refs {
1178        if ref_excluded(name, &negatives) {
1179            continue;
1180        }
1181        if let Some(local_ref) = match_positive(name, &positive) {
1182            if seen_remote_ref.insert(name.clone()) {
1183                matched_oids.insert(*oid);
1184                matched.push(crate::transfer::MatchedRef {
1185                    remote_ref: name.clone(),
1186                    local_ref,
1187                    oid: *oid,
1188                    force: refspecs_force(name, &positive),
1189                    is_tag: name.starts_with("refs/tags/"),
1190                });
1191            }
1192        }
1193    }
1194
1195    // TagMode: add tags. Tag-following needs the closure of fetched objects,
1196    // which we cannot compute remotely; the wire `include-tag` capability makes
1197    // the server send tag objects with the pack, so we add advertised tags by
1198    // mode here and let classification proceed once the pack lands. For
1199    // `Following` we approximate using the advertised remote odb if present
1200    // (it is not, over the wire), so we add following tags whose oid is among
1201    // the matched set after the fact — handled below using the local odb.
1202    //
1203    // `following_only` collects the oids of tags added *provisionally* under
1204    // `Following`. These must NOT be `want`ed up front: git's tag-following only
1205    // keeps a tag whose target is already reachable from the fetched heads, so
1206    // wanting the tag itself would drag down its (otherwise unreachable) target
1207    // and incorrectly keep the tag. They are pruned by `retain_following_tags`.
1208    let following_only = add_wire_tags(
1209        opts.tags,
1210        &remote_refs,
1211        &negatives,
1212        &mut matched,
1213        &mut matched_oids,
1214        &mut seen_remote_ref,
1215    );
1216
1217    // The client's current shallow grafts (drives the wire `shallow <oid>` lines
1218    // and the "this is a shallow request" decisions in the negotiators).
1219    let local_shallow = crate::shallow::load_shallow_oids(local_git_dir)?;
1220    let shallow_request = opts.has_deepen_request() || !local_shallow.is_empty();
1221
1222    // 4. Wants. Normally the matched oids that are absent locally. For a
1223    // deepen/`--unshallow` request the wanted tips may already be present (a prior
1224    // shallow fetch landed them); we must still `want` them so the server fills in
1225    // the now-reachable ancestors past the old boundary.
1226    let wants: Vec<ObjectId> = if shallow_request {
1227        matched_oids
1228            .iter()
1229            .copied()
1230            .filter(|oid| !following_only.contains(oid))
1231            .collect()
1232    } else {
1233        matched_oids
1234            .iter()
1235            .copied()
1236            .filter(|oid| !following_only.contains(oid) && !local_odb.exists(oid))
1237            .collect()
1238    };
1239
1240    // Shallow-boundary updates the server reports (`shallow`/`unshallow`), applied
1241    // to the local `shallow` file and surfaced in the outcome.
1242    let mut shallow_update = ShallowUpdate::default();
1243
1244    if !wants.is_empty() && !opts.dry_run {
1245        let (pack, su) = if let Some(caps) = v2_caps.as_ref() {
1246            let deepen = V2DeepenArgs::from_opts(opts, &local_shallow);
1247            negotiate_pack_v2(local_git_dir, conn, caps, &local_odb, &wants, &deepen, progress)?
1248        } else {
1249            negotiate_pack(local_git_dir, conn, &wants, opts, &local_shallow, progress)?
1250        };
1251        shallow_update = su;
1252        if !pack.is_empty() {
1253            let mut cursor = std::io::Cursor::new(pack);
1254            crate::unpack_objects::unpack_objects(
1255                &mut cursor,
1256                &local_odb,
1257                &crate::unpack_objects::UnpackOptions {
1258                    quiet: true,
1259                    ..Default::default()
1260                },
1261            )?;
1262        }
1263    }
1264
1265    // Apply the shallow/unshallow boundary updates to the on-disk `shallow` file
1266    // before classifying refs (so connectivity reflects the new graft set).
1267    if !opts.dry_run {
1268        crate::shallow::apply_shallow_updates(
1269            local_git_dir,
1270            &shallow_update.shallow,
1271            &shallow_update.unshallow,
1272        )?;
1273    }
1274
1275    // Close the write side once the v2 conversation is done so the server's
1276    // persistent `serve_loop` sees EOF and exits — even when we sent only an
1277    // `ls-refs` (no wants) and skipped `command=fetch`. Without this a streaming
1278    // transport (ssh subprocess, daemon socket) hangs at teardown. No-op for
1279    // v0/v1, where the server closes after its single response.
1280    if v2_caps.is_some() {
1281        conn.finish_send();
1282    }
1283
1284    // For TagMode::Following, prune tags whose target did not arrive in the
1285    // pack (now resolvable against the local odb, which holds the fetched
1286    // objects). All/None already handled; Following kept only when reachable.
1287    if opts.tags == crate::transfer::TagMode::Following {
1288        retain_following_tags(&local_odb, &mut matched, &matched_oids);
1289    }
1290
1291    // 5. Classify + apply ref updates (ancestry via the now-populated local repo).
1292    let local_repo = if opts.dry_run {
1293        None
1294    } else {
1295        crate::repo::Repository::open(local_git_dir, None).ok()
1296    };
1297
1298    let mut updates: Vec<RefUpdate> = Vec::new();
1299
1300    if opts.prune {
1301        prune_tracking_refs(
1302            local_git_dir,
1303            &positive,
1304            &remote_refs,
1305            opts.dry_run,
1306            &mut updates,
1307        )?;
1308    }
1309
1310    for m in &matched {
1311        let Some(local_ref) = &m.local_ref else {
1312            updates.push(RefUpdate {
1313                remote_ref: m.remote_ref.clone(),
1314                local_ref: None,
1315                old_oid: None,
1316                new_oid: Some(m.oid),
1317                mode: UpdateMode::NoChangeNeeded,
1318                note: Some("not stored (empty destination)".to_owned()),
1319            });
1320            continue;
1321        };
1322
1323        let old = crate::refs::resolve_ref(local_git_dir, local_ref).ok();
1324        let mode = classify_update(old.as_ref(), &m.oid, m.force, m.is_tag, local_repo.as_ref());
1325
1326        let write = matches!(
1327            mode,
1328            UpdateMode::New | UpdateMode::FastForward | UpdateMode::Forced
1329        );
1330        if write && !opts.dry_run {
1331            crate::refs::write_ref(local_git_dir, local_ref, &m.oid)?;
1332        }
1333
1334        updates.push(RefUpdate {
1335            remote_ref: m.remote_ref.clone(),
1336            local_ref: Some(local_ref.clone()),
1337            old_oid: old,
1338            new_oid: Some(m.oid),
1339            mode,
1340            note: None,
1341        });
1342    }
1343
1344    Ok(FetchOutcome {
1345        updates,
1346        default_branch,
1347        new_shallow: shallow_update.shallow,
1348        new_unshallow: shallow_update.unshallow,
1349    })
1350}
1351
1352/// Add advertised tags to the matched set per [`crate::transfer::TagMode`].
1353///
1354/// Over the wire we cannot peel remote tags before the pack arrives, so:
1355/// * `All` adds every advertised tag (and `want`s it unconditionally).
1356/// * `Following` provisionally adds every advertised tag here; unreachable ones
1357///   are dropped by [`retain_following_tags`] after the pack is ingested.
1358/// * `None` adds nothing.
1359///
1360/// Returns the oids of tags added under `Following` — the caller must keep these
1361/// out of the `want` list so an unreachable tag does not drag its target into
1362/// the pack (which would make it look reachable and survive the prune).
1363fn add_wire_tags(
1364    mode: crate::transfer::TagMode,
1365    remote_refs: &[(String, ObjectId)],
1366    negatives: &[RefspecItem],
1367    matched: &mut Vec<crate::transfer::MatchedRef>,
1368    matched_oids: &mut HashSet<ObjectId>,
1369    seen_remote_ref: &mut HashSet<String>,
1370) -> HashSet<ObjectId> {
1371    let mut following_only: HashSet<ObjectId> = HashSet::new();
1372    if mode == crate::transfer::TagMode::None {
1373        return following_only;
1374    }
1375    for (name, oid) in remote_refs {
1376        if !name.starts_with("refs/tags/") {
1377            continue;
1378        }
1379        if seen_remote_ref.contains(name) || ref_excluded(name, negatives) {
1380            continue;
1381        }
1382        seen_remote_ref.insert(name.clone());
1383        matched_oids.insert(*oid);
1384        if mode == crate::transfer::TagMode::Following {
1385            following_only.insert(*oid);
1386        }
1387        matched.push(crate::transfer::MatchedRef {
1388            remote_ref: name.clone(),
1389            local_ref: Some(name.clone()),
1390            oid: *oid,
1391            force: false,
1392            is_tag: true,
1393        });
1394    }
1395    following_only
1396}
1397
1398/// Drop provisional `Following` tags whose object (or peeled target) did not
1399/// arrive in the fetched pack — i.e. is not reachable from the other matched,
1400/// non-tag refs we fetched. Matches `git fetch`'s default tag-following: a tag
1401/// is kept when it points into the fetched history.
1402fn retain_following_tags(
1403    local_odb: &crate::odb::Odb,
1404    matched: &mut Vec<crate::transfer::MatchedRef>,
1405    matched_oids: &HashSet<ObjectId>,
1406) {
1407    // Roots: every non-tag matched ref we fetched.
1408    let roots: Vec<ObjectId> = matched
1409        .iter()
1410        .filter(|m| !m.is_tag)
1411        .map(|m| m.oid)
1412        .collect();
1413    let closure = reachable_closure(local_odb, &roots);
1414    matched.retain(|m| {
1415        if !m.is_tag {
1416            return true;
1417        }
1418        let peeled = peel_tag_target(local_odb, m.oid);
1419        // Keep when the tag object itself or its peeled target is reachable from
1420        // the fetched heads, and we actually have the object locally.
1421        let have = local_odb.exists(&m.oid);
1422        have && (closure.contains(&m.oid)
1423            || closure.contains(&peeled)
1424            || matched_oids.contains(&peeled))
1425    });
1426}
1427
1428/// Peel an (annotated) tag to its ultimate non-tag target using the local odb.
1429fn peel_tag_target(odb: &crate::odb::Odb, oid: ObjectId) -> ObjectId {
1430    let mut current = oid;
1431    for _ in 0..16 {
1432        let Ok(obj) = odb.read(&current) else {
1433            return current;
1434        };
1435        if obj.kind != crate::objects::ObjectKind::Tag {
1436            return current;
1437        }
1438        match crate::objects::parse_tag(&obj.data) {
1439            Ok(t) => current = t.object,
1440            Err(_) => return current,
1441        }
1442    }
1443    current
1444}
1445
1446/// Compute the object closure reachable from `roots` (commits -> trees ->
1447/// blobs, peeling tags), using the local odb. Best-effort: descent stops at
1448/// missing objects.
1449fn reachable_closure(odb: &crate::odb::Odb, roots: &[ObjectId]) -> HashSet<ObjectId> {
1450    use crate::objects::{parse_commit, parse_tag, parse_tree, ObjectKind};
1451
1452    let mut seen: HashSet<ObjectId> = HashSet::new();
1453    let mut stack: Vec<ObjectId> = roots.to_vec();
1454    while let Some(oid) = stack.pop() {
1455        if !seen.insert(oid) {
1456            continue;
1457        }
1458        let Ok(obj) = odb.read(&oid) else {
1459            continue;
1460        };
1461        match obj.kind {
1462            ObjectKind::Commit => {
1463                if let Ok(c) = parse_commit(&obj.data) {
1464                    stack.push(c.tree);
1465                    for p in c.parents {
1466                        stack.push(p);
1467                    }
1468                }
1469            }
1470            ObjectKind::Tree => {
1471                if let Ok(entries) = parse_tree(&obj.data) {
1472                    for e in entries {
1473                        stack.push(e.oid);
1474                    }
1475                }
1476            }
1477            ObjectKind::Tag => {
1478                if let Ok(t) = parse_tag(&obj.data) {
1479                    stack.push(t.object);
1480                }
1481            }
1482            ObjectKind::Blob => {}
1483        }
1484    }
1485    seen
1486}