I'm writing a git server (a better term would be i've been trying to write a git server for a long time).
I'm writing it in rust, with actix-web
and git2
(libgit2 bindings for rust).
As far as I understand from the documentation, the general process is as follows
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 depending on the service
query, returns to the git client the necessary information to carry out the second operation.
3 the git client with the response from the server 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 the URI repo.git/git-upload-pack
with the information of the changes that it requires as payload for fetch operations (pull/clone).
4 then the server processes the request and carries out the corresponding operation, and responds to git client with the result of 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();
//I'm stucked here!
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();
//I'm stucked here!
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.
How can I perform the operations per se, with the bare repository and the payload sent by git client?, Can anyone see what I'm missing?
The first route works fine and I can get the git client to make the second request. The problem is that I can't find the way to carry out the operations per se