1use std::collections::BTreeSet;
8use std::collections::HashSet;
9use std::io::Cursor;
10use std::path::Path;
11
12use crate::error::{Error, Result};
13use crate::pack::UploadPackRequest;
14use crate::pktline;
15
16const CAPABILITIES: &[&str] = &[
17 "ls-refs=unborn",
18 "fetch=shallow wait-for-done",
19 "object-format=sha1",
20];
21
22pub enum Command {
23 LsRefs(LsRefsRequest),
24 Fetch(FetchRequest),
25}
26
27#[derive(Default)]
28pub struct LsRefsRequest {
29 pub peel: bool,
30 pub symrefs: bool,
31 pub unborn: bool,
32 pub ref_prefixes: Vec<String>,
33}
34
35pub struct FetchRequest {
36 pub upload_request: UploadPackRequest,
37}
38
39pub struct ShallowUpdate {
40 pub shallow: Vec<gix::ObjectId>,
41 pub unshallow: Vec<gix::ObjectId>,
42}
43
44pub fn advertise_capabilities() -> Vec<u8> {
45 let mut out = Vec::new();
46 out.extend_from_slice(&pktline::encode_comment("service=git-upload-pack"));
47 out.extend_from_slice(pktline::flush());
48 out.extend_from_slice(&pktline::encode(b"version 2\n"));
49 for capability in CAPABILITIES {
50 out.extend_from_slice(&pktline::encode(format!("{capability}\n").as_bytes()));
51 }
52 out.extend_from_slice(pktline::flush());
53 out
54}
55
56pub fn parse_command_request(body: &[u8]) -> Result<Command> {
57 let lines = decode_pkt_lines(body)?;
58 let mut iter = lines.into_iter();
59
60 let command = iter
61 .next()
62 .ok_or_else(|| Error::Protocol("missing protocol v2 command".into()))?;
63 let command = command
64 .strip_prefix("command=")
65 .ok_or_else(|| Error::Protocol("invalid protocol v2 command line".into()))?;
66
67 let mut args = Vec::new();
68 let mut saw_delim = false;
69 for line in iter {
70 if line.is_empty() {
71 continue;
72 }
73 if line == "0001" {
74 saw_delim = true;
75 continue;
76 }
77 if saw_delim {
78 args.push(line);
79 }
80 }
81
82 match command {
83 "ls-refs" => parse_ls_refs(args),
84 "fetch" => parse_fetch(args),
85 _ => Err(Error::Protocol(format!(
86 "unsupported protocol v2 command: {command}"
87 ))),
88 }
89}
90
91pub fn ls_refs(repo_path: &Path, request: &LsRefsRequest) -> Result<Vec<u8>> {
92 let repo = gix::open(repo_path)?;
93 let mut refs = BTreeSet::new();
94
95 if let Ok(mut head) = repo.head() {
96 if let Some(id) = head
97 .try_peel_to_id()
98 .map_err(|e| Error::Protocol(e.to_string()))?
99 {
100 let mut line = format!("{} HEAD", id.detach());
101 if request.symrefs
102 && let Some(target) = head.referent_name()
103 {
104 line.push_str(&format!(" symref-target:{}", target.as_bstr()));
105 }
106 refs.insert(line);
107 } else if request.unborn
108 && let Some(target) = head.referent_name()
109 {
110 refs.insert(format!("unborn HEAD symref-target:{}", target.as_bstr()));
111 }
112 }
113
114 if let Ok(platform) = repo.references()
115 && let Ok(iter) = platform.all()
116 {
117 for mut reference in iter.flatten() {
118 let name = reference.name().as_bstr().to_string();
119 if !request.ref_prefixes.is_empty()
120 && !request
121 .ref_prefixes
122 .iter()
123 .any(|prefix| name.starts_with(prefix))
124 {
125 continue;
126 }
127
128 let mut line = match reference.try_id() {
129 Some(id) => format!("{} {name}", id.detach()),
130 None => match reference.peel_to_id() {
131 Ok(id) => format!("{} {name}", id.detach()),
132 Err(_) => continue,
133 },
134 };
135
136 if request.symrefs
137 && let Some(target) = reference.target().try_name()
138 {
139 line.push_str(&format!(" symref-target:{}", target.as_bstr()));
140 }
141
142 if request.peel
143 && let Ok(peeled) = reference.peel_to_id()
144 {
145 line.push_str(&format!(" peeled:{}", peeled.detach()));
146 }
147
148 refs.insert(line);
149 }
150 }
151
152 let mut out = Vec::new();
153 for line in refs {
154 out.extend_from_slice(&pktline::encode(format!("{line}\n").as_bytes()));
155 }
156 out.extend_from_slice(pktline::flush());
157 Ok(out)
158}
159
160pub fn encode_fetch_pack_response(pack_bytes: &[u8]) -> Vec<u8> {
161 let mut out = Vec::new();
162 out.extend_from_slice(&pktline::encode(b"packfile\n"));
163
164 let mut pos = 0;
165 while pos + 4 <= pack_bytes.len() {
166 let len_str = match std::str::from_utf8(&pack_bytes[pos..pos + 4]) {
167 Ok(v) => v,
168 Err(_) => break,
169 };
170 pos += 4;
171
172 if len_str == "0000" {
173 out.extend_from_slice(b"0000");
174 break;
175 }
176
177 let len = match usize::from_str_radix(len_str, 16) {
178 Ok(v) if v >= 4 && pos + (v - 4) <= pack_bytes.len() => v,
179 _ => break,
180 };
181
182 let frame = &pack_bytes[pos - 4..pos + (len - 4)];
183 let payload = &pack_bytes[pos..pos + (len - 4)];
184 pos += len - 4;
185
186 if payload.starts_with(&[0x01])
187 || payload.starts_with(&[0x02])
188 || payload.starts_with(&[0x03])
189 {
190 out.extend_from_slice(frame);
191 }
192 }
193
194 out
195}
196
197pub struct PrefixThenReader<R> {
198 prefix: Cursor<Vec<u8>>,
199 reader: R,
200}
201
202impl<R> PrefixThenReader<R> {
203 pub fn new(prefix: Vec<u8>, reader: R) -> Self {
204 Self {
205 prefix: Cursor::new(prefix),
206 reader,
207 }
208 }
209}
210
211pub struct PackSectionReader<R> {
212 reader: R,
213 buf: Vec<u8>,
214 out: Cursor<Vec<u8>>,
215 finished: bool,
216}
217
218impl<R> PackSectionReader<R> {
219 pub fn new(reader: R) -> Self {
220 Self {
221 reader,
222 buf: Vec::new(),
223 out: Cursor::new(Vec::new()),
224 finished: false,
225 }
226 }
227}
228
229impl<R: tokio::io::AsyncRead + Unpin> tokio::io::AsyncRead for PackSectionReader<R> {
230 fn poll_read(
231 mut self: std::pin::Pin<&mut Self>,
232 cx: &mut std::task::Context<'_>,
233 buf: &mut tokio::io::ReadBuf<'_>,
234 ) -> std::task::Poll<std::io::Result<()>> {
235 loop {
236 if (self.out.position() as usize) < self.out.get_ref().len() {
237 let remaining = &self.out.get_ref()[self.out.position() as usize..];
238 let to_copy = remaining.len().min(buf.remaining());
239 buf.put_slice(&remaining[..to_copy]);
240 let next = self.out.position() + to_copy as u64;
241 self.out.set_position(next);
242 return std::task::Poll::Ready(Ok(()));
243 }
244
245 if self.finished {
246 return std::task::Poll::Ready(Ok(()));
247 }
248
249 let mut frame_buf = [0u8; 8192];
250 let mut read_buf = tokio::io::ReadBuf::new(&mut frame_buf);
251 match std::pin::Pin::new(&mut self.reader).poll_read(cx, &mut read_buf) {
252 std::task::Poll::Pending => return std::task::Poll::Pending,
253 std::task::Poll::Ready(Err(err)) => return std::task::Poll::Ready(Err(err)),
254 std::task::Poll::Ready(Ok(())) => {
255 let filled = read_buf.filled();
256 if filled.is_empty() {
257 self.finished = true;
258 return std::task::Poll::Ready(Ok(()));
259 }
260 self.buf.extend_from_slice(filled);
261 }
262 }
263
264 let mut emitted = Vec::new();
265 loop {
266 if self.buf.len() < 4 {
267 break;
268 }
269 let len_str = match std::str::from_utf8(&self.buf[..4]) {
270 Ok(v) => v,
271 Err(_) => {
272 self.finished = true;
273 return std::task::Poll::Ready(Err(std::io::Error::other(
274 "invalid pkt-line prefix in pack response",
275 )));
276 }
277 };
278
279 if len_str == "0000" {
280 emitted.extend_from_slice(b"0000");
281 self.buf.drain(..4);
282 self.finished = true;
283 break;
284 }
285
286 let len = match usize::from_str_radix(len_str, 16) {
287 Ok(v) if v >= 4 => v,
288 _ => {
289 self.finished = true;
290 return std::task::Poll::Ready(Err(std::io::Error::other(
291 "invalid pkt-line length in pack response",
292 )));
293 }
294 };
295
296 if self.buf.len() < len {
297 break;
298 }
299
300 let frame = self.buf[..len].to_vec();
301 let payload = &frame[4..];
302 if payload.starts_with(&[0x01])
303 || payload.starts_with(&[0x02])
304 || payload.starts_with(&[0x03])
305 {
306 emitted.extend_from_slice(&frame);
307 }
308 self.buf.drain(..len);
309 }
310
311 if !emitted.is_empty() {
312 self.out = Cursor::new(emitted);
313 self.out.set_position(0);
314 }
315 }
316 }
317}
318
319impl<R: tokio::io::AsyncRead + Unpin> tokio::io::AsyncRead for PrefixThenReader<R> {
320 fn poll_read(
321 mut self: std::pin::Pin<&mut Self>,
322 cx: &mut std::task::Context<'_>,
323 buf: &mut tokio::io::ReadBuf<'_>,
324 ) -> std::task::Poll<std::io::Result<()>> {
325 if (self.prefix.position() as usize) < self.prefix.get_ref().len() {
326 let remaining = &self.prefix.get_ref()[self.prefix.position() as usize..];
327 let to_copy = remaining.len().min(buf.remaining());
328 buf.put_slice(&remaining[..to_copy]);
329 let next = self.prefix.position() + to_copy as u64;
330 self.prefix.set_position(next);
331 return std::task::Poll::Ready(Ok(()));
332 }
333
334 std::pin::Pin::new(&mut self.reader).poll_read(cx, buf)
335 }
336}
337
338pub fn encode_fetch_ready_and_acknowledgments(common: &[gix::ObjectId]) -> Vec<u8> {
339 let mut out = encode_fetch_acknowledgments(common);
340 if !common.is_empty() {
341 out.truncate(out.len() - 4);
342 out.extend_from_slice(&pktline::encode(b"ready\n"));
343 out.extend_from_slice(b"0001");
344 }
345 out
346}
347
348pub fn encode_fetch_acknowledgments(common: &[gix::ObjectId]) -> Vec<u8> {
349 let mut out = Vec::new();
350 out.extend_from_slice(&pktline::encode(b"acknowledgments\n"));
351
352 if common.is_empty() {
353 out.extend_from_slice(&pktline::encode(b"NAK\n"));
354 } else {
355 for oid in common {
356 out.extend_from_slice(&pktline::encode(format!("ACK {oid}\n").as_bytes()));
357 }
358 }
359
360 out.extend_from_slice(pktline::flush());
361 out
362}
363
364pub fn encode_shallow_info(update: &ShallowUpdate) -> Vec<u8> {
365 let mut out = Vec::new();
366 if update.shallow.is_empty() && update.unshallow.is_empty() {
367 return out;
368 }
369
370 out.extend_from_slice(&pktline::encode(b"shallow-info\n"));
371 for oid in &update.shallow {
372 out.extend_from_slice(&pktline::encode(format!("shallow {oid}\n").as_bytes()));
373 }
374 for oid in &update.unshallow {
375 out.extend_from_slice(&pktline::encode(format!("unshallow {oid}\n").as_bytes()));
376 }
377 out.extend_from_slice(b"0001");
378 out
379}
380
381pub fn common_haves(repo_path: &Path, request: &FetchRequest) -> Result<Vec<gix::ObjectId>> {
382 let repo = gix::open(repo_path)?;
383 let want_set: HashSet<gix::ObjectId> =
384 collect_want_closure(&repo, &request.upload_request.wants)?
385 .into_iter()
386 .collect();
387
388 Ok(request
389 .upload_request
390 .haves
391 .iter()
392 .copied()
393 .filter(|oid| want_set.contains(oid))
394 .collect())
395}
396
397pub fn apply_shallow_boundaries(
398 repo_path: &Path,
399 request: &mut FetchRequest,
400) -> Result<ShallowUpdate> {
401 let Some(depth) = request.upload_request.shallow.depth else {
402 return Ok(ShallowUpdate {
403 shallow: Vec::new(),
404 unshallow: Vec::new(),
405 });
406 };
407
408 let repo = gix::open(repo_path)?;
409 let previous_shallows = request.upload_request.shallow.client_shallows.clone();
410 let state = collect_depth_limited_commits(&repo, &request.upload_request, depth)?;
411
412 request.upload_request.object_ids = Some(state.included_objects.clone());
413 request
414 .upload_request
415 .haves
416 .extend(previous_shallows.iter().copied());
417
418 let next_shallows: HashSet<_> = state.shallow_boundary.iter().copied().collect();
419 let prev_shallows: HashSet<_> = previous_shallows.iter().copied().collect();
420
421 Ok(ShallowUpdate {
422 shallow: state
423 .shallow_boundary
424 .iter()
425 .copied()
426 .filter(|oid| !prev_shallows.contains(oid))
427 .collect(),
428 unshallow: previous_shallows
429 .into_iter()
430 .filter(|oid| !next_shallows.contains(oid))
431 .collect(),
432 })
433}
434
435struct DepthState {
436 included_objects: Vec<gix::ObjectId>,
437 shallow_boundary: Vec<gix::ObjectId>,
438}
439
440fn parse_ls_refs(args: Vec<String>) -> Result<Command> {
441 let mut request = LsRefsRequest::default();
442
443 for arg in args {
444 match arg.as_str() {
445 "peel" => request.peel = true,
446 "symrefs" => request.symrefs = true,
447 "unborn" => request.unborn = true,
448 _ => {
449 if let Some(prefix) = arg.strip_prefix("ref-prefix ") {
450 request.ref_prefixes.push(prefix.to_owned());
451 } else {
452 return Err(Error::Protocol(format!(
453 "unsupported ls-refs argument: {arg}"
454 )));
455 }
456 }
457 }
458 }
459
460 Ok(Command::LsRefs(request))
461}
462
463fn parse_fetch(args: Vec<String>) -> Result<Command> {
464 let mut wants = Vec::new();
465 let mut haves = Vec::new();
466 let mut done = false;
467 let mut capabilities = crate::pack::UploadPackCapabilities::default();
468 let mut shallow = crate::pack::ShallowRequest::default();
469
470 for arg in args {
471 if arg == "done" {
472 done = true;
473 } else if arg == "ofs-delta" {
474 capabilities.ofs_delta = true;
475 } else if arg == "deepen-relative" {
476 shallow.deepen_relative = true;
477 } else if arg == "thin-pack"
478 || arg == "no-progress"
479 || arg == "include-tag"
480 || arg == "wait-for-done"
481 {
482 continue;
483 } else if let Some(depth) = arg.strip_prefix("deepen ") {
484 shallow.depth = Some(
485 depth
486 .parse::<usize>()
487 .map_err(|_| Error::Protocol(format!("invalid deepen value: {depth}")))?,
488 );
489 } else if let Some(oid_hex) = arg.strip_prefix("shallow ") {
490 let oid = gix::ObjectId::from_hex(oid_hex.as_bytes())
491 .map_err(|_| Error::Protocol(format!("invalid OID in shallow: {oid_hex}")))?;
492 shallow.client_shallows.push(oid);
493 } else if let Some(oid_hex) = arg.strip_prefix("want ") {
494 let oid = gix::ObjectId::from_hex(oid_hex.as_bytes())
495 .map_err(|_| Error::Protocol(format!("invalid OID in want: {oid_hex}")))?;
496 wants.push(oid);
497 } else if let Some(oid_hex) = arg.strip_prefix("have ") {
498 let oid = gix::ObjectId::from_hex(oid_hex.as_bytes())
499 .map_err(|_| Error::Protocol(format!("invalid OID in have: {oid_hex}")))?;
500 haves.push(oid);
501 } else {
502 return Err(Error::Protocol(format!(
503 "unsupported fetch argument: {arg}"
504 )));
505 }
506 }
507
508 Ok(Command::Fetch(FetchRequest {
509 upload_request: UploadPackRequest {
510 wants,
511 haves,
512 done,
513 capabilities,
514 shallow,
515 object_ids: None,
516 },
517 }))
518}
519
520fn decode_pkt_lines(body: &[u8]) -> Result<Vec<String>> {
521 let mut pos = 0;
522 let mut out = Vec::new();
523
524 while pos < body.len() {
525 if pos + 4 > body.len() {
526 return Err(Error::Protocol("truncated pkt-line prefix".into()));
527 }
528
529 let len_str = std::str::from_utf8(&body[pos..pos + 4])
530 .map_err(|_| Error::Protocol("invalid pkt-line length prefix".into()))?;
531 pos += 4;
532
533 if len_str == "0000" {
534 break;
535 }
536 if len_str == "0001" {
537 out.push("0001".to_string());
538 continue;
539 }
540
541 let len = usize::from_str_radix(len_str, 16)
542 .map_err(|_| Error::Protocol("invalid pkt-line length".into()))?;
543 if len < 4 || pos + (len - 4) > body.len() {
544 return Err(Error::Protocol("invalid pkt-line frame length".into()));
545 }
546
547 let payload = &body[pos..pos + (len - 4)];
548 pos += len - 4;
549 let line = std::str::from_utf8(payload)
550 .map_err(|_| Error::Protocol("invalid UTF-8 in pkt-line".into()))?;
551 out.push(line.trim_end_matches('\n').to_owned());
552 }
553
554 Ok(out)
555}
556
557fn collect_want_closure(
558 repo: &gix::Repository,
559 wants: &[gix::ObjectId],
560) -> Result<Vec<gix::ObjectId>> {
561 let mut seen = HashSet::new();
562 let mut out = Vec::new();
563
564 let walk = repo
565 .rev_walk(wants.iter().copied())
566 .all()
567 .map_err(|e| Error::Protocol(e.to_string()))?;
568 for info_result in walk {
569 let info = info_result.map_err(|e| Error::Protocol(e.to_string()))?;
570 let commit_oid = info.id;
571 if !seen.insert(commit_oid) {
572 continue;
573 }
574 out.push(commit_oid);
575 }
576
577 Ok(out)
578}
579
580fn collect_depth_limited_commits(
581 repo: &gix::Repository,
582 request: &crate::pack::UploadPackRequest,
583 depth: usize,
584) -> Result<DepthState> {
585 use std::collections::{HashSet, VecDeque};
586
587 let mut queue = VecDeque::new();
588 let mut seen = HashSet::new();
589 let mut included_commits = Vec::new();
590 let mut included_objects = Vec::new();
591 let mut shallow_boundary = Vec::new();
592
593 let base_depth = if request.shallow.deepen_relative {
594 1usize
595 } else {
596 0
597 };
598 let limit = base_depth + depth;
599
600 for want in &request.wants {
601 queue.push_back((*want, 1usize));
602 }
603
604 while let Some((commit_oid, current_depth)) = queue.pop_front() {
605 if !seen.insert(commit_oid) {
606 continue;
607 }
608 included_commits.push(commit_oid);
609 included_objects.push(commit_oid);
610
611 let commit_obj = repo
612 .find_object(commit_oid)
613 .map_err(|e| Error::Protocol(e.to_string()))?;
614 let tree_oid = gix::objs::CommitRefIter::from_bytes(&commit_obj.data)
615 .tree_id()
616 .map_err(|e| Error::Protocol(e.to_string()))?;
617 collect_tree_oids(repo, tree_oid, &mut seen, &mut included_objects)?;
618 let parents: Vec<_> = gix::objs::CommitRefIter::from_bytes(&commit_obj.data)
619 .parent_ids()
620 .collect();
621
622 if current_depth >= limit || parents.is_empty() {
623 shallow_boundary.push(commit_oid);
624 continue;
625 }
626
627 for parent in parents {
628 queue.push_back((parent, current_depth + 1));
629 }
630 }
631
632 Ok(DepthState {
633 included_objects,
634 shallow_boundary,
635 })
636}
637
638fn collect_tree_oids(
639 repo: &gix::Repository,
640 root_tree_oid: gix::ObjectId,
641 seen: &mut HashSet<gix::ObjectId>,
642 oids: &mut Vec<gix::ObjectId>,
643) -> Result<()> {
644 let mut stack = vec![root_tree_oid];
645
646 while let Some(tree_oid) = stack.pop() {
647 if !seen.insert(tree_oid) {
648 continue;
649 }
650
651 let tree_obj = repo
652 .find_object(tree_oid)
653 .map_err(|e| Error::Protocol(e.to_string()))?;
654 oids.push(tree_oid);
655
656 for entry_result in gix::objs::TreeRefIter::from_bytes(&tree_obj.data) {
657 let entry = entry_result.map_err(|e| Error::Protocol(e.to_string()))?;
658 let entry_oid = entry.oid.to_owned();
659 let entry_mode = entry.mode;
660
661 if entry_mode.is_tree() {
662 stack.push(entry_oid);
663 } else if seen.insert(entry_oid) && !entry_mode.is_commit() {
664 oids.push(entry_oid);
665 }
666 }
667 }
668
669 Ok(())
670}
671
672#[cfg(test)]
673mod tests {
674 use super::*;
675
676 fn pkt(data: &str) -> Vec<u8> {
677 pktline::encode(data.as_bytes())
678 }
679
680 #[test]
681 fn parse_ls_refs_command() {
682 let mut body = Vec::new();
683 body.extend_from_slice(&pkt("command=ls-refs\n"));
684 body.extend_from_slice(b"0001");
685 body.extend_from_slice(&pkt("peel\n"));
686 body.extend_from_slice(&pkt("symrefs\n"));
687 body.extend_from_slice(&pkt("ref-prefix refs/heads/\n"));
688 body.extend_from_slice(b"0000");
689
690 let Command::LsRefs(req) = parse_command_request(&body).unwrap() else {
691 panic!("expected ls-refs command");
692 };
693 assert!(req.peel);
694 assert!(req.symrefs);
695 assert_eq!(req.ref_prefixes, vec!["refs/heads/"]);
696 }
697
698 #[test]
699 fn parse_fetch_command() {
700 let mut body = Vec::new();
701 body.extend_from_slice(&pkt("command=fetch\n"));
702 body.extend_from_slice(b"0001");
703 body.extend_from_slice(&pkt("ofs-delta\n"));
704 body.extend_from_slice(&pkt("want 0000000000000000000000000000000000000001\n"));
705 body.extend_from_slice(&pkt("done\n"));
706 body.extend_from_slice(b"0000");
707
708 let Command::Fetch(req) = parse_command_request(&body).unwrap() else {
709 panic!("expected fetch command");
710 };
711 assert_eq!(req.upload_request.wants.len(), 1);
712 assert!(req.upload_request.done);
713 assert!(req.upload_request.capabilities.ofs_delta);
714 }
715
716 #[test]
717 fn ls_refs_returns_unborn_head() {
718 let root = tempfile::TempDir::new().unwrap();
719 let repo_path = root.path().join("repo.git");
720 std::process::Command::new("git")
721 .args(["init", "--bare", repo_path.to_str().unwrap()])
722 .output()
723 .unwrap();
724 std::process::Command::new("git")
725 .args(["symbolic-ref", "HEAD", "refs/heads/main"])
726 .current_dir(&repo_path)
727 .output()
728 .unwrap();
729
730 let out = ls_refs(
731 &repo_path,
732 &LsRefsRequest {
733 unborn: true,
734 symrefs: true,
735 ..Default::default()
736 },
737 )
738 .unwrap();
739 let text = String::from_utf8(out).unwrap();
740 assert!(text.contains("unborn HEAD symref-target:refs/heads/main"));
741 }
742}