rust-lang / git2-rs

libgit2 bindings for Rust
https://docs.rs/git2
Apache License 2.0
1.67k stars 384 forks source link

How to perform pull/push operations over HTTP with git2? #905

Closed 04l3x closed 1 year ago

04l3x commented 1 year ago

Hi all, I've been trying to write an http module for a git server for some time now, and I'm stuck here.

The libgit2 documentation doesn't help much, and the git documentation is pretty general.

Reading a bit of documentation, the general process for doing push/pull requests over http, goes something like this.

1 the git client sends a first GET request to the URI repo.git/info/refs/ with the query '?service='.

2 the server processes this request and based on the service query returns to the git client the information needed to perform the second operation.

3 the git client with the server response prepares a second request in this case POST. To the URI repo.git/git-receive-pack with the changes it sends as payload for push operations; or to the URI repo.git/git-upload-pack with the information of the changes it requires as payload for fetch (pull/clone) operations.

4 The server then processes the request and performs the corresponding operation, and responds to the git client with the result of the operation.

Understanding this process and with some more information about git-receive-pack and git-send-pack, I have come up with something like this.

A route for handle first HTTP request:

///```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> {
    // resolves the path of the repo
}

and the routes for handle the second HTTP request

// for 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();
        todo!()
    } else {
        HttpResponse::InternalServerError().finish()
    }
}

// for pull
#[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();
        todo!()
    } else {
        HttpResponse::InternalServerError().finish()
    }
}

And that is all. I'm stuck here I have the two routes to handle the second HTTP request, in both cases I have both the bare repository and the payload sent by the git client, but I don't know what to do with them, I don't know how to perform the push/pull operations per se.

The documentation in both git and libgit2 does not talk much about how these operations are performed. It only says that when the client invokes the service to the server both processes connect and it is as if everything happens in an opaque way.

Can anyone see what I'm missing?

Note: I'm using actix-web as web server, and git2. The first route works fine and I can get the git client to make the second request.