You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何用git2与actix-web实现Git的HTTP推拉操作?

如何基于Rust+actix-web+git2实现Git HTTP服务器的POST请求处理

我正在用Rust搭配actix-web框架和git2(libgit2的Rust绑定)开发Git服务器,已经完成了Git HTTP操作的首次GET /{repo_name}.git/info/refs请求处理(能正常引导客户端发起第二次POST请求),但卡在了第二次POST请求的实现上:已经获取到裸仓库实例和客户端发送的payload,不知道如何执行实际的push/pull操作。

Git HTTP操作的通用流程已理清:

  • Git客户端向repo.git/info/refs/发送带?service=参数的GET请求
  • 服务器返回客户端后续操作所需信息
  • 客户端发起POST请求:push对应repo.git/git-receive-pack,pull/clone对应repo.git/git-upload-pack,携带payload
  • 服务器处理请求并返回结果

当前已完成的GET请求路由代码

///```bash
/// # fetch operation
/// git clone locahost:8080/git/my_repo.git
///
/// # post operation
/// git push locahost:8080/git/my_repo.git
///```
#[get("/{repo_name}.git/info/refs")]
async fn handshake(
    query: web::Query<Query>,
    path: web::Path<Path>,
    storage: web::Data<Storage>,
) -> HttpResponse {
    if let Some(path) = resolve_repo_path(&path.repo_name, &storage) {
        let mut info_refs_output = Command::new(&query.service)
            .arg("--http-backend-info-refs")
            .arg(&path)
            .output()
            .unwrap();

        let service = format!("# service={}\n", &query.service);
        let service = format!("00{:x}{}", service.len() + 4, service);

        let mut body = Vec::<u8>::new();

        body.append(&mut service.as_bytes().to_vec());
        body.append(&mut b"0000".to_vec());
        body.append(&mut info_refs_output.stdout);

        HttpResponse::Ok()
            .content_type(format!("application/x-{}-advertisement", &query.service,))
            .insert_header(("Cache-Control", "no-cache"))
            .body(body)
    } else {
        HttpResponse::NotFound().finish()
    }
}

fn resolve_repo_path<'u>(repo_name: &'u str, storage: &Storage) -> Option<String> {
    // 解析仓库路径的逻辑
}

卡住的POST请求路由代码

// 处理push请求
#[post("/{repo_name}.git/git-receive-pack")]
async fn upload(
    mut payload: web::Payload,
    storage: web::Data<Storage>,
    path: web::Path<Path>,
) -> HttpResponse {
    if let Some(path) = resolve_repo_path(&path.repo_name, &storage) {
        let repo = git2::Repository::open_bare(&path).unwrap();
        // 卡在这里,不知道怎么处理payload和执行push操作
        todo!()
    } else {
        HttpResponse::InternalServerError().finish()
    }
}

// 处理pull/clone请求
#[post("/{repo_name}.git/git-upload-pack")]
async fn download(
    mut payload: web::Payload,
    storage: web::Data<Storage>,
    path: web::Path<Path>,
) -> HttpResponse {
    if let Some(path) = resolve_repo_path(&path.repo_name, &storage) {
        let repo = git2::Repository::open_bare(&path).unwrap();
        // 卡在这里,不知道怎么处理payload和执行pull操作
        todo!()
    } else {
        HttpResponse::InternalServerError().finish()
    }
}

解决方案:实现POST请求处理逻辑

一、处理git-receive-pack(Push操作)

push操作核心是接收客户端的pack数据并更新仓库引用,git2的ReceivePackAPI可直接处理这一流程,需要注意异步流转同步流的适配:

use git2::receive::{ReceivePack, ReceivePackOptions};
use actix_web::web::BytesMut;
use std::io::{Read, Write};

// 适配actix-web异步Payload的同步Read实现
struct PayloadReader {
    payload: web::Payload,
    buffer: BytesMut,
}

impl PayloadReader {
    fn new(payload: web::Payload) -> Self {
        Self { payload, buffer: BytesMut::new() }
    }
}

impl Read for PayloadReader {
    fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
        while self.buffer.is_empty() {
            match actix_web::rt::block_on(self.payload.next()) {
                Some(Ok(bytes)) => self.buffer.extend(bytes),
                Some(Err(e)) => return Err(std::io::Error::new(std::io::ErrorKind::Other, e)),
                None => break,
            }
        }
        let copy_len = std::cmp::min(buf.len(), self.buffer.len());
        buf[..copy_len].copy_from_slice(&self.buffer[..copy_len]);
        self.buffer.advance(copy_len);
        Ok(copy_len)
    }
}

#[post("/{repo_name}.git/git-receive-pack")]
async fn upload(
    mut payload: web::Payload,
    storage: web::Data<Storage>,
    path: web::Path<Path>,
) -> HttpResponse {
    let repo_path = match resolve_repo_path(&path.repo_name, &storage) {
        Some(p) => p,
        None => return HttpResponse::NotFound().finish(),
    };

    let repo = match git2::Repository::open_bare(&repo_path) {
        Ok(r) => r,
        Err(e) => {
            eprintln!("Failed to open repo: {}", e);
            return HttpResponse::InternalServerError().finish();
        }
    };

    let mut input = PayloadReader::new(payload);
    let mut output = Vec::new();

    // 配置ReceivePack,可添加权限验证、分支保护逻辑
    let mut opts = ReceivePackOptions::new();
    opts.update_reference(|refname, _old, new| {
        // 示例:允许所有引用更新,实际需添加权限校验
        println!("Updating reference {} to {}", refname, new);
        Ok(())
    });

    // 执行push处理逻辑
    match ReceivePack::new(&repo, &mut input, &mut output, Some(opts)) {
        Ok(mut rp) => {
            if let Err(e) = rp.execute() {
                eprintln!("Receive pack failed: {}", e);
                return HttpResponse::InternalServerError().finish();
            }
        }
        Err(e) => {
            eprintln!("Failed to create receive pack: {}", e);
            return HttpResponse::InternalServerError().finish();
        }
    }

    HttpResponse::Ok()
        .content_type("application/x-git-receive-pack-result")
        .body(output)
}

二、处理git-upload-pack(Pull/Clone操作)

pull/clone操作核心是根据客户端请求生成并发送pack数据,git2的UploadPackAPI负责处理:

use git2::upload::UploadPack;
use std::io::Read;

// 复用之前定义的PayloadReader

#[post("/{repo_name}.git/git-upload-pack")]
async fn download(
    mut payload: web::Payload,
    storage: web::Data<Storage>,
    path: web::Path<Path>,
) -> HttpResponse {
    let repo_path = match resolve_repo_path(&path.repo_name, &storage) {
        Some(p) => p,
        None => return HttpResponse::NotFound().finish(),
    };

    let repo = match git2::Repository::open_bare(&repo_path) {
        Ok(r) => r,
        Err(e) => {
            eprintln!("Failed to open repo: {}", e);
            return HttpResponse::InternalServerError().finish();
        }
    };

    let mut input = PayloadReader::new(payload);
    let mut output = Vec::new();

    // 执行pull处理逻辑
    match UploadPack::new(&repo, &mut input, &mut output) {
        Ok(mut up) => {
            if let Err(e) = up.execute() {
                eprintln!("Upload pack failed: {}", e);
                return HttpResponse::InternalServerError().finish();
            }
        }
        Err(e) => {
            eprintln!("Failed to create upload pack: {}", e);
            return HttpResponse::InternalServerError().finish();
        }
    }

    HttpResponse::Ok()
        .content_type("application/x-git-upload-pack-result")
        .body(output)
}

你遗漏的关键点

  1. 异步流转同步流:actix-web的Payload是异步流,但git2 API是阻塞式的,必须实现适配的Read包装器,用block_on处理异步读取,避免阻塞线程池。
  2. Git协议格式:POST请求的响应需使用专用MIME类型,响应体由git2内部处理为符合Git协议的pack数据,无需手动添加00xx长度前缀。
  3. 错误处理:避免使用unwrap(),需捕获git2的错误并返回对应HTTP状态码(如403权限拒绝、404仓库不存在)。
  4. 权限验证:在ReceivePack的update_reference回调中必须添加权限检查,验证用户是否有权限推送目标分支。
  5. 仓库权限:确保服务器进程对裸仓库目录有读写权限,否则无法完成引用更新。
  6. 流式处理:大pack文件需流式处理,避免一次性加载到内存,PayloadReader的实现已覆盖此需求。

内容的提问来源于stack exchange,提问作者al3x

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.06 21:40:28