anvilsign in

collin/anvil

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, gix::hash::Kind::Sha1) {
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 =
453 gix::objs::CommitRefIter::from_bytes(&commit_obj.data, gix::hash::Kind::Sha1)
454 .tree_id()?;
455
456 oids.push(commit_oid);
457
458 collect_tree_oids(repo, tree_oid, &mut seen, &mut oids)?;
459 }
460
461 Ok(oids)
462}
463
464fn common_haves(
465 repo: &gix::Repository,
466 wants: &[gix::ObjectId],
467 haves: &[gix::ObjectId],
468) -> std::result::Result<Vec<gix::ObjectId>, Box<dyn std::error::Error + Send + Sync>> {
469 let want_set: HashSet<gix::ObjectId> =
470 collect_all_oids(repo, wants, &[])?.into_iter().collect();
471
472 Ok(haves
473 .iter()
474 .copied()
475 .filter(|oid| want_set.contains(oid))
476 .collect())
477}
478
479/// Generate the complete pack response for a Git upload-pack request.
480///
481/// Returns an `AsyncRead` producing the side-band-64k framed response that
482/// can be streamed as the HTTP response body.
483pub fn generate_pack(
484 repo_path: &Path,
485 request: &UploadPackRequest,
486) -> Result<impl AsyncRead + Send + Unpin + use<>> {
487 let repo_path = repo_path.to_path_buf();
488 let wants: Vec<gix::ObjectId> = request.wants.clone();
489 let haves: Vec<gix::ObjectId> = request.haves.clone();
490 let object_ids = request.object_ids.clone();
491 let done = request.done;
492 let ofs_delta = request.capabilities.ofs_delta;
493 let multi_ack = request.capabilities.multi_ack;
494 let multi_ack_detailed = request.capabilities.multi_ack_detailed;
495
496 let (tx, rx) = tokio::sync::mpsc::channel::<std::result::Result<Bytes, std::io::Error>>(64);
497
498 let handle = tokio::task::spawn_blocking(move || {
499 if let Err(e) = generate_pack_sync(
500 &repo_path,
501 &wants,
502 &haves,
503 object_ids,
504 GeneratePackOptions {
505 done,
506 ofs_delta,
507 multi_ack,
508 multi_ack_detailed,
509 },
510 &tx,
511 ) {
512 let _ = tx.blocking_send(Err(std::io::Error::other(e.to_string())));
513 }
514 });
515
516 // Log panics from the blocking task without blocking the stream
517 tokio::spawn(async move {
518 if let Err(e) = handle.await {
519 tracing::error!("pack generation task panicked: {e}");
520 }
521 });
522
523 let stream = tokio_stream::wrappers::ReceiverStream::new(rx);
524 Ok(StreamReader::new(stream))
525}
526
527/// Synchronous two-pass streaming pack generator.
528///
529/// Pass 1: collect OIDs only (lightweight -- no object data retained).
530/// Pass 2: re-read each object, compress, and stream it through `tx`.
531struct GeneratePackOptions {
532 done: bool,
533 ofs_delta: bool,
534 multi_ack: bool,
535 multi_ack_detailed: bool,
536}
537
538fn generate_pack_sync(
539 repo_path: &Path,
540 wants: &[gix::ObjectId],
541 haves: &[gix::ObjectId],
542 object_ids: Option<Vec<gix::ObjectId>>,
543 options: GeneratePackOptions,
544 tx: &tokio::sync::mpsc::Sender<std::result::Result<Bytes, std::io::Error>>,
545) -> std::result::Result<(), Box<dyn std::error::Error + Send + Sync>> {
546 const MAX_DELTA_BASES: usize = 8;
547 const MIN_DELTA_BLOB_SIZE: usize = 1024;
548
549 let repo = gix::open(repo_path)?;
550
551 let common = if !haves.is_empty() {
552 common_haves(&repo, wants, haves)?
553 } else {
554 Vec::new()
555 };
556
557 if options.multi_ack && !haves.is_empty() && !options.done {
558 for oid in &common {
559 let suffix = if options.multi_ack_detailed {
560 "common"
561 } else {
562 "continue"
563 };
564 send(tx, &encode_ack_line(*oid, Some(suffix)))?;
565 }
566 send(tx, &pktline::encode(b"NAK\n"))?;
567 return Ok(());
568 }
569
570 if options.multi_ack && !common.is_empty() {
571 send(tx, &encode_ack_line(*common.last().unwrap(), None))?;
572 } else {
573 // NAK line
574 send(tx, &pktline::encode(b"NAK\n"))?;
575 }
576
577 // Pass 1: collect OIDs only
578 let oids = match object_ids {
579 Some(oids) => oids,
580 None => collect_all_oids(&repo, wants, haves)?,
581 };
582
583 // Pass 2: stream each object
584 let mut hasher = Sha1::new();
585
586 // Pack header
587 let mut header = Vec::with_capacity(12);
588 header.extend_from_slice(b"PACK");
589 header.extend_from_slice(&2u32.to_be_bytes());
590 header.extend_from_slice(&(oids.len() as u32).to_be_bytes());
591 hasher.update(&header);
592 send_sideband(tx, &header)?;
593
594 let mut pack_offset = header.len() as u64;
595 let mut recent_blob_bases = Vec::<BlobDeltaBase>::new();
596
597 // Each object: read, compress, frame, send
598 for oid in &oids {
599 let obj = repo.find_object(*oid)?;
600 let full_entry = build_base_entry(obj.kind, &obj.data);
601 let mut used_delta = false;
602 let entry = if options.ofs_delta
603 && obj.kind == gix::object::Kind::Blob
604 && obj.data.len() >= MIN_DELTA_BLOB_SIZE
605 {
606 recent_blob_bases
607 .iter()
608 .filter(|base| base.data.len() >= MIN_DELTA_BLOB_SIZE)
609 .filter_map(|base| {
610 build_ofs_delta_entry(pack_offset, base.pack_offset, &base.data, &obj.data)
611 })
612 .min_by_key(Vec::len)
613 .filter(|delta_entry| delta_entry.len() < full_entry.len())
614 .inspect(|_| {
615 used_delta = true;
616 })
617 .unwrap_or(full_entry)
618 } else {
619 full_entry
620 };
621
622 hasher.update(&entry);
623 send_sideband(tx, &entry)?;
624
625 if obj.kind == gix::object::Kind::Blob
626 && !used_delta
627 && obj.data.len() >= MIN_DELTA_BLOB_SIZE
628 {
629 recent_blob_bases.push(BlobDeltaBase {
630 pack_offset,
631 data: obj.data.to_vec(),
632 });
633 if recent_blob_bases.len() > MAX_DELTA_BASES {
634 recent_blob_bases.remove(0);
635 }
636 }
637
638 pack_offset += entry.len() as u64;
639 }
640
641 // SHA-1 checksum over raw pack bytes
642 let checksum = hasher.finalize();
643 send_sideband(tx, &checksum)?;
644
645 // Flush
646 send(tx, b"0000")?;
647
648 Ok(())
649}
650
651#[cfg(test)]
652mod tests {
653 use std::path::{Path, PathBuf};
654 use std::process::Command;
655
656 use tempfile::TempDir;
657 use tokio::io::AsyncReadExt;
658
659 use super::*;
660
661 fn make_pktline(data: &str) -> Vec<u8> {
662 let len = data.len() + 4;
663 format!("{len:04x}{data}").into_bytes()
664 }
665
666 /// Create a bare repo with a single commit on the `main` branch.
667 fn create_repo_with_commit(root: &Path) -> PathBuf {
668 let bare_path = root.join("test.git");
669 let clone_path = root.join("workdir");
670
671 let out = Command::new("git")
672 .args(["init", "--bare", bare_path.to_str().unwrap()])
673 .output()
674 .expect("git init --bare failed");
675 assert!(out.status.success(), "git init --bare failed: {:?}", out);
676
677 let out = Command::new("git")
678 .args(["symbolic-ref", "HEAD", "refs/heads/main"])
679 .current_dir(&bare_path)
680 .output()
681 .expect("git symbolic-ref failed");
682 assert!(out.status.success());
683
684 let out = Command::new("git")
685 .args([
686 "clone",
687 bare_path.to_str().unwrap(),
688 clone_path.to_str().unwrap(),
689 ])
690 .output()
691 .expect("git clone failed");
692 assert!(out.status.success(), "git clone failed: {:?}", out);
693
694 for (key, val) in [("user.name", "Test User"), ("user.email", "test@test.com")] {
695 Command::new("git")
696 .args(["config", key, val])
697 .current_dir(&clone_path)
698 .output()
699 .expect("git config failed");
700 }
701
702 // Create a file and commit
703 std::fs::write(clone_path.join("README.md"), "# Test\n").unwrap();
704
705 Command::new("git")
706 .args(["add", "README.md"])
707 .current_dir(&clone_path)
708 .output()
709 .expect("git add failed");
710
711 let out = Command::new("git")
712 .args(["commit", "-m", "initial commit"])
713 .current_dir(&clone_path)
714 .env("GIT_AUTHOR_NAME", "Test User")
715 .env("GIT_AUTHOR_EMAIL", "test@test.com")
716 .env("GIT_COMMITTER_NAME", "Test User")
717 .env("GIT_COMMITTER_EMAIL", "test@test.com")
718 .output()
719 .expect("git commit failed");
720 assert!(out.status.success(), "git commit failed: {:?}", out);
721
722 let out = Command::new("git")
723 .args(["push", "origin", "main"])
724 .current_dir(&clone_path)
725 .output()
726 .expect("git push failed");
727 assert!(out.status.success(), "git push failed: {:?}", out);
728
729 bare_path
730 }
731
732 #[test]
733 fn parse_simple_want() {
734 let hash = "0000000000000000000000000000000000000001";
735 let mut body = make_pktline(&format!("want {hash}\n"));
736 body.extend_from_slice(b"00000009done\n");
737 let req = UploadPackRequest::parse(&body).unwrap();
738 assert_eq!(req.wants.len(), 1);
739 assert!(req.haves.is_empty());
740 assert!(req.done);
741 assert!(!req.capabilities.ofs_delta);
742 assert_eq!(req.shallow.depth, None);
743 }
744
745 #[test]
746 fn parse_wants_and_haves() {
747 let want = "0000000000000000000000000000000000000001";
748 let have = "0000000000000000000000000000000000000002";
749 let mut body = make_pktline(&format!("want {want}\n"));
750 body.extend_from_slice(b"0000");
751 body.extend_from_slice(&make_pktline(&format!("have {have}\n")));
752 body.extend_from_slice(b"0009done\n");
753 let req = UploadPackRequest::parse(&body).unwrap();
754 assert_eq!(req.wants.len(), 1);
755 assert_eq!(req.haves.len(), 1);
756 assert!(req.done);
757 assert!(!req.capabilities.ofs_delta);
758 assert!(req.shallow.client_shallows.is_empty());
759 }
760
761 #[test]
762 fn parse_ofs_delta_capability() {
763 let hash = "0000000000000000000000000000000000000001";
764 let mut body = make_pktline(&format!("want {hash} side-band-64k ofs-delta\n"));
765 body.extend_from_slice(b"0009done\n");
766 let req = UploadPackRequest::parse(&body).unwrap();
767 assert!(req.capabilities.ofs_delta);
768 }
769
770 #[test]
771 fn parse_multi_ack_capability() {
772 let hash = "0000000000000000000000000000000000000001";
773 let mut body = make_pktline(&format!("want {hash} multi_ack side-band-64k\n"));
774 body.extend_from_slice(b"0009done\n");
775 let req = UploadPackRequest::parse(&body).unwrap();
776 assert!(req.capabilities.multi_ack);
777 }
778
779 #[test]
780 fn parse_multi_ack_detailed_capability() {
781 let hash = "0000000000000000000000000000000000000001";
782 let mut body = make_pktline(&format!("want {hash} multi_ack_detailed side-band-64k\n"));
783 body.extend_from_slice(b"0009done\n");
784 let req = UploadPackRequest::parse(&body).unwrap();
785 assert!(req.capabilities.multi_ack);
786 assert!(req.capabilities.multi_ack_detailed);
787 }
788
789 #[test]
790 fn parse_shallow_request() {
791 let hash = "0000000000000000000000000000000000000001";
792 let mut body = make_pktline(&format!("want {hash}\n"));
793 body.extend_from_slice(&make_pktline("deepen 2\n"));
794 body.extend_from_slice(&make_pktline(&format!("shallow {hash}\n")));
795 body.extend_from_slice(&make_pktline("deepen-relative\n"));
796 body.extend_from_slice(b"0009done\n");
797 let req = UploadPackRequest::parse(&body).unwrap();
798 assert_eq!(req.shallow.depth, Some(2));
799 assert_eq!(
800 req.shallow.client_shallows,
801 vec![gix::ObjectId::from_hex(hash.as_bytes()).unwrap()]
802 );
803 assert!(req.shallow.deepen_relative);
804 }
805
806 #[tokio::test]
807 async fn generate_pack_for_clone() {
808 let dir = TempDir::new().unwrap();
809 let repo_path = create_repo_with_commit(dir.path());
810
811 // Get HEAD OID
812 let repo = gix::open(&repo_path).unwrap();
813 let head_oid = repo.head_id().unwrap().detach();
814 drop(repo);
815
816 let request = UploadPackRequest {
817 wants: vec![head_oid],
818 haves: vec![],
819 done: true,
820 capabilities: UploadPackCapabilities::default(),
821 shallow: ShallowRequest::default(),
822 object_ids: None,
823 };
824
825 let mut reader = generate_pack(&repo_path, &request).unwrap();
826 let mut buf = Vec::new();
827 reader.read_to_end(&mut buf).await.unwrap();
828
829 let response = String::from_utf8_lossy(&buf);
830 assert!(
831 response.contains("NAK"),
832 "response should contain NAK: {response:?}"
833 );
834
835 // Find PACK signature in the binary response
836 let pack_found = buf.windows(4).any(|window| window == b"PACK");
837 assert!(pack_found, "response should contain PACK signature");
838 }
839}