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    use crate::net_trace::net_trace;
1119    net_trace!(
1120        "fetch_remote: begin — protocol v{}, {} refspec(s), tags={:?}, depth={:?}",
1121        conn.protocol_version(),
1122        opts.refspecs.len(),
1123        opts.tags,
1124        opts.depth
1125    );
1126    let local_odb = open_odb(local_git_dir);
1127
1128    // 1. Remote refs + default branch.
1129    //
1130    // For protocol v2 the connect-time advertisement carries no refs (only the
1131    // capability block); we obtain them now with an `ls-refs` command, derived
1132    // from the fetch refspecs. For v0/v1 they come from the connect-time
1133    // advertisement directly.
1134    let (remote_refs, default_branch, v2_caps): (
1135        Vec<(String, ObjectId)>,
1136        Option<String>,
1137        Option<Vec<String>>,
1138    ) = if conn.protocol_version() >= 2 {
1139        let caps: Vec<String> = conn.capabilities().to_vec();
1140        let (refs, head_symref) =
1141            v2_ls_refs(conn, &caps, &local_odb, opts.tags, &opts.refspecs)?;
1142        let default_branch = head_symref.map(|t| {
1143            t.strip_prefix("refs/heads/")
1144                .unwrap_or(&t)
1145                .to_owned()
1146        });
1147        (refs, default_branch, Some(caps))
1148    } else {
1149        let default_branch = conn.head_symref().map(|t| {
1150            t.strip_prefix("refs/heads/")
1151                .unwrap_or(t)
1152                .to_owned()
1153        });
1154        let remote_refs: Vec<(String, ObjectId)> = conn
1155            .advertised_refs()
1156            .iter()
1157            .filter(|(n, _)| n != "HEAD" && !n.ends_with("^{}"))
1158            .cloned()
1159            .collect();
1160        (remote_refs, default_branch, None)
1161    };
1162    net_trace!(
1163        "fetch_remote: remote advertised {} ref(s){}",
1164        remote_refs.len(),
1165        v2_caps
1166            .as_ref()
1167            .map(|_| " (via v2 ls-refs)")
1168            .unwrap_or(" (v0/v1 advertisement)")
1169    );
1170
1171    // 2. Parse refspecs.
1172    let mut positive: Vec<RefspecItem> = Vec::new();
1173    let mut negatives: Vec<RefspecItem> = Vec::new();
1174    for spec in &opts.refspecs {
1175        let item = parse_fetch_refspec(spec)
1176            .map_err(|e| Error::Message(format!("invalid refspec '{spec}': {e}")))?;
1177        if item.negative {
1178            negatives.push(item);
1179        } else {
1180            positive.push(item);
1181        }
1182    }
1183    for spec in &opts.negative_refspecs {
1184        let item = parse_fetch_refspec(spec)
1185            .map_err(|e| Error::Message(format!("invalid negative refspec '{spec}': {e}")))?;
1186        negatives.push(item);
1187    }
1188
1189    // 3. Match refs to refspecs (mirror transfer::fetch_local).
1190    let mut matched: Vec<crate::transfer::MatchedRef> = Vec::new();
1191    let mut matched_oids: HashSet<ObjectId> = HashSet::new();
1192    let mut seen_remote_ref: HashSet<String> = HashSet::new();
1193    for (name, oid) in &remote_refs {
1194        if ref_excluded(name, &negatives) {
1195            continue;
1196        }
1197        if let Some(local_ref) = match_positive(name, &positive) {
1198            if seen_remote_ref.insert(name.clone()) {
1199                matched_oids.insert(*oid);
1200                matched.push(crate::transfer::MatchedRef {
1201                    remote_ref: name.clone(),
1202                    local_ref,
1203                    oid: *oid,
1204                    force: refspecs_force(name, &positive),
1205                    is_tag: name.starts_with("refs/tags/"),
1206                });
1207            }
1208        }
1209    }
1210
1211    // TagMode: add tags. Tag-following needs the closure of fetched objects,
1212    // which we cannot compute remotely; the wire `include-tag` capability makes
1213    // the server send tag objects with the pack, so we add advertised tags by
1214    // mode here and let classification proceed once the pack lands. For
1215    // `Following` we approximate using the advertised remote odb if present
1216    // (it is not, over the wire), so we add following tags whose oid is among
1217    // the matched set after the fact — handled below using the local odb.
1218    //
1219    // `following_only` collects the oids of tags added *provisionally* under
1220    // `Following`. These must NOT be `want`ed up front: git's tag-following only
1221    // keeps a tag whose target is already reachable from the fetched heads, so
1222    // wanting the tag itself would drag down its (otherwise unreachable) target
1223    // and incorrectly keep the tag. They are pruned by `retain_following_tags`.
1224    let following_only = add_wire_tags(
1225        opts.tags,
1226        &remote_refs,
1227        &negatives,
1228        &mut matched,
1229        &mut matched_oids,
1230        &mut seen_remote_ref,
1231    );
1232
1233    // The client's current shallow grafts (drives the wire `shallow <oid>` lines
1234    // and the "this is a shallow request" decisions in the negotiators).
1235    let local_shallow = crate::shallow::load_shallow_oids(local_git_dir)?;
1236    let shallow_request = opts.has_deepen_request() || !local_shallow.is_empty();
1237
1238    // 4. Wants. Normally the matched oids that are absent locally. For a
1239    // deepen/`--unshallow` request the wanted tips may already be present (a prior
1240    // shallow fetch landed them); we must still `want` them so the server fills in
1241    // the now-reachable ancestors past the old boundary.
1242    let wants: Vec<ObjectId> = if shallow_request {
1243        matched_oids
1244            .iter()
1245            .copied()
1246            .filter(|oid| !following_only.contains(oid))
1247            .collect()
1248    } else {
1249        matched_oids
1250            .iter()
1251            .copied()
1252            .filter(|oid| !following_only.contains(oid) && !local_odb.exists(oid))
1253            .collect()
1254    };
1255
1256    // Shallow-boundary updates the server reports (`shallow`/`unshallow`), applied
1257    // to the local `shallow` file and surfaced in the outcome.
1258    let mut shallow_update = ShallowUpdate::default();
1259
1260    net_trace!(
1261        "fetch_remote: {} matched ref(s), want {} object(s){}",
1262        matched.len(),
1263        wants.len(),
1264        if shallow_request { " (shallow request)" } else { "" }
1265    );
1266
1267    if !wants.is_empty() && !opts.dry_run {
1268        net_trace!("fetch_remote: negotiating + fetching pack…");
1269        let (pack, su) = if let Some(caps) = v2_caps.as_ref() {
1270            let deepen = V2DeepenArgs::from_opts(opts, &local_shallow);
1271            negotiate_pack_v2(local_git_dir, conn, caps, &local_odb, &wants, &deepen, progress)?
1272        } else {
1273            negotiate_pack(local_git_dir, conn, &wants, opts, &local_shallow, progress)?
1274        };
1275        shallow_update = su;
1276        net_trace!(
1277            "fetch_remote: received pack ({} bytes), unpacking…",
1278            pack.len()
1279        );
1280        if !pack.is_empty() {
1281            let mut cursor = std::io::Cursor::new(pack);
1282            crate::unpack_objects::unpack_objects(
1283                &mut cursor,
1284                &local_odb,
1285                &crate::unpack_objects::UnpackOptions {
1286                    quiet: true,
1287                    ..Default::default()
1288                },
1289            )?;
1290        }
1291    }
1292
1293    // Apply the shallow/unshallow boundary updates to the on-disk `shallow` file
1294    // before classifying refs (so connectivity reflects the new graft set).
1295    if !opts.dry_run {
1296        crate::shallow::apply_shallow_updates(
1297            local_git_dir,
1298            &shallow_update.shallow,
1299            &shallow_update.unshallow,
1300        )?;
1301    }
1302
1303    // Close the write side once the v2 conversation is done so the server's
1304    // persistent `serve_loop` sees EOF and exits — even when we sent only an
1305    // `ls-refs` (no wants) and skipped `command=fetch`. Without this a streaming
1306    // transport (ssh subprocess, daemon socket) hangs at teardown. No-op for
1307    // v0/v1, where the server closes after its single response.
1308    if v2_caps.is_some() {
1309        conn.finish_send();
1310    }
1311
1312    // For TagMode::Following, prune tags whose target did not arrive in the
1313    // pack (now resolvable against the local odb, which holds the fetched
1314    // objects). All/None already handled; Following kept only when reachable.
1315    if opts.tags == crate::transfer::TagMode::Following {
1316        retain_following_tags(&local_odb, &mut matched, &matched_oids);
1317    }
1318
1319    // 5. Classify + apply ref updates (ancestry via the now-populated local repo).
1320    let local_repo = if opts.dry_run {
1321        None
1322    } else {
1323        crate::repo::Repository::open(local_git_dir, None).ok()
1324    };
1325
1326    let mut updates: Vec<RefUpdate> = Vec::new();
1327
1328    if opts.prune {
1329        prune_tracking_refs(
1330            local_git_dir,
1331            &positive,
1332            &remote_refs,
1333            opts.dry_run,
1334            &mut updates,
1335        )?;
1336    }
1337
1338    for m in &matched {
1339        let Some(local_ref) = &m.local_ref else {
1340            updates.push(RefUpdate {
1341                remote_ref: m.remote_ref.clone(),
1342                local_ref: None,
1343                old_oid: None,
1344                new_oid: Some(m.oid),
1345                mode: UpdateMode::NoChangeNeeded,
1346                note: Some("not stored (empty destination)".to_owned()),
1347            });
1348            continue;
1349        };
1350
1351        let old = crate::refs::resolve_ref(local_git_dir, local_ref).ok();
1352        let mode = classify_update(old.as_ref(), &m.oid, m.force, m.is_tag, local_repo.as_ref());
1353
1354        let write = matches!(
1355            mode,
1356            UpdateMode::New | UpdateMode::FastForward | UpdateMode::Forced
1357        );
1358        if write && !opts.dry_run {
1359            crate::refs::write_ref(local_git_dir, local_ref, &m.oid)?;
1360        }
1361
1362        updates.push(RefUpdate {
1363            remote_ref: m.remote_ref.clone(),
1364            local_ref: Some(local_ref.clone()),
1365            old_oid: old,
1366            new_oid: Some(m.oid),
1367            mode,
1368            note: None,
1369        });
1370    }
1371
1372    net_trace!(
1373        "fetch_remote: done — {} ref update(s){}",
1374        updates.len(),
1375        default_branch
1376            .as_deref()
1377            .map(|b| format!(", default branch '{b}'"))
1378            .unwrap_or_default()
1379    );
1380    Ok(FetchOutcome {
1381        updates,
1382        default_branch,
1383        new_shallow: shallow_update.shallow,
1384        new_unshallow: shallow_update.unshallow,
1385    })
1386}
1387
1388/// Add advertised tags to the matched set per [`crate::transfer::TagMode`].
1389///
1390/// Over the wire we cannot peel remote tags before the pack arrives, so:
1391/// * `All` adds every advertised tag (and `want`s it unconditionally).
1392/// * `Following` provisionally adds every advertised tag here; unreachable ones
1393///   are dropped by [`retain_following_tags`] after the pack is ingested.
1394/// * `None` adds nothing.
1395///
1396/// Returns the oids of tags added under `Following` — the caller must keep these
1397/// out of the `want` list so an unreachable tag does not drag its target into
1398/// the pack (which would make it look reachable and survive the prune).
1399fn add_wire_tags(
1400    mode: crate::transfer::TagMode,
1401    remote_refs: &[(String, ObjectId)],
1402    negatives: &[RefspecItem],
1403    matched: &mut Vec<crate::transfer::MatchedRef>,
1404    matched_oids: &mut HashSet<ObjectId>,
1405    seen_remote_ref: &mut HashSet<String>,
1406) -> HashSet<ObjectId> {
1407    let mut following_only: HashSet<ObjectId> = HashSet::new();
1408    if mode == crate::transfer::TagMode::None {
1409        return following_only;
1410    }
1411    for (name, oid) in remote_refs {
1412        if !name.starts_with("refs/tags/") {
1413            continue;
1414        }
1415        if seen_remote_ref.contains(name) || ref_excluded(name, negatives) {
1416            continue;
1417        }
1418        seen_remote_ref.insert(name.clone());
1419        matched_oids.insert(*oid);
1420        if mode == crate::transfer::TagMode::Following {
1421            following_only.insert(*oid);
1422        }
1423        matched.push(crate::transfer::MatchedRef {
1424            remote_ref: name.clone(),
1425            local_ref: Some(name.clone()),
1426            oid: *oid,
1427            force: false,
1428            is_tag: true,
1429        });
1430    }
1431    following_only
1432}
1433
1434/// Drop provisional `Following` tags whose object (or peeled target) did not
1435/// arrive in the fetched pack — i.e. is not reachable from the other matched,
1436/// non-tag refs we fetched. Matches `git fetch`'s default tag-following: a tag
1437/// is kept when it points into the fetched history.
1438fn retain_following_tags(
1439    local_odb: &crate::odb::Odb,
1440    matched: &mut Vec<crate::transfer::MatchedRef>,
1441    matched_oids: &HashSet<ObjectId>,
1442) {
1443    // Roots: every non-tag matched ref we fetched.
1444    let roots: Vec<ObjectId> = matched
1445        .iter()
1446        .filter(|m| !m.is_tag)
1447        .map(|m| m.oid)
1448        .collect();
1449    let closure = reachable_closure(local_odb, &roots);
1450    matched.retain(|m| {
1451        if !m.is_tag {
1452            return true;
1453        }
1454        let peeled = peel_tag_target(local_odb, m.oid);
1455        // Keep when the tag object itself or its peeled target is reachable from
1456        // the fetched heads, and we actually have the object locally.
1457        let have = local_odb.exists(&m.oid);
1458        have && (closure.contains(&m.oid)
1459            || closure.contains(&peeled)
1460            || matched_oids.contains(&peeled))
1461    });
1462}
1463
1464/// Peel an (annotated) tag to its ultimate non-tag target using the local odb.
1465fn peel_tag_target(odb: &crate::odb::Odb, oid: ObjectId) -> ObjectId {
1466    let mut current = oid;
1467    for _ in 0..16 {
1468        let Ok(obj) = odb.read(&current) else {
1469            return current;
1470        };
1471        if obj.kind != crate::objects::ObjectKind::Tag {
1472            return current;
1473        }
1474        match crate::objects::parse_tag(&obj.data) {
1475            Ok(t) => current = t.object,
1476            Err(_) => return current,
1477        }
1478    }
1479    current
1480}
1481
1482/// Compute the object closure reachable from `roots` (commits -> trees ->
1483/// blobs, peeling tags), using the local odb. Best-effort: descent stops at
1484/// missing objects.
1485fn reachable_closure(odb: &crate::odb::Odb, roots: &[ObjectId]) -> HashSet<ObjectId> {
1486    use crate::objects::{parse_commit, parse_tag, parse_tree, ObjectKind};
1487
1488    let mut seen: HashSet<ObjectId> = HashSet::new();
1489    let mut stack: Vec<ObjectId> = roots.to_vec();
1490    while let Some(oid) = stack.pop() {
1491        if !seen.insert(oid) {
1492            continue;
1493        }
1494        let Ok(obj) = odb.read(&oid) else {
1495            continue;
1496        };
1497        match obj.kind {
1498            ObjectKind::Commit => {
1499                if let Ok(c) = parse_commit(&obj.data) {
1500                    stack.push(c.tree);
1501                    for p in c.parents {
1502                        stack.push(p);
1503                    }
1504                }
1505            }
1506            ObjectKind::Tree => {
1507                if let Ok(entries) = parse_tree(&obj.data) {
1508                    for e in entries {
1509                        stack.push(e.oid);
1510                    }
1511                }
1512            }
1513            ObjectKind::Tag => {
1514                if let Ok(t) = parse_tag(&obj.data) {
1515                    stack.push(t.object);
1516                }
1517            }
1518            ObjectKind::Blob => {}
1519        }
1520    }
1521    seen
1522}