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