Skip to main content

grit_lib/
push.rs

1//! Wire-protocol push orchestration over a [`crate::transport::Connection`].
2//!
3//! [`push_remote`] is the wire counterpart to [`crate::transfer::push_local`]:
4//! instead of copying objects between two on-disk repositories, it drives a
5//! `git-receive-pack` exchange over a live [`crate::transport::Connection`] —
6//! reading the receive-pack advertisement (remote refs + `.have` lines +
7//! capabilities), deciding each ref update against the advertised remote refs
8//! (reusing the same fast-forward / force / force-with-lease rules as
9//! `push_local`), streaming the minimal pack with [`crate::pack_objects::write_pack_for_push`]
10//! (using the advertised remote tips + `.have`s as the negotiation `haves`),
11//! streaming it, and parsing the `report-status` / `report-status-v2` reply into
12//! per-ref [`crate::push_report::PushRefResult`]s.
13//!
14//! This is the send-pack flow lifted from the CLI's `commands/send_pack.rs`
15//! (`run`, `report_has_rejections`, `demux_report_and_remote_messages`),
16//! generalized to run over the [`crate::transport::Connection`] reader/writer
17//! rather than a spawned `receive-pack` subprocess.
18//!
19//! Protocol v0/v1 only in this phase (the classic receive-pack advertisement).
20//! A protocol-v2 push would require the `command=push` round and is deferred.
21//!
22//! The wire OID width is the repository's hash algorithm (threaded through
23//! [`crate::odb::Odb::hash_algo`]), so SHA-256 repositories push correctly: the
24//! zero/null OID, the empty-pack trailer, and the advertisement parsing are all
25//! hash-width aware.
26
27use std::collections::{HashMap, HashSet};
28use std::io::{Cursor, ErrorKind, Read, Write};
29use std::path::Path;
30
31use crate::branch_tracking;
32use crate::environment::RepositoryOptions;
33use crate::error::{Error, Result};
34use crate::fetch::Progress;
35use crate::objects::{HashAlgo, ObjectId};
36use crate::pack_objects::PackStats;
37use crate::pkt_line::{self, Packet};
38use crate::progress::{poll_cancel, Cancel, ProgressPhase, ProgressUnit, ReceivedObjects};
39use crate::push_report::{PushRefResult, PushRefStatus};
40use crate::repo::Repository;
41use crate::sideband_lines::{RemoteLine, SidebandLines};
42use crate::transfer::{PackBuildOptions, PrePushGate, PushOptions, PushOutcome, PushRefSpec};
43use crate::transport::Connection;
44
45fn finish_push_outcome(
46    local: &Repository,
47    opts: &PushOptions,
48    results: Vec<PushRefResult>,
49    remote_messages: Vec<String>,
50) -> Result<PushOutcome> {
51    if !opts.dry_run {
52        if let Some(remote) = opts.tracking_remote.as_deref() {
53            branch_tracking::apply_push_remote_tracking_updates_in(local, remote, &results)?;
54        }
55    }
56    Ok(PushOutcome {
57        results,
58        remote_messages,
59    })
60}
61
62/// Forwards side-band channel 2 to an inner [`Progress`] sink and records complete lines.
63struct PushSidebandCollector<'a> {
64    inner: &'a mut dyn Progress,
65    remote_messages: Vec<String>,
66    pending: SidebandLines,
67}
68
69impl PushSidebandCollector<'_> {
70    fn record_remote_chunk(&mut self, bytes: &[u8]) {
71        let mut lines = Vec::new();
72        self.pending.feed(bytes, |line| lines.push(line));
73        for line in lines {
74            self.record_remote_line(line);
75        }
76    }
77
78    fn record_remote_line(&mut self, line: RemoteLine) {
79        if let RemoteLine::Line(text) = line {
80            if !text.is_empty() {
81                self.remote_messages.push(text);
82            }
83        }
84    }
85
86    fn finish_pending(&mut self) {
87        let mut lines = Vec::new();
88        self.pending.finish(|line| lines.push(line));
89        for line in lines {
90            self.record_remote_line(line);
91        }
92    }
93}
94
95impl crate::progress::ProgressSink for PushSidebandCollector<'_> {
96    fn start(&mut self, phase: ProgressPhase, total: Option<u64>) {
97        self.inner.start(phase, total);
98    }
99
100    fn start_with_unit(&mut self, phase: ProgressPhase, unit: ProgressUnit, total: Option<u64>) {
101        self.inner.start_with_unit(phase, unit, total);
102    }
103
104    fn inc(&mut self, units: u64) {
105        self.inner.inc(units);
106    }
107
108    fn set(&mut self, current: u64) {
109        self.inner.set(current);
110    }
111
112    fn finish(&mut self) {
113        self.inner.finish();
114    }
115
116    fn should_cancel(&self) -> bool {
117        self.inner.should_cancel()
118    }
119}
120
121impl Progress for PushSidebandCollector<'_> {
122    fn message(&mut self, bytes: &[u8]) {
123        self.inner.message(bytes);
124        self.record_remote_chunk(bytes);
125    }
126
127    fn remote_sideband_progress(&self) -> bool {
128        self.inner.remote_sideband_progress()
129    }
130
131    fn wants_received_objects(&self) -> bool {
132        self.inner.wants_received_objects()
133    }
134
135    fn received_objects(&mut self, counts: ReceivedObjects) {
136        self.inner.received_objects(counts);
137    }
138}
139
140/// The receive-pack capabilities we negotiate, in the order Git's `send-pack`
141/// lists them. `report-status-v2` is requested alongside `report-status` so a
142/// modern server can reply with the richer per-ref report; `side-band-64k` lets
143/// the server multiplex the report (band 1) and hook/diagnostic output (band 2).
144const PUSH_CAPS_BASE: &str = "report-status report-status-v2 quiet";
145
146/// Push refs to a remote over a live [`Connection`] speaking `git-receive-pack`.
147///
148/// The flow mirrors [`crate::transfer::push_local`], but the remote ref list and
149/// `.have` hints come from the connection's advertisement, the objects are
150/// streamed over the wire as a single pack, and per-ref acceptance/rejection is
151/// learned from the server's `report-status` reply (a server may reject an update
152/// our local checks would have accepted, e.g. `denyNonFastForwards` or a
153/// pre-receive hook).
154///
155/// Steps:
156/// 1. Read the receive-pack advertisement from `conn`: remote refs (name -> oid),
157///    `.have` oids, and capabilities (`report-status(-v2)`, `side-band-64k`,
158///    `ofs-delta`, `object-format`, `atomic`).
159/// 2. Decide each [`PushRefSpec`] against the advertised remote refs (up-to-date,
160///    new, fast-forward, forced, non-fast-forward rejection, force-with-lease
161///    stale) — the client-side gate before anything is sent.
162/// 3. Write the ref-update commands for the accepted, value-changing updates
163///    (`<old> <new> <ref>\0<caps>\n` first, `<old> <new> <ref>\n` rest), then a
164///    flush.
165/// 4. Stream the minimal pack with [`crate::pack_objects::write_pack_for_push`]
166///    (wants = new tips, haves = advertised remote tips + `.have`s) straight into
167///    the connection; deletion-only pushes send no pack.
168/// 5. Read + parse `report-status` / `report-status-v2` (demultiplexing the
169///    side-band if negotiated) and fold the per-ref `ok`/`ng` lines back into the
170///    decided results.
171///
172/// `progress` receives the remote's side-band channel-2 bytes (hook output,
173/// `remote: …` diagnostics) when `side-band-64k` is negotiated.
174///
175/// Protocol v0/v1 only; a v2 connection is rejected.
176///
177/// The client-side `pre-push` hook is not run here; [`crate::remote::Remote::push`] runs it.
178///
179/// # Errors
180///
181/// Returns an error if the connection is protocol v2, if a source object is
182/// missing from the local odb, if the pack build fails, or on wire/parse I/O
183/// failure. An atomic push to a server without `atomic` support returns
184/// [`Error::PushAtomicUnsupported`] before sending updates or objects.
185pub fn push_remote(
186    local_git_dir: &Path,
187    conn: &mut dyn Connection,
188    refs: &[PushRefSpec],
189    opts: &PushOptions,
190    progress: &mut dyn Progress,
191    cancel: &dyn Cancel,
192) -> Result<PushOutcome> {
193    let local = Repository::open(local_git_dir, &RepositoryOptions::empty())?;
194    push_remote_from(&local, conn, refs, opts, progress, cancel, None)
195}
196
197/// Like [`push_remote`], reading the objects to send through `local`'s object store and
198/// running `pre_push` after every update is decided against the advertisement and before
199/// any command or pack is sent.
200pub(crate) fn push_remote_from(
201    local: &Repository,
202    conn: &mut dyn Connection,
203    refs: &[PushRefSpec],
204    opts: &PushOptions,
205    progress: &mut dyn Progress,
206    cancel: &dyn Cancel,
207    pre_push: Option<&PrePushGate<'_>>,
208) -> Result<PushOutcome> {
209    push_remote_with(local, conn, refs, opts, progress, cancel, None, pre_push)
210}
211
212/// A smart-HTTP remote a push may send a resumable bundle to, when its
213/// receive-pack advertises `pushBundle` (see [`crate::push_bundle`]).
214pub(crate) struct BundlePushTarget<'a> {
215    /// Client for the TUS requests.
216    pub(crate) client: &'a dyn crate::transport::http::HttpClient,
217    /// The repository URL the advertisement came from.
218    pub(crate) repo_url: &'a str,
219}
220
221/// [`push_remote_from`], optionally sending large packs as a resumable bundle.
222#[allow(clippy::too_many_arguments)]
223pub(crate) fn push_remote_with(
224    local: &Repository,
225    conn: &mut dyn Connection,
226    refs: &[PushRefSpec],
227    opts: &PushOptions,
228    progress: &mut dyn Progress,
229    cancel: &dyn Cancel,
230    bundle_target: Option<BundlePushTarget<'_>>,
231    pre_push: Option<&PrePushGate<'_>>,
232) -> Result<PushOutcome> {
233    crate::trace::trace_span!("grit.push");
234    let local_git_dir = local.git_dir();
235    let mut sideband = PushSidebandCollector {
236        inner: progress,
237        remote_messages: Vec::new(),
238        pending: SidebandLines::new(),
239    };
240    poll_cancel(&mut sideband, cancel)?;
241    let stop = crate::progress::CancelSignal::new(cancel);
242    use crate::net_trace::net_trace;
243    net_trace!(
244        opts.network_trace,
245        opts.diagnostics.as_ref(),
246        "push_remote: begin — {} ref update(s), protocol v{}, {} push-option(s)",
247        refs.len(),
248        conn.protocol_version(),
249        opts.push_options.len()
250    );
251    if conn.protocol_version() >= 2 {
252        return Err(Error::Message(
253            "push_remote: protocol v2 not supported in this phase (use v0/v1)".to_owned(),
254        ));
255    }
256
257    let algo = local.odb().hash_algo();
258
259    // 1. Advertisement: split the connection's parsed advertisement into the
260    //    remote ref map and the `.have` hints, and read the negotiated caps.
261    let adv = AdvertisedState::from_connection(conn);
262    net_trace!(
263        opts.network_trace,
264        opts.diagnostics.as_ref(),
265        "push_remote: remote advertised {} ref(s)",
266        adv.remote_refs.len()
267    );
268
269    // Required capabilities must be checked before even a no-op flush, so an
270    // unsupported atomic push cannot fall back to independent ref updates.
271    require_push_capabilities(&adv, opts)?;
272
273    // 2–3. Decide each ref update client-side, handling atomic/dry-run/no-op
274    //       early-returns; the shared planner mirrors `push_local`'s gate.
275    let planned = plan_push(refs, local, &adv, opts, pre_push);
276    if planned.is_err() && !opts.dry_run {
277        // End the command list so the server sees an empty push rather than a hang-up.
278        let _ = crate::transport::send_client_flush(conn);
279    }
280    let mut plan = match planned? {
281        PlanOutcome::Send(plan) => plan,
282        PlanOutcome::Done(results) => {
283            if !opts.dry_run {
284                crate::transport::send_client_flush(conn)?;
285            }
286            return finish_push_outcome(local, opts, results, Vec::new());
287        }
288    };
289
290    // 4. Write the ref-update commands (first carries the cap list after a NUL),
291    //    then the pack — but only when there are objects to send. A deletion-only
292    //    push streams no pack at all (matching `git send-pack`); `receive-pack`
293    //    does not read one after a delete-only command block, so sending an empty
294    //    pack would leave unread bytes on the wire and reset the connection.
295    let commands = build_command_block(&plan, &adv, algo, opts)?;
296    let sent_refs: Vec<&str> = plan
297        .to_send
298        .iter()
299        .map(|&i| plan.decisions[i].result.remote_ref.as_str())
300        .collect();
301
302    // A large pack goes to the server's resumable endpoint instead, as a bundle
303    // uploaded over TUS; an upload an earlier run left unfinished is resumed
304    // without building the pack again.
305    let bundle_route = match (&bundle_target, adv.push_bundle.as_deref()) {
306        (Some(target), Some(endpoint)) if !opts.dry_run => {
307            crate::push_bundle::resolve_endpoint(target.repo_url, endpoint)
308                .map(|endpoint| (target, endpoint))
309        }
310        _ => None,
311    };
312    let pack = PushPack::for_plan(&plan, local, &adv)?;
313    let mut spooled_pack = None;
314    if let Some((target, endpoint)) = &bundle_route {
315        let session =
316            crate::push_bundle::Session::locate(local_git_dir, target.repo_url, &commands);
317        let ready = match &pack {
318            _ if session.has_bundle() => true,
319            None => false,
320            Some(pack) => {
321                let threshold = opts
322                    .resumable_threshold
323                    .unwrap_or(crate::push_bundle::DEFAULT_THRESHOLD);
324                let tips: Vec<(ObjectId, String)> = plan
325                    .to_send
326                    .iter()
327                    .filter_map(|&i| {
328                        let d = &plan.decisions[i];
329                        Some((d.new_tip?, d.result.remote_ref.clone()))
330                    })
331                    .collect();
332                let mut haves: Vec<ObjectId> = adv.remote_refs.values().copied().collect();
333                haves.extend_from_slice(&adv.advertised_haves);
334                match session.write_bundle(local.odb(), &haves, &tips, threshold, |w| {
335                    pack.write_to(local, w, &stop).map(drop)
336                })? {
337                    crate::push_bundle::Spooled::Bundle => true,
338                    crate::push_bundle::Spooled::Small(small) => {
339                        spooled_pack = Some(small);
340                        false
341                    }
342                }
343            }
344        };
345        if ready {
346            let raw = crate::push_bundle::push(
347                target.client,
348                &session,
349                endpoint,
350                &commands,
351                &sent_refs,
352                adv.server_sideband,
353                &mut sideband,
354            )?;
355            let report = if adv.server_sideband {
356                demux_report_and_remote_messages(&raw, &mut sideband)?
357            } else {
358                raw
359            };
360            apply_report_status(&report, &mut plan.decisions);
361            let results: Vec<_> = plan.decisions.into_iter().map(|d| d.result).collect();
362            sideband.finish_pending();
363            return finish_push_outcome(local, opts, results, sideband.remote_messages);
364        }
365    }
366
367    net_trace!(
368        opts.network_trace,
369        opts.diagnostics.as_ref(),
370        "push_remote: sending {} command(s)…",
371        plan.decisions.len()
372    );
373    conn.writer().write_all(&commands)?;
374    conn.writer().flush()?;
375
376    if let Some(pack) = &pack {
377        poll_cancel(&mut sideband, cancel)?;
378        net_trace!(
379            opts.network_trace,
380            opts.diagnostics.as_ref(),
381            "push_remote: sending pack…"
382        );
383        // The pack streams straight into the connection; a push of a whole
384        // repository never holds the packfile in memory.
385        let mut out = std::io::BufWriter::with_capacity(PACK_SEND_BUFFER, conn.writer());
386        let sent = match spooled_pack.take() {
387            Some(spooled) => spooled.copy_to(&mut out)?,
388            None => pack.write_to(local, &mut out, &stop)?.pack_bytes,
389        };
390        out.flush()?;
391        drop(out);
392        conn.writer().flush()?;
393        net_trace!(
394            opts.network_trace,
395            opts.diagnostics.as_ref(),
396            "push_remote: sent pack ({sent} bytes)"
397        );
398    } else {
399        net_trace!(
400            opts.network_trace,
401            opts.diagnostics.as_ref(),
402            "push_remote: no pack (deletion-only / up-to-date)"
403        );
404    }
405
406    // 5. Read the server's report. With side-band, band 1 carries the
407    //    report-status pkt-lines and band 2/3 carry remote diagnostics; without
408    //    it the raw stream is the report-status itself.
409    //
410    // Unlike the v2 *fetch* path, we do NOT half-close the write side before
411    // reading: `git-receive-pack` has no persistent v2 serve loop — it consumes
412    // the command list and the (length-delimited) pack, writes the report, and
413    // exits. It is still reading its input while we read its report, so closing
414    // our write half early makes it see a premature EOF ("the remote end hung up
415    // unexpectedly") and abort without sending the report. The server closes its
416    // own output once the report is written; the write half is released when the
417    // connection is dropped (after the child has already exited, so the `Drop`
418    // teardown does not block).
419    //
420    // Some receive-pack hosts (notably Git-for-Windows `git daemon` on Windows)
421    // reset the TCP connection immediately after sending a complete report-status
422    // instead of shutting down cleanly. Treat that as success when the bytes we
423    // already received parse as a full report for every ref we sent.
424    conn.wait_for_response(cancel)?;
425    let raw = read_receive_pack_response(conn.reader(), &sent_refs, adv.server_sideband, cancel)?;
426    let report = if adv.server_sideband {
427        demux_report_and_remote_messages(&raw, &mut sideband)?
428    } else {
429        raw
430    };
431
432    apply_report_status(&report, &mut plan.decisions);
433
434    let results: Vec<_> = plan.decisions.into_iter().map(|d| d.result).collect();
435    sideband.finish_pending();
436    let remote_messages = sideband.remote_messages;
437    net_trace!(
438        opts.network_trace,
439        opts.diagnostics.as_ref(),
440        "push_remote: done — {} result(s)",
441        results.len()
442    );
443    finish_push_outcome(local, opts, results, remote_messages)
444}
445
446/// Push refs to a remote over smart HTTP (`git-receive-pack`), returning a
447/// [`PushOutcome`].
448///
449/// This is the stateless-RPC counterpart to [`push_remote`]: instead of a duplex
450/// [`Connection`] it issues a `GET info/refs?service=git-receive-pack` discovery
451/// (the receive-pack advertisement: remote refs + `.have`s + capabilities), then
452/// a single `POST git-receive-pack` whose body is the ref-update commands
453/// (`<old> <new> <ref>\0<caps>\n` first, bare after, flush) followed by the
454/// packfile; the response is the `report-status` / `report-status-v2`.
455///
456/// The decision logic, command framing, pack building (thin + delta + advertised
457/// `.have` set + `ofs-delta` per caps), and report parsing are the *same* shared
458/// helpers used by [`push_remote`] — only the wire (discovery GET + one POST vs.
459/// the duplex socket) differs.
460///
461/// `client` is the embedder's [`crate::transport::http::HttpClient`]; `repo_url`
462/// is the remote repository URL (e.g. `http://host/repo.git`). `progress`
463/// receives the server's side-band channel-2 bytes (hook output, `remote: …`
464/// diagnostics) when `side-band-64k` is negotiated.
465///
466/// Protocol v0/v1 only; a v2 receive-pack advertisement is rejected (a v2 push
467/// would require the `command=push` round and is deferred — matching
468/// [`push_remote`]).
469///
470/// # Errors
471///
472/// Returns an error if discovery fails, the advertisement is protocol v2, a
473/// source object is missing from the local odb, the pack build fails, or on
474/// wire/parse I/O failure. An atomic push to a server without `atomic` support
475/// returns [`Error::PushAtomicUnsupported`] before sending updates or objects.
476pub fn push_http(
477    client: std::sync::Arc<dyn crate::transport::http::HttpClient>,
478    local_git_dir: &Path,
479    repo_url: &str,
480    refs: &[PushRefSpec],
481    opts: &PushOptions,
482    progress: &mut dyn Progress,
483    cancel: &dyn Cancel,
484) -> Result<PushOutcome> {
485    use crate::transport::http::SmartHttpTransport;
486    use crate::transport::{ConnectOptions, Service, Transport};
487    let connect = ConnectOptions {
488        protocol_version: 0,
489        diagnostics: opts.diagnostics.clone(),
490        network_trace: opts.network_trace,
491        ..Default::default()
492    };
493    let transport = SmartHttpTransport::new(client);
494    let mut conn = transport.connect(repo_url, Service::ReceivePack, &connect)?;
495    push_remote(local_git_dir, &mut *conn, refs, opts, progress, cancel)
496}
497
498/// The remote ref map + `.have` hints + capability flags parsed from a
499/// receive-pack advertisement. Shared by the duplex ([`push_remote`]) and
500/// stateless-HTTP ([`push_http`]) paths so the decision/command/pack logic is
501/// identical regardless of how the advertisement was obtained.
502struct AdvertisedState {
503    /// Real remote refs (name -> oid), excluding the `.have` carrier lines.
504    remote_refs: HashMap<String, ObjectId>,
505    /// `.have` object hints (objects the remote holds but does not name a ref for).
506    advertised_haves: Vec<ObjectId>,
507    /// Whether the server advertised `side-band-64k`/`side-band` (report demuxing).
508    server_sideband: bool,
509    /// Whether the server advertised `ofs-delta` (offset-relative delta bases).
510    server_ofs_delta: bool,
511    /// Whether the server advertised `push-options` (server-side push options).
512    server_push_options: bool,
513    /// Whether the server advertised `atomic` (one transaction for all refs).
514    server_atomic: bool,
515    /// The resumable push endpoint from `pushBundle=<path>`, when advertised.
516    push_bundle: Option<String>,
517}
518
519impl AdvertisedState {
520    /// Build from a live [`Connection`]'s parsed advertisement. The `.have` lines
521    /// are recorded by the connection as refs literally named `.have`, so peel
522    /// those out here; everything else is a real remote ref.
523    fn from_connection(conn: &mut dyn Connection) -> Self {
524        let mut remote_refs: HashMap<String, ObjectId> = HashMap::new();
525        let mut advertised_haves: Vec<ObjectId> = Vec::new();
526        for (name, oid) in conn.advertised_refs() {
527            if name == ".have" {
528                advertised_haves.push(*oid);
529            } else {
530                remote_refs.insert(name.clone(), *oid);
531            }
532        }
533        let caps = conn.capabilities();
534        Self {
535            remote_refs,
536            advertised_haves,
537            server_sideband: caps
538                .iter()
539                .any(|c| c == "side-band-64k" || c == "side-band"),
540            server_ofs_delta: caps.iter().any(|c| c == "ofs-delta"),
541            server_push_options: caps.iter().any(|c| c == "push-options"),
542            server_atomic: caps.iter().any(|c| c == "atomic"),
543            push_bundle: caps.iter().find_map(|c| {
544                c.strip_prefix(crate::serve::PUSH_BUNDLE_CAPABILITY)?
545                    .strip_prefix('=')
546                    .map(str::to_owned)
547            }),
548        }
549    }
550}
551
552/// The accepted, value-changing updates a push will actually send, plus the full
553/// per-ref decision list (so client-rejected/up-to-date refs are still reported).
554struct PushPlan {
555    decisions: Vec<PushDecision>,
556    /// Indices into `decisions` of the updates to send a command for.
557    to_send: Vec<usize>,
558}
559
560/// Outcome of [`plan_push`]: either a [`PushPlan`] to send over the wire, or a
561/// terminal set of results (atomic abort, all up-to-date / client-rejected,
562/// dry-run) that needs no wire round.
563enum PlanOutcome {
564    Send(PushPlan),
565    Done(Vec<PushRefResult>),
566}
567
568/// Decide every [`PushRefSpec`] client-side against the advertised remote refs,
569/// applying the atomic / dry-run / nothing-to-send gates. Shared by
570/// [`push_remote`] and [`push_http`] so both paths reach the wire with an
571/// identical decision set.
572fn plan_push(
573    refs: &[PushRefSpec],
574    local: &Repository,
575    adv: &AdvertisedState,
576    opts: &PushOptions,
577    pre_push: Option<&PrePushGate<'_>>,
578) -> Result<PlanOutcome> {
579    let mut decisions: Vec<PushDecision> = Vec::with_capacity(refs.len());
580    for spec in refs {
581        decisions.push(decide_push_wire(spec, local, &adv.remote_refs)?);
582    }
583    if let Some(gate) = pre_push {
584        gate.check(refs.iter().zip(decisions.iter().map(|d| &d.result)))?;
585    }
586
587    // Atomic: a single client-side rejection aborts the whole push without
588    // sending anything; the otherwise-accepted updates become AtomicPushFailed.
589    let any_rejected = decisions.iter().any(|d| d.result.status.is_error());
590    if opts.atomic && any_rejected {
591        for d in &mut decisions {
592            if matches!(d.result.status, PushRefStatus::Ok) {
593                d.result.status = PushRefStatus::AtomicPushFailed;
594                d.send = false;
595            }
596        }
597        return Ok(PlanOutcome::Done(
598            decisions.into_iter().map(|d| d.result).collect(),
599        ));
600    }
601
602    let to_send: Vec<usize> = decisions
603        .iter()
604        .enumerate()
605        .filter_map(|(i, d)| if d.send { Some(i) } else { None })
606        .collect();
607
608    // Nothing to send (all up-to-date / client-rejected): no wire round needed.
609    if to_send.is_empty() || opts.dry_run {
610        return Ok(PlanOutcome::Done(
611            decisions.into_iter().map(|d| d.result).collect(),
612        ));
613    }
614
615    Ok(PlanOutcome::Send(PushPlan { decisions, to_send }))
616}
617
618/// Validate capabilities required by the requested push options before writing
619/// any commands or pack data, including for dry-run and no-op pushes.
620fn require_push_capabilities(adv: &AdvertisedState, opts: &PushOptions) -> Result<()> {
621    if opts.atomic && !adv.server_atomic {
622        return Err(Error::PushAtomicUnsupported);
623    }
624    if !opts.push_options.is_empty() && !adv.server_push_options {
625        return Err(Error::PushOptionsUnsupported);
626    }
627    Ok(())
628}
629
630/// Build the ref-update command block: one pkt-line per accepted update plus a
631/// trailing flush. The first command carries the negotiated capability list
632/// after a NUL; the rest are bare. The OID width is the repository's hash
633/// algorithm (zero/null OID for create/delete). Shared by both push paths.
634///
635/// Atomic updates request `atomic` only when the server supports it. The caller
636/// must validate required capabilities with [`require_push_capabilities`] first.
637///
638/// When `opts.push_options` is non-empty, the negotiated capability list includes
639/// `push-options` and, after the command-list flush, one `push-option <value>`
640/// pkt-line per option is written followed by a second flush (matching Git's
641/// `send-pack`: command-list, flush, push-option lines, flush, then pack). The
642/// caller must have already verified the server advertised `push-options`.
643fn build_command_block(
644    plan: &PushPlan,
645    adv: &AdvertisedState,
646    algo: HashAlgo,
647    opts: &PushOptions,
648) -> Result<Vec<u8>> {
649    let zero_hex = "0".repeat(algo.hex_len());
650    let mut command_caps = PUSH_CAPS_BASE.to_owned();
651    if adv.server_sideband {
652        command_caps.push_str(" side-band-64k");
653    }
654    if !opts.push_options.is_empty() {
655        command_caps.push_str(" push-options");
656    }
657    if opts.atomic && adv.server_atomic {
658        command_caps.push_str(" atomic");
659    }
660    command_caps.push_str(&format!(" object-format={}", algo.name()));
661
662    let mut commands: Vec<u8> = Vec::new();
663    let mut first = true;
664    for &i in &plan.to_send {
665        let d = &plan.decisions[i];
666        let old_hex = d
667            .result
668            .old_oid
669            .map(|o| o.to_hex())
670            .unwrap_or_else(|| zero_hex.clone());
671        let new_hex = d
672            .result
673            .new_oid
674            .map(|o| o.to_hex())
675            .unwrap_or_else(|| zero_hex.clone());
676        // `write_line_to_vec` appends the pkt-line's trailing newline itself, so
677        // the command payload must NOT carry one of its own; otherwise the bare
678        // (second and later) command lines would frame as `<old> <new> <ref>\n`
679        // and `git-receive-pack` would read a refname with an embedded newline
680        // ("funny refname"). The first line's capability list rides the NUL.
681        let line = if first {
682            first = false;
683            format!(
684                "{old_hex} {new_hex} {}\0{command_caps}",
685                d.result.remote_ref
686            )
687        } else {
688            format!("{old_hex} {new_hex} {}", d.result.remote_ref)
689        };
690        pkt_line::write_line_to_vec(&mut commands, &line)?;
691    }
692    // Flush terminates the command list. When push-options are negotiated, the
693    // option lines follow this flush and are themselves terminated by a second
694    // flush before the pack (per the receive-pack protocol).
695    commands.extend_from_slice(b"0000");
696    if !opts.push_options.is_empty() {
697        for opt in &opts.push_options {
698            pkt_line::write_line_to_vec(&mut commands, opt)?;
699        }
700        commands.extend_from_slice(b"0000");
701    }
702    Ok(commands)
703}
704
705/// Buffer between the pack writer and the connection, so a duplex transport
706/// sees large writes rather than one per pack entry header.
707const PACK_SEND_BUFFER: usize = 64 * 1024;
708
709/// The pack a push sends: a thin, delta-compressed pack of the new tips minus
710/// everything the remote already advertised (its ref tips + `.have`s). Shared by
711/// both push paths so the wire bytes are identical regardless of transport.
712struct PushPack {
713    wants: Vec<ObjectId>,
714    haves: Vec<ObjectId>,
715    opts: PackBuildOptions,
716}
717
718impl PushPack {
719    /// The pack for `plan`, or `None` when there is nothing to pack — i.e. a
720    /// deletion-only push.
721    ///
722    /// Matching `git send-pack` (the pack is written only for non-delete
723    /// updates), no packfile — not even an empty one — is streamed for a pure
724    /// deletion. `git-receive-pack` does not read a pack after a delete-only
725    /// command block, so sending one leaves unread bytes on the wire and trips a
726    /// `ConnectionReset` on the streaming (daemon/ssh) transports.
727    fn for_plan(
728        plan: &PushPlan,
729        local: &Repository,
730        adv: &AdvertisedState,
731    ) -> Result<Option<Self>> {
732        let wants: Vec<ObjectId> = plan
733            .to_send
734            .iter()
735            .filter_map(|&i| plan.decisions[i].new_tip)
736            .collect();
737        if wants.is_empty() {
738            return Ok(None);
739        }
740        let haves = local.odb().present_oids(
741            adv.remote_refs
742                .values()
743                .chain(&adv.advertised_haves)
744                .copied(),
745        )?;
746        let local_cfg = crate::config::ConfigSet::load(
747            &crate::environment::Environment::empty(),
748            Some(local.git_dir()),
749            true,
750        )
751        .ok();
752        let mut opts = if haves.is_empty() {
753            PackBuildOptions::for_local_copy(local_cfg.as_ref())
754        } else {
755            PackBuildOptions::for_local_push(local_cfg.as_ref())
756        };
757        opts.use_ofs_delta = adv.server_ofs_delta;
758        Ok(Some(Self { wants, haves, opts }))
759    }
760
761    /// Stream the pack to `w`, stopping with [`Error::Cancelled`] once `stop` fires.
762    fn write_to(
763        &self,
764        local: &Repository,
765        w: &mut dyn Write,
766        stop: &crate::progress::CancelSignal<'_>,
767    ) -> Result<PackStats> {
768        crate::pack_objects::write_pack_for_push_with_cancel(
769            local.odb(),
770            &self.wants,
771            &self.haves,
772            &self.opts,
773            w,
774            stop,
775        )
776    }
777}
778
779/// A client-side push decision for one ref, plus what to send over the wire.
780struct PushDecision {
781    result: PushRefResult,
782    /// The new tip object to pack (None for deletions / no-ops).
783    new_tip: Option<ObjectId>,
784    /// Whether to send a ref-update command for this ref to the server.
785    send: bool,
786}
787
788/// Decide one [`PushRefSpec`] against the advertised remote refs, without any
789/// I/O to the remote. Mirrors [`crate::transfer`]'s `decide_push`, but the
790/// "remote current" value comes from the advertisement map rather than an
791/// on-disk remote ref.
792fn decide_push_wire(
793    spec: &PushRefSpec,
794    local: &Repository,
795    remote_refs: &HashMap<String, ObjectId>,
796) -> Result<PushDecision> {
797    let local_odb = local.odb();
798    let remote_current = remote_refs.get(&spec.dst).copied();
799
800    let no_op = |status: PushRefStatus,
801                 old: Option<ObjectId>,
802                 new: Option<ObjectId>,
803                 deletion: bool,
804                 message: Option<String>| {
805        PushDecision {
806            result: PushRefResult {
807                local_ref: crate::transfer::push_spec_local_ref(spec),
808                remote_ref: spec.dst.clone(),
809                old_oid: old,
810                new_oid: new,
811                forced: false,
812                deletion,
813                status,
814                message,
815            },
816            new_tip: None,
817            send: false,
818        }
819    };
820
821    // Up-to-date trumps every lease (creating/moving a ref to where it already
822    // is succeeds, even when a force-with-lease expectation does not hold).
823    if !spec.delete {
824        if let Some(src) = spec.src {
825            if remote_current == Some(src) {
826                return Ok(no_op(
827                    PushRefStatus::UpToDate,
828                    remote_current,
829                    Some(src),
830                    false,
831                    None,
832                ));
833            }
834        }
835    }
836
837    // Absence lease: a destination that already exists fails the lease.
838    if spec.expect_absent && remote_current.is_some() {
839        return Ok(no_op(
840            PushRefStatus::RejectStale,
841            remote_current,
842            spec.src,
843            spec.delete,
844            Some("stale info".to_owned()),
845        ));
846    }
847
848    // Compare-and-swap (force-with-lease): the remote's current value must match.
849    if let Some(expected) = spec.expected_old {
850        if remote_current != Some(expected) {
851            return Ok(no_op(
852                PushRefStatus::RejectStale,
853                remote_current,
854                spec.src,
855                spec.delete,
856                Some("stale info".to_owned()),
857            ));
858        }
859    }
860
861    if spec.delete {
862        return Ok(match remote_current {
863            Some(_) => PushDecision {
864                result: PushRefResult {
865                    local_ref: crate::transfer::push_spec_local_ref(spec),
866                    remote_ref: spec.dst.clone(),
867                    old_oid: remote_current,
868                    new_oid: None,
869                    forced: false,
870                    deletion: true,
871                    status: PushRefStatus::Ok,
872                    message: None,
873                },
874                new_tip: None,
875                send: true,
876            },
877            None => no_op(
878                PushRefStatus::RejectRemoteRefMissing,
879                None,
880                None,
881                true,
882                Some("remote ref does not exist".to_owned()),
883            ),
884        });
885    }
886
887    let Some(src) = spec.src else {
888        return Err(Error::Message(format!(
889            "push to '{}' has no source object and is not a deletion",
890            spec.dst
891        )));
892    };
893    if !local_odb.contains(&src)? {
894        return Err(Error::Message(format!(
895            "source object {src} for '{}' is missing from the local object store",
896            spec.dst
897        )));
898    }
899
900    // New ref: nothing on the remote yet — always allowed.
901    let Some(old) = remote_current else {
902        return Ok(PushDecision {
903            result: PushRefResult {
904                local_ref: crate::transfer::push_spec_local_ref(spec),
905                remote_ref: spec.dst.clone(),
906                old_oid: None,
907                new_oid: Some(src),
908                forced: false,
909                deletion: false,
910                status: PushRefStatus::Ok,
911                message: None,
912            },
913            new_tip: Some(src),
914            send: true,
915        });
916    };
917
918    if !spec.force && !local_odb.contains(&old)? {
919        return Ok(no_op(
920            PushRefStatus::RejectFetchFirst,
921            Some(old),
922            Some(src),
923            false,
924            Some("fetch first".to_owned()),
925        ));
926    }
927
928    // Existing ref: fast-forward when the remote's current commit is an ancestor
929    // of the source; otherwise non-fast-forward (allowed only with force).
930    let is_ff = if spec.force {
931        false
932    } else if crate::merge_base::is_ancestor_odb_walk_with_graph(
933        local_odb,
934        local_odb.commit_graph_chain().as_deref(),
935        old,
936        src,
937    )? {
938        true
939    } else {
940        crate::merge_base::is_ancestor(local, old, src).unwrap_or(false)
941    };
942
943    if is_ff {
944        Ok(PushDecision {
945            result: PushRefResult {
946                local_ref: crate::transfer::push_spec_local_ref(spec),
947                remote_ref: spec.dst.clone(),
948                old_oid: Some(old),
949                new_oid: Some(src),
950                forced: false,
951                deletion: false,
952                status: PushRefStatus::Ok,
953                message: None,
954            },
955            new_tip: Some(src),
956            send: true,
957        })
958    } else if spec.force {
959        Ok(PushDecision {
960            result: PushRefResult {
961                local_ref: crate::transfer::push_spec_local_ref(spec),
962                remote_ref: spec.dst.clone(),
963                old_oid: Some(old),
964                new_oid: Some(src),
965                forced: true,
966                deletion: false,
967                status: PushRefStatus::Ok,
968                message: None,
969            },
970            new_tip: Some(src),
971            send: true,
972        })
973    } else {
974        Ok(PushDecision {
975            result: PushRefResult {
976                local_ref: crate::transfer::push_spec_local_ref(spec),
977                remote_ref: spec.dst.clone(),
978                old_oid: Some(old),
979                new_oid: Some(src),
980                forced: false,
981                deletion: false,
982                status: PushRefStatus::RejectNonFastForward,
983                message: Some("non-fast-forward".to_owned()),
984            },
985            new_tip: None,
986            send: false,
987        })
988    }
989}
990
991/// Read the receive-pack report stream until EOF or an abrupt connection close.
992///
993/// When the server resets the connection after sending a complete
994/// `report-status`, the accumulated bytes are returned as success so tracking
995/// refs can be updated. Incomplete buffers still surface as I/O errors.
996///
997/// `cancel` is checked before each read; it stops the read with
998/// [`Error::Cancelled`] only while the report is still incomplete.
999pub(crate) fn read_receive_pack_response(
1000    reader: &mut dyn Read,
1001    sent_refs: &[&str],
1002    server_sideband: bool,
1003    cancel: &dyn Cancel,
1004) -> Result<Vec<u8>> {
1005    let mut raw = Vec::new();
1006    let mut buf = [0u8; 8192];
1007    loop {
1008        if cancel.is_cancelled() {
1009            if receive_pack_response_complete(&raw, sent_refs, server_sideband)? {
1010                break;
1011            }
1012            return Err(Error::Cancelled);
1013        }
1014        match reader.read(&mut buf) {
1015            Ok(0) => break,
1016            Ok(n) => raw.extend_from_slice(&buf[..n]),
1017            Err(e) if receive_pack_read_recoverable(&e) => {
1018                if receive_pack_response_complete(&raw, sent_refs, server_sideband)? {
1019                    break;
1020                }
1021                return Err(Error::Io(e));
1022            }
1023            Err(e) => return Err(Error::Io(e)),
1024        }
1025    }
1026    if !receive_pack_response_complete(&raw, sent_refs, server_sideband)? {
1027        return Err(Error::Message(
1028            "receive-pack response ended before a complete report-status".to_owned(),
1029        ));
1030    }
1031    Ok(raw)
1032}
1033
1034fn receive_pack_read_recoverable(e: &std::io::Error) -> bool {
1035    matches!(
1036        e.kind(),
1037        ErrorKind::ConnectionReset | ErrorKind::BrokenPipe | ErrorKind::UnexpectedEof
1038    )
1039}
1040
1041/// Whether `raw` (optionally side-band wrapped) contains a full report-status
1042/// for every ref named in `sent_refs`.
1043fn receive_pack_response_complete(
1044    raw: &[u8],
1045    sent_refs: &[&str],
1046    server_sideband: bool,
1047) -> Result<bool> {
1048    let report = if server_sideband {
1049        demux_sideband_report(raw, None)?
1050    } else {
1051        raw.to_vec()
1052    };
1053    Ok(report_lines_complete(&report, sent_refs))
1054}
1055
1056/// True when `report` includes an `unpack …` line and an `ok`/`ng` line for
1057/// every ref the client sent on the wire.
1058fn report_lines_complete(report: &[u8], sent_refs: &[&str]) -> bool {
1059    let mut saw_unpack = false;
1060    let mut per_ref = HashSet::<String>::new();
1061    let mut cursor = Cursor::new(report);
1062    while let Ok(Some(pkt)) = pkt_line::read_packet(&mut cursor) {
1063        let Packet::Data(line) = pkt else {
1064            continue;
1065        };
1066        let line = line.trim_end();
1067        if line.starts_with("unpack ") {
1068            saw_unpack = true;
1069        } else if let Some(refname) = line.strip_prefix("ok ") {
1070            per_ref.insert(refname.trim().to_owned());
1071        } else if let Some(rest) = line.strip_prefix("ng ") {
1072            let (refname, _) = rest.split_once(' ').unwrap_or((rest, ""));
1073            per_ref.insert(refname.trim().to_owned());
1074        }
1075    }
1076    saw_unpack && sent_refs.iter().all(|r| per_ref.contains(*r))
1077}
1078
1079/// Parse the server's `report-status` / `report-status-v2` stream and fold each
1080/// per-ref `ok`/`ng` line back into the matching decision.
1081///
1082/// The report is:
1083/// ```text
1084/// unpack ok\n            (or `unpack <error>\n`)
1085/// ok <ref>\n             (per accepted ref)
1086/// ng <ref> <reason>\n    (per rejected ref)
1087/// ```
1088/// An `ng` line demotes the decided result to [`PushRefStatus::RemoteRejected`]
1089/// with the server's reason; an `unpack` failure demotes every sent ref. Lifted
1090/// from the CLI's `report_has_rejections`, extended to capture the reason and the
1091/// `unpack` status.
1092fn apply_report_status(report: &[u8], decisions: &mut [PushDecision]) {
1093    let mut by_ref: HashMap<&str, usize> = HashMap::new();
1094    for (i, d) in decisions.iter().enumerate() {
1095        if d.send {
1096            by_ref.insert(d.result.remote_ref.as_str(), i);
1097        }
1098    }
1099    // Resolve indices up front to avoid borrow conflicts while mutating.
1100    let mut unpack_error: Option<String> = None;
1101    let mut updates: Vec<(usize, Option<String>)> = Vec::new();
1102
1103    let mut cursor = Cursor::new(report);
1104    while let Ok(Some(pkt)) = pkt_line::read_packet(&mut cursor) {
1105        let Packet::Data(line) = pkt else {
1106            continue;
1107        };
1108        let line = line.trim_end();
1109        if let Some(rest) = line.strip_prefix("unpack ") {
1110            if rest.trim() != "ok" {
1111                unpack_error = Some(rest.trim().to_owned());
1112            }
1113        } else if let Some(refname) = line.strip_prefix("ok ") {
1114            // Accepted: keep the decided (Ok/UpToDate) status.
1115            let _ = by_ref.get(refname.trim());
1116        } else if let Some(rest) = line.strip_prefix("ng ") {
1117            // `ng <ref> <reason>`: the remote declined this update.
1118            let (refname, reason) = rest.split_once(' ').unwrap_or((rest, ""));
1119            if let Some(&idx) = by_ref.get(refname.trim()) {
1120                let msg = if reason.trim().is_empty() {
1121                    None
1122                } else {
1123                    Some(reason.trim().to_owned())
1124                };
1125                updates.push((idx, msg));
1126            }
1127        }
1128    }
1129
1130    for (idx, msg) in updates {
1131        decisions[idx].result.status = PushRefStatus::RemoteRejected;
1132        decisions[idx].result.message = msg;
1133    }
1134
1135    // A failed `unpack` rejects every ref we sent that the server did not
1136    // already mark as failed.
1137    if let Some(reason) = unpack_error {
1138        for d in decisions.iter_mut() {
1139            if d.send && !matches!(d.result.status, PushRefStatus::RemoteRejected) {
1140                d.result.status = PushRefStatus::RemoteRejected;
1141                d.result.message = Some(format!("unpack failed: {reason}"));
1142            }
1143        }
1144    }
1145}
1146
1147/// Extract band 1 (report-status) from a side-band receive-pack response.
1148///
1149/// Bands 2 and 3 are optionally forwarded to `progress`; pass `None` for
1150/// side-effect-free completeness checks.
1151fn demux_sideband_report(input: &[u8], mut progress: Option<&mut dyn Progress>) -> Result<Vec<u8>> {
1152    let mut report = Vec::new();
1153    let mut i = 0usize;
1154    while i + 4 <= input.len() {
1155        let len = match pkt_line::parse_hex_len(&input[i..i + 4]) {
1156            Ok(l) => l,
1157            Err(_) => break,
1158        };
1159        i += 4;
1160        if len == 0 {
1161            // Flush packet: a delimiter between report sections, keep scanning.
1162            continue;
1163        }
1164        if len < 4 || i + (len - 4) > input.len() {
1165            break;
1166        }
1167        let payload = &input[i..i + (len - 4)];
1168        i += len - 4;
1169        if payload.is_empty() {
1170            continue;
1171        }
1172        let band = payload[0];
1173        let data = &payload[1..];
1174        match band {
1175            1 => report.extend_from_slice(data),
1176            2 | 3 => {
1177                if let Some(p) = progress.as_deref_mut() {
1178                    p.message(data);
1179                }
1180            }
1181            _ => {}
1182        }
1183    }
1184    Ok(report)
1185}
1186
1187/// Split a side-band stream: band 1 (report-status) is returned; band 2/3
1188/// (remote diagnostics) is forwarded to `progress`. Lifted from the CLI's
1189/// `demux_report_and_remote_messages`, but progress goes to the callback rather
1190/// than directly to stderr (the public API must not assume stdout/stderr).
1191fn demux_report_and_remote_messages(input: &[u8], progress: &mut dyn Progress) -> Result<Vec<u8>> {
1192    demux_sideband_report(input, Some(progress))
1193}
1194
1195#[cfg(test)]
1196mod tests {
1197    use super::*;
1198    use crate::objects::ObjectKind;
1199
1200    fn make_decision(refname: &str, send: bool) -> PushDecision {
1201        PushDecision {
1202            result: PushRefResult {
1203                local_ref: None,
1204                remote_ref: refname.to_owned(),
1205                old_oid: None,
1206                new_oid: None,
1207                forced: false,
1208                deletion: false,
1209                status: PushRefStatus::Ok,
1210                message: None,
1211            },
1212            new_tip: None,
1213            send,
1214        }
1215    }
1216
1217    fn report_bytes(lines: &[&str]) -> Vec<u8> {
1218        let mut buf = Vec::new();
1219        for l in lines {
1220            pkt_line::write_line_to_vec(&mut buf, l).unwrap();
1221        }
1222        buf.extend_from_slice(b"0000");
1223        buf
1224    }
1225
1226    fn adv_state(sideband: bool, ofs_delta: bool, push_options: bool) -> AdvertisedState {
1227        AdvertisedState {
1228            remote_refs: HashMap::new(),
1229            advertised_haves: Vec::new(),
1230            server_sideband: sideband,
1231            server_ofs_delta: ofs_delta,
1232            server_push_options: push_options,
1233            server_atomic: false,
1234            push_bundle: None,
1235        }
1236    }
1237
1238    #[derive(Default)]
1239    struct TestConnection {
1240        caps: Vec<String>,
1241        refs: Vec<(String, ObjectId)>,
1242        input: Cursor<Vec<u8>>,
1243        output: Vec<u8>,
1244        stateless: bool,
1245    }
1246
1247    impl Connection for TestConnection {
1248        fn reader(&mut self) -> &mut dyn Read {
1249            &mut self.input
1250        }
1251        fn writer(&mut self) -> &mut dyn std::io::Write {
1252            &mut self.output
1253        }
1254        fn advertised_refs(&self) -> &[(String, ObjectId)] {
1255            &self.refs
1256        }
1257        fn capabilities(&self) -> &[String] {
1258            &self.caps
1259        }
1260        fn head_symref(&self) -> Option<&str> {
1261            None
1262        }
1263        fn protocol_version(&self) -> u8 {
1264            0
1265        }
1266        fn stateless_rpc(&self) -> bool {
1267            self.stateless
1268        }
1269    }
1270
1271    #[test]
1272    fn advertisement_recognizes_atomic_capability_token() {
1273        for caps in [
1274            vec![],
1275            vec!["atomic"],
1276            vec!["atomic=false"],
1277            vec!["atomic", "side-band-64k", "ofs-delta", "push-options"],
1278        ] {
1279            let mut conn = TestConnection {
1280                caps: caps.iter().map(|s| (*s).to_owned()).collect(),
1281                ..Default::default()
1282            };
1283            let adv = AdvertisedState::from_connection(&mut conn);
1284            assert_eq!(adv.server_atomic, caps.contains(&"atomic"));
1285            assert_eq!(adv.server_sideband, caps.contains(&"side-band-64k"));
1286            assert_eq!(adv.server_ofs_delta, caps.contains(&"ofs-delta"));
1287            assert_eq!(adv.server_push_options, caps.contains(&"push-options"));
1288        }
1289    }
1290
1291    #[test]
1292    fn command_block_requests_atomic_only_on_first_command_when_supported_and_requested() {
1293        let new = ObjectId::from_hex(&"1".repeat(40)).unwrap();
1294        let plan = PushPlan {
1295            decisions: vec![
1296                send_decision("refs/heads/one", new),
1297                send_decision("refs/heads/two", new),
1298            ],
1299            to_send: vec![0, 1],
1300        };
1301        for supported in [false, true] {
1302            for requested in [false, true] {
1303                let mut adv = adv_state(true, true, true);
1304                adv.server_atomic = supported;
1305                let opts = PushOptions {
1306                    atomic: requested,
1307                    push_options: vec!["ci.skip".to_owned()],
1308                    ..Default::default()
1309                };
1310                let block = build_command_block(&plan, &adv, HashAlgo::Sha1, &opts).unwrap();
1311                let pkts = decode_block(&block);
1312                let (command, caps) = pkts[0].as_deref().unwrap().split_once('\0').unwrap();
1313                assert_eq!(
1314                    command,
1315                    format!("{} {} refs/heads/one", "0".repeat(40), new)
1316                );
1317                let caps: Vec<_> = caps.split_whitespace().collect();
1318                assert_eq!(caps.contains(&"atomic"), supported && requested);
1319                assert!(caps.contains(&"push-options"));
1320                assert!(caps.contains(&"side-band-64k"));
1321                assert!(caps.contains(&"object-format=sha1"));
1322                assert_eq!(
1323                    pkts[1].as_deref().unwrap(),
1324                    format!("{} {} refs/heads/two", "0".repeat(40), new)
1325                );
1326                assert_eq!(&pkts[2..], &[None, Some("ci.skip".to_owned()), None]);
1327            }
1328        }
1329    }
1330
1331    #[test]
1332    fn unsupported_atomic_push_fails_before_any_update_or_pack_bytes() {
1333        let dir = tempfile::tempdir().unwrap();
1334        let repo = Repository::init(dir.path(), &crate::repo::InitOptions::default()).unwrap();
1335        let refs = [PushRefSpec {
1336            src: Some(repo.odb().write(ObjectKind::Blob, b"payload").unwrap()),
1337            dst: "refs/tags/payload".to_owned(),
1338            ..Default::default()
1339        }];
1340        for stateless in [false, true] {
1341            for dry_run in [false, true] {
1342                // Also check no-op pushes: their flush must not precede the error.
1343                for refs in [&[][..], &refs[..]] {
1344                    let mut conn = TestConnection {
1345                        stateless,
1346                        ..Default::default()
1347                    };
1348                    let opts = PushOptions {
1349                        atomic: true,
1350                        dry_run,
1351                        ..Default::default()
1352                    };
1353                    let err = push_remote(
1354                        repo.git_dir(),
1355                        &mut conn,
1356                        refs,
1357                        &opts,
1358                        &mut crate::fetch::NoProgress,
1359                        &crate::progress::NeverCancel,
1360                    )
1361                    .unwrap_err();
1362                    assert!(matches!(err, Error::PushAtomicUnsupported), "{err:?}");
1363                    assert!(
1364                        conn.output.is_empty(),
1365                        "unsupported atomic push wrote bytes"
1366                    );
1367                    let mut adv = adv_state(false, false, false);
1368                    adv.server_atomic = true;
1369                    require_push_capabilities(&adv, &opts).unwrap();
1370                }
1371            }
1372        }
1373    }
1374
1375    /// Decode a command block into a flat list of packets: each `Data(line)`
1376    /// becomes `Some(line)` and each flush becomes `None`, so the framing
1377    /// (command lines / flush / push-option lines / flush) is fully visible.
1378    fn decode_block(block: &[u8]) -> Vec<Option<String>> {
1379        let mut cur = Cursor::new(block);
1380        let mut out = Vec::new();
1381        while let Ok(pkt) = pkt_line::read_packet(&mut cur) {
1382            match pkt {
1383                Some(Packet::Data(s)) => out.push(Some(s.trim_end_matches('\n').to_owned())),
1384                Some(Packet::Flush) => out.push(None),
1385                _ => break,
1386            }
1387        }
1388        out
1389    }
1390
1391    fn push_pack_bytes(
1392        plan: &PushPlan,
1393        repo: &Repository,
1394        adv: &AdvertisedState,
1395    ) -> Result<Option<Vec<u8>>> {
1396        PushPack::for_plan(plan, repo, adv)?
1397            .map(|pack| {
1398                let mut bytes = Vec::new();
1399                pack.write_to(repo, &mut bytes, &crate::progress::CancelSignal::never())?;
1400                Ok(bytes)
1401            })
1402            .transpose()
1403    }
1404
1405    fn flaky_push_repo(
1406        dir: &Path,
1407    ) -> (
1408        Repository,
1409        std::sync::Arc<crate::odb::store::flaky::FlakyStore>,
1410    ) {
1411        let flaky = crate::odb::store::flaky::FlakyStore::new();
1412        let git_dir = dir.join("repo.git");
1413        let repo = crate::repo::RepositoryBuilder::new(RepositoryOptions::empty())
1414            .odb_builder(
1415                crate::odb::OdbBuilder::files(git_dir.join("objects"))
1416                    .primary(flaky.clone())
1417                    .alternates(false),
1418            )
1419            .ref_store(std::sync::Arc::new(
1420                crate::refs::store::MemoryRefStore::new(),
1421            ))
1422            .open(&git_dir, None)
1423            .unwrap();
1424        (repo, flaky)
1425    }
1426
1427    #[test]
1428    fn push_pack_haves_fail_on_backend_error_instead_of_being_dropped() {
1429        let dir = tempfile::tempdir().unwrap();
1430        let (repo, flaky) = flaky_push_repo(dir.path());
1431        let odb = repo.odb();
1432        let blob = odb.write(ObjectKind::Blob, b"to send").unwrap();
1433        let remote_tip = odb.write(ObjectKind::Blob, b"remote has this").unwrap();
1434        let plan = PushPlan {
1435            decisions: vec![send_decision("refs/heads/main", blob)],
1436            to_send: vec![0],
1437        };
1438        let mut adv = adv_state(false, true, false);
1439        adv.remote_refs
1440            .insert("refs/heads/main".to_owned(), remote_tip);
1441        assert!(push_pack_bytes(&plan, &repo, &adv).unwrap().is_some());
1442
1443        flaky.fail_contains(true);
1444        let err = push_pack_bytes(&plan, &repo, &adv).unwrap_err();
1445        assert!(crate::odb::store::flaky::is_injected(&err), "{err:?}");
1446    }
1447
1448    #[test]
1449    fn push_source_check_fails_on_backend_error_instead_of_missing() {
1450        let dir = tempfile::tempdir().unwrap();
1451        let (repo, flaky) = flaky_push_repo(dir.path());
1452        let src = repo.odb().write(ObjectKind::Blob, b"source").unwrap();
1453        let spec = PushRefSpec {
1454            src: Some(src),
1455            src_ref: None,
1456            dst: "refs/heads/main".to_owned(),
1457            force: false,
1458            delete: false,
1459            expected_old: None,
1460            expect_absent: false,
1461            hook_src_ref: None,
1462        };
1463        let decision = decide_push_wire(&spec, &repo, &HashMap::new()).unwrap();
1464        assert_eq!(decision.new_tip, Some(src));
1465
1466        flaky.fail_contains(true);
1467        let err = decide_push_wire(&spec, &repo, &HashMap::new())
1468            .err()
1469            .unwrap();
1470        assert!(crate::odb::store::flaky::is_injected(&err), "{err:?}");
1471    }
1472
1473    fn send_decision(refname: &str, new_oid: ObjectId) -> PushDecision {
1474        PushDecision {
1475            result: PushRefResult {
1476                local_ref: None,
1477                remote_ref: refname.to_owned(),
1478                old_oid: None,
1479                new_oid: Some(new_oid),
1480                forced: false,
1481                deletion: false,
1482                status: PushRefStatus::Ok,
1483                message: None,
1484            },
1485            new_tip: Some(new_oid),
1486            send: true,
1487        }
1488    }
1489
1490    #[test]
1491    fn command_block_without_push_options_has_no_capability_or_lines() {
1492        let new = ObjectId::from_hex(&"1".repeat(40)).unwrap();
1493        let plan = PushPlan {
1494            decisions: vec![send_decision("refs/heads/main", new)],
1495            to_send: vec![0],
1496        };
1497        let block = build_command_block(
1498            &plan,
1499            &adv_state(false, false, true),
1500            HashAlgo::Sha1,
1501            &PushOptions::default(),
1502        )
1503        .unwrap();
1504        let pkts = decode_block(&block);
1505        // command line, then a single terminating flush — nothing else.
1506        assert_eq!(pkts.len(), 2);
1507        let cmd = pkts[0].as_deref().unwrap();
1508        assert!(
1509            cmd.contains("refs/heads/main"),
1510            "first line is the ref command, got {cmd:?}"
1511        );
1512        assert!(
1513            !cmd.contains("push-options"),
1514            "no push-options capability without options, got {cmd:?}"
1515        );
1516        assert_eq!(pkts[1], None, "single trailing flush");
1517    }
1518
1519    #[test]
1520    fn command_block_with_push_options_negotiates_cap_and_emits_lines() {
1521        let new = ObjectId::from_hex(&"1".repeat(40)).unwrap();
1522        let plan = PushPlan {
1523            decisions: vec![send_decision("refs/heads/main", new)],
1524            to_send: vec![0],
1525        };
1526        let opts = PushOptions {
1527            push_options: vec!["ci.skip".to_owned(), "reviewer=alice".to_owned()],
1528            ..Default::default()
1529        };
1530        let block = build_command_block(&plan, &adv_state(true, true, true), HashAlgo::Sha1, &opts)
1531            .unwrap();
1532        let pkts = decode_block(&block);
1533        // command line | flush | push-option ci.skip | push-option reviewer=alice | flush
1534        assert_eq!(
1535            pkts,
1536            vec![
1537                pkts[0].clone(),
1538                None,
1539                Some("ci.skip".to_owned()),
1540                Some("reviewer=alice".to_owned()),
1541                None,
1542            ],
1543            "push-option lines must follow the command-list flush, then a flush"
1544        );
1545        let cmd = pkts[0].as_deref().unwrap();
1546        assert!(
1547            cmd.contains("push-options"),
1548            "capability list must advertise push-options, got {cmd:?}"
1549        );
1550        // The first command line still carries the rest of the negotiated caps.
1551        assert!(cmd.contains("report-status"));
1552        assert!(cmd.contains("side-band-64k"));
1553        assert!(cmd.contains("object-format=sha1"));
1554    }
1555
1556    #[test]
1557    fn require_push_options_errors_typed_when_server_lacks_capability() {
1558        let opts = PushOptions {
1559            push_options: vec!["x".to_owned()],
1560            ..PushOptions::default()
1561        };
1562        // Server did NOT advertise push-options: typed error, not Message.
1563        let err = require_push_capabilities(&adv_state(true, true, false), &opts).unwrap_err();
1564        assert!(
1565            matches!(err, Error::PushOptionsUnsupported),
1566            "expected PushOptionsUnsupported, got {err:?}"
1567        );
1568        assert_eq!(
1569            err.to_string(),
1570            "the receiving end does not support push options"
1571        );
1572        // Server advertised it: ok.
1573        require_push_capabilities(&adv_state(true, true, true), &opts).unwrap();
1574        // No options: ok regardless of capability.
1575        require_push_capabilities(&adv_state(true, true, false), &PushOptions::default()).unwrap();
1576    }
1577
1578    #[test]
1579    fn report_ng_demotes_to_remote_rejected() {
1580        let mut decisions = vec![
1581            make_decision("refs/heads/main", true),
1582            make_decision("refs/heads/topic", true),
1583        ];
1584        let report = report_bytes(&[
1585            "unpack ok",
1586            "ok refs/heads/main",
1587            "ng refs/heads/topic non-fast-forward",
1588        ]);
1589        apply_report_status(&report, &mut decisions);
1590        assert_eq!(decisions[0].result.status, PushRefStatus::Ok);
1591        assert_eq!(decisions[1].result.status, PushRefStatus::RemoteRejected);
1592        assert_eq!(
1593            decisions[1].result.message.as_deref(),
1594            Some("non-fast-forward")
1595        );
1596    }
1597
1598    #[test]
1599    fn report_unpack_failure_rejects_all_sent() {
1600        let mut decisions = vec![make_decision("refs/heads/main", true)];
1601        let report = report_bytes(&["unpack index-pack abort"]);
1602        apply_report_status(&report, &mut decisions);
1603        assert_eq!(decisions[0].result.status, PushRefStatus::RemoteRejected);
1604        assert!(decisions[0]
1605            .result
1606            .message
1607            .as_deref()
1608            .unwrap()
1609            .starts_with("unpack failed:"));
1610    }
1611
1612    #[test]
1613    fn demux_separates_report_and_progress() {
1614        struct Cap(Vec<u8>);
1615        impl crate::progress::ProgressSink for Cap {}
1616        impl Progress for Cap {
1617            fn message(&mut self, bytes: &[u8]) {
1618                self.0.extend_from_slice(bytes);
1619            }
1620        }
1621        // Band 1 = report, band 2 = progress.
1622        let mut wire = Vec::new();
1623        let mut band1 = vec![1u8];
1624        band1.extend_from_slice(b"unpack ok\n");
1625        pkt_line::write_packet_raw(&mut wire, &band1).unwrap();
1626        let mut band2 = vec![2u8];
1627        band2.extend_from_slice(b"hello from hook\n");
1628        pkt_line::write_packet_raw(&mut wire, &band2).unwrap();
1629        wire.extend_from_slice(b"0000");
1630
1631        let mut cap = Cap(Vec::new());
1632        let report = demux_report_and_remote_messages(&wire, &mut cap).unwrap();
1633        assert_eq!(report, b"unpack ok\n");
1634        assert_eq!(cap.0, b"hello from hook\n");
1635    }
1636
1637    #[test]
1638    fn report_lines_complete_requires_unpack_and_every_sent_ref() {
1639        let sent = ["refs/heads/main"];
1640        let full = report_bytes(&["unpack ok", "ok refs/heads/main"]);
1641        assert!(report_lines_complete(&full, &sent));
1642        let missing_ok = report_bytes(&["unpack ok"]);
1643        assert!(!report_lines_complete(&missing_ok, &sent));
1644        let missing_unpack = report_bytes(&["ok refs/heads/main"]);
1645        assert!(!report_lines_complete(&missing_unpack, &sent));
1646        let ng_ok = report_bytes(&["unpack ok", "ng refs/heads/main denied"]);
1647        assert!(report_lines_complete(&ng_ok, &sent));
1648    }
1649
1650    struct AbruptAfterRead {
1651        payload: Vec<u8>,
1652        pos: usize,
1653        err_kind: ErrorKind,
1654    }
1655
1656    impl Read for AbruptAfterRead {
1657        fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
1658            if self.pos < self.payload.len() {
1659                let n = std::cmp::min(buf.len(), self.payload.len() - self.pos);
1660                buf[..n].copy_from_slice(&self.payload[self.pos..self.pos + n]);
1661                self.pos += n;
1662                return Ok(n);
1663            }
1664            Err(std::io::Error::new(self.err_kind, "forcibly closed"))
1665        }
1666    }
1667
1668    fn sideband_receive_pack_wire(hook_line: &[u8]) -> Vec<u8> {
1669        let report_body = report_bytes(&["unpack ok", "ok refs/heads/main"]);
1670        let mut band1 = vec![1u8];
1671        band1.extend_from_slice(&report_body);
1672        let mut wire = Vec::new();
1673        pkt_line::write_packet_raw(&mut wire, &band1).unwrap();
1674        let mut band2 = vec![2u8];
1675        band2.extend_from_slice(hook_line);
1676        pkt_line::write_packet_raw(&mut wire, &band2).unwrap();
1677        wire.extend_from_slice(b"0000");
1678        wire
1679    }
1680
1681    struct EofAfterRead {
1682        payload: Vec<u8>,
1683        pos: usize,
1684    }
1685
1686    impl Read for EofAfterRead {
1687        fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
1688            if self.pos < self.payload.len() {
1689                let n = std::cmp::min(buf.len(), self.payload.len() - self.pos);
1690                buf[..n].copy_from_slice(&self.payload[self.pos..self.pos + n]);
1691                self.pos += n;
1692                Ok(n)
1693            } else {
1694                Ok(0)
1695            }
1696        }
1697    }
1698
1699    /// Mirrors `push_remote`: read the wire response, then demux side-band once.
1700    fn read_push_report(
1701        reader: &mut dyn Read,
1702        sent_refs: &[&str],
1703        sideband: bool,
1704        progress: &mut dyn Progress,
1705    ) -> Result<Vec<u8>> {
1706        let raw =
1707            read_receive_pack_response(reader, sent_refs, sideband, &crate::progress::NeverCancel)?;
1708        if sideband {
1709            demux_report_and_remote_messages(&raw, progress)
1710        } else {
1711            Ok(raw)
1712        }
1713    }
1714
1715    #[test]
1716    fn read_receive_pack_response_tolerates_reset_after_full_report() {
1717        let report = report_bytes(&["unpack ok", "ok refs/heads/main"]);
1718        let mut reader = AbruptAfterRead {
1719            payload: report.clone(),
1720            pos: 0,
1721            err_kind: ErrorKind::ConnectionReset,
1722        };
1723        let out = read_receive_pack_response(
1724            &mut reader,
1725            &["refs/heads/main"],
1726            false,
1727            &crate::progress::NeverCancel,
1728        )
1729        .expect("reset");
1730        assert_eq!(out, report);
1731    }
1732
1733    #[test]
1734    fn a_cancel_stops_an_incomplete_report_but_keeps_a_complete_one() {
1735        /// Hands out `payload` in one read, then fires `cancel`.
1736        struct CancelAfterRead {
1737            payload: Cursor<Vec<u8>>,
1738            cancel: crate::progress::FlagCancel,
1739        }
1740        impl Read for CancelAfterRead {
1741            fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
1742                let n = self.payload.read(buf)?;
1743                self.cancel.cancel();
1744                Ok(n)
1745            }
1746        }
1747        let refs = ["refs/heads/main"];
1748        for (lines, complete) in [
1749            (&["unpack ok"][..], false),
1750            (&["unpack ok", "ok refs/heads/main"][..], true),
1751        ] {
1752            let cancel = crate::progress::FlagCancel::new();
1753            let report = report_bytes(lines);
1754            let mut reader = CancelAfterRead {
1755                payload: Cursor::new(report.clone()),
1756                cancel: cancel.clone(),
1757            };
1758            let out = read_receive_pack_response(&mut reader, &refs, false, &cancel);
1759            if complete {
1760                assert_eq!(out.expect("complete report"), report);
1761            } else {
1762                assert!(matches!(out, Err(Error::Cancelled)), "{out:?}");
1763            }
1764        }
1765    }
1766
1767    #[test]
1768    fn read_receive_pack_response_fails_reset_on_incomplete_report() {
1769        let partial = report_bytes(&["unpack ok"]);
1770        let mut reader = AbruptAfterRead {
1771            payload: partial,
1772            pos: 0,
1773            err_kind: ErrorKind::ConnectionReset,
1774        };
1775        let err = read_receive_pack_response(
1776            &mut reader,
1777            &["refs/heads/main"],
1778            false,
1779            &crate::progress::NeverCancel,
1780        )
1781        .unwrap_err();
1782        assert!(
1783            matches!(err, Error::Io(_)),
1784            "incomplete report must stay an I/O error, got {err:?}"
1785        );
1786    }
1787
1788    #[test]
1789    fn sideband_remote_messages_flush_unterminated_hook_text() {
1790        struct Noop;
1791        impl crate::progress::ProgressSink for Noop {}
1792        impl Progress for Noop {}
1793        let mut inner = Noop;
1794        let mut sideband = PushSidebandCollector {
1795            inner: &mut inner,
1796            remote_messages: Vec::new(),
1797            pending: SidebandLines::new(),
1798        };
1799        let hook = b"policy: no ticket";
1800        let wire = sideband_receive_pack_wire(hook);
1801        demux_report_and_remote_messages(&wire, &mut sideband).unwrap();
1802        sideband.finish_pending();
1803        assert_eq!(
1804            sideband.remote_messages,
1805            vec!["policy: no ticket".to_owned()]
1806        );
1807    }
1808
1809    #[test]
1810    fn sideband_diagnostics_delivered_once_on_clean_eof() {
1811        struct Cap(Vec<u8>);
1812        impl crate::progress::ProgressSink for Cap {}
1813        impl Progress for Cap {
1814            fn message(&mut self, bytes: &[u8]) {
1815                self.0.extend_from_slice(bytes);
1816            }
1817        }
1818        let hook = b"hook output\n";
1819        let wire = sideband_receive_pack_wire(hook);
1820        let mut reader = EofAfterRead {
1821            payload: wire,
1822            pos: 0,
1823        };
1824        let mut cap = Cap(Vec::new());
1825        read_push_report(&mut reader, &["refs/heads/main"], true, &mut cap)
1826            .expect("clean eof side-band push report");
1827        assert_eq!(cap.0.as_slice(), hook);
1828    }
1829
1830    #[test]
1831    fn sideband_diagnostics_delivered_once_on_connection_reset() {
1832        struct Cap(Vec<u8>);
1833        impl crate::progress::ProgressSink for Cap {}
1834        impl Progress for Cap {
1835            fn message(&mut self, bytes: &[u8]) {
1836                self.0.extend_from_slice(bytes);
1837            }
1838        }
1839        let hook = b"hook output\n";
1840        let wire = sideband_receive_pack_wire(hook);
1841        let mut reader = AbruptAfterRead {
1842            payload: wire,
1843            pos: 0,
1844            err_kind: ErrorKind::ConnectionReset,
1845        };
1846        let mut cap = Cap(Vec::new());
1847        read_push_report(&mut reader, &["refs/heads/main"], true, &mut cap)
1848            .expect("reset after complete side-band report");
1849        assert_eq!(cap.0.as_slice(), hook);
1850    }
1851
1852    /// Initial push of one branch tip must not disable deltas just because there
1853    /// is a single want (regression for #1941).
1854    #[cfg_attr(
1855        not(feature = "full-tests"),
1856        ignore = "starts system git or is heavy; run with --features full-tests"
1857    )]
1858    #[test]
1859    fn push_pack_initial_branch_push_uses_deltas() {
1860        use std::process::Command;
1861
1862        fn pack_delta_count(bytes: &[u8], algo_len: usize) -> usize {
1863            fn read_type_size(b: &[u8]) -> (u8, usize, usize) {
1864                let type_code = (b[0] >> 4) & 7;
1865                let mut size = (b[0] & 0x0f) as usize;
1866                let mut shift = 4;
1867                let mut i = 1;
1868                while b[i - 1] & 0x80 != 0 {
1869                    size |= ((b[i] & 0x7f) as usize) << shift;
1870                    shift += 7;
1871                    i += 1;
1872                }
1873                (type_code, size, i)
1874            }
1875            fn zlib_consume(b: &[u8]) -> usize {
1876                use std::io::Read as _;
1877                let mut dec = flate2::bufread::ZlibDecoder::new(b);
1878                let mut sink = Vec::new();
1879                dec.read_to_end(&mut sink).expect("zlib");
1880                dec.total_in() as usize
1881            }
1882            let nr = u32::from_be_bytes(bytes[8..12].try_into().unwrap()) as usize;
1883            let mut pos = 12usize;
1884            let mut deltas = 0usize;
1885            for _ in 0..nr {
1886                let (type_code, _, consumed) = read_type_size(&bytes[pos..]);
1887                pos += consumed;
1888                match type_code {
1889                    6 | 7 => {
1890                        deltas += 1;
1891                        if type_code == 7 {
1892                            pos += algo_len;
1893                        } else {
1894                            while bytes[pos] & 0x80 != 0 {
1895                                pos += 1;
1896                            }
1897                            pos += 1;
1898                        }
1899                    }
1900                    _ => {}
1901                }
1902                pos += zlib_consume(&bytes[pos..]);
1903            }
1904            deltas
1905        }
1906
1907        let tmp = tempfile::tempdir().unwrap();
1908        let dir = tmp.path();
1909        for i in 0..40 {
1910            if i == 0 {
1911                Command::new("git")
1912                    .current_dir(dir)
1913                    .args(["init", "-q", "-b", "main", "."])
1914                    .env("GIT_CONFIG_GLOBAL", "/dev/null")
1915                    .env("GIT_CONFIG_SYSTEM", "/dev/null")
1916                    .status()
1917                    .unwrap();
1918            }
1919            std::fs::write(dir.join(format!("f{i:03}.txt")), format!("content {i}\n")).unwrap();
1920            std::fs::write(dir.join("shared.txt"), format!("shared rev {i}\n")).unwrap();
1921            Command::new("git")
1922                .current_dir(dir)
1923                .args(["add", "-A"])
1924                .env("GIT_CONFIG_GLOBAL", "/dev/null")
1925                .env("GIT_CONFIG_SYSTEM", "/dev/null")
1926                .status()
1927                .unwrap();
1928            Command::new("git")
1929                .current_dir(dir)
1930                .args(["commit", "-q", "-m", &format!("commit {i}")])
1931                .env("GIT_AUTHOR_NAME", "T")
1932                .env("GIT_AUTHOR_EMAIL", "t@example.com")
1933                .env("GIT_AUTHOR_DATE", "2005-04-07T22:13:13 +0200")
1934                .env("GIT_COMMITTER_NAME", "T")
1935                .env("GIT_COMMITTER_EMAIL", "t@example.com")
1936                .env("GIT_COMMITTER_DATE", "2005-04-07T22:13:13 +0200")
1937                .env("GIT_CONFIG_GLOBAL", "/dev/null")
1938                .env("GIT_CONFIG_SYSTEM", "/dev/null")
1939                .status()
1940                .unwrap();
1941        }
1942        Command::new("git")
1943            .current_dir(dir)
1944            .args(["repack", "-adf"])
1945            .env("GIT_CONFIG_GLOBAL", "/dev/null")
1946            .env("GIT_CONFIG_SYSTEM", "/dev/null")
1947            .status()
1948            .unwrap();
1949        let git_dir = dir.join(".git");
1950        let repo = Repository::open(&git_dir, &RepositoryOptions::empty()).unwrap();
1951        let odb = repo.odb();
1952        let tip = crate::refs::resolve_ref(&git_dir, "HEAD").expect("head");
1953        let plan = PushPlan {
1954            decisions: vec![send_decision("refs/heads/main", tip)],
1955            to_send: vec![0],
1956        };
1957        let adv = adv_state(false, true, false);
1958        let pack = push_pack_bytes(&plan, &repo, &adv)
1959            .unwrap()
1960            .expect("pack bytes");
1961        let deltas = pack_delta_count(&pack, odb.hash_algo().len());
1962        assert!(
1963            deltas > 0,
1964            "initial push pack must contain delta objects (got {deltas})"
1965        );
1966    }
1967}