anvilsign in

collin/anvil

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