Skip to main content

gitserver_core/
pack.rs

1use std::collections::HashSet;
2use std::path::Path;
3
4use bytes::Bytes;
5use sha1::{Digest, Sha1};
6use tokio::io::AsyncRead;
7use tokio_util::io::StreamReader;
8
9use crate::error::{Error, Result};
10use crate::pktline;
11
12#[derive(Debug, Clone, Default)]
13pub struct UploadPackCapabilities {
14    pub ofs_delta: bool,
15    pub multi_ack: bool,
16    pub multi_ack_detailed: bool,
17}
18
19#[derive(Debug, Clone, Default)]
20pub struct ShallowRequest {
21    pub depth: Option<usize>,
22    pub client_shallows: Vec<gix::ObjectId>,
23    pub deepen_relative: bool,
24}
25
26/// A parsed upload-pack request from a Git client.
27pub struct UploadPackRequest {
28    pub wants: Vec<gix::ObjectId>,
29    pub haves: Vec<gix::ObjectId>,
30    pub done: bool,
31    pub capabilities: UploadPackCapabilities,
32    pub shallow: ShallowRequest,
33    pub object_ids: Option<Vec<gix::ObjectId>>,
34}
35
36impl UploadPackRequest {
37    /// Parse a pkt-line encoded upload-pack request body.
38    ///
39    /// The body contains:
40    /// - "want <oid> [capabilities]\n" lines
41    /// - flush packet "0000"
42    /// - "have <oid>\n" lines (optional)
43    /// - "done\n"
44    pub fn parse(body: &[u8]) -> Result<Self> {
45        let mut wants = Vec::new();
46        let mut haves = Vec::new();
47        let mut done = false;
48        let mut capabilities = UploadPackCapabilities::default();
49        let mut shallow = ShallowRequest::default();
50        let mut pos = 0;
51
52        while pos < body.len() {
53            // Check for flush packet
54            if body[pos..].starts_with(b"0000") {
55                pos += 4;
56                continue;
57            }
58
59            // Read 4-byte hex length prefix
60            if pos + 4 > body.len() {
61                break;
62            }
63            let len_str = std::str::from_utf8(&body[pos..pos + 4])
64                .map_err(|_| Error::Protocol("invalid pkt-line length prefix".into()))?;
65            let len = usize::from_str_radix(len_str, 16)
66                .map_err(|_| Error::Protocol("invalid pkt-line length".into()))?;
67
68            if len == 0 {
69                // flush packet already handled above, but just in case
70                pos += 4;
71                continue;
72            }
73
74            if len < 4 || pos + len > body.len() {
75                break;
76            }
77
78            let payload = &body[pos + 4..pos + len];
79            let line = std::str::from_utf8(payload)
80                .map_err(|_| Error::Protocol("invalid UTF-8 in pkt-line".into()))?;
81            let line = line.trim_end_matches('\n');
82
83            if line == "done" {
84                done = true;
85            } else if let Some(rest) = line.strip_prefix("deepen ") {
86                let depth = rest
87                    .parse::<usize>()
88                    .map_err(|_| Error::Protocol(format!("invalid deepen value: {rest}")))?;
89                shallow.depth = Some(depth);
90            } else if line == "deepen-relative" {
91                shallow.deepen_relative = true;
92            } else if let Some(rest) = line.strip_prefix("shallow ") {
93                let oid = gix::ObjectId::from_hex(rest.as_bytes())
94                    .map_err(|_| Error::Protocol(format!("invalid OID in shallow: {rest}")))?;
95                shallow.client_shallows.push(oid);
96            } else if let Some(rest) = line.strip_prefix("want ") {
97                let mut parts = rest.split_ascii_whitespace();
98                let oid_hex = parts
99                    .next()
100                    .ok_or_else(|| Error::Protocol("missing OID in want".into()))?;
101                let oid = gix::ObjectId::from_hex(oid_hex.as_bytes())
102                    .map_err(|_| Error::Protocol(format!("invalid OID in want: {oid_hex}")))?;
103                if wants.is_empty() {
104                    for capability in parts {
105                        if capability == "ofs-delta" {
106                            capabilities.ofs_delta = true;
107                        } else if capability == "multi_ack" {
108                            capabilities.multi_ack = true;
109                        } else if capability == "multi_ack_detailed" {
110                            capabilities.multi_ack = true;
111                            capabilities.multi_ack_detailed = true;
112                        }
113                    }
114                }
115                wants.push(oid);
116            } else if let Some(rest) = line.strip_prefix("have ") {
117                let oid_hex = rest
118                    .split_ascii_whitespace()
119                    .next()
120                    .ok_or_else(|| Error::Protocol("missing OID in have".into()))?;
121                let oid = gix::ObjectId::from_hex(oid_hex.as_bytes())
122                    .map_err(|_| Error::Protocol(format!("invalid OID in have: {oid_hex}")))?;
123                haves.push(oid);
124            }
125
126            pos += len;
127        }
128
129        Ok(Self {
130            wants,
131            haves,
132            done,
133            capabilities,
134            shallow,
135            object_ids: None,
136        })
137    }
138}
139
140/// Encode the variable-length pack object header.
141///
142/// Format: first byte = MSB continuation + 3-bit type + 4-bit size
143/// Subsequent bytes: 7-bit size chunks with MSB continuation
144fn encode_pack_object_header(obj_type: u8, size: usize) -> Vec<u8> {
145    let mut header = Vec::new();
146    let mut byte = (obj_type << 4) | (size as u8 & 0x0f);
147    let mut remaining = size >> 4;
148
149    if remaining > 0 {
150        byte |= 0x80; // set continuation bit
151        header.push(byte);
152        while remaining > 0 {
153            byte = remaining as u8 & 0x7f;
154            remaining >>= 7;
155            if remaining > 0 {
156                byte |= 0x80;
157            }
158            header.push(byte);
159        }
160    } else {
161        header.push(byte);
162    }
163
164    header
165}
166
167fn encode_ofs_delta_base_distance(mut distance: u64) -> Vec<u8> {
168    debug_assert!(distance > 0, "offset deltas must point backwards");
169
170    let mut buf = [0u8; 10];
171    let mut bytes_written = 1;
172    buf[buf.len() - 1] = distance as u8 & 0x7f;
173
174    for out in buf.iter_mut().rev().skip(1) {
175        distance >>= 7;
176        if distance == 0 {
177            break;
178        }
179        distance -= 1;
180        *out = 0x80 | (distance as u8 & 0x7f);
181        bytes_written += 1;
182    }
183
184    buf[buf.len() - bytes_written..].to_vec()
185}
186
187fn encode_delta_size(mut size: usize, out: &mut Vec<u8>) {
188    loop {
189        let mut byte = (size & 0x7f) as u8;
190        size >>= 7;
191        if size > 0 {
192            byte |= 0x80;
193        }
194        out.push(byte);
195        if size == 0 {
196            break;
197        }
198    }
199}
200
201fn encode_delta_copy_instruction(out: &mut Vec<u8>, offset: usize, size: usize) {
202    debug_assert!(size > 0 && size <= 0x10000);
203
204    let command_pos = out.len();
205    out.push(0x80);
206    let mut command = 0x80;
207
208    if offset & 0xff != 0 {
209        command |= 0x01;
210        out.push(offset as u8);
211    }
212    if (offset >> 8) & 0xff != 0 {
213        command |= 0x02;
214        out.push((offset >> 8) as u8);
215    }
216    if (offset >> 16) & 0xff != 0 {
217        command |= 0x04;
218        out.push((offset >> 16) as u8);
219    }
220    if (offset >> 24) & 0xff != 0 {
221        command |= 0x08;
222        out.push((offset >> 24) as u8);
223    }
224
225    if size != 0x10000 {
226        if size & 0xff != 0 {
227            command |= 0x10;
228            out.push(size as u8);
229        }
230        if (size >> 8) & 0xff != 0 {
231            command |= 0x20;
232            out.push((size >> 8) as u8);
233        }
234        if (size >> 16) & 0xff != 0 {
235            command |= 0x40;
236            out.push((size >> 16) as u8);
237        }
238    }
239
240    out[command_pos] = command;
241}
242
243fn encode_delta_copy(out: &mut Vec<u8>, mut offset: usize, mut size: usize) {
244    while size > 0 {
245        let chunk = size.min(0x10000);
246        encode_delta_copy_instruction(out, offset, chunk);
247        offset += chunk;
248        size -= chunk;
249    }
250}
251
252fn encode_delta_insert(out: &mut Vec<u8>, data: &[u8]) {
253    for chunk in data.chunks(0x7f) {
254        out.push(chunk.len() as u8);
255        out.extend_from_slice(chunk);
256    }
257}
258
259fn encode_blob_delta(base: &[u8], target: &[u8]) -> Option<Vec<u8>> {
260    let mut prefix = 0;
261    let max_prefix = base.len().min(target.len());
262    while prefix < max_prefix && base[prefix] == target[prefix] {
263        prefix += 1;
264    }
265
266    let max_suffix = base
267        .len()
268        .saturating_sub(prefix)
269        .min(target.len().saturating_sub(prefix));
270    let mut suffix = 0;
271    while suffix < max_suffix && base[base.len() - 1 - suffix] == target[target.len() - 1 - suffix]
272    {
273        suffix += 1;
274    }
275
276    if prefix == 0 && suffix == 0 {
277        return None;
278    }
279
280    let mut delta = Vec::new();
281    encode_delta_size(base.len(), &mut delta);
282    encode_delta_size(target.len(), &mut delta);
283
284    if prefix > 0 {
285        encode_delta_copy(&mut delta, 0, prefix);
286    }
287
288    let insert_start = prefix;
289    let insert_end = target.len() - suffix;
290    encode_delta_insert(&mut delta, &target[insert_start..insert_end]);
291
292    if suffix > 0 {
293        encode_delta_copy(&mut delta, base.len() - suffix, suffix);
294    }
295
296    Some(delta)
297}
298
299fn build_base_entry(kind: gix::object::Kind, data: &[u8]) -> Vec<u8> {
300    let type_num = object_type_number(kind);
301    let obj_header = encode_pack_object_header(type_num, data.len());
302    let compressed = miniz_oxide::deflate::compress_to_vec_zlib(data, 6);
303
304    let mut entry = Vec::with_capacity(obj_header.len() + compressed.len());
305    entry.extend_from_slice(&obj_header);
306    entry.extend_from_slice(&compressed);
307    entry
308}
309
310fn build_ofs_delta_entry(
311    pack_offset: u64,
312    base_pack_offset: u64,
313    base_data: &[u8],
314    target_data: &[u8],
315) -> Option<Vec<u8>> {
316    let delta = encode_blob_delta(base_data, target_data)?;
317    let obj_header = encode_pack_object_header(6, delta.len());
318    let base_distance = encode_ofs_delta_base_distance(pack_offset - base_pack_offset);
319    let compressed = miniz_oxide::deflate::compress_to_vec_zlib(&delta, 6);
320
321    let mut entry = Vec::with_capacity(obj_header.len() + base_distance.len() + compressed.len());
322    entry.extend_from_slice(&obj_header);
323    entry.extend_from_slice(&base_distance);
324    entry.extend_from_slice(&compressed);
325    Some(entry)
326}
327
328struct BlobDeltaBase {
329    pack_offset: u64,
330    data: Vec<u8>,
331}
332
333/// Map gix object kind to pack type number.
334fn object_type_number(kind: gix::object::Kind) -> u8 {
335    match kind {
336        gix::object::Kind::Commit => 1,
337        gix::object::Kind::Tree => 2,
338        gix::object::Kind::Blob => 3,
339        gix::object::Kind::Tag => 4,
340    }
341}
342
343/// Send raw bytes through the channel.
344fn send(
345    tx: &tokio::sync::mpsc::Sender<std::result::Result<Bytes, std::io::Error>>,
346    data: &[u8],
347) -> std::result::Result<(), Box<dyn std::error::Error + Send + Sync>> {
348    tx.blocking_send(Ok(Bytes::copy_from_slice(data)))
349        .map_err(|_| "receiver dropped".into())
350}
351
352/// Send pack data through the channel wrapped in side-band-64k framing
353/// (band 1 = pack data).
354///
355/// Respects LARGE_PACKET_MAX: each pkt-line frame carries at most
356/// 65520 - 4 (prefix) - 1 (band byte) = 65515 bytes of payload.
357fn send_sideband(
358    tx: &tokio::sync::mpsc::Sender<std::result::Result<Bytes, std::io::Error>>,
359    data: &[u8],
360) -> std::result::Result<(), Box<dyn std::error::Error + Send + Sync>> {
361    const MAX_DATA_PER_FRAME: usize = 65515;
362
363    for chunk in data.chunks(MAX_DATA_PER_FRAME) {
364        let pkt_len = 4 + 1 + chunk.len();
365        let mut frame = Vec::with_capacity(pkt_len);
366        frame.extend_from_slice(format!("{pkt_len:04x}").as_bytes());
367        frame.push(0x01); // band 1 = pack data
368        frame.extend_from_slice(chunk);
369        send(tx, &frame)?;
370    }
371
372    Ok(())
373}
374
375fn encode_ack_line(oid: gix::ObjectId, suffix: Option<&str>) -> Vec<u8> {
376    let mut line = format!("ACK {oid}");
377    if let Some(suffix) = suffix {
378        line.push(' ');
379        line.push_str(suffix);
380    }
381    line.push('\n');
382    pktline::encode(line.as_bytes())
383}
384
385/// Recursively collect tree and blob OIDs reachable from `tree_oid`.
386///
387/// Uses a single `find_object` call per object and parses raw tree
388/// bytes via `TreeRefIter` to avoid a second ODB lookup.
389fn collect_tree_oids(
390    repo: &gix::Repository,
391    tree_oid: gix::ObjectId,
392    seen: &mut HashSet<gix::ObjectId>,
393    oids: &mut Vec<gix::ObjectId>,
394) -> std::result::Result<(), Box<dyn std::error::Error + Send + Sync>> {
395    if !seen.insert(tree_oid) {
396        return Ok(());
397    }
398
399    let tree_obj = repo.find_object(tree_oid)?;
400    let tree_data = tree_obj.data.to_vec();
401    oids.push(tree_oid);
402
403    for entry_result in gix::objs::TreeRefIter::from_bytes(&tree_data) {
404        let entry = entry_result?;
405        let entry_oid = entry.oid.to_owned();
406        let entry_mode = entry.mode;
407
408        if entry_mode.is_tree() {
409            collect_tree_oids(repo, entry_oid, seen, oids)?;
410        } else if seen.insert(entry_oid) && !entry_mode.is_commit() {
411            oids.push(entry_oid);
412        }
413    }
414
415    Ok(())
416}
417
418/// Walk commits from `wants` (excluding `haves`) and collect all
419/// reachable ObjectIds (commits, trees, blobs).
420///
421/// Pass 1 of the two-pass streaming approach: only OIDs are stored,
422/// not object data.
423fn collect_all_oids(
424    repo: &gix::Repository,
425    wants: &[gix::ObjectId],
426    haves: &[gix::ObjectId],
427) -> std::result::Result<Vec<gix::ObjectId>, Box<dyn std::error::Error + Send + Sync>> {
428    let have_set: HashSet<gix::ObjectId> = haves.iter().copied().collect();
429    let mut seen = HashSet::new();
430    let mut oids = Vec::new();
431
432    // Mark have objects as already seen so we skip them
433    for have in haves {
434        seen.insert(*have);
435    }
436
437    let walk = repo
438        .rev_walk(wants.iter().copied())
439        .with_hidden(haves.iter().copied())
440        .all()?;
441
442    for info_result in walk {
443        let info = info_result?;
444        let commit_oid = info.id;
445
446        if have_set.contains(&commit_oid) || !seen.insert(commit_oid) {
447            continue;
448        }
449
450        // Extract tree OID from raw commit bytes (single ODB read)
451        let commit_obj = repo.find_object(commit_oid)?;
452        let tree_oid = gix::objs::CommitRefIter::from_bytes(&commit_obj.data).tree_id()?;
453
454        oids.push(commit_oid);
455
456        collect_tree_oids(repo, tree_oid, &mut seen, &mut oids)?;
457    }
458
459    Ok(oids)
460}
461
462fn common_haves(
463    repo: &gix::Repository,
464    wants: &[gix::ObjectId],
465    haves: &[gix::ObjectId],
466) -> std::result::Result<Vec<gix::ObjectId>, Box<dyn std::error::Error + Send + Sync>> {
467    let want_set: HashSet<gix::ObjectId> =
468        collect_all_oids(repo, wants, &[])?.into_iter().collect();
469
470    Ok(haves
471        .iter()
472        .copied()
473        .filter(|oid| want_set.contains(oid))
474        .collect())
475}
476
477/// Generate the complete pack response for a Git upload-pack request.
478///
479/// Returns an `AsyncRead` producing the side-band-64k framed response that
480/// can be streamed as the HTTP response body.
481pub fn generate_pack(
482    repo_path: &Path,
483    request: &UploadPackRequest,
484) -> Result<impl AsyncRead + Send + Unpin + use<>> {
485    let repo_path = repo_path.to_path_buf();
486    let wants: Vec<gix::ObjectId> = request.wants.clone();
487    let haves: Vec<gix::ObjectId> = request.haves.clone();
488    let object_ids = request.object_ids.clone();
489    let done = request.done;
490    let ofs_delta = request.capabilities.ofs_delta;
491    let multi_ack = request.capabilities.multi_ack;
492    let multi_ack_detailed = request.capabilities.multi_ack_detailed;
493
494    let (tx, rx) = tokio::sync::mpsc::channel::<std::result::Result<Bytes, std::io::Error>>(64);
495
496    let handle = tokio::task::spawn_blocking(move || {
497        if let Err(e) = generate_pack_sync(
498            &repo_path,
499            &wants,
500            &haves,
501            object_ids,
502            GeneratePackOptions {
503                done,
504                ofs_delta,
505                multi_ack,
506                multi_ack_detailed,
507            },
508            &tx,
509        ) {
510            let _ = tx.blocking_send(Err(std::io::Error::other(e.to_string())));
511        }
512    });
513
514    // Log panics from the blocking task without blocking the stream
515    tokio::spawn(async move {
516        if let Err(e) = handle.await {
517            tracing::error!("pack generation task panicked: {e}");
518        }
519    });
520
521    let stream = tokio_stream::wrappers::ReceiverStream::new(rx);
522    Ok(StreamReader::new(stream))
523}
524
525/// Synchronous two-pass streaming pack generator.
526///
527/// Pass 1: collect OIDs only (lightweight -- no object data retained).
528/// Pass 2: re-read each object, compress, and stream it through `tx`.
529struct GeneratePackOptions {
530    done: bool,
531    ofs_delta: bool,
532    multi_ack: bool,
533    multi_ack_detailed: bool,
534}
535
536fn generate_pack_sync(
537    repo_path: &Path,
538    wants: &[gix::ObjectId],
539    haves: &[gix::ObjectId],
540    object_ids: Option<Vec<gix::ObjectId>>,
541    options: GeneratePackOptions,
542    tx: &tokio::sync::mpsc::Sender<std::result::Result<Bytes, std::io::Error>>,
543) -> std::result::Result<(), Box<dyn std::error::Error + Send + Sync>> {
544    const MAX_DELTA_BASES: usize = 8;
545    const MIN_DELTA_BLOB_SIZE: usize = 1024;
546
547    let repo = gix::open(repo_path)?;
548
549    let common = if !haves.is_empty() {
550        common_haves(&repo, wants, haves)?
551    } else {
552        Vec::new()
553    };
554
555    if options.multi_ack && !haves.is_empty() && !options.done {
556        for oid in &common {
557            let suffix = if options.multi_ack_detailed {
558                "common"
559            } else {
560                "continue"
561            };
562            send(tx, &encode_ack_line(*oid, Some(suffix)))?;
563        }
564        send(tx, &pktline::encode(b"NAK\n"))?;
565        return Ok(());
566    }
567
568    if options.multi_ack && !common.is_empty() {
569        send(tx, &encode_ack_line(*common.last().unwrap(), None))?;
570    } else {
571        // NAK line
572        send(tx, &pktline::encode(b"NAK\n"))?;
573    }
574
575    // Pass 1: collect OIDs only
576    let oids = match object_ids {
577        Some(oids) => oids,
578        None => collect_all_oids(&repo, wants, haves)?,
579    };
580
581    // Pass 2: stream each object
582    let mut hasher = Sha1::new();
583
584    // Pack header
585    let mut header = Vec::with_capacity(12);
586    header.extend_from_slice(b"PACK");
587    header.extend_from_slice(&2u32.to_be_bytes());
588    header.extend_from_slice(&(oids.len() as u32).to_be_bytes());
589    hasher.update(&header);
590    send_sideband(tx, &header)?;
591
592    let mut pack_offset = header.len() as u64;
593    let mut recent_blob_bases = Vec::<BlobDeltaBase>::new();
594
595    // Each object: read, compress, frame, send
596    for oid in &oids {
597        let obj = repo.find_object(*oid)?;
598        let full_entry = build_base_entry(obj.kind, &obj.data);
599        let mut used_delta = false;
600        let entry = if options.ofs_delta
601            && obj.kind == gix::object::Kind::Blob
602            && obj.data.len() >= MIN_DELTA_BLOB_SIZE
603        {
604            recent_blob_bases
605                .iter()
606                .filter(|base| base.data.len() >= MIN_DELTA_BLOB_SIZE)
607                .filter_map(|base| {
608                    build_ofs_delta_entry(pack_offset, base.pack_offset, &base.data, &obj.data)
609                })
610                .min_by_key(Vec::len)
611                .filter(|delta_entry| delta_entry.len() < full_entry.len())
612                .inspect(|_| {
613                    used_delta = true;
614                })
615                .unwrap_or(full_entry)
616        } else {
617            full_entry
618        };
619
620        hasher.update(&entry);
621        send_sideband(tx, &entry)?;
622
623        if obj.kind == gix::object::Kind::Blob
624            && !used_delta
625            && obj.data.len() >= MIN_DELTA_BLOB_SIZE
626        {
627            recent_blob_bases.push(BlobDeltaBase {
628                pack_offset,
629                data: obj.data.to_vec(),
630            });
631            if recent_blob_bases.len() > MAX_DELTA_BASES {
632                recent_blob_bases.remove(0);
633            }
634        }
635
636        pack_offset += entry.len() as u64;
637    }
638
639    // SHA-1 checksum over raw pack bytes
640    let checksum = hasher.finalize();
641    send_sideband(tx, &checksum)?;
642
643    // Flush
644    send(tx, b"0000")?;
645
646    Ok(())
647}
648
649#[cfg(test)]
650mod tests {
651    use std::path::{Path, PathBuf};
652    use std::process::Command;
653
654    use tempfile::TempDir;
655    use tokio::io::AsyncReadExt;
656
657    use super::*;
658
659    fn make_pktline(data: &str) -> Vec<u8> {
660        let len = data.len() + 4;
661        format!("{len:04x}{data}").into_bytes()
662    }
663
664    /// Create a bare repo with a single commit on the `main` branch.
665    fn create_repo_with_commit(root: &Path) -> PathBuf {
666        let bare_path = root.join("test.git");
667        let clone_path = root.join("workdir");
668
669        let out = Command::new("git")
670            .args(["init", "--bare", bare_path.to_str().unwrap()])
671            .output()
672            .expect("git init --bare failed");
673        assert!(out.status.success(), "git init --bare failed: {:?}", out);
674
675        let out = Command::new("git")
676            .args(["symbolic-ref", "HEAD", "refs/heads/main"])
677            .current_dir(&bare_path)
678            .output()
679            .expect("git symbolic-ref failed");
680        assert!(out.status.success());
681
682        let out = Command::new("git")
683            .args([
684                "clone",
685                bare_path.to_str().unwrap(),
686                clone_path.to_str().unwrap(),
687            ])
688            .output()
689            .expect("git clone failed");
690        assert!(out.status.success(), "git clone failed: {:?}", out);
691
692        for (key, val) in [("user.name", "Test User"), ("user.email", "test@test.com")] {
693            Command::new("git")
694                .args(["config", key, val])
695                .current_dir(&clone_path)
696                .output()
697                .expect("git config failed");
698        }
699
700        // Create a file and commit
701        std::fs::write(clone_path.join("README.md"), "# Test\n").unwrap();
702
703        Command::new("git")
704            .args(["add", "README.md"])
705            .current_dir(&clone_path)
706            .output()
707            .expect("git add failed");
708
709        let out = Command::new("git")
710            .args(["commit", "-m", "initial commit"])
711            .current_dir(&clone_path)
712            .env("GIT_AUTHOR_NAME", "Test User")
713            .env("GIT_AUTHOR_EMAIL", "test@test.com")
714            .env("GIT_COMMITTER_NAME", "Test User")
715            .env("GIT_COMMITTER_EMAIL", "test@test.com")
716            .output()
717            .expect("git commit failed");
718        assert!(out.status.success(), "git commit failed: {:?}", out);
719
720        let out = Command::new("git")
721            .args(["push", "origin", "main"])
722            .current_dir(&clone_path)
723            .output()
724            .expect("git push failed");
725        assert!(out.status.success(), "git push failed: {:?}", out);
726
727        bare_path
728    }
729
730    #[test]
731    fn parse_simple_want() {
732        let hash = "0000000000000000000000000000000000000001";
733        let mut body = make_pktline(&format!("want {hash}\n"));
734        body.extend_from_slice(b"00000009done\n");
735        let req = UploadPackRequest::parse(&body).unwrap();
736        assert_eq!(req.wants.len(), 1);
737        assert!(req.haves.is_empty());
738        assert!(req.done);
739        assert!(!req.capabilities.ofs_delta);
740        assert_eq!(req.shallow.depth, None);
741    }
742
743    #[test]
744    fn parse_wants_and_haves() {
745        let want = "0000000000000000000000000000000000000001";
746        let have = "0000000000000000000000000000000000000002";
747        let mut body = make_pktline(&format!("want {want}\n"));
748        body.extend_from_slice(b"0000");
749        body.extend_from_slice(&make_pktline(&format!("have {have}\n")));
750        body.extend_from_slice(b"0009done\n");
751        let req = UploadPackRequest::parse(&body).unwrap();
752        assert_eq!(req.wants.len(), 1);
753        assert_eq!(req.haves.len(), 1);
754        assert!(req.done);
755        assert!(!req.capabilities.ofs_delta);
756        assert!(req.shallow.client_shallows.is_empty());
757    }
758
759    #[test]
760    fn parse_ofs_delta_capability() {
761        let hash = "0000000000000000000000000000000000000001";
762        let mut body = make_pktline(&format!("want {hash} side-band-64k ofs-delta\n"));
763        body.extend_from_slice(b"0009done\n");
764        let req = UploadPackRequest::parse(&body).unwrap();
765        assert!(req.capabilities.ofs_delta);
766    }
767
768    #[test]
769    fn parse_multi_ack_capability() {
770        let hash = "0000000000000000000000000000000000000001";
771        let mut body = make_pktline(&format!("want {hash} multi_ack side-band-64k\n"));
772        body.extend_from_slice(b"0009done\n");
773        let req = UploadPackRequest::parse(&body).unwrap();
774        assert!(req.capabilities.multi_ack);
775    }
776
777    #[test]
778    fn parse_multi_ack_detailed_capability() {
779        let hash = "0000000000000000000000000000000000000001";
780        let mut body = make_pktline(&format!("want {hash} multi_ack_detailed side-band-64k\n"));
781        body.extend_from_slice(b"0009done\n");
782        let req = UploadPackRequest::parse(&body).unwrap();
783        assert!(req.capabilities.multi_ack);
784        assert!(req.capabilities.multi_ack_detailed);
785    }
786
787    #[test]
788    fn parse_shallow_request() {
789        let hash = "0000000000000000000000000000000000000001";
790        let mut body = make_pktline(&format!("want {hash}\n"));
791        body.extend_from_slice(&make_pktline("deepen 2\n"));
792        body.extend_from_slice(&make_pktline(&format!("shallow {hash}\n")));
793        body.extend_from_slice(&make_pktline("deepen-relative\n"));
794        body.extend_from_slice(b"0009done\n");
795        let req = UploadPackRequest::parse(&body).unwrap();
796        assert_eq!(req.shallow.depth, Some(2));
797        assert_eq!(
798            req.shallow.client_shallows,
799            vec![gix::ObjectId::from_hex(hash.as_bytes()).unwrap()]
800        );
801        assert!(req.shallow.deepen_relative);
802    }
803
804    #[tokio::test]
805    async fn generate_pack_for_clone() {
806        let dir = TempDir::new().unwrap();
807        let repo_path = create_repo_with_commit(dir.path());
808
809        // Get HEAD OID
810        let repo = gix::open(&repo_path).unwrap();
811        let head_oid = repo.head_id().unwrap().detach();
812        drop(repo);
813
814        let request = UploadPackRequest {
815            wants: vec![head_oid],
816            haves: vec![],
817            done: true,
818            capabilities: UploadPackCapabilities::default(),
819            shallow: ShallowRequest::default(),
820            object_ids: None,
821        };
822
823        let mut reader = generate_pack(&repo_path, &request).unwrap();
824        let mut buf = Vec::new();
825        reader.read_to_end(&mut buf).await.unwrap();
826
827        let response = String::from_utf8_lossy(&buf);
828        assert!(
829            response.contains("NAK"),
830            "response should contain NAK: {response:?}"
831        );
832
833        // Find PACK signature in the binary response
834        let pack_found = buf.windows(4).any(|window| window == b"PACK");
835        assert!(pack_found, "response should contain PACK signature");
836    }
837}