+//! Smart HTTP git protocol handlers.+//!+//! Provides handlers for `info/refs` (ref advertisement) and `git-upload-pack`+//! (pack negotiation and transport), enabling `git clone` and `git fetch` over+//! HTTP without CGI.+//!+//! Both handlers shell out to `git upload-pack --stateless-rpc`, following the+//! same streaming pattern as the archive handler.++use std::process::Stdio;++use axum::body::Body;+use axum::extract::{Query, State};+use axum::http::HeaderMap;+use axum::http::header;+use axum::response::IntoResponse;+use serde::Deserialize;+use tokio::process::Command;+use tokio_util::io::ReaderStream;++use crate::error::{Error, Result};+use crate::git;+use crate::routes::{AppState, RepoName};++use super::archive::reaper;++/// Query parameters for `info/refs`.+#[derive(Deserialize)]+pub(super) struct InfoRefsQuery {+ service: Option<String>,+}++/// Encodes a string as a pkt-line (4-digit hex length + payload).+fn pkt_line(s: &str) -> Vec<u8> {+ let len = s.len() + 4;+ format!("{len:04x}{s}").into_bytes()+}++/// Handles `GET /{repo}/info/refs?service=git-upload-pack`.+///+/// Returns a ref advertisement so clients can discover branches and tags before+/// fetching. The response is a pkt-line stream with a `# service=git-upload-pack`+/// header, followed by the output of `git upload-pack --stateless-rpc --advertise-refs`.+pub async fn info_refs(+ State(state): State<AppState>,+ RepoName(repo): RepoName,+ Query(query): Query<InfoRefsQuery>,+) -> Result<impl IntoResponse> {+ let _service = query+ .service+ .filter(|s| s == "git-upload-pack")+ .ok_or_else(|| Error::BadRequest("unknown or missing service".into()))?;++ let repo_path = state.root.join(&repo);+ if !repo_path.exists() {+ return Err(Error::RepoNotFound(repo));+ }++ let _git_repo = git::open_repo(&state.root, &repo)?;++ let output = Command::new("git")+ .arg("upload-pack")+ .arg("--stateless-rpc")+ .arg("--advertise-refs")+ .arg(repo_path.as_os_str())+ .stdout(Stdio::piped())+ .stderr(Stdio::piped())+ .output()+ .await+ .map_err(Error::Io)?;++ if !output.status.success() {+ let stderr = String::from_utf8_lossy(&output.stderr);+ return Err(Error::GitCorrupt(format!(+ "git upload-pack --advertise-refs failed: {stderr}"+ )));+ }++ let mut buf = Vec::new();+ buf.extend_from_slice(&pkt_line("# service=git-upload-pack\n"));+ buf.extend_from_slice(b"0000");+ buf.extend_from_slice(&output.stdout);++ Ok((+ [(+ header::CONTENT_TYPE,+ "application/x-git-upload-pack-advertisement",+ )],+ buf,+ ))+}++/// Handles `POST /{repo}/git-upload-pack`.+///+/// Processes a pack-request body (wants / haves) and streams back the generated+/// pack file. The response is a pkt-line stream with status/progress messages+/// in band 2 and pack data in band 1.+pub async fn upload_pack(+ State(state): State<AppState>,+ RepoName(repo): RepoName,+ headers: HeaderMap,+ body: String,+) -> Result<impl IntoResponse> {+ let content_type = headers+ .get(header::CONTENT_TYPE)+ .and_then(|v| v.to_str().ok())+ .unwrap_or("");+ if content_type != "application/x-git-upload-pack-request" {+ return Err(Error::BadRequest(+ "expected application/x-git-upload-pack-request".into(),+ ));+ }++ let repo_path = state.root.join(&repo);+ if !repo_path.exists() {+ return Err(Error::RepoNotFound(repo));+ }++ let _git_repo = git::open_repo(&state.root, &repo)?;++ let mut child = Command::new("git")+ .arg("upload-pack")+ .arg("--stateless-rpc")+ .arg(repo_path.as_os_str())+ .stdin(Stdio::piped())+ .stdout(Stdio::piped())+ .stderr(Stdio::piped())+ .spawn()+ .map_err(Error::Io)?;++ let mut stdin = child.stdin.take().expect("stdin configured");+ let stdout = child.stdout.take().expect("stdout configured");+ let stderr = child.stderr.take().expect("stderr configured");++ tokio::spawn(async move {+ use tokio::io::AsyncWriteExt;+ let _ = stdin.write_all(body.as_bytes()).await;+ let _ = stdin.shutdown().await;+ });++ tokio::spawn(reaper(child, stderr, "git upload-pack"));++ let stream = ReaderStream::new(stdout);+ Ok((+ [(header::CONTENT_TYPE, "application/x-git-upload-pack-result")],+ Body::from_stream(stream),+ ))+}