Skip to content

Commit

Permalink
refactor: extract handler functions into separate file
Browse files Browse the repository at this point in the history
  • Loading branch information
justinrubek committed Feb 16, 2023
1 parent f233e2e commit f932711
Show file tree
Hide file tree
Showing 2 changed files with 178 additions and 172 deletions.
173 changes: 173 additions & 0 deletions crates/http/src/handlers.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
use crate::{
error::{self, Error, Result},
message::{self},
GitCodec, ServerState,
};
use axum::{
body::Bytes,
extract::{Path, Query, State},
};
use std::{collections::HashMap, sync::Arc};
use tokio::io::AsyncWriteExt;
use tokio_util::codec::Encoder;
use tracing::{debug, info};

// Generate a response for git-receive-pack using the `git` executable
#[allow(dead_code)]
pub(crate) async fn list_refs_child(
State(app_state): State<Arc<ServerState>>,
Path((owner, repo)): Path<(String, String)>,
Query(query): Query<HashMap<String, String>>,
) -> Result<impl axum::response::IntoResponse> {
info!("Received data for {}/{}", owner, repo);

let service = query.get("service").ok_or(error::Error::MissingService)?;
info!(?service);

// determine the path to the repo
let repo_path = app_state.repo_path.join(owner).join(repo);

// Call `git receive-pack --http-backend-info-refs` to get the data
let child = tokio::process::Command::new("git")
.arg("receive-pack")
.arg("--http-backend-info-refs")
.arg(&repo_path)
.output()
.await?;

let mut codec = GitCodec;
let mut buf = bytes::BytesMut::new();

codec.encode(
message::GitMessage::ServiceHeader(message::GitService::ReceivePack),
&mut buf,
)?;
codec.encode(message::GitMessage::Flush, &mut buf)?;

// With the header finished, just return all the data from the child process
buf.extend_from_slice(&child.stdout);

debug!(?buf, "Encoded data");

Ok((
[
(
axum::http::header::CONTENT_TYPE,
"application/x-git-receive-pack-advertisement",
),
(axum::http::header::CACHE_CONTROL, "no-cache"),
],
buf.freeze(),
))
}

/// Generate a response for git-receive-pack using git2
#[allow(dead_code)]
pub(crate) async fn list_refs(
State(app_state): State<Arc<ServerState>>,
Path((owner, repo)): Path<(String, String)>,
Query(query): Query<HashMap<String, String>>,
) -> Result<impl axum::response::IntoResponse> {
debug!("Received data for {}/{}", owner, repo);

let service = query.get("service").ok_or(error::Error::MissingService)?;
debug!(?service);

// determine the path to the repo
let repo_path = app_state.repo_path.join(owner).join(repo);
let repo = git2::Repository::open_bare(&repo_path)?;

// generate the ref response the client needs
let mut buf = bytes::BytesMut::new();
let mut codec = GitCodec;

let refs = repo.references()?;
// format the references how git wants them - the reference hash and the reference name
let mut refs = refs
.into_iter()
.filter_map(|r| r.ok())
.map(|r| {
let target = r.target().unwrap().to_string();
let name = r.name().unwrap();

let mut buf = bytes::BytesMut::new();
buf.extend_from_slice(target.as_ref());
buf.extend_from_slice(b" ");
buf.extend_from_slice(name.as_ref());

buf
})
.collect::<Vec<_>>();

codec.encode(
message::GitMessage::ServiceHeader(message::GitService::ReceivePack),
&mut buf,
)?;
codec.encode(message::GitMessage::Flush, &mut buf)?;

// We need to attach capabilities to the first ref
let capabilities =
b"\0report-status report-status-v2 delete-refs side-band-64k quiet atomic ofs-delta object-format=sha1 agent=git/thoenix";
// If there are no refs, we need to add a dummy ref representing a "null sha1"
let empty_sha = b"0000000000000000000000000000000000000000";

if refs.is_empty() {
let mut data = bytes::BytesMut::new();
data.extend_from_slice(empty_sha);
data.extend_from_slice(capabilities);
codec.encode(message::GitMessage::Data(data.split().to_vec()), &mut buf)?;
} else {
refs.iter_mut().enumerate().try_for_each(|(i, r)| {
if i == 0 {
r.extend_from_slice(capabilities);
}

codec.encode(message::GitMessage::Data(r.to_vec()), &mut buf)?;

Ok::<(), Error>(())
})?;
}
codec.encode(message::GitMessage::Flush, &mut buf).unwrap();

debug!(?refs, ?repo_path, ?buf);

Ok((
[
(
axum::http::header::CONTENT_TYPE,
"application/x-git-receive-pack-advertisement",
),
(axum::http::header::CACHE_CONTROL, "no-cache"),
],
buf.freeze(),
))
}

pub(crate) async fn receive_pack(
State(app_state): State<Arc<ServerState>>,
Path((owner, repo)): Path<(String, String)>,
payload: Bytes,
) -> Result<Bytes> {
info!(%owner, %repo, "Received send-pack data");
let len = payload.len();
info!(?len);

let repo_path = app_state.repo_path.join(owner).join(repo);

// invoke git-receive-pack and pipe the payload to it
let mut child = tokio::process::Command::new("git")
.arg("receive-pack")
.arg(&repo_path)
.stdin(std::process::Stdio::piped())
.stdout(std::process::Stdio::piped())
.spawn()?;

let mut stdin = child.stdin.take().unwrap();
stdin.write_all(&payload).await?;

let output = child.wait_with_output().await?;

info!(?output);

Ok(bytes::Bytes::from(output.stdout))
}
177 changes: 5 additions & 172 deletions crates/http/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,19 +1,17 @@
use axum::{
body::Bytes,
extract::{Path, Query, State},
routing::{get, post},
Router,
};
use std::{collections::HashMap, net::SocketAddr, path::PathBuf, sync::Arc};
use tokio::io::AsyncWriteExt;
use tokio_util::codec::Encoder;
use tracing::{debug, info};
use std::{net::SocketAddr, path::PathBuf, sync::Arc};

pub mod codec;
pub mod error;
pub mod handlers;
pub mod message;

use error::{Error, Result};
use error::Result;
#[allow(unused_imports)]
use handlers::{list_refs, list_refs_child, receive_pack};
use message::GitCodec;

pub struct ServerState {
Expand Down Expand Up @@ -43,7 +41,6 @@ impl Server {
// git. This could potentially be done using `git_pack::data::entry::Entry` to parse
// the file and `git_odn` to write? The method to implement is not clear yet.
let app = Router::new()
.route("/", get(root))
// .route("/configs/:owner/:repo.git/info/refs", get(list_refs_child))
.route("/configs/:owner/:repo.git/info/refs", get(list_refs))
.route(
Expand All @@ -61,167 +58,3 @@ impl Server {
Ok(())
}
}

async fn root() -> &'static str {
"Hello, World!"
}

// Generate a response for git-receive-pack using the `git` executable
#[allow(dead_code)]
async fn list_refs_child(
State(app_state): State<Arc<ServerState>>,
Path((owner, repo)): Path<(String, String)>,
Query(query): Query<HashMap<String, String>>,
) -> Result<impl axum::response::IntoResponse> {
info!("Received data for {}/{}", owner, repo);

let service = query.get("service").ok_or(error::Error::MissingService)?;
info!(?service);

// determine the path to the repo
let repo_path = app_state.repo_path.join(owner).join(repo);

// Call `git receive-pack --http-backend-info-refs` to get the data
let child = tokio::process::Command::new("git")
.arg("receive-pack")
.arg("--http-backend-info-refs")
.arg(&repo_path)
.output()
.await?;

let mut codec = GitCodec;
let mut buf = bytes::BytesMut::new();

codec.encode(
message::GitMessage::ServiceHeader(message::GitService::ReceivePack),
&mut buf,
)?;
codec.encode(message::GitMessage::Flush, &mut buf)?;

// With the header finished, just return all the data from the child process
buf.extend_from_slice(&child.stdout);

debug!(?buf, "Encoded data");

Ok((
[
(
axum::http::header::CONTENT_TYPE,
"application/x-git-receive-pack-advertisement",
),
(axum::http::header::CACHE_CONTROL, "no-cache"),
],
buf.freeze(),
))
}

/// Generate a response for git-receive-pack using git2
#[allow(dead_code)]
async fn list_refs(
State(app_state): State<Arc<ServerState>>,
Path((owner, repo)): Path<(String, String)>,
Query(query): Query<HashMap<String, String>>,
) -> Result<impl axum::response::IntoResponse> {
debug!("Received data for {}/{}", owner, repo);

let service = query.get("service").ok_or(error::Error::MissingService)?;
debug!(?service);

// determine the path to the repo
let repo_path = app_state.repo_path.join(owner).join(repo);
let repo = git2::Repository::open_bare(&repo_path)?;

// generate the ref response the client needs
let mut buf = bytes::BytesMut::new();
let mut codec = GitCodec;

let refs = repo.references()?;
// format the references how git wants them - the reference hash and the reference name
let mut refs = refs
.into_iter()
.filter_map(|r| r.ok())
.map(|r| {
let target = r.target().unwrap().to_string();
let name = r.name().unwrap();

let mut buf = bytes::BytesMut::new();
buf.extend_from_slice(target.as_ref());
buf.extend_from_slice(b" ");
buf.extend_from_slice(name.as_ref());

buf
})
.collect::<Vec<_>>();

codec.encode(
message::GitMessage::ServiceHeader(message::GitService::ReceivePack),
&mut buf,
)?;
codec.encode(message::GitMessage::Flush, &mut buf)?;

// We need to attach capabilities to the first ref
let capabilities =
b"\0report-status report-status-v2 delete-refs side-band-64k quiet atomic ofs-delta object-format=sha1 agent=git/thoenix";
// If there are no refs, we need to add a dummy ref representing a "null sha1"
let empty_sha = b"0000000000000000000000000000000000000000";

if refs.is_empty() {
let mut data = bytes::BytesMut::new();
data.extend_from_slice(empty_sha);
data.extend_from_slice(capabilities);
codec.encode(message::GitMessage::Data(data.split().to_vec()), &mut buf)?;
} else {
refs.iter_mut().enumerate().try_for_each(|(i, r)| {
if i == 0 {
r.extend_from_slice(capabilities);
}

codec.encode(message::GitMessage::Data(r.to_vec()), &mut buf)?;

Ok::<(), Error>(())
})?;
}
codec.encode(message::GitMessage::Flush, &mut buf).unwrap();

debug!(?refs, ?repo_path, ?buf);

Ok((
[
(
axum::http::header::CONTENT_TYPE,
"application/x-git-receive-pack-advertisement",
),
(axum::http::header::CACHE_CONTROL, "no-cache"),
],
buf.freeze(),
))
}

async fn receive_pack(
State(app_state): State<Arc<ServerState>>,
Path((owner, repo)): Path<(String, String)>,
payload: Bytes,
) -> Result<Bytes> {
info!(%owner, %repo, "Received send-pack data");
let len = payload.len();
info!(?len);

let repo_path = app_state.repo_path.join(owner).join(repo);

// invoke git-receive-pack and pipe the payload to it
let mut child = tokio::process::Command::new("git")
.arg("receive-pack")
.arg(&repo_path)
.stdin(std::process::Stdio::piped())
.stdout(std::process::Stdio::piped())
.spawn()?;

let mut stdin = child.stdin.take().unwrap();
stdin.write_all(&payload).await?;

let output = child.wait_with_output().await?;

info!(?output);

Ok(bytes::Bytes::from(output.stdout))
}

0 comments on commit f932711

Please sign in to comment.