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