git_http_backend/actix/
git_receive_pack.rs

1use crate::GitConfig;
2use actix_web::http::{header, StatusCode};
3use actix_web::web::Payload;
4use actix_web::{web, HttpRequest, HttpResponse, HttpResponseBuilder, Responder};
5use flate2::read::GzDecoder;
6use futures_util::StreamExt;
7use std::io;
8use std::io::{Cursor, Read, Write};
9use std::process::{Command, Stdio};
10use tracing::info;
11
12pub async fn git_receive_pack(
13    request: HttpRequest,
14    mut payload: Payload,
15    service: web::Data<impl GitConfig>,
16) -> impl Responder {
17    let uri = request.uri();
18    let path = uri.path().to_string().replace("/git-receive-pack", "");
19    let path = service.rewrite(path).await;
20    if !path.join("HEAD").exists() || !path.join("config").exists() {
21        return HttpResponse::BadRequest().body("Repository not found or invalid.");
22    }
23
24    let is_bare_repo = match std::fs::read_to_string(path.join("config")) {
25        Ok(config) => config.contains("bare = true"),
26        Err(_) => false,
27    };
28    if !is_bare_repo {
29        return HttpResponse::BadRequest().body("Push operation requires a bare repository.");
30    }
31
32    let version = request
33        .headers()
34        .get("Git-Protocol")
35        .and_then(|v| v.to_str().ok())
36        .unwrap_or("")
37        .to_string();
38
39    let mut resp = HttpResponseBuilder::new(StatusCode::OK);
40    resp.append_header(("Content-Type", "application/x-git-receive-pack-advertise"));
41    resp.append_header(("Connection", "Keep-Alive"));
42    resp.append_header(("Transfer-Encoding", "chunked"));
43    resp.append_header(("X-Content-Type-Options", "nosniff"));
44
45    let mut cmd = Command::new("git");
46    cmd.arg("receive-pack");
47    cmd.arg("--stateless-rpc");
48    cmd.arg(".");
49    if !version.is_empty() {
50        cmd.env("GIT_PROTOCOL", version);
51    }
52    cmd.stderr(Stdio::piped());
53    cmd.stdin(Stdio::piped());
54    cmd.stdout(Stdio::piped());
55    cmd.current_dir(&path);
56
57    let mut git_process = match cmd.spawn() {
58        Ok(process) => process,
59        Err(e) => {
60            info!("Error running git command: {}", e);
61            return HttpResponse::InternalServerError().body("Error running git command");
62        }
63    };
64
65    let mut stdin = git_process.stdin.take().unwrap();
66    let mut stdout = git_process.stdout.take().unwrap();
67
68    let mut bytes = web::BytesMut::new();
69    while let Some(chunk) = payload.next().await {
70        match chunk {
71            Ok(data) => bytes.extend_from_slice(&data),
72            Err(e) => {
73                return HttpResponse::InternalServerError()
74                    .body(format!("Failed to read request body: {}", e))
75            }
76        }
77    }
78
79    let body_data = match request
80        .headers()
81        .get(header::CONTENT_ENCODING)
82        .and_then(|v| v.to_str().ok())
83    {
84        Some(encoding) if encoding.contains("gzip") => {
85            let mut decoder = GzDecoder::new(Cursor::new(bytes));
86            let mut decoded_data = Vec::new();
87            if let Err(e) = io::copy(&mut decoder, &mut decoded_data) {
88                return HttpResponse::InternalServerError()
89                    .body(format!("Failed to decode gzip body: {}", e));
90            }
91            decoded_data
92        }
93        _ => bytes.to_vec(),
94    };
95
96    if let Err(e) = stdin.write_all(&body_data) {
97        return HttpResponse::InternalServerError()
98            .body(format!("Failed to write to git process: {}", e));
99    }
100    drop(stdin);
101
102    let body_stream = actix_web::body::BodyStream::new(async_stream::stream! {
103        let mut buffer = [0; 8192];
104        loop {
105            match stdout.read(&mut buffer) {
106                Ok(0) => break, // EOF
107                Ok(n) => yield Ok::<_, io::Error>(web::Bytes::copy_from_slice(&buffer[..n])),
108                Err(e) => {
109                    eprintln!("Error reading stdout: {}", e);
110                    break;
111                }
112            }
113        }
114    });
115    resp.body(body_stream)
116}