Skip to main content

alkgit_substrate/
ingest.rs

1//! The substrate's `GitPackIngest` implement ([`PackIngest`]) — ADR-020
2//! §2's ingestion composition, for real: the received pack indexes into
3//! a budgeted host working area (thin bases resolving through the
4//! substrate Find), the indexed objects unpack into alkblobs as
5//! loose-form objects under the git-object family (the wave-7 object
6//! front's family put — the store's verification-on-put *is* the oid
7//! re-derivation, corruption cannot enter undetected), the guards ride
8//! the w8-pin-registry's custody seam, connectivity closes the graph
9//! from the pushed tips, and the working area is discarded. The
10//! reference shape is POC-O's `ingest.rs`; the reference semantics are
11//! the wave-3 doubles' `MemPackIngest` (`tests/common/mod.rs` in the
12//! core crate).
13//!
14//! # The recorded design decisions (the wave-8 gate's reading list)
15//!
16//! - **The decompressed-size cap's enforcement, layered** (the task's
17//!   named design point — how the `Limits::max_object_size` bound
18//!   holds through gix's own index/inflate machinery):
19//!   1. the *counting reader* is the defense-in-depth layer the
20//!      wave-3 gate confirmed: the pack stream's byte total is bounded
21//!      by `Limits::max_pack_size`; a breach is a pack-level failure
22//!      mapping into the `unpack` ng leg, never `Err` (the wire side's
23//!      relay breaches separately fail the session closed);
24//!   2. the *indexing leg* rides gix's indexing machinery
25//!      (`gix_pack::Bundle::write_to_directory`) with its
26//!      `alloc_limit_bytes` knob set to the cap: gix's decode paths vet
27//!      every allocation-relevant pack-declared quantity against the
28//!      limit before reserving, so a hostile declaration beyond the cap
29//!      errors before any allocation, and the machinery's
30//!      exact-decompression discipline (a stream that stops early or
31//!      overproduces against its declared buffer is corruption) makes a
32//!      lying-small declaration fail too — the hostile-declared-size
33//!      class maps to the unpack ng leg over the real machinery;
34//!   3. the *unpack leg* is this module's own incremental machinery —
35//!      the wave-5 doubles' guard verbatim: each entry's declared size
36//!      is vetted against the cap before anything is read, the inflate
37//!      reads in bounded chunks accumulating only produced bytes (the
38//!      running total aborts at the cap), and each delta's materialized
39//!      result is checked against its declaration and produced
40//!      incrementally — nothing on the unpack path ever sizes an
41//!      allocation from the pack's own declaration.
42//! - **The working area's shape**: a fresh per-push directory under the
43//!   constructor's root knob (no env — conventions 4/5); the disk
44//!   shadow is the pack the counting reader bounds (pack + index ≈ the
45//!   pack budget — no new `Limits` field, ADR-009's no-new-kinds rule).
46//!   The authoritative object home is the store; the area (temporary
47//!   pack + index) is discarded after unpack — on every path, the
48//!   explicit return paths' removal riding a best-effort residue
49//!   removal that also covers a panic's unwind (Drop's bounded
50//!   directory rm — the one blocking best-effort on the async side).
51//! - **Unpack order and the store's role**: the indexing machinery
52//!   normalizes the pack (thin-base entries inserted physically and
53//!   their ref-deltas rewritten, in-pack ref-deltas resolved through
54//!   the index), so the unpack iterates the written pack in pack order
55//!   over the index's entries; every materialized object re-derives
56//!   (the pack-side oid answer), cross-checks against the index's own
57//!   oid, and goes through the store's batch form — one
58//!   [`alkblobs::Batch`] group over the working set (the store's
59//!   pin-before-publish ordering, one commit discipline). Existing
60//!   content dedups in the store; re-putting every indexed object
61//!   (rather than skipping known ones) keeps the sweep race closed: an
62//!   object present-but-sweep-eligible could otherwise vanish
63//!   mid-window under a bare presence check. Delta bases read back
64//!   through the Find front (verification-on-get is the same
65//!   re-derivation guard), one base body at a time — the unpack
66//!   stage's memory is the cap's working set, never O(pack).
67//! - **Connectivity**: the full-graph walk over the substrate Find
68//!   from the pushed tips — commits walk parents and trees, trees walk
69//!   entries, tags walk targets, blobs terminate (`gix-fsck`'s
70//!   `Connectivity::check_commit` does not walk parents; this is the
71//!   owning walk, the same two-stage discipline as generation). A
72//!   missing closure member fails into the `unpack` ng leg naming the
73//!   missing object.
74//! - **Status shapes**: pack-level validation failures (parse, index,
75//!   thin-base miss, the cap, connectivity) land in
76//!   [`PreparedPush::unpack`](alkgit::PreparedPush)'s ng leg with a
77//!   server-chosen reason; `updates` returns the input list verbatim
78//!   on every path (ADR-018 §5); `Err` stays
79//!   `StorageError` — infrastructure only
80//!   (working-area construction, task joins, backing-store legs). The
81//!   empty pack (zero objects) is valid and reports as such; the
82//!   push's tips must then exist in the store (the same connectivity
83//!   walk, vacuous over deletes-only updates). The ng legs retain
84//!   nothing — their guards drop with the prepare call and the
85//!   just-put objects are the same sweep-eligible recovery the crash
86//!   case rides; a fully ingested push retains its [`alkblobs::Batch`]
87//!   into the composition-internal `PendingPinRegistry` keyed by
88//!   `repo_id`, correlated content-wise with the verbatim prepared
89//!   update list (the custody seam the w8-pin-registry task owns).
90//!
91//! # Threading
92//!
93//! The whole composition runs on `tokio::task::spawn_blocking` with
94//! owned handles moved in (backend.md §concurrency model — the permit
95//! stays wire-side, the impl's threading invisible); every sync→async
96//! hop goes through [`ObjectFront::block_on`] from the blocking
97//! contexts — no consumer constructs its own bridge.
98
99use std::collections::{HashMap, HashSet};
100use std::io::{self, Read as _};
101use std::path::PathBuf;
102use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
103use std::sync::Arc;
104use std::time::{SystemTime, UNIX_EPOCH};
105
106use alkblobs::Batch;
107use gix_hash::ObjectId;
108use gix_object::Kind;
109use gix_pack::bundle::write::Options as BundleOptions;
110use gix_pack::data::entry::Header as PackHeader;
111
112use alkgit::{
113    GitPackIngest, Limits, PreparedPush, PushOptions, RefUpdate, RepoRecord, StorageError,
114};
115use async_trait::async_trait;
116
117use crate::odb::{key_to_oid, loose_form, store_leg_error, ObjectFront, SessionFind};
118use crate::pins::{PendingPinRegistry, PinCustody};
119
120/// The derive algorithm every family put carries (the odb layer's
121/// serving-path member — the sha1 git-object family, OQ-05).
122use crate::odb::GIT_OBJECT_ALGORITHM;
123
124/// The inflate window the unpack leg reads in — bounded output slices;
125/// the produced bytes accumulate beyond the window under the cap.
126const INFLATE_CHUNK_BYTES: usize = 65536;
127
128/// The per-process uniqueness counter behind the working-area
129/// directory names (pid + nanoseconds + the counter — no client input
130/// ever reaches a filesystem path).
131static WORK_AREA_COUNTER: AtomicU64 = AtomicU64::new(0);
132
133/// The substrate's [`GitPackIngest`] implement.
134///
135/// The construction is the composition entry's (substrate.md §public
136/// API — the receive-pack composition is internal to the trait impls,
137/// not a public member; the pending-pin registry is shared with the
138/// wrapped `GitRefs` implement the same entry wires — the w8
139/// composition-entry task joins the names into the OQ-03 inventory).
140/// The working-area root is the constructor knob.
141pub struct PackIngest {
142    front: ObjectFront,
143    pins: Arc<PendingPinRegistry>,
144    root: PathBuf,
145}
146
147impl PackIngest {
148    /// The implement over the object front, the pending-pin registry
149    /// shared with the wired `GitRefs` implement, and the working-area
150    /// root — the assembler-level knob the composition entry's config
151    /// carries (no env).
152    pub(crate) fn new(front: ObjectFront, pins: Arc<PendingPinRegistry>, root: PathBuf) -> Self {
153        PackIngest { front, pins, root }
154    }
155}
156
157/// One leg's failure split: every pack-level validation problem rides
158/// the ng leg as its server-chosen reason; a backing-store failure is
159/// the seam's infrastructure class ([`StorageError`], the `Err`).
160enum LegFailure {
161    Pack(String),
162    Backing(StorageError),
163}
164
165impl From<StorageError> for LegFailure {
166    fn from(error: StorageError) -> Self {
167        LegFailure::Backing(error)
168    }
169}
170
171impl From<String> for LegFailure {
172    fn from(reason: String) -> Self {
173        LegFailure::Pack(reason)
174    }
175}
176
177/// The best-effort residue remover on the working area: held across
178/// the prepare call, dropped on every exit — the return paths' removal
179/// and the panic-path's unwind reach the same cleanup.
180struct WorkArea(PathBuf);
181
182impl Drop for WorkArea {
183    fn drop(&mut self) {
184        let _ = std::fs::remove_dir_all(&self.0);
185    }
186}
187
188/// The counting reader (the defense-in-depth layer the wave-3 gate
189/// confirmed): past `limit` every read errors — the indexing machinery
190/// fails and the push reports the `unpack` ng leg, never `Err`. EOF
191/// before the budget is the ordinary stream end (an under-length input
192/// is the machinery's own parse error); a pack of exactly the budget
193/// consumes without a further read.
194struct BudgetedReader<R> {
195    inner: R,
196    bytes_seen: u64,
197    limit: u64,
198}
199
200impl<R> BudgetedReader<R> {
201    fn new(inner: R, limit: u64) -> Self {
202        BudgetedReader {
203            inner,
204            bytes_seen: 0,
205            limit,
206        }
207    }
208}
209
210impl<R: io::Read> io::Read for BudgetedReader<R> {
211    fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
212        if self.bytes_seen >= self.limit {
213            return Err(io::Error::other(format!(
214                "the pack stream exceeded the maximum pack size budget of {} bytes",
215                self.limit
216            )));
217        }
218        let read = self.inner.read(buf)?;
219        self.bytes_seen += read as u64;
220        Ok(read)
221    }
222}
223
224/// A store read's failure classification along the ingest legs: a
225/// corruption-classified backing state is a pack-level failure (the ng
226/// leg names the corruption), every other backing failure is
227/// infrastructure.
228fn store_read_failure(oid: &ObjectId, error: StorageError) -> LegFailure {
229    match error {
230        StorageError::Invalid(message) => {
231            LegFailure::Pack(format!("corrupt object {oid}: {message}"))
232        }
233        other => LegFailure::Backing(other),
234    }
235}
236
237/// The indexing leg (blocking): the budgeted stream indexes into the
238/// working area — pack + index on disk over a `Verify` iteration, thin
239/// bases resolving through the substrate Find — returning the written
240/// pair. A zero-object pack yields `None`: the machinery never reaches
241/// a zero-object pack's trailer, so the leg verifies it itself (the
242/// doubles' zero-object branch). Every error here is pack-level (the
243/// ng leg's server-chosen reason).
244fn index_leg(
245    pack: Box<dyn io::Read + Send>,
246    dir: PathBuf,
247    thin: SessionFind,
248    budget: u64,
249    cap: u64,
250) -> Result<Option<(PathBuf, PathBuf)>, String> {
251    let mut counted = BudgetedReader::new(pack, budget);
252    let mut header = [0u8; gix_pack::data::header::SIZE];
253    counted
254        .read_exact(&mut header)
255        .map_err(|_| "the pack stream ended in the header".to_string())?;
256    let (_version, object_count) = gix_pack::data::header::decode(&header)
257        .map_err(|error| format!("the pack header does not decode: {error}"))?;
258
259    if object_count == 0 {
260        let mut trailer = [0u8; gix_hash::Kind::Sha1.len_in_bytes()];
261        counted
262            .read_exact(&mut trailer)
263            .map_err(|_| "the zero-object pack stream ends before the trailer".to_string())?;
264        let mut hasher = gix_hash::hasher(gix_hash::Kind::Sha1);
265        hasher.update(&header);
266        let digest = hasher
267            .try_finalize()
268            .map_err(|error| format!("the pack trailer does not hash: {error}"))?;
269        if digest != ObjectId::from(trailer) {
270            return Err(
271                "the zero-object pack's trailer does not verify against the pack bytes".to_owned(),
272            );
273        }
274        return Ok(None);
275    }
276
277    let interrupt = AtomicBool::new(false);
278    let mut progress = gix_utils::progress::Discard;
279    let mut pack_stream = io::BufReader::new(io::Cursor::new(header.to_vec()).chain(counted));
280    let outcome = gix_pack::Bundle::write_to_directory(
281        &mut pack_stream,
282        Some(&dir),
283        &mut progress,
284        &interrupt,
285        Some(thin),
286        gix_hash::Kind::Sha1,
287        BundleOptions {
288            thread_limit: Some(1),
289            iteration_mode: gix_pack::data::input::Mode::Verify,
290            index_version: Default::default(),
291            alloc_limit_bytes: Some(usize::try_from(cap).unwrap_or(usize::MAX)),
292            compression: gix_zlib::Compression::DEFAULT,
293        },
294    )
295    .map_err(|error| format!("the pack does not index: {error}"))?;
296    match (outcome.index_path, outcome.data_path) {
297        (Some(index_path), Some(pack_path)) => Ok(Some((pack_path, index_path))),
298        _ => Ok(None),
299    }
300}
301
302/// A materialized pack entry's oid (the pack-side re-derivation; the
303/// store's verification-on-put repeats it at put time).
304fn rederive_object(kind: Kind, body: &[u8]) -> Result<ObjectId, String> {
305    gix_object::compute_hash(gix_hash::Kind::Sha1, kind, body)
306        .map_err(|error| format!("the pack entry object does not hash: {error}"))
307}
308
309/// The async-hop discipline at the put seam, in one function: the
310/// loose form goes through the store's batch form
311/// ([`alkblobs::Store::put_in_batch`]) via [`ObjectFront::block_on`]
312/// from the blocking context (the one-place bridge rule), and the
313/// returned family key must re-derive to the expected oid — the
314/// verification-on-put's cross-check on the batch path.
315fn put_in_batch(
316    front: &ObjectFront,
317    batch: &mut Batch,
318    kind: Kind,
319    body: &[u8],
320    expected: ObjectId,
321) -> Result<(), StorageError> {
322    let form = loose_form(kind, body);
323    let store = front.store().clone();
324    let future = async {
325        store
326            .put_in_batch(
327                batch,
328                GIT_OBJECT_ALGORITHM,
329                Some(form.len() as u64),
330                io::Cursor::new(&form),
331            )
332            .await
333    };
334    let key = front
335        .block_on(future)
336        .map_err(|error| store_leg_error("alkblobs put_in_batch", error))?;
337    if key_to_oid(&key) != Some(expected) {
338        return Err(StorageError::Invalid(format!(
339            "the put returned a key that does not re-derive to {expected}"
340        )));
341    }
342    Ok(())
343}
344
345/// One delta base read back through the Find front
346/// ([`ObjectFront::get`]'s verification-on-get is the read's
347/// re-derivation); the delivered kind must match the delta chain's.
348fn base_object(
349    front: &ObjectFront,
350    base_oid: ObjectId,
351    base_kind: Kind,
352) -> Result<(Kind, Vec<u8>), LegFailure> {
353    let found = front.block_on(front.get(&base_oid));
354    match found {
355        Ok(None) => Err(LegFailure::Pack(format!("missing object {base_oid}"))),
356        Err(error) => Err(store_read_failure(&base_oid, error)),
357        Ok(Some((delivered_kind, body))) => {
358            if delivered_kind != base_kind {
359                return Err(LegFailure::Pack(format!(
360                    "the base object {base_oid} delivered kind {delivered_kind:?} against \
361                     the delta chain's kind {base_kind:?}"
362                )));
363            }
364            Ok((base_kind, body))
365        }
366    }
367}
368
369/// One pack-entry size field of a delta stream header: 7-bit
370/// little-endian groups with the continuation bit.
371fn delta_size_at(delta: &[u8], offset: usize) -> Result<(u64, usize), String> {
372    let mut size = 0u64;
373    let mut shift = 0u32;
374    let mut cursor = offset;
375    loop {
376        let Some(&byte) = delta.get(cursor) else {
377            return Err("the delta header is truncated".to_string());
378        };
379        cursor += 1;
380        size |= u64::from(byte & 0x7f) << shift;
381        shift += 7;
382        if byte & 0x80 == 0 {
383            return Ok((size, cursor - offset));
384        }
385        if shift >= 64 {
386            return Err("the delta header size overflow".to_string());
387        }
388    }
389}
390
391/// Applies a git delta stream to its base: the declared base size must
392/// match, the declared result stays under `cap` (the decompressed-size
393/// cap — the delta's materialized result is one of the objects the
394/// pack produces), and the instructions must produce exactly the
395/// declared result size. The result accumulates incrementally — the
396/// declared size is checked against, never pre-allocated; the loop
397/// itself aborts once the produced bytes pass the declaration.
398fn apply_delta(base: &[u8], delta: &[u8], cap: u64) -> Result<Vec<u8>, String> {
399    let (base_size, base_header_len) = delta_size_at(delta, 0)?;
400    let (result_size, result_header_len) = delta_size_at(delta, base_header_len)?;
401    if base_size != u64::try_from(base.len()).unwrap_or(u64::MAX) {
402        return Err(format!(
403            "the delta declares a base size of {base_size} but the base object is {} bytes",
404            base.len()
405        ));
406    }
407    if result_size > u64::from(u32::MAX) {
408        return Err("the delta declares an out-of-bounds result size".to_string());
409    }
410    if result_size > cap {
411        return Err(format!(
412            "the delta declares a materialized result of {result_size} bytes beyond \
413             the ingestion decompressed-size cap of {cap} bytes"
414        ));
415    }
416    let mut out = Vec::new();
417    let mut cursor = base_header_len + result_header_len;
418    while let Some(&cmd) = delta.get(cursor) {
419        cursor += 1;
420        if cmd & 0x80 != 0 {
421            let mut ofs = 0u64;
422            let mut size = 0u64;
423            for (bit, shift) in [(0x01, 0), (0x02, 8), (0x04, 16), (0x08, 24)] {
424                if cmd & bit != 0 {
425                    let Some(&byte) = delta.get(cursor) else {
426                        return Err("the delta copy instruction is truncated".to_string());
427                    };
428                    cursor += 1;
429                    ofs |= u64::from(byte) << shift;
430                }
431            }
432            for (bit, shift) in [(0x10, 0), (0x20, 8), (0x40, 16)] {
433                if cmd & bit != 0 {
434                    let Some(&byte) = delta.get(cursor) else {
435                        return Err("the delta copy instruction is truncated".to_string());
436                    };
437                    cursor += 1;
438                    size |= u64::from(byte) << shift;
439                }
440            }
441            if size == 0 {
442                size = 0x10000;
443            }
444            let (ofs, size) = (
445                usize::try_from(ofs).map_err(|_| "the delta copy offset overflows".to_string())?,
446                usize::try_from(size).map_err(|_| "the delta copy size overflows".to_string())?,
447            );
448            let Some(end) = ofs.checked_add(size) else {
449                return Err("the delta copy range overflows".to_string());
450            };
451            let Some(bytes) = base.get(ofs..end) else {
452                return Err("the delta copy range exceeds the base object size".to_string());
453            };
454            out.extend_from_slice(bytes);
455        } else if cmd == 0 {
456            return Err("the delta command 0 is reserved and invalid".to_string());
457        } else {
458            let end = cursor + usize::from(cmd);
459            let Some(bytes) = delta.get(cursor..end) else {
460                return Err("the delta insert data is truncated".to_string());
461            };
462            cursor = end;
463            out.extend_from_slice(bytes);
464        }
465        if u64::try_from(out.len()).unwrap_or(u64::MAX) > result_size {
466            return Err(format!(
467                "the delta instructions produce more than the declared {result_size} bytes"
468            ));
469        }
470    }
471    if out.len() as u64 != result_size {
472        return Err(format!(
473            "the delta instructions produce {} bytes but the delta declares {result_size}",
474            out.len()
475        ));
476    }
477    Ok(out)
478}
479
480/// The cap-vetted, chunked inflate of one pack entry's compressed
481/// stream — the unpack path's incremental reading: the produced bytes
482/// accumulate under the cap, the declared length is only *checked
483/// against* at the stream's end, never pre-allocated.
484fn inflate_slice(compressed: &[u8], declared: u64, cap: u64) -> Result<Vec<u8>, String> {
485    let mut out: Vec<u8> = Vec::new();
486    let mut decompressor = gix_zlib::Decompress::new();
487    let mut reader = io::Cursor::new(compressed);
488    let mut chunk = vec![0u8; INFLATE_CHUNK_BYTES];
489    loop {
490        let written = gix_zlib::stream::inflate::read(&mut reader, &mut decompressor, &mut chunk)
491            .map_err(|error| format!("the pack entry does not inflate: {error}"))?;
492        out.extend_from_slice(&chunk[..written]);
493        if u64::try_from(out.len()).unwrap_or(u64::MAX) > cap {
494            return Err(format!(
495                "the pack entry inflates past the ingestion decompressed-size cap of {cap} bytes"
496            ));
497        }
498        if written < chunk.len() {
499            break;
500        }
501    }
502    if out.len() as u64 != declared {
503        return Err(format!(
504            "the pack entry inflates to {} bytes but {declared} were declared",
505            out.len()
506        ));
507    }
508    Ok(out)
509}
510
511/// The unpack-and-connectivity leg (blocking): every indexed object
512/// materializes incrementally under the cap over the working-area
513/// pack, re-derives, cross-checks against the index's own oid, and
514/// goes through the family put inside the one [`Batch`]; then the
515/// connectivity walk closes from the pushed tips. `Ok` carries the
516/// batch out (the pin bridge's retained custody); every pack-level
517/// failure abandons the batch (its pins drop together — sweep-eligible
518/// recovery) and names the reason.
519fn ingest_leg(
520    front: ObjectFront,
521    index_path: PathBuf,
522    tips: Vec<ObjectId>,
523    cap: u64,
524) -> Result<Batch, LegFailure> {
525    let bundle = gix_pack::Bundle::at(&index_path, gix_hash::Kind::Sha1)
526        .map_err(|error| LegFailure::Pack(format!("the indexed pack does not open: {error}")))?;
527    let mut batch = front.store().batch();
528    let indexed_by_offset: HashMap<u64, ObjectId> = bundle
529        .index
530        .iter()
531        .map(|entry| (entry.pack_offset, entry.oid))
532        .collect();
533    let mut staged: HashMap<u64, (ObjectId, Kind)> = HashMap::new();
534    let mut parked: Vec<(u64, ObjectId)> = Vec::new();
535
536    for offset in bundle.index.sorted_offsets() {
537        stage_entry(
538            &bundle,
539            &front,
540            &mut batch,
541            &mut staged,
542            &mut parked,
543            offset,
544            cap,
545        )?;
546    }
547    while !parked.is_empty() {
548        let drained = std::mem::take(&mut parked);
549        let before = staged.len();
550        for (offset, _base_id) in drained {
551            stage_entry(
552                &bundle,
553                &front,
554                &mut batch,
555                &mut staged,
556                &mut parked,
557                offset,
558                cap,
559            )?;
560        }
561        if staged.len() == before {
562            let (_, base_id) = parked[0];
563            return Err(LegFailure::Pack(format!(
564                "the delta base {base_id} does not resolve within the pack"
565            )));
566        }
567    }
568
569    for (offset, indexed) in &indexed_by_offset {
570        match staged.get(offset) {
571            Some((materialized, _)) if materialized == indexed => {}
572            Some((materialized, _)) => {
573                return Err(LegFailure::Pack(format!(
574                    "the pack entry at offset {offset} materialized to {materialized}, \
575                     not its indexed object {indexed}"
576                )));
577            }
578            None => {
579                let base = *indexed;
580                return Err(LegFailure::Pack(format!(
581                    "the delta base {base} does not resolve within the pack"
582                )));
583            }
584        }
585    }
586
587    connectivity_leg(&front, &tips)?;
588    Ok(batch)
589}
590
591/// Stages one pack entry: base entries inflate incrementally (the
592/// declared size vetted against the cap first, the produced bytes
593/// accumulated in bounded chunks); delta entries resolve their base —
594/// by pack offset (ofs, always backward and staged) or by oid through
595/// the index (ref, possibly forward and parked for the fixpoint) — and
596/// apply the delta stream incrementally against the base read back
597/// through the Find; every staged object goes through the family put.
598/// `Ok` for a staged or parked entry, the ng leg's failure for a
599/// resolution failure.
600fn stage_entry(
601    bundle: &gix_pack::Bundle,
602    front: &ObjectFront,
603    batch: &mut Batch,
604    staged: &mut HashMap<u64, (ObjectId, Kind)>,
605    parked: &mut Vec<(u64, ObjectId)>,
606    offset: u64,
607    cap: u64,
608) -> Result<(), LegFailure> {
609    let entry = bundle.pack.entry(offset).map_err(|error| {
610        LegFailure::Pack(format!(
611            "the pack entry at {offset} does not parse: {error}"
612        ))
613    })?;
614    let declared = entry.decompressed_size;
615    if declared > cap {
616        return Err(LegFailure::Pack(format!(
617            "the pack entry at {offset} declares {declared} decompressed bytes beyond \
618             the ingestion decompressed-size cap of {cap} bytes"
619        )));
620    }
621    let compressed = bundle
622        .pack
623        .entry_slice(entry.data_offset..bundle.pack.pack_end() as u64)
624        .ok_or_else(|| {
625            LegFailure::Pack(format!(
626                "the pack entry at {offset} reads outside the pack data file"
627            ))
628        })?;
629
630    let staged_object = match entry.header {
631        PackHeader::Blob | PackHeader::Tree | PackHeader::Commit | PackHeader::Tag => {
632            let kind = entry.header.as_kind().ok_or_else(|| {
633                LegFailure::Pack(format!("the pack entry at {offset} carries no object kind"))
634            })?;
635            let body = inflate_slice(compressed, declared, cap)?;
636            let oid = rederive_object(kind, &body)?;
637            put_in_batch(front, batch, kind, &body, oid)?;
638            Some((oid, kind))
639        }
640        PackHeader::OfsDelta { base_distance } => {
641            let base_offset = entry
642                .checked_base_pack_offset(base_distance)
643                .ok_or_else(|| {
644                    LegFailure::Pack(format!(
645                        "the ofs delta at pack offset {offset} names an impossible base \
646                         distance {base_distance}"
647                    ))
648                })?;
649            let Some((base_oid, base_kind)) = staged.get(&base_offset).copied() else {
650                return Err(LegFailure::Pack(format!(
651                    "the ofs delta's base at distance {base_distance} does not resolve \
652                     within the pack"
653                )));
654            };
655            let (_, base_body) = base_object(front, base_oid, base_kind)?;
656            let payload = inflate_slice(compressed, declared, cap)?;
657            let body = apply_delta(&base_body, &payload, cap)?;
658            let oid = rederive_object(base_kind, &body)?;
659            put_in_batch(front, batch, base_kind, &body, oid)?;
660            Some((oid, base_kind))
661        }
662        PackHeader::RefDelta { base_id } => {
663            let base_offset = match bundle.index.lookup(base_id) {
664                Some(base_index) => bundle.index.pack_offset_at_index(base_index),
665                None => {
666                    return Err(LegFailure::Pack(format!(
667                        "the delta base {base_id} is neither in the pack nor in the repository"
668                    )));
669                }
670            };
671            match staged.get(&base_offset).copied() {
672                Some((base_oid, base_kind)) => {
673                    let (_, base_body) = base_object(front, base_oid, base_kind)?;
674                    let payload = inflate_slice(compressed, declared, cap)?;
675                    let body = apply_delta(&base_body, &payload, cap)?;
676                    let oid = rederive_object(base_kind, &body)?;
677                    put_in_batch(front, batch, base_kind, &body, oid)?;
678                    Some((oid, base_kind))
679                }
680                None => {
681                    parked.push((offset, base_id));
682                    None
683                }
684            }
685        }
686    };
687    if let Some((oid, kind)) = staged_object {
688        staged.insert(offset, (oid, kind));
689    }
690    Ok(())
691}
692
693/// The full-graph connectivity walk from the pushed tips: commits walk
694/// parents and trees, trees walk entries, tags walk targets, blobs
695/// close the walk (the owning implementation walks — `gix-fsck`'s
696/// `Connectivity::check_commit` does not walk parents). A missing
697/// closure member is the ng leg's failure naming the object; a
698/// corruption found on the walk is the same class.
699fn connectivity_leg(front: &ObjectFront, tips: &[ObjectId]) -> Result<(), LegFailure> {
700    let mut seen: HashSet<ObjectId> = HashSet::new();
701    let mut stack: Vec<ObjectId> = tips.to_vec();
702    while let Some(oid) = stack.pop() {
703        if !seen.insert(oid) {
704            continue;
705        }
706        let (kind, body) = {
707            let found = front.block_on(front.get(&oid));
708            match found {
709                Ok(None) => return Err(LegFailure::Pack(format!("missing object {oid}"))),
710                Err(error) => return Err(store_read_failure(&oid, error)),
711                Ok(Some((kind, body))) => (kind, body),
712            }
713        };
714        match kind {
715            Kind::Commit => {
716                let commit = gix_object::CommitRef::from_bytes(&body, gix_hash::Kind::Sha1)
717                    .map_err(|error| LegFailure::Pack(format!("corrupt commit {oid}: {error}")))?;
718                stack.push(commit.tree());
719                for parent in commit.parents() {
720                    stack.push(parent);
721                }
722            }
723            Kind::Tree => {
724                let tree = gix_object::TreeRef::from_bytes(&body, gix_hash::Kind::Sha1)
725                    .map_err(|error| LegFailure::Pack(format!("corrupt tree {oid}: {error}")))?;
726                for entry in &tree.entries {
727                    stack.push(entry.oid.into());
728                }
729            }
730            Kind::Tag => {
731                let tag = gix_object::TagRef::from_bytes(&body, gix_hash::Kind::Sha1)
732                    .map_err(|error| LegFailure::Pack(format!("corrupt tag {oid}: {error}")))?;
733                stack.push(tag.target());
734            }
735            Kind::Blob => {}
736        }
737    }
738    Ok(())
739}
740
741#[async_trait]
742impl GitPackIngest for PackIngest {
743    /// Unpack + index + connectivity; validates and prepares the ref
744    /// updates, never applies them. Pack-level validation failures
745    /// land in [`PreparedPush::unpack`]'s ng leg — parse, budget, the
746    /// decompressed-size cap, thin-base and delta-base resolution,
747    /// connectivity; a backing-store corruption or a
748    /// working-area/task infrastructure failure is the
749    /// [`StorageError`]. `updates` returns verbatim on every path. The
750    /// successful legs retain the unpacked working set's [`Batch`]
751    /// (the pack bridge's custody) into the pending-pin registry,
752    /// content-correlated with the prepared list — the ref CAS's
753    /// drain commits it after the CAS commits.
754    async fn prepare(
755        &self,
756        repo: &RepoRecord,
757        updates: Vec<RefUpdate>,
758        pack: Box<dyn io::Read + Send>,
759        _push_options: Option<PushOptions>,
760        limits: Limits,
761    ) -> Result<PreparedPush, StorageError> {
762        let nanos = SystemTime::now()
763            .duration_since(UNIX_EPOCH)
764            .map(|duration| duration.as_nanos())
765            .unwrap_or_default();
766        let dir = self.root.join(format!(
767            "ingest-{}-{}-{}",
768            std::process::id(),
769            nanos,
770            WORK_AREA_COUNTER.fetch_add(1, Ordering::Relaxed)
771        ));
772        tokio::fs::create_dir_all(&dir).await.map_err(|error| {
773            StorageError::Io(format!("the working area failed to create: {error}"))
774        })?;
775        let _area = WorkArea(dir.clone());
776
777        let indexed = {
778            let front = self.front.clone();
779            let dir = dir.clone();
780            let thin = front.session();
781            let budget = limits.max_pack_size;
782            let cap = limits.max_object_size;
783            tokio::task::spawn_blocking(move || index_leg(pack, dir, thin, budget, cap))
784                .await
785                .map_err(|join| {
786                    StorageError::Io(format!("the pack indexing task failed: {join}"))
787                })?
788        };
789
790        match indexed {
791            Ok(None) => {
792                let front = self.front.clone();
793                let tips: Vec<ObjectId> = updates.iter().filter_map(|update| update.new).collect();
794                let verdict = tokio::task::spawn_blocking(move || connectivity_leg(&front, &tips))
795                    .await
796                    .map_err(|join| {
797                        StorageError::Io(format!("the pack connectivity task failed: {join}"))
798                    })?;
799                match verdict {
800                    Ok(()) => {
801                        self.pins.retain(
802                            &repo.repo_id,
803                            updates.clone(),
804                            PinCustody::Pins(Vec::new()),
805                        );
806                        Ok(PreparedPush {
807                            unpack: Ok(()),
808                            updates,
809                        })
810                    }
811                    Err(LegFailure::Pack(reason)) => Ok(PreparedPush {
812                        unpack: Err(reason),
813                        updates,
814                    }),
815                    Err(LegFailure::Backing(error)) => Err(error),
816                }
817            }
818            Ok(Some(pack_and_index)) => {
819                let front = self.front.clone();
820                let tips: Vec<ObjectId> = updates.iter().filter_map(|update| update.new).collect();
821                let cap = limits.max_object_size;
822                let leg = move || {
823                    let (_pack_path, index_path) = pack_and_index;
824                    ingest_leg(front, index_path, tips, cap)
825                };
826                let verdict = tokio::task::spawn_blocking(leg).await.map_err(|join| {
827                    StorageError::Io(format!("the pack ingestion task failed: {join}"))
828                })?;
829                match verdict {
830                    Ok(batch) => {
831                        self.pins
832                            .retain(&repo.repo_id, updates.clone(), PinCustody::Batch(batch));
833                        Ok(PreparedPush {
834                            unpack: Ok(()),
835                            updates,
836                        })
837                    }
838                    Err(LegFailure::Pack(reason)) => Ok(PreparedPush {
839                        unpack: Err(reason),
840                        updates,
841                    }),
842                    Err(LegFailure::Backing(error)) => Err(error),
843                }
844            }
845            Err(reason) => Ok(PreparedPush {
846                unpack: Err(reason),
847                updates,
848            }),
849        }
850    }
851}
852
853#[cfg(test)]
854mod tests {
855    use std::collections::BTreeMap;
856
857    use crate::testutil::record as repo;
858    use crate::testutil::scratch_dir as scratch_root;
859    use std::path::Path;
860    use std::process::Stdio;
861    use std::sync::atomic::AtomicU64;
862    use std::sync::Arc;
863
864    use alkcall::core::Identity;
865    use gix_hash::Kind as HashKind;
866    use gix_hash::ObjectId;
867    use gix_object::Kind;
868    use gix_pack::data::entry::Header as PackHeader;
869    use gix_pack::data::input::Entry as PackEntry;
870    use tokio::net::TcpListener;
871    use tokio::sync::Semaphore;
872
873    use alkgit::{
874        serve_duplex_resolved, AccessAction, GitPackGen, GitPackIngest, GitRefs, GitRegistry,
875        GitRegistryStore, Limits, Service, Visibility,
876    };
877
878    use super::*;
879    use crate::liveness::RefClosure;
880    use crate::pins::{CommittedRefs, MemMutationLegs};
881    use crate::reactor::Reactor;
882    use crate::refs::InMemoryRefTable;
883    use crate::registry::{
884        CachedRegistryProvider, InMemoryRegistry, RegistryBackend, RegistryWrite,
885    };
886
887    fn up(name: &str, expected: Option<u8>, new: Option<u8>) -> RefUpdate {
888        RefUpdate {
889            name: name.to_owned(),
890            expected: expected.map(|byte| ObjectId::from([byte; 20])),
891            new: new.map(|byte| ObjectId::from([byte; 20])),
892        }
893    }
894
895    fn up_oid(name: &str, expected: Option<ObjectId>, new: Option<ObjectId>) -> RefUpdate {
896        RefUpdate {
897            name: name.to_owned(),
898            expected,
899            new,
900        }
901    }
902
903    fn mem_front() -> ObjectFront {
904        ObjectFront::new(Arc::new(alkblobs::Store::mem_only()))
905    }
906
907    async fn put_object(front: &ObjectFront, kind: Kind, content: &[u8]) -> ObjectId {
908        let (key, _guard) = front.put(kind, content).await.unwrap();
909        key_to_oid(&key).expect("the put's key is the family key")
910    }
911
912    fn tree_bytes(entries: &[(&str, &ObjectId)]) -> Vec<u8> {
913        let mut tree = Vec::new();
914        for (name, oid) in entries {
915            tree.extend_from_slice(format!("100644 {name}\0").as_bytes());
916            tree.extend_from_slice(oid.as_bytes());
917        }
918        tree
919    }
920
921    fn commit_bytes(tree: &ObjectId, parents: &[&ObjectId]) -> Vec<u8> {
922        let mut body = format!("tree {tree}\n");
923        for parent in parents {
924            body.push_str(&format!("parent {parent}\n"));
925        }
926        body.push_str(
927            "author A <a@example.invalid> 1750000000 +0000\n\
928             committer A <a@example.invalid> 1750000000 +0000\n\nthe message\n",
929        );
930        body.into_bytes()
931    }
932
933    async fn push(
934        ingest: &PackIngest,
935        repo: &RepoRecord,
936        updates: Vec<RefUpdate>,
937        pack: Vec<u8>,
938    ) -> PreparedPush {
939        ingest
940            .prepare(
941                repo,
942                updates,
943                Box::new(std::io::Cursor::new(pack)),
944                None,
945                Limits::default(),
946            )
947            .await
948            .expect("the pack-level session never errors here")
949    }
950
951    /// The test sink: a shared buffer behind the boxed write seam.
952    #[derive(Clone, Default)]
953    struct BytesSink(Arc<parking_lot::Mutex<Vec<u8>>>);
954
955    impl BytesSink {
956        fn bytes(&self) -> Vec<u8> {
957            self.0.lock().clone()
958        }
959    }
960
961    impl std::io::Write for BytesSink {
962        fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
963            self.0.lock().extend_from_slice(buf);
964            Ok(buf.len())
965        }
966
967        fn flush(&mut self) -> std::io::Result<()> {
968            Ok(())
969        }
970    }
971
972    async fn generate_pack(front: &ObjectFront, wants: Vec<ObjectId>) -> Vec<u8> {
973        let packgen = crate::packgen::PackGen::direct(front.clone());
974        let sink = BytesSink::default();
975        packgen
976            .generate(
977                &repo("origin"),
978                wants,
979                Vec::new(),
980                Limits::default(),
981                Box::new(sink.clone()),
982            )
983            .await
984            .expect("the fixture pack generates");
985        sink.bytes()
986    }
987
988    struct Fixture {
989        blob: ObjectId,
990        tree_base: ObjectId,
991        tree_tip: ObjectId,
992        base: ObjectId,
993        tip: ObjectId,
994        tag: ObjectId,
995    }
996
997    async fn build_fixture(front: &ObjectFront) -> Fixture {
998        let blob = put_object(front, Kind::Blob, b"hello\n").await;
999        let second = put_object(front, Kind::Blob, b"second\n").await;
1000        let tree_base = put_object(front, Kind::Tree, &tree_bytes(&[("hello", &blob)])).await;
1001        let tree_tip = put_object(
1002            front,
1003            Kind::Tree,
1004            &tree_bytes(&[("hello", &blob), ("second", &second)]),
1005        )
1006        .await;
1007        let base = put_object(front, Kind::Commit, &commit_bytes(&tree_base, &[])).await;
1008        let tip = put_object(front, Kind::Commit, &commit_bytes(&tree_tip, &[&base])).await;
1009        let tag = put_object(front, Kind::Tag, &tag_bytes(&base)).await;
1010        Fixture {
1011            blob,
1012            tree_base,
1013            tree_tip,
1014            base,
1015            tip,
1016            tag,
1017        }
1018    }
1019
1020    fn tag_bytes(target: &ObjectId) -> Vec<u8> {
1021        format!(
1022            "object {target}\ntype commit\ntag v1.0.0\n\
1023             tagger A <a@example.invalid> 1750000000 +0000\n\nannotated\n"
1024        )
1025        .into_bytes()
1026    }
1027
1028    fn deflate_bytes(bytes: &[u8]) -> Vec<u8> {
1029        let mut deflate =
1030            gix_zlib::stream::deflate::Write::new(Vec::new(), gix_zlib::Compression::DEFAULT);
1031        std::io::Write::write_all(&mut deflate, bytes).expect("fixture bytes deflate");
1032        std::io::Write::flush(&mut deflate).expect("the fixture deflate stream finishes");
1033        deflate.into_inner()
1034    }
1035
1036    fn pack_entry_base(kind: Kind, body: &[u8], pack_offset: u64) -> PackEntry {
1037        let header = match kind {
1038            Kind::Blob => PackHeader::Blob,
1039            Kind::Tree => PackHeader::Tree,
1040            Kind::Commit => PackHeader::Commit,
1041            Kind::Tag => PackHeader::Tag,
1042        };
1043        let compressed = deflate_bytes(body);
1044        PackEntry {
1045            header,
1046            header_size: header.size(body.len() as u64) as u16,
1047            pack_offset,
1048            compressed: Some(compressed.clone()),
1049            compressed_size: compressed.len() as u64,
1050            crc32: None,
1051            decompressed_size: body.len() as u64,
1052            trailer: None,
1053        }
1054    }
1055
1056    fn pack_entry_delta(header: PackHeader, payload: &[u8], pack_offset: u64) -> PackEntry {
1057        let compressed = deflate_bytes(payload);
1058        PackEntry {
1059            header,
1060            header_size: header.size(payload.len() as u64) as u16,
1061            pack_offset,
1062            compressed: Some(compressed.clone()),
1063            compressed_size: compressed.len() as u64,
1064            crc32: None,
1065            decompressed_size: payload.len() as u64,
1066            trailer: None,
1067        }
1068    }
1069
1070    fn pack_bytes(entries: &[PackEntry]) -> Vec<u8> {
1071        let mut out = Vec::new();
1072        out.extend_from_slice(&gix_pack::data::header::encode(
1073            gix_pack::data::Version::V2,
1074            entries.len() as u32,
1075        ));
1076        for entry in entries {
1077            entry
1078                .header
1079                .write_to(entry.decompressed_size, &mut out)
1080                .expect("fixture entry headers write");
1081            out.extend_from_slice(
1082                entry
1083                    .compressed
1084                    .as_ref()
1085                    .expect("fixture entries keep bytes"),
1086            );
1087        }
1088        let mut hasher = gix_hash::hasher(HashKind::Sha1);
1089        hasher.update(&out);
1090        let digest = hasher
1091            .try_finalize()
1092            .expect("fixture pack trailers hash over the pack bytes");
1093        out.extend_from_slice(digest.as_slice());
1094        out
1095    }
1096
1097    fn delta_size_varint(size: u64) -> Vec<u8> {
1098        let mut out = Vec::new();
1099        let mut rest = size;
1100        loop {
1101            let mut byte = rest as u8 & 0x7f;
1102            rest >>= 7;
1103            if rest != 0 {
1104                byte |= 0x80;
1105            }
1106            out.push(byte);
1107            if rest == 0 {
1108                return out;
1109            }
1110        }
1111    }
1112
1113    fn delta_stream(base_size: u64, result: &[u8]) -> Vec<u8> {
1114        let mut out = delta_size_varint(base_size);
1115        out.extend(delta_size_varint(base_size + result.len() as u64));
1116        if base_size > 0 {
1117            let mut cmd = 0x80u8;
1118            let mut size_bytes = Vec::new();
1119            let mut rest = base_size;
1120            for bit in [0x10u8, 0x20, 0x40] {
1121                if rest & 0xff != 0 {
1122                    cmd |= bit;
1123                    size_bytes.push(rest as u8);
1124                }
1125                rest >>= 8;
1126            }
1127            out.push(cmd);
1128            out.extend_from_slice(&size_bytes);
1129        }
1130        for chunk in result.chunks(127) {
1131            out.push(chunk.len() as u8);
1132            out.extend_from_slice(chunk);
1133        }
1134        out
1135    }
1136
1137    fn hostile_delta_stream(base_size: u64, declared_result: u64) -> Vec<u8> {
1138        let mut out = delta_size_varint(base_size);
1139        out.extend(delta_size_varint(declared_result));
1140        out
1141    }
1142
1143    /// The liveness source over the (empty refs) table — the
1144    /// sweep-driving registration.
1145    fn closure_of(front: &ObjectFront, rows: &Arc<InMemoryRefTable>) -> Arc<RefClosure> {
1146        RefClosure::new(
1147            front.clone(),
1148            rows.clone() as Arc<dyn crate::liveness::RefRows>,
1149            Arc::new(AtomicU64::new(0)),
1150        )
1151    }
1152
1153    /// The ordinary path end-to-end in-process: a generated pack for a
1154    /// fixture chain ingests into a fresh store, every object verified
1155    /// under its git oid, the working set's batch retained into the
1156    /// pending-pin registry (the custody holding across a sweep), the
1157    /// ref CAS drains it after committing (the legs ride), and the
1158    /// working area is gone on the way out.
1159    #[tokio::test(flavor = "multi_thread")]
1160    async fn an_ordinary_pack_ingests_pins_and_discards_its_area() {
1161        let temp = scratch_root("ingest", "ordinary");
1162        let origin = mem_front();
1163        let fx = build_fixture(&origin).await;
1164        let pack = generate_pack(&origin, vec![fx.tip, fx.tag]).await;
1165        assert!(pack.starts_with(b"PACK"));
1166
1167        let front = mem_front();
1168        let pins = Arc::new(PendingPinRegistry::new());
1169        let ingest = PackIngest::new(front.clone(), pins.clone(), temp.join("work"));
1170        let updates = vec![
1171            up_oid("refs/heads/main", None, Some(fx.tip)),
1172            up_oid("refs/tags/v1", None, Some(fx.tag)),
1173        ];
1174        let prepared = push(&ingest, &repo("r"), updates.clone(), pack).await;
1175        assert!(prepared.unpack.is_ok(), "{:?}", prepared.unpack);
1176        assert_eq!(prepared.updates, updates, "verbatim on the ok path");
1177        assert_eq!(pins.pending_count("r"), 1, "the custody retained");
1178
1179        for oid in [fx.tip, fx.base, fx.tag, fx.blob, fx.tree_base, fx.tree_tip] {
1180            assert!(
1181                front.stat(&oid).await.unwrap().is_some(),
1182                "object {oid} landed and stats"
1183            );
1184        }
1185
1186        let store = front.store().clone();
1187        let rows = Arc::new(InMemoryRefTable::new());
1188        let source = closure_of(&front, &rows);
1189        source.register(&store).expect("the closure registers");
1190        let report = store.sweep().await.expect("the sweep runs");
1191        assert_eq!(
1192            report.deleted, 0,
1193            "the retained custody holds the push's objects across the sweep window"
1194        );
1195
1196        let durable = Arc::new(InMemoryRegistry::new());
1197        durable.put(&repo("r")).await.expect("the repo registers");
1198        let provider = Arc::new(CachedRegistryProvider::new());
1199        let mem_store = Arc::new(alkstore_mem::MemStore::new());
1200        let reactor = Arc::new(Reactor::new(
1201            mem_store.clone(),
1202            durable.clone(),
1203            provider.clone(),
1204        ));
1205        let legs = Arc::new(MemMutationLegs::new(durable.clone(), reactor));
1206        let inner: Arc<dyn GitRefs> = rows.clone();
1207        let wrapped = CommittedRefs::new(inner, pins.clone(), legs);
1208        let outcomes = wrapped
1209            .apply_updates(&repo("r"), updates.clone(), false)
1210            .await
1211            .expect("the CAS commits");
1212        assert!(outcomes.iter().all(|outcome| outcome.result.is_ok()));
1213        assert_eq!(pins.pending_count("r"), 0, "the entry drained post-CAS");
1214        let watermark = durable.watermark().await.expect("the watermark reads");
1215        assert_eq!(
1216            watermark, 2,
1217            "the marker write anchored the watermark (1 = the put)"
1218        );
1219
1220        let work = temp.join("work");
1221        assert!(
1222            dir_is_empty(&work),
1223            "the working area is discarded (its root may remain, empty)"
1224        );
1225        let _ = tokio::fs::remove_dir_all(&temp).await;
1226    }
1227
1228    /// The thin push: one ref-delta entry whose base exists only in the
1229    /// store (the default thin-pack behavior) resolves through the
1230    /// substrate Find; the pack's own object lands under its oid.
1231    #[tokio::test]
1232    async fn a_thin_pack_resolves_its_bases_through_the_find() {
1233        let temp = scratch_root("ingest", "thin");
1234        let front = mem_front();
1235        let base_body = b"base\n".as_slice();
1236        let base_oid = put_object(&front, Kind::Blob, base_body).await;
1237        let delta = delta_stream(base_body.len() as u64, b"-delta");
1238        let result_body = {
1239            let mut body = base_body.to_vec();
1240            body.extend_from_slice(b"-delta");
1241            body
1242        };
1243        let result_oid = gix_object::compute_hash(HashKind::Sha1, Kind::Blob, &result_body)
1244            .expect("the fixture result hashes");
1245        let entry = pack_entry_delta(PackHeader::RefDelta { base_id: base_oid }, &delta, 0);
1246        let pack = pack_bytes(&[entry]);
1247
1248        let pins = Arc::new(PendingPinRegistry::new());
1249        let ingest = PackIngest::new(front.clone(), pins.clone(), temp.join("work"));
1250        let updates = vec![up_oid("refs/heads/main", None, Some(result_oid))];
1251        let prepared = push(&ingest, &repo("r"), updates.clone(), pack).await;
1252        assert!(
1253            prepared.unpack.is_ok(),
1254            "the thin base resolves through the Find: {:?}",
1255            prepared.unpack
1256        );
1257        assert_eq!(prepared.updates, updates);
1258        assert_eq!(pins.pending_count("r"), 1, "the custody retained");
1259        assert!(
1260            front.stat(&result_oid).await.unwrap().is_some(),
1261            "the pack's own object landed"
1262        );
1263        assert!(
1264            front.stat(&base_oid).await.unwrap().is_some(),
1265            "the thin base's normalized entry landed through the store's dedup"
1266        );
1267        let _ = tokio::fs::remove_dir_all(&temp).await;
1268    }
1269
1270    fn dir_is_empty(path: &Path) -> bool {
1271        match std::fs::read_dir(path) {
1272            Ok(mut entries) => entries.next().is_none(),
1273            Err(_) => true,
1274        }
1275    }
1276
1277    /// The empty pack (zero objects) is valid and reports as such: the
1278    /// trailer the machinery never reaches is verified by the leg
1279    /// itself, the push reports ok, the tips' connectivity closes over
1280    /// the store, and the custody retains the (empty) prepared list.
1281    #[tokio::test]
1282    async fn the_empty_pack_is_valid_and_reports_as_such() {
1283        let temp = scratch_root("ingest", "empty-pack");
1284        let front = mem_front();
1285        let tip = put_object(&front, Kind::Blob, b"already here\n").await;
1286        let pack = pack_bytes(&[]);
1287
1288        let pins = Arc::new(PendingPinRegistry::new());
1289        let ingest = PackIngest::new(front.clone(), pins.clone(), temp.join("work"));
1290        let updates = vec![up_oid("refs/heads/main", None, Some(tip))];
1291        let prepared = push(&ingest, &repo("r"), updates.clone(), pack).await;
1292        assert!(prepared.unpack.is_ok(), "{:?}", prepared.unpack);
1293        assert_eq!(prepared.updates, updates, "verbatim on the empty path");
1294        assert_eq!(pins.pending_count("r"), 1, "the empty custody retained");
1295        assert!(dir_is_empty(&temp.join("work")), "nothing was written");
1296        let _ = tokio::fs::remove_dir_all(&temp).await;
1297    }
1298
1299    /// The zero-object pack's *trailer* is itself invalid: the leg's
1300    /// own verification fails it into the ng leg (the machinery is
1301    /// never reached), never `Err`, updates verbatim, nothing retained.
1302    #[tokio::test]
1303    async fn a_zero_pack_with_a_bad_trailer_fails_into_unpack_ng() {
1304        let temp = scratch_root("ingest", "empty-bad");
1305        let front = mem_front();
1306        let mut pack = Vec::new();
1307        pack.extend_from_slice(&gix_pack::data::header::encode(
1308            gix_pack::data::Version::V2,
1309            0,
1310        ));
1311        pack.extend_from_slice(&[7u8; 20]);
1312
1313        let pins = Arc::new(PendingPinRegistry::new());
1314        let ingest = PackIngest::new(front.clone(), pins.clone(), temp.join("work"));
1315        let updates = vec![up("refs/heads/main", None, None)];
1316        let prepared = push(&ingest, &repo("r"), updates.clone(), pack).await;
1317        let reason = prepared.unpack.expect_err("the bad trailer fails the pack");
1318        assert!(!reason.is_empty(), "the ng reason is server-chosen");
1319        assert_eq!(prepared.updates, updates, "verbatim on the ng path");
1320        assert_eq!(pins.pending_count("r"), 0, "nothing retained on ng");
1321        let _ = tokio::fs::remove_dir_all(&temp).await;
1322    }
1323
1324    /// A pack entry whose *declared* decompressed size is hostile (a
1325    /// bomb-shaped declaration far above anything the compressed
1326    /// stream can produce) fails into the `unpack` ng leg — the
1327    /// doubles' hostile-declared-size row re-landed over the real
1328    /// machinery: the declaration never reaches an allocation, `Err`
1329    /// stays unreachable, nothing lands, nothing is retained.
1330    #[tokio::test]
1331    async fn a_hostile_declared_decompressed_size_fails_into_unpack_ng() {
1332        let temp = scratch_root("ingest", "hostile-declared");
1333        let front = mem_front();
1334        let mut hostile = pack_entry_base(Kind::Blob, b"", 0);
1335        hostile.decompressed_size = 1u64 << 40;
1336        let pack = pack_bytes(&[hostile]);
1337
1338        let pins = Arc::new(PendingPinRegistry::new());
1339        let ingest = PackIngest::new(front.clone(), pins.clone(), temp.join("work"));
1340        let updates = vec![up_oid(
1341            "refs/heads/main",
1342            None,
1343            Some(ObjectId::from([6u8; 20])),
1344        )];
1345        let prepared = push(&ingest, &repo("r"), updates.clone(), pack).await;
1346        let reason = prepared
1347            .unpack
1348            .expect_err("the hostile declaration fails into the ng leg");
1349        assert!(
1350            !reason.is_empty(),
1351            "the ng reason is server-chosen: {reason}"
1352        );
1353        assert_eq!(prepared.updates, updates, "verbatim on the ng path");
1354        assert_eq!(pins.pending_count("r"), 0, "nothing retained on ng");
1355        let _ = tokio::fs::remove_dir_all(&temp).await;
1356    }
1357
1358    /// A delta stream whose declared materialized result exceeds the
1359    /// ingestion cap fails into the `unpack` ng leg — the applier's
1360    /// cap check names the decompressed-size cap before any result
1361    /// bytes accumulate — never `Err`, nothing lands, nothing retained.
1362    #[tokio::test]
1363    async fn a_delta_declaring_a_result_beyond_the_cap_fails_into_unpack_ng() {
1364        let temp = scratch_root("ingest", "delta-cap");
1365        let front = mem_front();
1366        let base_body = b"delta-base".to_vec();
1367        let base_entry = pack_entry_base(Kind::Blob, &base_body, 0);
1368        let delta_entry = pack_entry_delta(
1369            PackHeader::OfsDelta {
1370                base_distance: base_entry.bytes_in_pack(),
1371            },
1372            &hostile_delta_stream(base_body.len() as u64, 200_000),
1373            base_entry.bytes_in_pack(),
1374        );
1375        let pack = pack_bytes(&[base_entry, delta_entry]);
1376        let pins = Arc::new(PendingPinRegistry::new());
1377        let ingest = PackIngest::new(front.clone(), pins.clone(), temp.join("work"));
1378        let limits = Limits {
1379            max_object_size: 65536,
1380            ..Limits::default()
1381        };
1382        let updates = vec![up_oid(
1383            "refs/heads/main",
1384            None,
1385            Some(ObjectId::from([7u8; 20])),
1386        )];
1387        let prepared = ingest
1388            .prepare(
1389                &repo("r"),
1390                updates.clone(),
1391                Box::new(std::io::Cursor::new(pack)),
1392                None,
1393                limits,
1394            )
1395            .await
1396            .unwrap();
1397        let reason = prepared
1398            .unpack
1399            .expect_err("the hostile delta result fails into the ng leg");
1400        assert!(
1401            reason.contains("fit in memory") || reason.contains("cap"),
1402            "the machinery's vet (or the applier's cap check) carries the reason: {reason}"
1403        );
1404        assert_eq!(prepared.updates, updates, "verbatim on the ng path");
1405        assert_eq!(pins.pending_count("r"), 0, "nothing retained on ng");
1406        let _ = tokio::fs::remove_dir_all(&temp).await;
1407    }
1408
1409    /// The unpack leg's own incremental inflate: a stream that
1410    /// *produces* beyond the cap (a lying-small declaration cannot
1411    /// overrun — the machinery catches the mismatch — so the row
1412    /// drives the applier directly) aborts at the cap naming it; the
1413    /// produced bytes never exceed the cap plus one chunk.
1414    #[tokio::test]
1415    async fn the_incremental_inflate_aborts_at_the_cap() {
1416        let big = vec![0u8; 200_000];
1417        let compressed = deflate_bytes(&big);
1418        let error = inflate_slice(&compressed, big.len() as u64, 65536)
1419            .expect_err("the stream's production exceeds the cap");
1420        assert!(error.contains("cap"), "the reason names the cap: {error}");
1421    }
1422
1423    /// The unpack leg's own delta applier: a *lying* declaration (the
1424    /// declared result stays under the cap but the instructions
1425    /// produce more) aborts as produced-beyond-declared; a hostile
1426    /// declaration beyond the cap aborts on the declaration before any
1427    /// produced bytes.
1428    #[tokio::test]
1429    async fn the_delta_applier_vets_the_result_incrementally() {
1430        let base = b"0123456789".to_vec();
1431        let mut delta = delta_size_varint(base.len() as u64);
1432        delta.extend(delta_size_varint(20));
1433        delta.push(0x90);
1434        delta.push(10);
1435        let mut instructions = vec![15u8];
1436        instructions.extend_from_slice(&[b'X'; 15]);
1437        delta.extend_from_slice(&instructions);
1438        let error = apply_delta(&base, &delta, 1024).expect_err("overproducing fails");
1439        assert!(
1440            error.contains("declared"),
1441            "the reason names the declaration: {error}"
1442        );
1443        let error = apply_delta(
1444            &base,
1445            &hostile_delta_stream(base.len() as u64, 1 << 20),
1446            1024,
1447        )
1448        .expect_err("the hostile declaration fails on the cap");
1449        assert!(
1450            error.contains("cap"),
1451            "the reason names the decompressed-size cap: {error}"
1452        );
1453    }
1454
1455    /// The in-pack *forward* ref-delta: the base later in the pack is
1456    /// parked in the unpack leg's fixpoint and resolves once staged.
1457    #[tokio::test]
1458    async fn an_in_pack_forward_ref_delta_resolves() {
1459        let temp = scratch_root("ingest", "forward-ref");
1460        let front = mem_front();
1461        let base_body = b"the later base\n".to_vec();
1462        let delta_result = {
1463            let mut body = base_body.clone();
1464            body.extend_from_slice(b"-delta");
1465            body
1466        };
1467        let delta_oid = gix_object::compute_hash(HashKind::Sha1, Kind::Blob, &delta_result)
1468            .expect("the fixture hashes");
1469        let base_oid = gix_object::compute_hash(HashKind::Sha1, Kind::Blob, &base_body)
1470            .expect("the fixture hashes");
1471        let delta_payload = delta_stream(base_body.len() as u64, b"-delta");
1472        let delta = pack_entry_delta(
1473            PackHeader::RefDelta { base_id: base_oid },
1474            &delta_payload,
1475            0,
1476        );
1477        let base = pack_entry_base(Kind::Blob, &base_body, delta.bytes_in_pack());
1478        let pack = pack_bytes(&[delta, base]);
1479
1480        let pins = Arc::new(PendingPinRegistry::new());
1481        let ingest = PackIngest::new(front.clone(), pins.clone(), temp.join("work"));
1482        let updates = vec![up_oid("refs/heads/main", None, Some(delta_oid))];
1483        let prepared = push(&ingest, &repo("r"), updates.clone(), pack).await;
1484        assert!(
1485            prepared.unpack.is_ok(),
1486            "the forward ref-delta parks and resolves: {:?}",
1487            prepared.unpack
1488        );
1489        assert!(front.stat(&delta_oid).await.unwrap().is_some());
1490        assert!(front.stat(&base_oid).await.unwrap().is_some());
1491        let _ = tokio::fs::remove_dir_all(&temp).await;
1492    }
1493
1494    /// An ofs-delta pack resolves backward in one pass: the delta's
1495    /// materialized result re-derives to git's oid and lands.
1496    #[tokio::test]
1497    async fn an_ofs_delta_pack_resolves_backward() {
1498        let temp = scratch_root("ingest", "ofs");
1499        let front = mem_front();
1500        let base_body = b"the base body\n".to_vec();
1501        let result_body = {
1502            let mut body = base_body.clone();
1503            body.extend_from_slice(b"-delta");
1504            body
1505        };
1506        let result_oid = gix_object::compute_hash(HashKind::Sha1, Kind::Blob, &result_body)
1507            .expect("the fixture hashes");
1508        let base_entry = pack_entry_base(Kind::Blob, &base_body, 0);
1509        let delta = pack_entry_delta(
1510            PackHeader::OfsDelta {
1511                base_distance: base_entry.bytes_in_pack(),
1512            },
1513            &delta_stream(base_body.len() as u64, b"-delta"),
1514            base_entry.bytes_in_pack(),
1515        );
1516        let pack = pack_bytes(&[base_entry, delta]);
1517
1518        let pins = Arc::new(PendingPinRegistry::new());
1519        let ingest = PackIngest::new(front.clone(), pins.clone(), temp.join("work"));
1520        let updates = vec![up_oid("refs/heads/main", None, Some(result_oid))];
1521        let prepared = push(&ingest, &repo("r"), updates.clone(), pack).await;
1522        assert!(
1523            prepared.unpack.is_ok(),
1524            "the ofsdelta resolves: {:?}",
1525            prepared.unpack
1526        );
1527        assert!(front.stat(&result_oid).await.unwrap().is_some());
1528        let _ = tokio::fs::remove_dir_all(&temp).await;
1529    }
1530
1531    /// The connectivity walk is the whole closure over the store
1532    /// (parents included): a commit whose tree references a ghost
1533    /// fails into the `unpack` ng leg naming the missing member; the
1534    /// abandoned batch's objects are sweep-eligible (a failed push is
1535    /// a failed push; a sweep reclaims them GC-decreed).
1536    #[tokio::test(flavor = "multi_thread")]
1537    async fn a_missing_closure_member_fails_the_connectivity_ng() {
1538        let temp = scratch_root("ingest", "connectivity");
1539        let front = mem_front();
1540        let ghost = ObjectId::from([0xE9u8; 20]);
1541        let commit = put_object(&front, Kind::Commit, &commit_bytes(&ghost, &[])).await;
1542        let pack = pack_bytes(&[pack_entry_base(Kind::Commit, &commit_bytes(&ghost, &[]), 0)]);
1543        let pins = Arc::new(PendingPinRegistry::new());
1544        let ingest = PackIngest::new(front.clone(), pins.clone(), temp.join("work"));
1545        let updates = vec![up_oid("refs/heads/main", None, Some(commit))];
1546        let prepared = push(&ingest, &repo("r"), updates.clone(), pack).await;
1547        let reason = prepared
1548            .unpack
1549            .expect_err("the missing tree fails the walk");
1550        assert!(
1551            reason.contains("missing object"),
1552            "the reason names the missing member: {reason}"
1553        );
1554        assert_eq!(prepared.updates, updates, "verbatim on the ng path");
1555        assert_eq!(pins.pending_count("r"), 0, "nothing retained on ng");
1556
1557        let store = front.store().clone();
1558        let rows = Arc::new(InMemoryRefTable::new());
1559        let source = closure_of(&front, &rows);
1560        source.register(&store).expect("the closure registers");
1561        let report = store.sweep().await.expect("the sweep runs");
1562        assert_eq!(report.deleted, 1, "the abandoned batch's objects recover");
1563        assert!(front.stat(&commit).await.unwrap().is_none());
1564        let _ = tokio::fs::remove_dir_all(&temp).await;
1565    }
1566
1567    /// Garbage, truncated, and digest-corrupt pack streams all fail
1568    /// into the `unpack` ng leg (server-chosen reasons, never `Err`,
1569    /// updates verbatim, nothing landed or retained, the working area
1570    /// discarded on every path).
1571    #[tokio::test]
1572    async fn malformed_packs_fail_into_unpack_ng() {
1573        let front = mem_front();
1574
1575        let pins = Arc::new(PendingPinRegistry::new());
1576        let ingest = PackIngest::new(
1577            front.clone(),
1578            pins.clone(),
1579            scratch_root("ingest", "malformed").join("work"),
1580        );
1581        let updates = vec![up("refs/heads/main", None, Some(1))];
1582        let prepared = push(
1583            &ingest,
1584            &repo("r"),
1585            updates.clone(),
1586            b"garbage garbage ga".to_vec(),
1587        )
1588        .await;
1589        let reason = prepared.unpack.expect_err("garbage fails into the ng leg");
1590        assert!(!reason.is_empty(), "the ng reason is server-chosen");
1591        assert_eq!(prepared.updates, updates, "verbatim on the ng path");
1592        assert_eq!(pins.pending_count("r"), 0, "nothing retained on ng");
1593
1594        let pins = Arc::new(PendingPinRegistry::new());
1595        let ingest = PackIngest::new(
1596            front.clone(),
1597            pins.clone(),
1598            scratch_root("ingest", "truncated").join("work"),
1599        );
1600        let legit = pack_bytes(&[pack_entry_base(Kind::Blob, b"truncated away\n", 0)]);
1601        let truncated = legit[..legit.len() - 24].to_vec();
1602        let prepared = push(&ingest, &repo("r"), updates.clone(), truncated).await;
1603        assert!(prepared.unpack.is_err(), "a truncated stream cannot pass");
1604
1605        let pins = Arc::new(PendingPinRegistry::new());
1606        let ingest = PackIngest::new(
1607            front,
1608            pins.clone(),
1609            scratch_root("ingest", "digest").join("work"),
1610        );
1611        let mut digest_broken = pack_bytes(&[pack_entry_base(Kind::Blob, b"payload\n", 0)]);
1612        let last = digest_broken.len() - 1;
1613        digest_broken[last] ^= 0xff;
1614        let prepared = push(&ingest, &repo("r"), updates.clone(), digest_broken).await;
1615        assert!(prepared.unpack.is_err(), "a bad digest cannot pass");
1616        assert_eq!(pins.pending_count("r"), 0, "nothing retained on ng");
1617    }
1618
1619    /// The real client drives the implement: `git push` over the
1620    /// git-protocol door (the test's own transport shape — doors are
1621    /// family infrastructure) through the core's published serve entry
1622    /// — the fresh push (full pack) lands; the incremental push rides
1623    /// the thin-pack path; the pending custody drains through the
1624    /// committed-ref side's CAS; the marker legs anchor the
1625    /// watermark. Skipped quietly when no `git` binary is installed.
1626    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
1627    async fn a_real_git_push_round_trips_through_the_implement() {
1628        if std::process::Command::new("git")
1629            .arg("--version")
1630            .output()
1631            .is_err()
1632        {
1633            eprintln!("skipping row: no `git` binary on PATH");
1634            return;
1635        }
1636        let temp = scratch_root("ingest", "git-push");
1637        std::fs::create_dir_all(&temp).expect("the scratch root creates");
1638        let front = mem_front();
1639        let pins = Arc::new(PendingPinRegistry::new());
1640        let ingest: Arc<dyn GitPackIngest> = Arc::new(PackIngest::new(
1641            front.clone(),
1642            pins.clone(),
1643            temp.join("ingest-root"),
1644        ));
1645        let durable = Arc::new(InMemoryRegistry::new());
1646        let provider = Arc::new(CachedRegistryProvider::new());
1647        let backend = Arc::new(RegistryBackend::with_cached_provider(
1648            durable.clone(),
1649            provider.clone(),
1650        ));
1651        let registry: Arc<dyn GitRegistry> = backend.clone();
1652        let registry_store: Arc<dyn GitRegistryStore> = backend;
1653        let identity = Identity {
1654            id: "u-push".to_owned(),
1655            scopes: Vec::new(),
1656            resources: Default::default(),
1657        };
1658        let record = RepoRecord {
1659            repo_id: "repo".to_owned(),
1660            visibility: Visibility::Private,
1661            grants: BTreeMap::from([(
1662                identity.id.clone(),
1663                vec![AccessAction::Read, AccessAction::Write],
1664            )]),
1665        };
1666        registry_store.put_repo(&record).await.unwrap();
1667
1668        let rows = Arc::new(InMemoryRefTable::new());
1669        let mem_store = Arc::new(alkstore_mem::MemStore::new());
1670        let reactor = Arc::new(Reactor::new(
1671            mem_store.clone(),
1672            durable.clone(),
1673            provider.clone(),
1674        ));
1675        let legs = Arc::new(MemMutationLegs::new(durable.clone(), reactor));
1676        let refs: Arc<dyn GitRefs> = Arc::new(CommittedRefs::new(rows.clone(), pins.clone(), legs));
1677
1678        let packgen: Arc<dyn GitPackGen> = Arc::new(crate::packgen::PackGen::direct(front.clone()));
1679        let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
1680        let port = listener.local_addr().unwrap().port();
1681        let limits = Limits::default();
1682        let pipeline = Arc::new(Semaphore::new(limits.max_concurrent_blocking_tasks));
1683        let push_ref = refs.clone();
1684        let push_registry = registry.clone();
1685        let push_gen = packgen.clone();
1686        let push_ingest = ingest.clone();
1687        let ident = identity.clone();
1688        let door = tokio::spawn(async move {
1689            loop {
1690                let Ok((mut conn, _peer)) = listener.accept().await else {
1691                    break;
1692                };
1693                let refs = push_ref.clone();
1694                let registry = push_registry.clone();
1695                let gen = push_gen.clone();
1696                let ingest = push_ingest.clone();
1697                let identity = ident.clone();
1698                let pipeline = pipeline.clone();
1699                tokio::spawn(async move {
1700                    let Some(frame) = read_daemon_frame(&mut conn).await else {
1701                        return;
1702                    };
1703                    let Some(repo_id) = parse_daemon_frame(&frame) else {
1704                        return;
1705                    };
1706                    let _ = serve_duplex_resolved(
1707                        &repo_id,
1708                        Some(&identity),
1709                        Service::ReceivePack,
1710                        conn,
1711                        limits,
1712                        registry,
1713                        refs,
1714                        gen,
1715                        ingest,
1716                        false,
1717                        pipeline,
1718                    )
1719                    .await;
1720                });
1721            }
1722        });
1723
1724        let work = temp.join("work");
1725        run_git(
1726            &temp,
1727            &["init", "-q", "-b", "main", work.to_string_lossy().as_ref()],
1728        )
1729        .await;
1730        std::fs::write(work.join("a.txt"), "alpha\n").unwrap();
1731        run_git(&work, &["add", "."]).await;
1732        run_git(
1733            &work,
1734            &[
1735                "-c",
1736                "user.name=A",
1737                "-c",
1738                "user.email=a@example.invalid",
1739                "commit",
1740                "-q",
1741                "-m",
1742                "one",
1743            ],
1744        )
1745        .await;
1746        let url = format!("git://127.0.0.1:{port}/repo");
1747        run_git(&work, &["push", "-q", &url, "main"]).await;
1748
1749        let tip1 = run_git(&work, &["rev-parse", "HEAD"]).await;
1750        let tree1 = run_git(&work, &["rev-parse", "HEAD^{tree}"]).await;
1751        let blob1 = run_git(&work, &["rev-parse", "HEAD:a.txt"]).await;
1752        let parse = |hex_text: &String| {
1753            ObjectId::from_hex(hex_text.trim().as_bytes()).expect("the git output is hex")
1754        };
1755        let tip1 = parse(&tip1);
1756        let tree1 = parse(&tree1);
1757        let blob1 = parse(&blob1);
1758        let main = rows
1759            .list_refs(&record)
1760            .await
1761            .unwrap()
1762            .into_iter()
1763            .find(|line| line.name == "refs/heads/main")
1764            .expect("the ref row landed");
1765        assert_eq!(main.oid, Some(tip1), "the push's tip is the ref row");
1766        for (oid, kind) in [
1767            (tip1, Kind::Commit),
1768            (tree1, Kind::Tree),
1769            (blob1, Kind::Blob),
1770        ] {
1771            assert_eq!(
1772                front.kind(&oid).await.unwrap(),
1773                Some(kind),
1774                "the pushed object {oid} verified into the store"
1775            );
1776        }
1777        assert_eq!(
1778            pins.pending_count("repo"),
1779            0,
1780            "the custody drained through the committed-ref side's CAS"
1781        );
1782        let watermark_one = durable.watermark().await.unwrap();
1783
1784        std::fs::write(work.join("b.txt"), "beta\n").unwrap();
1785        run_git(&work, &["add", "."]).await;
1786        run_git(
1787            &work,
1788            &[
1789                "-c",
1790                "user.name=A",
1791                "-c",
1792                "user.email=a@example.invalid",
1793                "commit",
1794                "-q",
1795                "-m",
1796                "two",
1797            ],
1798        )
1799        .await;
1800        run_git(&work, &["push", "-q", &url, "main"]).await;
1801        let tip2 = run_git(&work, &["rev-parse", "HEAD"]).await;
1802        let tip2 = parse(&tip2);
1803        let main = rows
1804            .list_refs(&record)
1805            .await
1806            .unwrap()
1807            .into_iter()
1808            .find(|line| line.name == "refs/heads/main")
1809            .expect("the ref row");
1810        assert_eq!(
1811            main.oid,
1812            Some(tip2),
1813            "the incremental push advanced the row"
1814        );
1815        assert_eq!(front.kind(&tip2).await.unwrap(), Some(Kind::Commit));
1816        assert_eq!(
1817            pins.pending_count("repo"),
1818            0,
1819            "the second push's custody drained"
1820        );
1821        let watermark_two = durable.watermark().await.unwrap();
1822        assert_eq!(
1823            watermark_two,
1824            watermark_one + 1,
1825            "each committed push rode its marker leg"
1826        );
1827
1828        door.abort();
1829        let _ = tokio::fs::remove_dir_all(&temp).await;
1830    }
1831
1832    /// The pkt-line's read (bounded); `None` when the peer closed.
1833    async fn read_daemon_frame(conn: &mut tokio::net::TcpStream) -> Option<Vec<u8>> {
1834        let mut header = [0u8; 4];
1835        tokio::time::timeout(std::time::Duration::from_secs(10), async {
1836            tokio::io::AsyncReadExt::read_exact(conn, &mut header).await
1837        })
1838        .await
1839        .ok()?
1840        .ok()?;
1841        let len = usize::from_str_radix(std::str::from_utf8(&header).expect("hex header"), 16)
1842            .expect("hex length");
1843        if !(4..=(1 << 16)).contains(&len) {
1844            return None;
1845        }
1846        let mut payload = vec![0u8; len - 4];
1847        tokio::io::AsyncReadExt::read_exact(conn, &mut payload)
1848            .await
1849            .ok()?;
1850        Some(payload)
1851    }
1852
1853    /// The daemon frame's request line: `<service> <repo>` NUL-attached
1854    /// `host=…` — only the receive-pack service is served here (the
1855    /// row's shape).
1856    fn parse_daemon_frame(frame: &[u8]) -> Option<String> {
1857        let text = String::from_utf8(frame.to_vec()).ok()?;
1858        let mut segments = text.split('\0');
1859        let (service, repo_path) = segments.next()?.split_once(' ')?;
1860        if service != "git-receive-pack" {
1861            return None;
1862        }
1863        let repo_id = repo_path.strip_prefix('/').unwrap_or(repo_path);
1864        if repo_id.is_empty() {
1865            return None;
1866        }
1867        Some(repo_id.to_owned())
1868    }
1869
1870    /// One isolated real-git command; its stdout (the rows assert
1871    /// exit status themselves).
1872    async fn run_git(dir: &Path, args: &[&str]) -> String {
1873        let mut command = tokio::process::Command::new("git");
1874        command
1875            .arg("-C")
1876            .arg(dir)
1877            .args(args)
1878            .env_clear()
1879            .env("PATH", std::env::var("PATH").unwrap_or_default())
1880            .env("GIT_CONFIG_NOSYSTEM", "1")
1881            .env("GIT_CONFIG_GLOBAL", "/dev/null")
1882            .env("GIT_TERMINAL_PROMPT", "0")
1883            .stdin(Stdio::null())
1884            .stdout(Stdio::piped())
1885            .stderr(Stdio::piped());
1886        let output = command.output().await.expect("the git command spawns");
1887        assert!(
1888            output.status.success(),
1889            "git {args:?} failed: {}{}",
1890            String::from_utf8_lossy(&output.stdout),
1891            String::from_utf8_lossy(&output.stderr)
1892        );
1893        String::from_utf8_lossy(&output.stdout).into_owned()
1894    }
1895}