🏡 index : ~doyle/rgit.git

author Jordan Doyle <jordan@doyle.la> 2022-07-23 14:05:49.0 +01:00:00
committer Jordan Doyle <jordan@doyle.la> 2022-07-23 14:14:56.0 +01:00:00
commit
f74037c1b1ba30e54d3f1b6008589ff60568e6b2 [patch]
tree
1b2167139680215a00d79a20e3460b56cfc12fa4
parent
c2201124c0ecd4dacc4ecef79a12e6287839b618
download
f74037c1b1ba30e54d3f1b6008589ff60568e6b2.tar.gz

Split repo methods into their own modules



Diff

 build.rs                          |   3 ++-
 rustfmt.toml                      |   7 +++++++
 src/git.rs                        |  39 +++++++++++++++++++++------------------
 src/git_cgi.rs                    |  12 +++++++-----
 src/main.rs                       |  13 ++++++++-----
 src/syntax_highlight.rs           |  13 ++++++++-----
 src/database/indexer.rs           |   7 +++++--
 src/methods/index.rs              |   8 +++-----
 src/methods/repo.rs               | 509 --------------------------------------------------------------------------------
 src/database/schema/commit.rs     |   7 ++++---
 src/database/schema/prefixes.rs   |   3 ++-
 src/database/schema/repository.rs |  12 ++++--------
 src/database/schema/tag.rs        |   8 ++++----
 src/methods/repo/about.rs         |  29 +++++++++++++++++++++++++++++
 src/methods/repo/commit.rs        |  40 ++++++++++++++++++++++++++++++++++++++++
 src/methods/repo/diff.rs          |  59 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 src/methods/repo/log.rs           |  60 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 src/methods/repo/mod.rs           | 176 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 src/methods/repo/refs.rs          |  50 ++++++++++++++++++++++++++++++++++++++++++++++++++
 src/methods/repo/smart_git.rs     |  53 +++++++++++++++++++++++++++++++++++++++++++++++++++++
 src/methods/repo/summary.rs       |  55 +++++++++++++++++++++++++++++++++++++++++++++++++++++++
 src/methods/repo/tag.rs           |  37 +++++++++++++++++++++++++++++++++++++
 src/methods/repo/tree.rs          |  78 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 23 files changed, 712 insertions(+), 566 deletions(-)

diff --git a/build.rs b/build.rs
index cbcc334..b16d138 100644
--- a/build.rs
+++ a/build.rs
@@ -1,8 +1,9 @@
use anyhow::Context;
use std::{
    io::Write,
    path::{Path, PathBuf},
};

use anyhow::Context;

#[derive(Copy, Clone)]
pub struct Paths<'a> {
diff --git a/rustfmt.toml b/rustfmt.toml
new file mode 100644
index 0000000..d440511 100644
--- /dev/null
+++ a/rustfmt.toml
@@ -1,0 +1,7 @@
edition = "2021"
## not yet supported on stable
#imports_granularity = "Crate"
newline_style = "Unix"
## not yet supported on stable
#group_imports = "StdExternalCrate"
use_field_init_shorthand = true
diff --git a/src/git.rs b/src/git.rs
index c95cde7..f1414c4 100644
--- a/src/git.rs
+++ a/src/git.rs
@@ -1,8 +1,12 @@
use std::ffi::OsStr;
use std::path::Path;
use std::{borrow::Cow, fmt::Write, path::PathBuf, sync::Arc, time::Duration};
use std::{
    borrow::Cow,
    ffi::OsStr,
    fmt::Write,
    path::{Path, PathBuf},
    sync::Arc,
    time::Duration,
};

use crate::syntax_highlight::ComrakSyntectAdapter;
use anyhow::{Context, Result};
use bytes::{Bytes, BytesMut};
use comrak::{ComrakOptions, ComrakPlugins};
@@ -11,11 +15,15 @@
};
use moka::future::Cache;
use parking_lot::Mutex;
use syntect::html::{ClassStyle, ClassedHTMLGenerator};
use syntect::parsing::SyntaxSet;
use syntect::util::LinesWithEndings;
use syntect::{
    html::{ClassStyle, ClassedHTMLGenerator},
    parsing::SyntaxSet,
    util::LinesWithEndings,
};
use time::OffsetDateTime;
use tracing::instrument;

use crate::syntax_highlight::ComrakSyntectAdapter;

pub struct Git {
    commits: Cache<Oid, Arc<Commit>>,
@@ -109,8 +117,7 @@
                    let extension = path
                        .extension()
                        .or_else(|| path.file_name())
                        .map(|v| v.to_string_lossy())
                        .unwrap_or_else(|| Cow::Borrowed(""));
                        .map_or_else(|| Cow::Borrowed(""), OsStr::to_string_lossy);
                    let content = format_file(blob.content(), &extension, &self.git.syntax_set)?;

                    return Ok(PathDestination::File(FileWithContent {
@@ -187,8 +194,7 @@
                tagger: tag.tagger().map(TryInto::try_into).transpose()?,
                message: tag
                    .message_bytes()
                    .map(String::from_utf8_lossy)
                    .unwrap_or_else(|| Cow::Borrowed(""))
                    .map_or_else(|| Cow::Borrowed(""), String::from_utf8_lossy)
                    .into_owned(),
                tagged_object,
            })
@@ -440,13 +446,11 @@
            parents: commit.parent_ids().map(|v| v.to_string()).collect(),
            summary: commit
                .summary_bytes()
                .map(String::from_utf8_lossy)
                .unwrap_or_else(|| Cow::Borrowed(""))
                .map_or_else(|| Cow::Borrowed(""), String::from_utf8_lossy)
                .into_owned(),
            body: commit
                .body_bytes()
                .map(String::from_utf8_lossy)
                .unwrap_or_else(|| Cow::Borrowed(""))
                .map_or_else(|| Cow::Borrowed(""), String::from_utf8_lossy)
                .into_owned(),
            diff_stats: String::with_capacity(0),
            diff: String::with_capacity(0),
@@ -559,8 +563,7 @@
            if let Some(path) = delta.new_file().path() {
                path.extension()
                    .or_else(|| path.file_name())
                    .map(|v| v.to_string_lossy())
                    .unwrap_or_else(|| Cow::Borrowed(""))
                    .map_or_else(|| Cow::Borrowed(""), OsStr::to_string_lossy)
            } else {
                Cow::Borrowed("")
            }
@@ -572,7 +575,7 @@
            .unwrap_or_else(|| syntax_set.find_syntax_plain_text());
        let mut html_generator =
            ClassedHTMLGenerator::new_with_class_style(syntax, syntax_set, ClassStyle::Spaced);
        let _ = html_generator.parse_html_for_line_which_includes_newline(&line);
        let _res = html_generator.parse_html_for_line_which_includes_newline(&line);
        if let Some(class) = class {
            let _ = write!(diff_output, r#"<span class="diff-{class}">"#);
        }
diff --git a/src/git_cgi.rs b/src/git_cgi.rs
index b425da9..d46101f 100644
--- a/src/git_cgi.rs
+++ a/src/git_cgi.rs
@@ -1,10 +1,12 @@
use std::str::FromStr;

use anyhow::{bail, Context, Result};
use axum::body::{boxed, Body};
use axum::http::header::HeaderName;
use axum::http::HeaderValue;
use axum::response::Response;
use axum::{
    body::{boxed, Body},
    http::{header::HeaderName, HeaderValue},
    response::Response,
};
use httparse::Status;
use std::str::FromStr;

// https://en.wikipedia.org/wiki/Common_Gateway_Interface
pub fn cgi_to_response(buffer: &[u8]) -> Result<Response> {
diff --git a/src/main.rs b/src/main.rs
index 8d04be6..baf0d2d 100644
--- a/src/main.rs
+++ a/src/main.rs
@@ -1,15 +1,18 @@
#![deny(clippy::pedantic)]

use std::{sync::Arc, time::Duration};

use askama::Template;
use axum::http::StatusCode;
use axum::response::IntoResponse;
use axum::{
    body::Body, handler::Handler, http, http::HeaderValue, response::Response, routing::get,
    body::Body,
    handler::Handler,
    http,
    http::{HeaderValue, StatusCode},
    response::{IntoResponse, Response},
    routing::get,
    Extension, Router,
};
use bat::assets::HighlightingAssets;
use std::sync::Arc;
use std::time::Duration;
use syntect::html::ClassStyle;
use tower_layer::layer_fn;
use tracing::{info, instrument};
diff --git a/src/syntax_highlight.rs b/src/syntax_highlight.rs
index c3e1c1e..f0fab88 100644
--- a/src/syntax_highlight.rs
+++ a/src/syntax_highlight.rs
@@ -1,8 +1,11 @@
use comrak::adapters::SyntaxHighlighterAdapter;
use std::collections::HashMap;
use syntect::html::{ClassStyle, ClassedHTMLGenerator};
use syntect::parsing::SyntaxSet;
use syntect::util::LinesWithEndings;

use comrak::adapters::SyntaxHighlighterAdapter;
use syntect::{
    html::{ClassStyle, ClassedHTMLGenerator},
    parsing::SyntaxSet,
    util::LinesWithEndings,
};

pub struct ComrakSyntectAdapter<'a> {
    pub(crate) syntax_set: &'a SyntaxSet,
@@ -18,7 +21,7 @@
            ClassedHTMLGenerator::new_with_class_style(syntax, self.syntax_set, ClassStyle::Spaced);

        for line in LinesWithEndings::from(code) {
            let _ = html_generator.parse_html_for_line_which_includes_newline(line);
            let _res = html_generator.parse_html_for_line_which_includes_newline(line);
        }

        format!(
diff --git a/src/database/indexer.rs b/src/database/indexer.rs
index 0259c49..65187ed 100644
--- a/src/database/indexer.rs
+++ a/src/database/indexer.rs
@@ -1,6 +1,9 @@
use std::{
    collections::HashSet,
    path::{Path, PathBuf},
};

use git2::Sort;
use std::collections::HashSet;
use std::path::{Path, PathBuf};
use time::OffsetDateTime;
use tracing::{info, info_span};

diff --git a/src/methods/index.rs b/src/methods/index.rs
index 46e2a64..747ee22 100644
--- a/src/methods/index.rs
+++ a/src/methods/index.rs
@@ -1,13 +1,11 @@
use anyhow::Context;
use std::collections::BTreeMap;

use anyhow::Context;
use askama::Template;
use axum::response::Response;
use axum::Extension;
use axum::{response::Response, Extension};

use super::filters;
use crate::database::schema::repository::Repository;
use crate::into_response;
use crate::{database::schema::repository::Repository, into_response};

#[derive(Template)]
#[template(path = "index.html")]
diff --git a/src/methods/repo.rs b/src/methods/repo.rs
deleted file mode 100644
index 47a6f06..0000000 100644
--- a/src/methods/repo.rs
+++ /dev/null
@@ -1,509 +1,0 @@
use anyhow::Context;
use std::collections::BTreeMap;
use std::{
    fmt::{Debug, Display, Formatter},
    io::Write,
    ops::Deref,
    path::{Path, PathBuf},
    process::Stdio,
    sync::Arc,
};

use askama::Template;
use axum::http::StatusCode;
use axum::{
    body::HttpBody,
    extract::Query,
    handler::Handler,
    http,
    http::HeaderValue,
    http::Request,
    response::{IntoResponse, Response},
    Extension,
};
use bytes::Bytes;
use path_clean::PathClean;
use serde::Deserialize;
use tower::{util::BoxCloneService, Service};
use yoke::Yoke;

use super::filters;
use crate::database::schema::commit::YokedCommit;
use crate::database::schema::tag::YokedTag;
use crate::git::{DetailedTag, FileWithContent, PathDestination, ReadmeFormat, TreeItem};
use crate::{git::Commit, into_response, layers::UnwrapInfallible, Git};

#[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<PathBuf>);

impl Deref for RepositoryPath {
    type Target = Path;

    fn deref(&self) -> &Self::Target {
        &self.0
    }
}

pub type Result<T> = std::result::Result<T, Error>;

pub struct Error(anyhow::Error);

impl From<Arc<anyhow::Error>> for Error {
    fn from(e: Arc<anyhow::Error>) -> Self {
        Self(anyhow::Error::msg(format!("{:?}", e)))
    }
}

impl From<anyhow::Error> 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()
    }
}

// this is some wicked, wicked abuse of axum right here...
pub async fn service<ReqBody: HttpBody + Send + Debug + 'static>(
    mut request: Request<ReqBody>,
) -> Response
where
    <ReqBody as HttpBody>::Data: Send + Sync,
    <ReqBody as HttpBody>::Error: std::error::Error + Send + Sync,
{
    let mut uri_parts: Vec<&str> = request
        .uri()
        .path()
        .trim_start_matches('/')
        .trim_end_matches('/')
        .split('/')
        .collect();

    let mut child_path = None;

    macro_rules! h {
        ($handler:ident) => {
            BoxCloneService::new($handler.into_service())
        };
    }

    let mut service = match uri_parts.pop() {
        Some("about") => BoxCloneService::new(handle_about.into_service()),
        // TODO: https://man.archlinux.org/man/git-http-backend.1.en
        // TODO: GIT_PROTOCOL
        Some("refs") if uri_parts.last() == Some(&"info") => {
            uri_parts.pop();
            h!(handle_info_refs)
        }
        Some("git-upload-pack") => h!(handle_git_upload_pack),
        Some("refs") => h!(handle_refs),
        Some("log") => h!(handle_log),
        Some("tree") => h!(handle_tree),
        Some("commit") => h!(handle_commit),
        Some("diff") => h!(handle_diff),
        Some("patch") => h!(handle_patch),
        Some("tag") => h!(handle_tag),
        Some(v) => {
            uri_parts.push(v);

            // match tree children
            if uri_parts.iter().any(|v| *v == "tree") {
                // TODO: this needs fixing up so it doesn't accidentally match repos that have
                //  `tree` in their path
                let mut reconstructed_path = Vec::new();

                while let Some(part) = uri_parts.pop() {
                    if part == "tree" {
                        break;
                    }

                    // TODO: FIXME
                    reconstructed_path.insert(0, part);
                }

                child_path = Some(reconstructed_path.into_iter().collect::<PathBuf>().clean());

                h!(handle_tree)
            } else {
                h!(handle_summary)
            }
        }
        None => panic!("not found"),
    };

    let uri = uri_parts.into_iter().collect::<PathBuf>().clean();
    let path = Path::new("../test-git").canonicalize().unwrap().join(&uri);

    request.extensions_mut().insert(ChildPath(child_path));
    request.extensions_mut().insert(Repository(uri));
    request.extensions_mut().insert(RepositoryPath(path));

    service
        .call(request)
        .await
        .unwrap_infallible()
        .into_response()
}

pub struct Refs {
    heads: BTreeMap<String, YokedCommit>,
    tags: Vec<(String, YokedTag)>,
}

#[derive(Template)]
#[template(path = "repo/summary.html")]
pub struct SummaryView<'a> {
    repo: Repository,
    refs: Refs,
    commit_list: Vec<&'a crate::database::schema::commit::Commit<'a>>,
}

pub async fn handle_summary(
    Extension(repo): Extension<Repository>,
    Extension(db): Extension<sled::Db>,
) -> Result<Response> {
    let repository = crate::database::schema::repository::Repository::open(&db, &*repo)?
        .context("Repository does not exist")?;
    let commit_tree = repository.get().commit_tree(&db, "refs/heads/master")?;
    let commits = commit_tree.fetch_latest(11, 0).await;
    let commit_list = commits.iter().map(Yoke::get).collect();

    let mut heads = BTreeMap::new();
    for head in repository.get().heads(&db) {
        let commit_tree = repository.get().commit_tree(&db, &head)?;
        let name = head.strip_prefix("refs/heads/");

        if let (Some(name), Some(commit)) = (name, commit_tree.fetch_latest_one()) {
            heads.insert(name.to_string(), commit);
        }
    }

    let tags = repository
        .get()
        .tag_tree(&db)
        .context("Failed to fetch indexed tags")?
        .fetch_all();

    Ok(into_response(&SummaryView {
        repo,
        refs: Refs { heads, tags },
        commit_list,
    }))
}

#[derive(Deserialize)]
pub struct TagQuery {
    #[serde(rename = "h")]
    name: String,
}

#[derive(Template)]
#[template(path = "repo/tag.html")]
pub struct TagView {
    repo: Repository,
    tag: DetailedTag,
}

pub async fn handle_tag(
    Extension(repo): Extension<Repository>,
    Extension(RepositoryPath(repository_path)): Extension<RepositoryPath>,
    Extension(git): Extension<Arc<Git>>,
    Query(query): Query<TagQuery>,
) -> Result<Response> {
    let open_repo = git.repo(repository_path).await?;
    let tag = open_repo.tag_info(&query.name).await?;

    Ok(into_response(&TagView { repo, tag }))
}

#[derive(Deserialize)]
pub struct LogQuery {
    #[serde(rename = "ofs")]
    offset: Option<usize>,
    #[serde(rename = "h")]
    branch: Option<String>,
}

#[derive(Template)]
#[template(path = "repo/log.html")]
pub struct LogView<'a> {
    repo: Repository,
    commits: Vec<&'a crate::database::schema::commit::Commit<'a>>,
    next_offset: Option<usize>,
    branch: Option<String>,
}

pub async fn handle_log(
    Extension(repo): Extension<Repository>,
    Extension(db): Extension<sled::Db>,
    Query(query): Query<LogQuery>,
) -> Result<Response> {
    let offset = query.offset.unwrap_or(0);

    let reference = format!("refs/heads/{}", query.branch.as_deref().unwrap_or("master"));
    let repository = crate::database::schema::repository::Repository::open(&db, &*repo)?
        .context("Repository does not exist")?;
    let commit_tree = repository.get().commit_tree(&db, &reference)?;
    let mut commits = commit_tree.fetch_latest(101, offset).await;

    let next_offset = if commits.len() == 101 {
        commits.pop();
        Some(offset + 100)
    } else {
        None
    };

    let commits = commits.iter().map(Yoke::get).collect();

    Ok(into_response(&LogView {
        repo,
        commits,
        next_offset,
        branch: query.branch,
    }))
}

#[derive(Deserialize)]
pub struct SmartGitQuery {
    service: String,
}

pub async fn handle_info_refs(
    Extension(RepositoryPath(repository_path)): Extension<RepositoryPath>,
    Query(query): Query<SmartGitQuery>,
) -> Result<Response> {
    // todo: tokio command
    let out = std::process::Command::new("git")
        .arg("http-backend")
        .env("REQUEST_METHOD", "GET")
        .env("PATH_INFO", "/info/refs")
        .env("GIT_PROJECT_ROOT", repository_path)
        .env("QUERY_STRING", format!("service={}", query.service))
        .output()
        .unwrap();

    Ok(crate::git_cgi::cgi_to_response(&out.stdout)?)
}

pub async fn handle_git_upload_pack(
    Extension(RepositoryPath(repository_path)): Extension<RepositoryPath>,
    body: Bytes,
) -> Result<Response> {
    // todo: tokio command
    let mut child = std::process::Command::new("git")
        .arg("http-backend")
        // todo: read all this from request
        .env("REQUEST_METHOD", "POST")
        .env("CONTENT_TYPE", "application/x-git-upload-pack-request")
        .env("PATH_INFO", "/git-upload-pack")
        .env("GIT_PROJECT_ROOT", repository_path)
        .stdout(Stdio::piped())
        .stdin(Stdio::piped())
        .spawn()
        .unwrap();
    child.stdin.as_mut().unwrap().write_all(&body).unwrap();
    let out = child.wait_with_output().unwrap();

    Ok(crate::git_cgi::cgi_to_response(&out.stdout)?)
}

#[derive(Template)]
#[template(path = "repo/refs.html")]
pub struct RefsView {
    repo: Repository,
    refs: Refs,
}

pub async fn handle_refs(
    Extension(repo): Extension<Repository>,
    Extension(db): Extension<sled::Db>,
) -> Result<Response> {
    let repository = crate::database::schema::repository::Repository::open(&db, &*repo)?
        .context("Repository does not exist")?;

    let mut heads = BTreeMap::new();
    for head in repository.get().heads(&db) {
        let commit_tree = repository.get().commit_tree(&db, &head)?;
        let name = head.strip_prefix("refs/heads/");

        if let (Some(name), Some(commit)) = (name, commit_tree.fetch_latest_one()) {
            heads.insert(name.to_string(), commit);
        }
    }

    let tags = repository
        .get()
        .tag_tree(&db)
        .context("Failed to fetch indexed tags")?
        .fetch_all();

    Ok(into_response(&RefsView {
        repo,
        refs: Refs { heads, tags },
    }))
}

#[derive(Template)]
#[template(path = "repo/about.html")]
pub struct AboutView {
    repo: Repository,
    readme: Option<(ReadmeFormat, Arc<str>)>,
}

pub async fn handle_about(
    Extension(repo): Extension<Repository>,
    Extension(RepositoryPath(repository_path)): Extension<RepositoryPath>,
    Extension(git): Extension<Arc<Git>>,
) -> Result<Response> {
    let open_repo = git.clone().repo(repository_path).await?;
    let readme = open_repo.readme().await?;

    Ok(into_response(&AboutView { repo, readme }))
}

#[derive(Template)]
#[template(path = "repo/commit.html")]
pub struct CommitView {
    pub repo: Repository,
    pub commit: Arc<Commit>,
}

#[derive(Deserialize)]
pub struct CommitQuery {
    id: Option<String>,
}

pub async fn handle_commit(
    Extension(repo): Extension<Repository>,
    Extension(RepositoryPath(repository_path)): Extension<RepositoryPath>,
    Extension(git): Extension<Arc<Git>>,
    Query(query): Query<CommitQuery>,
) -> Result<Response> {
    let open_repo = git.repo(repository_path).await?;
    let commit = if let Some(commit) = query.id {
        open_repo.commit(&commit).await?
    } else {
        Arc::new(open_repo.latest_commit().await?)
    };

    Ok(into_response(&CommitView { repo, commit }))
}

#[derive(Deserialize)]
pub struct TreeQuery {
    id: Option<String>,
    #[serde(rename = "h")]
    branch: Option<String>,
}

impl Display for TreeQuery {
    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
        let mut prefix = "?";

        if let Some(id) = self.id.as_deref() {
            write!(f, "{}id={}", prefix, id)?;
            prefix = "&";
        }

        if let Some(branch) = self.branch.as_deref() {
            write!(f, "{}h={}", prefix, branch)?;
        }

        Ok(())
    }
}

pub async fn handle_tree(
    Extension(repo): Extension<Repository>,
    Extension(RepositoryPath(repository_path)): Extension<RepositoryPath>,
    Extension(ChildPath(child_path)): Extension<ChildPath>,
    Extension(git): Extension<Arc<Git>>,
    Query(query): Query<TreeQuery>,
) -> Result<Response> {
    #[derive(Template)]
    #[template(path = "repo/tree.html")]
    pub struct TreeView {
        pub repo: Repository,
        pub items: Vec<TreeItem>,
        pub query: TreeQuery,
    }

    #[derive(Template)]
    #[template(path = "repo/file.html")]
    pub struct FileView {
        pub repo: Repository,
        pub file: FileWithContent,
    }

    let open_repo = git.repo(repository_path).await?;

    Ok(
        match open_repo
            .path(child_path, query.id.as_deref(), query.branch.clone())
            .await?
        {
            PathDestination::Tree(items) => into_response(&TreeView { repo, items, query }),
            PathDestination::File(file) => into_response(&FileView { repo, file }),
        },
    )
}

#[derive(Template)]
#[template(path = "repo/diff.html")]
pub struct DiffView {
    pub repo: Repository,
    pub commit: Arc<Commit>,
}

pub async fn handle_diff(
    Extension(repo): Extension<Repository>,
    Extension(RepositoryPath(repository_path)): Extension<RepositoryPath>,
    Extension(git): Extension<Arc<Git>>,
    Query(query): Query<CommitQuery>,
) -> Result<Response> {
    let open_repo = git.repo(repository_path).await?;
    let commit = if let Some(commit) = query.id {
        open_repo.commit(&commit).await?
    } else {
        Arc::new(open_repo.latest_commit().await?)
    };

    Ok(into_response(&DiffView { repo, commit }))
}

pub async fn handle_patch(
    Extension(RepositoryPath(repository_path)): Extension<RepositoryPath>,
    Extension(git): Extension<Arc<Git>>,
    Query(query): Query<CommitQuery>,
) -> Result<Response> {
    let open_repo = git.repo(repository_path).await?;
    let commit = if let Some(commit) = query.id {
        open_repo.commit(&commit).await?
    } else {
        Arc::new(open_repo.latest_commit().await?)
    };

    let headers = [(
        http::header::CONTENT_TYPE,
        HeaderValue::from_static("text/plain"),
    )];

    Ok((headers, commit.diff_plain.clone()).into_response())
}
diff --git a/src/database/schema/commit.rs b/src/database/schema/commit.rs
index faa80fe..978e6e3 100644
--- a/src/database/schema/commit.rs
+++ a/src/database/schema/commit.rs
@@ -1,11 +1,12 @@
use crate::database::schema::Yoked;
use std::{borrow::Cow, ops::Deref};

use git2::{Oid, Signature};
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use sled::IVec;
use std::borrow::Cow;
use std::ops::Deref;
use time::OffsetDateTime;
use yoke::{Yoke, Yokeable};

use crate::database::schema::Yoked;

#[derive(Serialize, Deserialize, Debug, Yokeable)]
pub struct Commit<'a> {
diff --git a/src/database/schema/prefixes.rs b/src/database/schema/prefixes.rs
index cad0eec..8cfd627 100644
--- a/src/database/schema/prefixes.rs
+++ a/src/database/schema/prefixes.rs
@@ -1,5 +1,6 @@
use crate::database::schema::repository::RepositoryId;
use std::path::Path;

use crate::database::schema::repository::RepositoryId;

#[repr(u8)]
pub enum TreePrefix {
diff --git a/src/database/schema/repository.rs b/src/database/schema/repository.rs
index 0769fca..a8c8c9b 100644
--- a/src/database/schema/repository.rs
+++ a/src/database/schema/repository.rs
@@ -1,17 +1,13 @@
use crate::database::schema::commit::CommitTree;
use crate::database::schema::prefixes::TreePrefix;
use crate::database::schema::tag::TagTree;
use crate::database::schema::Yoked;
use std::{borrow::Cow, collections::BTreeMap, ops::Deref, path::Path};

use anyhow::{Context, Result};
use nom::AsBytes;
use serde::{Deserialize, Serialize};
use sled::IVec;
use std::borrow::Cow;
use std::collections::BTreeMap;
use std::ops::Deref;
use std::path::Path;
use time::OffsetDateTime;
use yoke::{Yoke, Yokeable};

use crate::database::schema::{commit::CommitTree, prefixes::TreePrefix, tag::TagTree, Yoked};

#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Hash, Yokeable)]
pub struct Repository<'a> {
diff --git a/src/database/schema/tag.rs b/src/database/schema/tag.rs
index 9958ee4..942ee5d 100644
--- a/src/database/schema/tag.rs
+++ a/src/database/schema/tag.rs
@@ -1,11 +1,11 @@
use crate::database::schema::commit::Author;
use crate::database::schema::Yoked;
use std::{collections::HashSet, ops::Deref};

use git2::Signature;
use serde::{Deserialize, Serialize};
use sled::IVec;
use std::collections::HashSet;
use std::ops::Deref;
use yoke::{Yoke, Yokeable};

use crate::database::schema::{commit::Author, Yoked};

#[derive(Serialize, Deserialize, Debug, Yokeable)]
pub struct Tag<'a> {
diff --git a/src/methods/repo/about.rs b/src/methods/repo/about.rs
new file mode 100644
index 0000000..7dda1e1 100644
--- /dev/null
+++ a/src/methods/repo/about.rs
@@ -1,0 +1,29 @@
use std::sync::Arc;

use askama::Template;
use axum::{response::Response, Extension};

use crate::{
    git::ReadmeFormat,
    into_response,
    methods::repo::{Repository, RepositoryPath, Result},
    Git,
};

#[derive(Template)]
#[template(path = "repo/about.html")]
pub struct View {
    repo: Repository,
    readme: Option<(ReadmeFormat, Arc<str>)>,
}

pub async fn handle(
    Extension(repo): Extension<Repository>,
    Extension(RepositoryPath(repository_path)): Extension<RepositoryPath>,
    Extension(git): Extension<Arc<Git>>,
) -> Result<Response> {
    let open_repo = git.clone().repo(repository_path).await?;
    let readme = open_repo.readme().await?;

    Ok(into_response(&View { repo, readme }))
}
diff --git a/src/methods/repo/commit.rs b/src/methods/repo/commit.rs
new file mode 100644
index 0000000..e3fea6d 100644
--- /dev/null
+++ a/src/methods/repo/commit.rs
@@ -1,0 +1,40 @@
use std::sync::Arc;

use askama::Template;
use axum::{extract::Query, response::Response, Extension};
use serde::Deserialize;

use crate::{
    git::Commit,
    into_response,
    methods::repo::{Repository, RepositoryPath, Result},
    Git,
};

#[derive(Template)]
#[template(path = "repo/commit.html")]
pub struct View {
    pub repo: Repository,
    pub commit: Arc<Commit>,
}

#[derive(Deserialize)]
pub struct UriQuery {
    pub id: Option<String>,
}

pub async fn handle(
    Extension(repo): Extension<Repository>,
    Extension(RepositoryPath(repository_path)): Extension<RepositoryPath>,
    Extension(git): Extension<Arc<Git>>,
    Query(query): Query<UriQuery>,
) -> Result<Response> {
    let open_repo = git.repo(repository_path).await?;
    let commit = if let Some(commit) = query.id {
        open_repo.commit(&commit).await?
    } else {
        Arc::new(open_repo.latest_commit().await?)
    };

    Ok(into_response(&View { repo, commit }))
}
diff --git a/src/methods/repo/diff.rs b/src/methods/repo/diff.rs
new file mode 100644
index 0000000..942bc3d 100644
--- /dev/null
+++ a/src/methods/repo/diff.rs
@@ -1,0 +1,59 @@
use std::sync::Arc;

use askama::Template;
use axum::{
    extract::Query,
    http::HeaderValue,
    response::{IntoResponse, Response},
    Extension,
};

use crate::{
    git::Commit,
    http, into_response,
    methods::repo::{commit::UriQuery, Repository, RepositoryPath, Result},
    Git,
};

#[derive(Template)]
#[template(path = "repo/diff.html")]
pub struct View {
    pub repo: Repository,
    pub commit: Arc<Commit>,
}

pub async fn handle(
    Extension(repo): Extension<Repository>,
    Extension(RepositoryPath(repository_path)): Extension<RepositoryPath>,
    Extension(git): Extension<Arc<Git>>,
    Query(query): Query<UriQuery>,
) -> Result<Response> {
    let open_repo = git.repo(repository_path).await?;
    let commit = if let Some(commit) = query.id {
        open_repo.commit(&commit).await?
    } else {
        Arc::new(open_repo.latest_commit().await?)
    };

    Ok(into_response(&View { repo, commit }))
}

pub async fn handle_plain(
    Extension(RepositoryPath(repository_path)): Extension<RepositoryPath>,
    Extension(git): Extension<Arc<Git>>,
    Query(query): Query<UriQuery>,
) -> Result<Response> {
    let open_repo = git.repo(repository_path).await?;
    let commit = if let Some(commit) = query.id {
        open_repo.commit(&commit).await?
    } else {
        Arc::new(open_repo.latest_commit().await?)
    };

    let headers = [(
        http::header::CONTENT_TYPE,
        HeaderValue::from_static("text/plain"),
    )];

    Ok((headers, commit.diff_plain.clone()).into_response())
}
diff --git a/src/methods/repo/log.rs b/src/methods/repo/log.rs
new file mode 100644
index 0000000..096d6de 100644
--- /dev/null
+++ a/src/methods/repo/log.rs
@@ -1,0 +1,60 @@
use anyhow::Context;
use askama::Template;
use axum::{extract::Query, response::Response, Extension};
use serde::Deserialize;
use yoke::Yoke;

use crate::{
    into_response,
    methods::{
        filters,
        repo::{Repository, Result},
    },
};

#[derive(Deserialize)]
pub struct UriQuery {
    #[serde(rename = "ofs")]
    offset: Option<usize>,
    #[serde(rename = "h")]
    branch: Option<String>,
}

#[derive(Template)]
#[template(path = "repo/log.html")]
pub struct View<'a> {
    repo: Repository,
    commits: Vec<&'a crate::database::schema::commit::Commit<'a>>,
    next_offset: Option<usize>,
    branch: Option<String>,
}

pub async fn handle(
    Extension(repo): Extension<Repository>,
    Extension(db): Extension<sled::Db>,
    Query(query): Query<UriQuery>,
) -> Result<Response> {
    let offset = query.offset.unwrap_or(0);

    let reference = format!("refs/heads/{}", query.branch.as_deref().unwrap_or("master"));
    let repository = crate::database::schema::repository::Repository::open(&db, &*repo)?
        .context("Repository does not exist")?;
    let commit_tree = repository.get().commit_tree(&db, &reference)?;
    let mut commits = commit_tree.fetch_latest(101, offset).await;

    let next_offset = if commits.len() == 101 {
        commits.pop();
        Some(offset + 100)
    } else {
        None
    };

    let commits = commits.iter().map(Yoke::get).collect();

    Ok(into_response(&View {
        repo,
        commits,
        next_offset,
        branch: query.branch,
    }))
}
diff --git a/src/methods/repo/mod.rs b/src/methods/repo/mod.rs
new file mode 100644
index 0000000..0cbc06a 100644
--- /dev/null
+++ a/src/methods/repo/mod.rs
@@ -1,0 +1,176 @@
mod about;
mod commit;
mod diff;
mod log;
mod refs;
mod smart_git;
mod summary;
mod tag;
mod tree;

use std::{
    collections::BTreeMap,
    fmt::Debug,
    ops::Deref,
    path::{Path, PathBuf},
    sync::Arc,
};

use axum::{
    body::HttpBody,
    handler::Handler,
    http::{Request, StatusCode},
    response::{IntoResponse, Response},
};
use path_clean::PathClean;
use tower::{util::BoxCloneService, Service};

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_git_upload_pack, handle_info_refs},
    summary::handle as handle_summary,
    tag::handle as handle_tag,
    tree::handle as handle_tree,
};
use crate::{
    database::schema::{commit::YokedCommit, tag::YokedTag},
    layers::UnwrapInfallible,
};

// 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<ReqBody>(mut request: Request<ReqBody>) -> Response
where
    ReqBody: HttpBody + Send + Debug + 'static,
    <ReqBody as HttpBody>::Data: Send + Sync,
    <ReqBody as HttpBody>::Error: std::error::Error + Send + Sync,
{
    let mut uri_parts: Vec<&str> = request
        .uri()
        .path()
        .trim_start_matches('/')
        .trim_end_matches('/')
        .split('/')
        .collect();

    let mut child_path = None;

    macro_rules! h {
        ($handler:ident) => {
            BoxCloneService::new($handler.into_service())
        };
    }

    let mut service = match uri_parts.pop() {
        Some("about") => h!(handle_about),
        // TODO: https://man.archlinux.org/man/git-http-backend.1.en
        // TODO: GIT_PROTOCOL
        Some("refs") if uri_parts.last() == Some(&"info") => {
            uri_parts.pop();
            h!(handle_info_refs)
        }
        Some("git-upload-pack") => h!(handle_git_upload_pack),
        Some("refs") => h!(handle_refs),
        Some("log") => h!(handle_log),
        Some("tree") => h!(handle_tree),
        Some("commit") => h!(handle_commit),
        Some("diff") => h!(handle_diff),
        Some("patch") => h!(handle_patch),
        Some("tag") => h!(handle_tag),
        Some(v) => {
            uri_parts.push(v);

            // match tree children
            if uri_parts.iter().any(|v| *v == "tree") {
                // TODO: this needs fixing up so it doesn't accidentally match repos that have
                //  `tree` in their path
                let mut reconstructed_path = Vec::new();

                while let Some(part) = uri_parts.pop() {
                    if part == "tree" {
                        break;
                    }

                    // TODO: FIXME
                    reconstructed_path.insert(0, part);
                }

                child_path = Some(reconstructed_path.into_iter().collect::<PathBuf>().clean());

                h!(handle_tree)
            } else {
                h!(handle_summary)
            }
        }
        None => panic!("not found"),
    };

    let uri = uri_parts.into_iter().collect::<PathBuf>().clean();
    let path = Path::new("../test-git").canonicalize().unwrap().join(&uri);

    request.extensions_mut().insert(ChildPath(child_path));
    request.extensions_mut().insert(Repository(uri));
    request.extensions_mut().insert(RepositoryPath(path));

    service
        .call(request)
        .await
        .unwrap_infallible()
        .into_response()
}

#[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<PathBuf>);

impl Deref for RepositoryPath {
    type Target = Path;

    fn deref(&self) -> &Self::Target {
        &self.0
    }
}

pub type Result<T> = std::result::Result<T, Error>;

pub struct Error(anyhow::Error);

impl From<Arc<anyhow::Error>> for Error {
    fn from(e: Arc<anyhow::Error>) -> Self {
        Self(anyhow::Error::msg(format!("{:?}", e)))
    }
}

impl From<anyhow::Error> 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<String, YokedCommit>,
    tags: Vec<(String, YokedTag)>,
}
diff --git a/src/methods/repo/refs.rs b/src/methods/repo/refs.rs
new file mode 100644
index 0000000..bf4f256 100644
--- /dev/null
+++ a/src/methods/repo/refs.rs
@@ -1,0 +1,50 @@
use std::collections::BTreeMap;

use anyhow::Context;
use askama::Template;
use axum::{response::Response, Extension};

use crate::{
    into_response,
    methods::{
        filters,
        repo::{Refs, Repository, Result},
    },
};

#[derive(Template)]
#[template(path = "repo/refs.html")]
pub struct View {
    repo: Repository,
    refs: Refs,
}

#[allow(clippy::unused_async)]
pub async fn handle(
    Extension(repo): Extension<Repository>,
    Extension(db): Extension<sled::Db>,
) -> Result<Response> {
    let repository = crate::database::schema::repository::Repository::open(&db, &*repo)?
        .context("Repository does not exist")?;

    let mut heads = BTreeMap::new();
    for head in repository.get().heads(&db) {
        let commit_tree = repository.get().commit_tree(&db, &head)?;
        let name = head.strip_prefix("refs/heads/");

        if let (Some(name), Some(commit)) = (name, commit_tree.fetch_latest_one()) {
            heads.insert(name.to_string(), commit);
        }
    }

    let tags = repository
        .get()
        .tag_tree(&db)
        .context("Failed to fetch indexed tags")?
        .fetch_all();

    Ok(into_response(&View {
        repo,
        refs: Refs { heads, tags },
    }))
}
diff --git a/src/methods/repo/smart_git.rs b/src/methods/repo/smart_git.rs
new file mode 100644
index 0000000..91512b4 100644
--- /dev/null
+++ a/src/methods/repo/smart_git.rs
@@ -1,0 +1,53 @@
use std::{io::Write, process::Stdio};

use axum::{extract::Query, response::Response, Extension};
use bytes::Bytes;
use serde::Deserialize;

use crate::methods::repo::{RepositoryPath, Result};

#[derive(Deserialize)]
pub struct UriQuery {
    service: String,
}

#[allow(clippy::unused_async)]
pub async fn handle_info_refs(
    Extension(RepositoryPath(repository_path)): Extension<RepositoryPath>,
    Query(query): Query<UriQuery>,
) -> Result<Response> {
    // todo: tokio command
    let out = std::process::Command::new("git")
        .arg("http-backend")
        .env("REQUEST_METHOD", "GET")
        .env("PATH_INFO", "/info/refs")
        .env("GIT_PROJECT_ROOT", repository_path)
        .env("QUERY_STRING", format!("service={}", query.service))
        .output()
        .unwrap();

    Ok(crate::git_cgi::cgi_to_response(&out.stdout)?)
}

#[allow(clippy::unused_async)]
pub async fn handle_git_upload_pack(
    Extension(RepositoryPath(repository_path)): Extension<RepositoryPath>,
    body: Bytes,
) -> Result<Response> {
    // todo: tokio command
    let mut child = std::process::Command::new("git")
        .arg("http-backend")
        // todo: read all this from request
        .env("REQUEST_METHOD", "POST")
        .env("CONTENT_TYPE", "application/x-git-upload-pack-request")
        .env("PATH_INFO", "/git-upload-pack")
        .env("GIT_PROJECT_ROOT", repository_path)
        .stdout(Stdio::piped())
        .stdin(Stdio::piped())
        .spawn()
        .unwrap();
    child.stdin.as_mut().unwrap().write_all(&body).unwrap();
    let out = child.wait_with_output().unwrap();

    Ok(crate::git_cgi::cgi_to_response(&out.stdout)?)
}
diff --git a/src/methods/repo/summary.rs b/src/methods/repo/summary.rs
new file mode 100644
index 0000000..1fa9e9b 100644
--- /dev/null
+++ a/src/methods/repo/summary.rs
@@ -1,0 +1,55 @@
use std::collections::BTreeMap;

use anyhow::Context;
use askama::Template;
use axum::{response::Response, Extension};
use yoke::Yoke;

use crate::{
    into_response,
    methods::{
        filters,
        repo::{Refs, Repository, Result},
    },
};

#[derive(Template)]
#[template(path = "repo/summary.html")]
pub struct View<'a> {
    repo: Repository,
    refs: Refs,
    commit_list: Vec<&'a crate::database::schema::commit::Commit<'a>>,
}

pub async fn handle(
    Extension(repo): Extension<Repository>,
    Extension(db): Extension<sled::Db>,
) -> Result<Response> {
    let repository = crate::database::schema::repository::Repository::open(&db, &*repo)?
        .context("Repository does not exist")?;
    let commit_tree = repository.get().commit_tree(&db, "refs/heads/master")?;
    let commits = commit_tree.fetch_latest(11, 0).await;
    let commit_list = commits.iter().map(Yoke::get).collect();

    let mut heads = BTreeMap::new();
    for head in repository.get().heads(&db) {
        let commit_tree = repository.get().commit_tree(&db, &head)?;
        let name = head.strip_prefix("refs/heads/");

        if let (Some(name), Some(commit)) = (name, commit_tree.fetch_latest_one()) {
            heads.insert(name.to_string(), commit);
        }
    }

    let tags = repository
        .get()
        .tag_tree(&db)
        .context("Failed to fetch indexed tags")?
        .fetch_all();

    Ok(into_response(&View {
        repo,
        refs: Refs { heads, tags },
        commit_list,
    }))
}
diff --git a/src/methods/repo/tag.rs b/src/methods/repo/tag.rs
new file mode 100644
index 0000000..acdaf0c 100644
--- /dev/null
+++ a/src/methods/repo/tag.rs
@@ -1,0 +1,37 @@
use std::sync::Arc;

use askama::Template;
use axum::{extract::Query, response::Response, Extension};
use serde::Deserialize;

use crate::{
    git::DetailedTag,
    into_response,
    methods::repo::{Repository, RepositoryPath, Result},
    Git,
};

#[derive(Deserialize)]
pub struct UriQuery {
    #[serde(rename = "h")]
    name: String,
}

#[derive(Template)]
#[template(path = "repo/tag.html")]
pub struct View {
    repo: Repository,
    tag: DetailedTag,
}

pub async fn handle(
    Extension(repo): Extension<Repository>,
    Extension(RepositoryPath(repository_path)): Extension<RepositoryPath>,
    Extension(git): Extension<Arc<Git>>,
    Query(query): Query<UriQuery>,
) -> Result<Response> {
    let open_repo = git.repo(repository_path).await?;
    let tag = open_repo.tag_info(&query.name).await?;

    Ok(into_response(&View { repo, tag }))
}
diff --git a/src/methods/repo/tree.rs b/src/methods/repo/tree.rs
new file mode 100644
index 0000000..e20434d 100644
--- /dev/null
+++ a/src/methods/repo/tree.rs
@@ -1,0 +1,78 @@
use std::{
    fmt::{Display, Formatter},
    sync::Arc,
};

use askama::Template;
use axum::{extract::Query, response::Response, Extension};
use serde::Deserialize;

use crate::{
    git::{FileWithContent, PathDestination, TreeItem},
    into_response,
    methods::{
        filters,
        repo::{ChildPath, Repository, RepositoryPath, Result},
    },
    Git,
};

#[derive(Deserialize)]
pub struct UriQuery {
    id: Option<String>,
    #[serde(rename = "h")]
    branch: Option<String>,
}

impl Display for UriQuery {
    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
        let mut prefix = "?";

        if let Some(id) = self.id.as_deref() {
            write!(f, "{}id={}", prefix, id)?;
            prefix = "&";
        }

        if let Some(branch) = self.branch.as_deref() {
            write!(f, "{}h={}", prefix, branch)?;
        }

        Ok(())
    }
}

#[derive(Template)]
#[template(path = "repo/tree.html")]
#[allow(clippy::module_name_repetitions)]
pub struct TreeView {
    pub repo: Repository,
    pub items: Vec<TreeItem>,
    pub query: UriQuery,
}

#[derive(Template)]
#[template(path = "repo/file.html")]
pub struct FileView {
    pub repo: Repository,
    pub file: FileWithContent,
}

pub async fn handle(
    Extension(repo): Extension<Repository>,
    Extension(RepositoryPath(repository_path)): Extension<RepositoryPath>,
    Extension(ChildPath(child_path)): Extension<ChildPath>,
    Extension(git): Extension<Arc<Git>>,
    Query(query): Query<UriQuery>,
) -> Result<Response> {
    let open_repo = git.repo(repository_path).await?;

    Ok(
        match open_repo
            .path(child_path, query.id.as_deref(), query.branch.clone())
            .await?
        {
            PathDestination::Tree(items) => into_response(&TreeView { repo, items, query }),
            PathDestination::File(file) => into_response(&FileView { repo, file }),
        },
    )
}