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
26pub 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 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 if body[pos..].starts_with(b"0000") {
55 pos += 4;
56 continue;
57 }
58
59 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 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
140fn 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; 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
333fn 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
343fn 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
352fn 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); 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
385fn 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
418fn 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 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 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
477pub 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 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
525struct 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 send(tx, &pktline::encode(b"NAK\n"))?;
573 }
574
575 let oids = match object_ids {
577 Some(oids) => oids,
578 None => collect_all_oids(&repo, wants, haves)?,
579 };
580
581 let mut hasher = Sha1::new();
583
584 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 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 let checksum = hasher.finalize();
641 send_sideband(tx, &checksum)?;
642
643 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 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 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 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 let pack_found = buf.windows(4).any(|window| window == b"PACK");
835 assert!(pack_found, "response should contain PACK signature");
836 }
837}