| 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 | |
| 13 | use std::path::Path; |
| 14 | use std::pin::Pin; |
| 15 | |
| 16 | use gitserver_core::backend::GitBackend; |
| 17 | use gitserver_core::{ |
| 18 | pack, |
| 19 | pktline, |
| 20 | protocol_v2, |
| 21 | }; |
| 22 | use tokio::io::AsyncRead; |
| 23 | |
| 24 | use crate::error::Result; |
| 25 | |
| 26 | /// The two git wire services. |
| 27 | #[derive(Debug, Copy, Clone, Eq, PartialEq)] |
| 28 | pub enum Service { |
| 29 | /// `git-upload-pack` — serves fetch/clone. |
| 30 | UploadPack, |
| 31 | /// `git-receive-pack` — accepts push. |
| 32 | ReceivePack, |
| 33 | } |
| 34 | |
| 35 | impl 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`. |
| 64 | pub 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). |
| 69 | pub 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. |
| 79 | pub 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. |
| 96 | pub 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. |
| 140 | pub 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. |
| 149 | pub async fn receive_pack<R>(repo_path: &Path, request: R) -> Result<Vec<u8>> |
| 150 | where |
| 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 | } |