Parent [>]
1//! Smart HTTP git protocol handlers.
2//!
3//! Provides handlers for `info/refs` (ref advertisement) and `git-upload-pack`
4//! (pack negotiation and transport), enabling `git clone` and `git fetch` over
5//! HTTP without CGI.
6//!
7//! Both handlers shell out to `git upload-pack --stateless-rpc`, following the
8//! same streaming pattern as the archive handler.
9
10use std::process::Stdio;
11
12use axum::body::Body;
13use axum::extract::{Query, State};
14use axum::http::HeaderMap;
15use axum::http::header;
16use axum::response::IntoResponse;
17use serde::Deserialize;
18use tokio::process::Command;
19use tokio_util::io::ReaderStream;
20
21use crate::error::{Error, Result};
22use crate::git;
23use crate::routes::{AppState, RepoName};
24
25use super::archive::reaper;
26
27/// Query parameters for `info/refs`.
28#[derive(Deserialize)]
29pub(super) struct InfoRefsQuery {
30    service: Option<String>,
31}
32
33/// Encodes a string as a pkt-line (4-digit hex length + payload).
34fn pkt_line(s: &str) -> Vec<u8> {
35    let len = s.len() + 4;
36    format!("{len:04x}{s}").into_bytes()
37}
38
39/// Handles `GET /{repo}/info/refs?service=git-upload-pack`.
40///
41/// Returns a ref advertisement so clients can discover branches and tags before
42/// fetching.  The response is a pkt-line stream with a `# service=git-upload-pack`
43/// header, followed by the output of `git upload-pack --stateless-rpc --advertise-refs`.
44pub async fn info_refs(
45    State(state): State<AppState>,
46    RepoName(repo): RepoName,
47    Query(query): Query<InfoRefsQuery>,
48) -> Result<impl IntoResponse> {
49    let _service = query
50        .service
51        .filter(|s| s == "git-upload-pack")
52        .ok_or_else(|| Error::BadRequest("unknown or missing service".into()))?;
53
54    let repo_path = state.root.join(&repo);
55    if !repo_path.exists() {
56        return Err(Error::RepoNotFound(repo));
57    }
58
59    let _git_repo = git::open_repo(&state.root, &repo)?;
60
61    let output = Command::new("git")
62        .arg("upload-pack")
63        .arg("--stateless-rpc")
64        .arg("--advertise-refs")
65        .arg(repo_path.as_os_str())
66        .stdout(Stdio::piped())
67        .stderr(Stdio::piped())
68        .output()
69        .await
70        .map_err(Error::Io)?;
71
72    if !output.status.success() {
73        let stderr = String::from_utf8_lossy(&output.stderr);
74        return Err(Error::GitCorrupt(format!(
75            "git upload-pack --advertise-refs failed: {stderr}"
76        )));
77    }
78
79    let mut buf = Vec::new();
80    buf.extend_from_slice(&pkt_line("# service=git-upload-pack\n"));
81    buf.extend_from_slice(b"0000");
82    buf.extend_from_slice(&output.stdout);
83
84    Ok((
85        [(
86            header::CONTENT_TYPE,
87            "application/x-git-upload-pack-advertisement",
88        )],
89        buf,
90    ))
91}
92
93/// Handles `POST /{repo}/git-upload-pack`.
94///
95/// Processes a pack-request body (wants / haves) and streams back the generated
96/// pack file.  The response is a pkt-line stream with status/progress messages
97/// in band 2 and pack data in band 1.
98pub async fn upload_pack(
99    State(state): State<AppState>,
100    RepoName(repo): RepoName,
101    headers: HeaderMap,
102    body: String,
103) -> Result<impl IntoResponse> {
104    let content_type = headers
105        .get(header::CONTENT_TYPE)
106        .and_then(|v| v.to_str().ok())
107        .unwrap_or("");
108    if content_type != "application/x-git-upload-pack-request" {
109        return Err(Error::BadRequest(
110            "expected application/x-git-upload-pack-request".into(),
111        ));
112    }
113
114    let repo_path = state.root.join(&repo);
115    if !repo_path.exists() {
116        return Err(Error::RepoNotFound(repo));
117    }
118
119    let _git_repo = git::open_repo(&state.root, &repo)?;
120
121    let mut child = Command::new("git")
122        .arg("upload-pack")
123        .arg("--stateless-rpc")
124        .arg(repo_path.as_os_str())
125        .stdin(Stdio::piped())
126        .stdout(Stdio::piped())
127        .stderr(Stdio::piped())
128        .spawn()
129        .map_err(Error::Io)?;
130
131    let mut stdin = child.stdin.take().expect("stdin configured");
132    let stdout = child.stdout.take().expect("stdout configured");
133    let stderr = child.stderr.take().expect("stderr configured");
134
135    tokio::spawn(async move {
136        use tokio::io::AsyncWriteExt;
137        let _ = stdin.write_all(body.as_bytes()).await;
138        let _ = stdin.shutdown().await;
139    });
140
141    tokio::spawn(reaper(child, stderr, "git upload-pack"));
142
143    let stream = ReaderStream::new(stdout);
144    Ok((
145        [(header::CONTENT_TYPE, "application/x-git-upload-pack-result")],
146        Body::from_stream(stream),
147    ))
148}