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