anvilsign in

collin/anvil

1//! Git smart-HTTP protocol, transport-agnostic.
2//!
3//! These functions take a bare-repository path plus request bytes and return
4//! response bytes (or a packfile stream), with no dependency on the HTTP
5//! framework. `anvil-web` adapts them to axum; a future `anvil-ssh` can reuse
6//! the same engine over an SSH channel.
7//!
8//! The protocol engine is [`gitserver_core`]; this module orchestrates it into
9//! the request/response shapes a transport needs. We target git **protocol v2**
10//! for fetch (the modern default) with a v0 fallback, and the classic v0
11//! advertisement for `receive-pack` (push).
12
13use std::path::Path;
14use std::pin::Pin;
15
16use gitserver_core::backend::GitBackend;
17use gitserver_core::{pack, pktline, protocol_v2};
18use tokio::io::AsyncRead;
19
20use crate::error::Result;
21
22/// The two git wire services.
23#[derive(Debug, Copy, Clone, Eq, PartialEq)]
24pub enum Service {
25 /// `git-upload-pack` — serves fetch/clone.
26 UploadPack,
27 /// `git-receive-pack` — accepts push.
28 ReceivePack,
29}
30
31impl Service {
32 pub fn as_str(self) -> &'static str {
33 match self {
34 Service::UploadPack => "git-upload-pack",
35 Service::ReceivePack => "git-receive-pack",
36 }
37 }
38
39 /// Parse the `?service=` query value used by `GET /info/refs`.
40 pub fn from_query(value: &str) -> Option<Self> {
41 match value {
42 "git-upload-pack" => Some(Service::UploadPack),
43 "git-receive-pack" => Some(Service::ReceivePack),
44 _ => None,
45 }
46 }
47
48 /// The `Content-Type` for this service's `info/refs` advertisement.
49 pub fn advertisement_content_type(self) -> String {
50 format!("application/x-{}-advertisement", self.as_str())
51 }
52
53 /// The `Content-Type` for this service's RPC result.
54 pub fn result_content_type(self) -> String {
55 format!("application/x-{}-result", self.as_str())
56 }
57}
58
59/// A boxed packfile stream produced by `upload-pack`.
60pub type PackStream = Pin<Box<dyn AsyncRead + Send>>;
61
62/// Outcome of a `git-upload-pack` RPC: either a fully buffered response (an
63/// `ls-refs` reply or an acknowledgments-only negotiation round) or a streamed
64/// packfile (prefixed with any acknowledgments/shallow info).
65pub enum UploadPack {
66 Buffered(Vec<u8>),
67 Pack(PackStream),
68}
69
70/// Build the `GET /info/refs` advertisement body for `service`.
71///
72/// `protocol_v2` should be true only when the client requested v2
73/// (`Git-Protocol: version=2`) for `upload-pack`; `receive-pack` always uses the
74/// v0-style advertisement.
75pub fn advertise(repo_path: &Path, service: Service, protocol_v2: bool) -> Result<Vec<u8>> {
76 let body = match service {
77 Service::UploadPack if protocol_v2 => protocol_v2::advertise_capabilities(),
78 Service::UploadPack => GitBackend::new(repo_path.to_path_buf()).advertise_refs()?,
79 Service::ReceivePack => {
80 let mut body = pktline::encode_comment("service=git-receive-pack");
81 body.extend_from_slice(pktline::flush());
82 body.extend_from_slice(
83 &GitBackend::new(repo_path.to_path_buf()).advertise_receive_refs()?,
84 );
85 body
86 }
87 };
88 Ok(body)
89}
90
91/// Handle a `POST /git-upload-pack` request using protocol v2.
92pub async fn upload_pack_v2(repo_path: &Path, request: &[u8]) -> Result<UploadPack> {
93 let backend = GitBackend::new(repo_path.to_path_buf());
94 match protocol_v2::parse_command_request(request)? {
95 protocol_v2::Command::LsRefs(req) => {
96 Ok(UploadPack::Buffered(protocol_v2::ls_refs(repo_path, &req)?))
97 }
98 protocol_v2::Command::Fetch(mut req) => {
99 let shallow = protocol_v2::apply_shallow_boundaries(repo_path, &mut req)?;
100 let is_shallow_negotiation = req.upload_request.shallow.depth.is_some();
101
102 // Negotiation round (no packfile yet): reply with acknowledgments.
103 if !req.upload_request.done && !is_shallow_negotiation {
104 let common = protocol_v2::common_haves(repo_path, &req)?;
105 let mut body = protocol_v2::encode_fetch_acknowledgments(&common);
106 body.extend_from_slice(&protocol_v2::encode_shallow_info(&shallow));
107 return Ok(UploadPack::Buffered(body));
108 }
109
110 let reader = backend.upload_pack(&req.upload_request).await?;
111
112 let mut prefix = if is_shallow_negotiation
113 && !req.upload_request.haves.is_empty()
114 && !req.upload_request.done
115 {
116 protocol_v2::encode_fetch_ready_and_acknowledgments(&protocol_v2::common_haves(
117 repo_path, &req,
118 )?)
119 } else {
120 Vec::new()
121 };
122 prefix.extend_from_slice(&protocol_v2::encode_shallow_info(&shallow));
123 prefix.extend_from_slice(&pktline::encode(b"packfile\n"));
124
125 let stream = protocol_v2::PrefixThenReader::new(
126 prefix,
127 protocol_v2::PackSectionReader::new(reader),
128 );
129 Ok(UploadPack::Pack(Box::pin(stream)))
130 }
131 }
132}
133
134/// Handle a `POST /git-upload-pack` request using protocol v0/v1 (fallback for
135/// clients that did not request v2). Returns the raw packfile stream.
136pub async fn upload_pack_v0(repo_path: &Path, request: &[u8]) -> Result<PackStream> {
137 let backend = GitBackend::new(repo_path.to_path_buf());
138 let parsed = pack::UploadPackRequest::parse(request)?;
139 let reader = backend.upload_pack(&parsed).await?;
140 Ok(Box::pin(reader))
141}
142
143/// Handle a `POST /git-receive-pack` request (push). Returns the report-status
144/// response body.
145pub async fn receive_pack<R>(repo_path: &Path, request: R) -> Result<Vec<u8>>
146where
147 R: AsyncRead + Unpin + Send + 'static,
148{
149 let backend = GitBackend::new(repo_path.to_path_buf());
150 Ok(backend.receive_pack(request).await?)
151}