mod about; mod commit; mod diff; mod log; mod refs; mod smart_git; mod snapshot; mod summary; mod tag; mod tree; use std::{ collections::BTreeMap, ops::Deref, path::{Path, PathBuf}, sync::{Arc, LazyLock}, }; use axum::{ body::Body, handler::Handler, http::{Request, StatusCode}, response::{IntoResponse, Response}, }; use path_clean::PathClean; use self::{ about::handle as handle_about, commit::handle as handle_commit, diff::{handle as handle_diff, handle_plain as handle_patch}, log::handle as handle_log, refs::handle as handle_refs, smart_git::handle as handle_smart_git, snapshot::handle as handle_snapshot, summary::handle as handle_summary, tag::handle as handle_tag, tree::handle as handle_tree, }; use crate::database::schema::tag::YokedString; use crate::database::schema::{commit::YokedCommit, tag::YokedTag}; pub const DEFAULT_BRANCHES: [&str; 2] = ["refs/heads/master", "refs/heads/main"]; // this is some wicked, wicked abuse of axum right here... #[allow(clippy::trait_duplication_in_bounds)] // clippy seems a bit.. lost pub async fn service(mut request: Request) -> Response { let scan_path = request .extensions() .get::>() .expect("scan_path missing"); let ParsedUri { uri, child_path, action, } = parse_uri(request.uri().path().trim_matches('/')); let uri = Path::new(uri).clean(); let path = scan_path.join(&uri); let db = request .extensions() .get::>() .expect("db extension missing"); if path.as_os_str().is_empty() || !crate::database::schema::repository::Repository::exists(db, &uri).unwrap_or_default() { return RepositoryNotFound.into_response(); } request.extensions_mut().insert(ChildPath(child_path)); request.extensions_mut().insert(Repository(uri)); request.extensions_mut().insert(RepositoryPath(path)); match action { HandlerAction::About => handle_about.call(request, None::<()>).await, HandlerAction::SmartGit => handle_smart_git.call(request, None::<()>).await, HandlerAction::Refs => handle_refs.call(request, None::<()>).await, HandlerAction::Log => handle_log.call(request, None::<()>).await, HandlerAction::Tree => handle_tree.call(request, None::<()>).await, HandlerAction::Commit => handle_commit.call(request, None::<()>).await, HandlerAction::Diff => handle_diff.call(request, None::<()>).await, HandlerAction::Patch => handle_patch.call(request, None::<()>).await, HandlerAction::Tag => handle_tag.call(request, None::<()>).await, HandlerAction::Snapshot => handle_snapshot.call(request, None::<()>).await, HandlerAction::Summary => handle_summary.call(request, None::<()>).await, } } #[derive(Debug, PartialEq, Eq)] struct ParsedUri<'a> { action: HandlerAction, uri: &'a str, child_path: Option, } fn parse_uri(uri: &str) -> ParsedUri<'_> { let mut uri_parts = memchr::memchr_iter(b'/', uri.as_bytes()); let original_uri = uri; let (action, mut uri) = if let Some(idx) = uri_parts.next_back() { (uri.get(idx + 1..), &uri[..idx]) } else { (None, uri) }; match action { Some("about") => ParsedUri { action: HandlerAction::About, uri, child_path: None, }, Some("git-upload-pack") => ParsedUri { action: HandlerAction::SmartGit, uri, child_path: None, }, Some("refs") => { if let Some(idx) = uri_parts.next_back() { if uri.get(idx + 1..) == Some("info") { ParsedUri { action: HandlerAction::SmartGit, uri: &uri[..idx], child_path: None, } } else { ParsedUri { action: HandlerAction::Refs, uri, child_path: None, } } } else { ParsedUri { action: HandlerAction::Refs, uri, child_path: None, } } } Some("log") => ParsedUri { action: HandlerAction::Log, uri, child_path: None, }, Some("tree") => ParsedUri { action: HandlerAction::Tree, uri, child_path: None, }, Some("commit") => ParsedUri { action: HandlerAction::Commit, uri, child_path: None, }, Some("diff") => ParsedUri { action: HandlerAction::Diff, uri, child_path: None, }, Some("patch") => ParsedUri { action: HandlerAction::Patch, uri, child_path: None, }, Some("tag") => ParsedUri { action: HandlerAction::Tag, uri, child_path: None, }, Some("snapshot") => ParsedUri { action: HandlerAction::Snapshot, uri, child_path: None, }, Some(_) => { static TREE_FINDER: LazyLock = LazyLock::new(|| memchr::memmem::Finder::new(b"/tree/")); uri = original_uri; // match tree children if let Some(idx) = TREE_FINDER.find(uri.as_bytes()) { ParsedUri { action: HandlerAction::Tree, uri: &uri[..idx], // 6 is the length of /tree/ child_path: Some(Path::new(&uri[idx + 6..]).clean()), } } else { ParsedUri { action: HandlerAction::Summary, uri, child_path: None, } } } None => ParsedUri { action: HandlerAction::Summary, uri, child_path: None, }, } } #[derive(Copy, Clone, Debug, PartialEq, Eq)] enum HandlerAction { About, SmartGit, Refs, Log, Tree, Commit, Diff, Patch, Tag, Snapshot, Summary, } #[derive(Clone)] pub struct Repository(pub PathBuf); impl Deref for Repository { type Target = Path; fn deref(&self) -> &Self::Target { &self.0 } } #[derive(Clone)] pub struct RepositoryPath(pub PathBuf); #[derive(Clone)] pub struct ChildPath(pub Option); impl Deref for RepositoryPath { type Target = Path; fn deref(&self) -> &Self::Target { &self.0 } } pub type Result = std::result::Result; pub struct InvalidRequest; impl IntoResponse for InvalidRequest { fn into_response(self) -> Response { (StatusCode::NOT_FOUND, "Invalid request").into_response() } } pub struct RepositoryNotFound; impl IntoResponse for RepositoryNotFound { fn into_response(self) -> Response { (StatusCode::NOT_FOUND, "Repository not found").into_response() } } pub struct Error(anyhow::Error); impl From> for Error { fn from(e: Arc) -> Self { Self(anyhow::Error::msg(format!("{e:?}"))) } } impl From for Error { fn from(e: anyhow::Error) -> Self { Self(e) } } impl IntoResponse for Error { fn into_response(self) -> Response { (StatusCode::INTERNAL_SERVER_ERROR, format!("{:?}", self.0)).into_response() } } pub struct Refs { heads: BTreeMap, tags: Vec<(YokedString, YokedTag)>, }