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

