🏡 index : ~doyle/rgit.git

author Jordan Doyle <jordan@doyle.la> 2022-07-21 22:54:14.0 +01:00:00
committer Jordan Doyle <jordan@doyle.la> 2022-07-21 22:54:14.0 +01:00:00
commit
d56e2f90efed3b851db1abaca16e515b5d0931e3 [patch]
tree
079333901217e801c88e08f4da58cdc8dd0b6075
parent
0419661aa966b05395ffddce6e8975dba5e73e4e
download
d56e2f90efed3b851db1abaca16e515b5d0931e3.tar.gz

Nicer error handling when calling out to git2



Diff

 Cargo.lock          |  19 +++++++++++++++++++
 Cargo.toml          |   2 ++
 src/git.rs          | 286 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------------------
 src/main.rs         |  15 ++++++++++++---
 src/methods/repo.rs | 149 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----------
 5 files changed, 286 insertions(+), 185 deletions(-)

diff --git a/Cargo.lock b/Cargo.lock
index 58c6bad..7933fd1 100644
--- a/Cargo.lock
+++ a/Cargo.lock
@@ -200,6 +200,18 @@
]

[[package]]
name = "axum-macros"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6293dae2ec708e679da6736e857cf8532886ef258e92930f38279c12641628b8"
dependencies = [
 "heck",
 "proc-macro2",
 "quote",
 "syn",
]

[[package]]
name = "base64"
version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -948,6 +960,12 @@
version = "0.12.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "607c8a29735385251a339424dd462993c0fed8fa09d378f259377df08c126022"

[[package]]
name = "heck"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2540771e65fc8cb83cd6e8a237f70c319bd5c29f78ed1084ba5d50eeac86f7f9"

[[package]]
name = "hermit-abi"
@@ -1918,6 +1936,7 @@
 "anyhow",
 "askama",
 "axum",
 "axum-macros",
 "bat",
 "bincode",
 "bytes",
diff --git a/Cargo.toml b/Cargo.toml
index 2577212..644500c 100644
--- a/Cargo.toml
+++ a/Cargo.toml
@@ -7,7 +7,9 @@

[dependencies]
askama = "0.11"
anyhow = "1.0"
axum = "0.5"
axum-macros = "0.2"
bat = { version = "0.21", default-features = false, features = ["build-assets"] }
bytes = "1.1"
bincode = "1.3"
diff --git a/src/git.rs b/src/git.rs
index 048b73c..631cfaa 100644
--- a/src/git.rs
+++ a/src/git.rs
@@ -1,8 +1,9 @@
use std::ffi::OsStr;
use std::path::Path;
use std::{borrow::Cow, fmt::Write, path::PathBuf, sync::Arc, time::Duration};

use crate::syntax_highlight::ComrakSyntectAdapter;
use anyhow::{Context, Result};
use bytes::{Bytes, BytesMut};
use comrak::{ComrakOptions, ComrakPlugins};
use git2::{
@@ -46,19 +47,20 @@

impl Git {
    #[instrument(skip(self))]
    pub async fn repo(self: Arc<Self>, repo_path: PathBuf) -> Arc<OpenRepository> {
    pub async fn repo(self: Arc<Self>, repo_path: PathBuf) -> Result<Arc<OpenRepository>> {
        let repo = tokio::task::spawn_blocking({
            let repo_path = repo_path.clone();
            move || git2::Repository::open(repo_path).unwrap()
            move || git2::Repository::open(repo_path)
        })
        .await
        .unwrap();
        .context("Failed to join Tokio task")?
        .context("Failed to open repository")?;

        Arc::new(OpenRepository {
        Ok(Arc::new(OpenRepository {
            git: self,
            cache_key: repo_path,
            repo: Mutex::new(repo),
        })
        }))
    }
}

@@ -74,59 +76,72 @@
        path: Option<PathBuf>,
        tree_id: Option<&str>,
        branch: Option<String>,
    ) -> PathDestination {
        let tree_id = tree_id.map(Oid::from_str).transpose().unwrap();
    ) -> Result<PathDestination> {
        let tree_id = tree_id
            .map(Oid::from_str)
            .transpose()
            .context("Failed to parse tree hash")?;

        tokio::task::spawn_blocking(move || {
            let repo = self.repo.lock();

            let mut tree = if let Some(tree_id) = tree_id {
                repo.find_tree(tree_id).unwrap()
                repo.find_tree(tree_id)
                    .context("Couldn't find tree with given id")?
            } else if let Some(branch) = branch {
                let branch = repo.find_branch(&branch, BranchType::Local).unwrap();
                branch.get().peel_to_tree().unwrap()
                let branch = repo.find_branch(&branch, BranchType::Local)?;
                branch
                    .get()
                    .peel_to_tree()
                    .context("Couldn't find tree for branch")?
            } else {
                let head = repo.head().unwrap();
                head.peel_to_tree().unwrap()
                let head = repo.head()?;
                head.peel_to_tree()
                    .context("Couldn't find tree from HEAD")?
            };

            if let Some(path) = path.as_ref() {
                let item = tree.get_path(path).unwrap();
                let object = item.to_object(&repo).unwrap();
                let item = tree.get_path(path).context("Path doesn't exist in tree")?;
                let object = item
                    .to_object(&repo)
                    .context("Path in tree isn't an object")?;

                if let Some(blob) = object.as_blob() {
                    let name = item.name().unwrap().to_string();
                    let path = path.clone().join(&name);
                    // TODO: use Path here instead of a lossy utf8 conv
                    let name = String::from_utf8_lossy(item.name_bytes());
                    let path = path.clone().join(&*name);

                    let extension = path
                        .extension()
                        .or_else(|| path.file_name())
                        .unwrap()
                        .to_string_lossy();
                    let content = format_file(blob.content(), &extension, &self.git.syntax_set);

                    return PathDestination::File(FileWithContent {
                        .map(|v| v.to_string_lossy())
                        .unwrap_or_else(|| Cow::Borrowed(""));
                    let content = format_file(blob.content(), &extension, &self.git.syntax_set)?;

                    return Ok(PathDestination::File(FileWithContent {
                        metadata: File {
                            mode: item.filemode(),
                            size: blob.size(),
                            path,
                            name,
                            name: name.into_owned(),
                        },
                        content,
                    });
                    }));
                } else if let Ok(new_tree) = object.into_tree() {
                    tree = new_tree;
                } else {
                    panic!("unknown item kind");
                    anyhow::bail!("Given path not tree nor blob... what is it?!");
                }
            }

            let mut tree_items = Vec::new();

            for item in tree.iter() {
                let object = item.to_object(&repo).unwrap();

                let name = item.name().unwrap().to_string();
                let object = item
                    .to_object(&repo)
                    .context("Expected item in tree to be object but it wasn't")?;

                let name = String::from_utf8_lossy(item.name_bytes()).into_owned();
                let path = path.clone().unwrap_or_default().join(&name);

                if let Some(blob) = object.as_blob() {
@@ -145,14 +160,14 @@
                }
            }

            PathDestination::Tree(tree_items)
            Ok(PathDestination::Tree(tree_items))
        })
        .await
        .unwrap()
        .context("Failed to join Tokio task")?
    }

    #[instrument(skip(self))]
    pub async fn tag_info(self: Arc<Self>, tag_name: &str) -> DetailedTag {
    pub async fn tag_info(self: Arc<Self>, tag_name: &str) -> Result<DetailedTag> {
        let reference = format!("refs/tags/{tag_name}");
        let tag_name = tag_name.to_string();

@@ -161,10 +176,10 @@

            let tag = repo
                .find_reference(&reference)
                .unwrap()
                .context("Given reference does not exist in repository")?
                .peel_to_tag()
                .unwrap();
            let tag_target = tag.target().unwrap();
                .context("Couldn't get to a tag from the given reference")?;
            let tag_target = tag.target().context("Couldn't find tagged object")?;

            let tagged_object = match tag_target.kind() {
                Some(ObjectType::Commit) => Some(TaggedObject::Commit(tag_target.id().to_string())),
@@ -172,45 +187,49 @@
                None | Some(_) => None,
            };

            DetailedTag {
            Ok(DetailedTag {
                name: tag_name,
                tagger: tag.tagger().map(Into::into),
                message: tag.message().unwrap().to_string(),
                tagger: tag.tagger().map(TryInto::try_into).transpose()?,
                message: tag
                    .message_bytes()
                    .map(String::from_utf8_lossy)
                    .unwrap_or_else(|| Cow::Borrowed(""))
                    .into_owned(),
                tagged_object,
            }
            })
        })
        .await
        .unwrap()
        .context("Failed to join Tokio task")?
    }

    #[instrument(skip(self))]
    pub async fn refs(self: Arc<Self>) -> Arc<Refs> {
    pub async fn refs(self: Arc<Self>) -> Result<Arc<Refs>, Arc<anyhow::Error>> {
        let git = self.git.clone();

        git.refs
            .get_with(self.cache_key.clone(), async move {
            .try_get_with(self.cache_key.clone(), async move {
                tokio::task::spawn_blocking(move || {
                    let repo = self.repo.lock();

                    let ref_iter = repo.references().unwrap();
                    let ref_iter = repo.references().context("Couldn't get list of references for repository")?;

                    let mut built_refs = Refs::default();

                    for ref_ in ref_iter {
                        let ref_ = ref_.unwrap();
                        let ref_ = ref_?;

                        if ref_.is_branch() {
                            let commit = ref_.peel_to_commit().unwrap();
                            let commit = ref_.peel_to_commit().context("Reference is apparently a branch but I couldn't get to the HEAD of it")?;

                            built_refs.branch.push(Branch {
                                name: ref_.shorthand().unwrap().to_string(),
                                commit: commit.into(),
                                name: String::from_utf8_lossy(ref_.shorthand_bytes()).into_owned(),
                                commit: commit.try_into()?,
                            });
                        } else if ref_.is_tag() {
                            if let Ok(tag) = ref_.peel_to_tag() {
                                built_refs.tag.push(Tag {
                                    name: ref_.shorthand().unwrap().to_string(),
                                    tagger: tag.tagger().map(Into::into),
                                    name: String::from_utf8_lossy(ref_.shorthand_bytes()).into_owned(),
                                    tagger: tag.tagger().map(TryInto::try_into).transpose()?,
                                });
                            }
                        }
@@ -225,28 +244,34 @@
                        two_tagger.cmp(&one_tagger)
                    });

                    Arc::new(built_refs)
                    Ok(Arc::new(built_refs))
                })
                .await
                .unwrap()
                .context("Failed to join Tokio task")?
            })
            .await
    }

    #[instrument(skip(self))]
    pub async fn readme(self: Arc<Self>) -> Option<(ReadmeFormat, Arc<str>)> {
    pub async fn readme(
        self: Arc<Self>,
    ) -> Result<Option<(ReadmeFormat, Arc<str>)>, Arc<anyhow::Error>> {
        const README_FILES: &[&str] = &["README.md", "README", "README.txt"];

        let git = self.git.clone();

        git.readme_cache
            .get_with(self.cache_key.clone(), async move {
            .try_get_with(self.cache_key.clone(), async move {
                tokio::task::spawn_blocking(move || {
                    let repo = self.repo.lock();

                    let head = repo.head().unwrap();
                    let commit = head.peel_to_commit().unwrap();
                    let tree = commit.tree().unwrap();
                    let head = repo.head().context("Couldn't find HEAD of repository")?;
                    let commit = head.peel_to_commit().context(
                        "Couldn't find the commit that the HEAD of the repository refers to",
                    )?;
                    let tree = commit
                        .tree()
                        .context("Couldn't get the tree that the HEAD refers to")?;

                    for name in README_FILES {
                        let tree_entry = if let Some(file) = tree.get_name(name) {
@@ -273,64 +298,68 @@

                        if Path::new(name).extension().and_then(OsStr::to_str) == Some("md") {
                            let value = parse_and_transform_markdown(content, &self.git.syntax_set);
                            return Some((ReadmeFormat::Markdown, Arc::from(value)));
                            return Ok(Some((ReadmeFormat::Markdown, Arc::from(value))));
                        }

                        return Some((ReadmeFormat::Plaintext, Arc::from(content)));
                        return Ok(Some((ReadmeFormat::Plaintext, Arc::from(content))));
                    }

                    None
                    Ok(None)
                })
                .await
                .unwrap()
                .context("Failed to join Tokio task")?
            })
            .await
    }

    #[instrument(skip(self))]
    pub async fn latest_commit(self: Arc<Self>) -> Commit {
    pub async fn latest_commit(self: Arc<Self>) -> Result<Commit> {
        tokio::task::spawn_blocking(move || {
            let repo = self.repo.lock();

            let head = repo.head().unwrap();
            let commit = head.peel_to_commit().unwrap();
            let head = repo.head().context("Couldn't find HEAD of repository")?;
            let commit = head
                .peel_to_commit()
                .context("Couldn't find commit HEAD of repository refers to")?;
            let (diff_plain, diff_output, diff_stats) =
                fetch_diff_and_stats(&repo, &commit, &self.git.syntax_set);
                fetch_diff_and_stats(&repo, &commit, &self.git.syntax_set)?;

            let mut commit = Commit::from(commit);
            let mut commit = Commit::try_from(commit)?;
            commit.diff_stats = diff_stats;
            commit.diff = diff_output;
            commit.diff_plain = diff_plain;
            commit
            Ok(commit)
        })
        .await
        .unwrap()
        .context("Failed to join Tokio task")?
    }

    #[instrument(skip(self))]
    pub async fn commit(self: Arc<Self>, commit: &str) -> Arc<Commit> {
        let commit = Oid::from_str(commit).unwrap();
    pub async fn commit(self: Arc<Self>, commit: &str) -> Result<Arc<Commit>, Arc<anyhow::Error>> {
        let commit = Oid::from_str(commit)
            .map_err(anyhow::Error::from)
            .map_err(Arc::new)?;

        let git = self.git.clone();

        git.commits
            .get_with(commit, async move {
            .try_get_with(commit, async move {
                tokio::task::spawn_blocking(move || {
                    let repo = self.repo.lock();

                    let commit = repo.find_commit(commit).unwrap();
                    let commit = repo.find_commit(commit)?;
                    let (diff_plain, diff_output, diff_stats) =
                        fetch_diff_and_stats(&repo, &commit, &self.git.syntax_set);
                        fetch_diff_and_stats(&repo, &commit, &self.git.syntax_set)?;

                    let mut commit = Commit::from(commit);
                    let mut commit = Commit::try_from(commit)?;
                    commit.diff_stats = diff_stats;
                    commit.diff = diff_output;
                    commit.diff_plain = diff_plain;

                    Arc::new(commit)
                    Ok(Arc::new(commit))
                })
                .await
                .unwrap()
                .context("Failed to join Tokio task")?
            })
            .await
    }
@@ -427,14 +456,16 @@
    time: OffsetDateTime,
}

impl From<Signature<'_>> for CommitUser {
    fn from(v: Signature<'_>) -> Self {
        CommitUser {
            name: v.name().unwrap().to_string(),
            email: v.email().unwrap().to_string(),
impl TryFrom<Signature<'_>> for CommitUser {
    type Error = anyhow::Error;

    fn try_from(v: Signature<'_>) -> Result<Self> {
        Ok(CommitUser {
            name: String::from_utf8_lossy(v.name_bytes()).into_owned(),
            email: String::from_utf8_lossy(v.email_bytes()).into_owned(),
            email_md5: format!("{:x}", md5::compute(v.email_bytes())),
            time: OffsetDateTime::from_unix_timestamp(v.when().seconds()).unwrap(),
        }
            time: OffsetDateTime::from_unix_timestamp(v.when().seconds())?,
        })
    }
}

@@ -469,21 +500,31 @@
    pub diff: String,
    pub diff_plain: Bytes,
}

impl TryFrom<git2::Commit<'_>> for Commit {
    type Error = anyhow::Error;

impl From<git2::Commit<'_>> for Commit {
    fn from(commit: git2::Commit<'_>) -> Self {
        Commit {
            author: commit.author().into(),
            committer: commit.committer().into(),
    fn try_from(commit: git2::Commit<'_>) -> Result<Self> {
        Ok(Commit {
            author: CommitUser::try_from(commit.author())?,
            committer: CommitUser::try_from(commit.committer())?,
            oid: commit.id().to_string(),
            tree: commit.tree_id().to_string(),
            parents: commit.parent_ids().map(|v| v.to_string()).collect(),
            summary: commit.summary().unwrap().to_string(),
            body: commit.body().map(ToString::to_string).unwrap_or_default(),
            summary: commit
                .summary_bytes()
                .map(String::from_utf8_lossy)
                .unwrap_or_else(|| Cow::Borrowed(""))
                .into_owned(),
            body: commit
                .body_bytes()
                .map(String::from_utf8_lossy)
                .unwrap_or_else(|| Cow::Borrowed(""))
                .into_owned(),
            diff_stats: String::with_capacity(0),
            diff: String::with_capacity(0),
            diff_plain: Bytes::new(),
        }
        })
    }
}

@@ -522,37 +563,35 @@
    repo: &git2::Repository,
    commit: &git2::Commit<'_>,
    syntax_set: &SyntaxSet,
) -> (Bytes, String, String) {
    let current_tree = commit.tree().unwrap();
) -> Result<(Bytes, String, String)> {
    let current_tree = commit.tree().context("Couldn't get tree for the commit")?;
    let parent_tree = commit.parents().next().and_then(|v| v.tree().ok());
    let mut diff_opts = DiffOptions::new();
    let mut diff = repo
        .diff_tree_to_tree(
            parent_tree.as_ref(),
            Some(&current_tree),
            Some(&mut diff_opts),
        )
        .unwrap();
    let mut diff = repo.diff_tree_to_tree(
        parent_tree.as_ref(),
        Some(&current_tree),
        Some(&mut diff_opts),
    )?;

    let mut diff_plain = BytesMut::new();
    let email = diff.format_email(1, 1, commit, None).unwrap();
    let email = diff
        .format_email(1, 1, commit, None)
        .context("Couldn't build diff for commit")?;
    diff_plain.extend_from_slice(&*email);

    let diff_stats = diff
        .stats()
        .unwrap()
        .to_buf(DiffStatsFormat::FULL, 80)
        .unwrap()
        .stats()?
        .to_buf(DiffStatsFormat::FULL, 80)?
        .as_str()
        .unwrap()
        .unwrap_or("")
        .to_string();
    let diff_output = format_diff(&diff, syntax_set);
    let diff_output = format_diff(&diff, syntax_set)?;

    (diff_plain.freeze(), diff_output, diff_stats)
    Ok((diff_plain.freeze(), diff_output, diff_stats))
}

fn format_file(content: &[u8], extension: &str, syntax_set: &SyntaxSet) -> String {
    let content = std::str::from_utf8(content).unwrap();
fn format_file(content: &[u8], extension: &str, syntax_set: &SyntaxSet) -> Result<String> {
    let content = String::from_utf8_lossy(content);

    let syntax = syntax_set
        .find_syntax_by_extension(extension)
@@ -560,20 +599,20 @@
    let mut html_generator =
        ClassedHTMLGenerator::new_with_class_style(syntax, syntax_set, ClassStyle::Spaced);

    for line in LinesWithEndings::from(content) {
    for line in LinesWithEndings::from(&content) {
        html_generator
            .parse_html_for_line_which_includes_newline(line)
            .unwrap();
            .context("Couldn't parse line of file")?;
    }

    format!(
    Ok(format!(
        "<code>{}</code>",
        html_generator.finalize().replace('\n', "</code>\n<code>")
    )
    ))
}

#[instrument(skip(diff, syntax_set))]
fn format_diff(diff: &git2::Diff<'_>, syntax_set: &SyntaxSet) -> String {
fn format_diff(diff: &git2::Diff<'_>, syntax_set: &SyntaxSet) -> Result<String> {
    let mut diff_output = String::new();

    diff.print(DiffFormat::Patch, |delta, _diff_hunk, diff_line| {
@@ -590,11 +629,14 @@
        let line = String::from_utf8_lossy(diff_line.content());

        let extension = if should_highlight_as_source {
            let path = delta.new_file().path().unwrap();
            path.extension()
                .or_else(|| path.file_name())
                .unwrap()
                .to_string_lossy()
            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(""))
            } else {
                Cow::Borrowed("")
            }
        } else {
            Cow::Borrowed("patch")
        };
@@ -603,11 +645,9 @@
            .unwrap_or_else(|| syntax_set.find_syntax_plain_text());
        let mut html_generator =
            ClassedHTMLGenerator::new_with_class_style(syntax, syntax_set, ClassStyle::Spaced);
        html_generator
            .parse_html_for_line_which_includes_newline(&line)
            .unwrap();
        let _ = html_generator.parse_html_for_line_which_includes_newline(&line);
        if let Some(class) = class {
            write!(diff_output, r#"<span class="diff-{class}">"#).unwrap();
            let _ = write!(diff_output, r#"<span class="diff-{class}">"#);
        }
        diff_output.push_str(&html_generator.finalize());
        if class.is_some() {
@@ -616,7 +656,7 @@

        true
    })
    .unwrap();
    .context("Failed to prepare diff")?;

    diff_output
    Ok(diff_output)
}
diff --git a/src/main.rs b/src/main.rs
index 0d3b9c2..8d04be6 100644
--- a/src/main.rs
+++ a/src/main.rs
@@ -55,11 +55,20 @@

    let theme = bat_assets.get_theme("GitHub");
    let css = syntect::html::css_for_theme_with_class_style(theme, ClassStyle::Spaced).unwrap();
    let css = Box::leak(format!(r#"@media (prefers-color-scheme: light){{{}}}"#, css).into_boxed_str().into_boxed_bytes());
    let css = Box::leak(
        format!(r#"@media (prefers-color-scheme: light){{{}}}"#, css)
            .into_boxed_str()
            .into_boxed_bytes(),
    );

    let dark_theme = bat_assets.get_theme("TwoDark");
    let dark_css = syntect::html::css_for_theme_with_class_style(dark_theme, ClassStyle::Spaced).unwrap();
    let dark_css = Box::leak(format!(r#"@media (prefers-color-scheme: dark){{{}}}"#, dark_css).into_boxed_str().into_boxed_bytes());
    let dark_css =
        syntect::html::css_for_theme_with_class_style(dark_theme, ClassStyle::Spaced).unwrap();
    let dark_css = Box::leak(
        format!(r#"@media (prefers-color-scheme: dark){{{}}}"#, dark_css)
            .into_boxed_str()
            .into_boxed_bytes(),
    );

    let app = Router::new()
        .route("/", get(methods::index::handle))
diff --git a/src/methods/repo.rs b/src/methods/repo.rs
index 4be9dcd..9fe2ea4 100644
--- a/src/methods/repo.rs
+++ a/src/methods/repo.rs
@@ -8,6 +8,7 @@
};

use askama::Template;
use axum::http::StatusCode;
use axum::{
    body::HttpBody,
    extract::Query,
@@ -53,6 +54,28 @@
    }
}

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>,
@@ -71,22 +94,28 @@

    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();
            BoxCloneService::new(handle_info_refs.into_service())
            h!(handle_info_refs)
        }
        Some("git-upload-pack") => BoxCloneService::new(handle_git_upload_pack.into_service()),
        Some("refs") => BoxCloneService::new(handle_refs.into_service()),
        Some("log") => BoxCloneService::new(handle_log.into_service()),
        Some("tree") => BoxCloneService::new(handle_tree.into_service()),
        Some("commit") => BoxCloneService::new(handle_commit.into_service()),
        Some("diff") => BoxCloneService::new(handle_diff.into_service()),
        Some("patch") => BoxCloneService::new(handle_patch.into_service()),
        Some("tag") => BoxCloneService::new(handle_tag.into_service()),
        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);

@@ -107,9 +136,9 @@

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

                BoxCloneService::new(handle_tree.into_service())
                h!(handle_tree)
            } else {
                BoxCloneService::new(handle_summary.into_service())
                h!(handle_summary)
            }
        }
        None => panic!("not found"),
@@ -142,20 +171,20 @@
    Extension(RepositoryPath(repository_path)): Extension<RepositoryPath>,
    Extension(git): Extension<Arc<Git>>,
    Extension(db): Extension<sled::Db>,
) -> Response {
    let open_repo = git.repo(repository_path).await;
    let refs = open_repo.refs().await;
) -> Result<Response> {
    let open_repo = git.repo(repository_path).await?;
    let refs = open_repo.refs().await?;

    let repository = crate::database::schema::repository::Repository::open(&db, &*repo).unwrap();
    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();

    into_response(&SummaryView {
    Ok(into_response(&SummaryView {
        repo,
        refs,
        commit_list,
    })
    }))
}

#[derive(Deserialize)]
@@ -176,11 +205,11 @@
    Extension(RepositoryPath(repository_path)): Extension<RepositoryPath>,
    Extension(git): Extension<Arc<Git>>,
    Query(query): Query<TagQuery>,
) -> Response {
    let open_repo = git.repo(repository_path).await;
    let tag = open_repo.tag_info(&query.name).await;
) -> Result<Response> {
    let open_repo = git.repo(repository_path).await?;
    let tag = open_repo.tag_info(&query.name).await?;

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

#[derive(Deserialize)]
@@ -204,7 +233,7 @@
    Extension(repo): Extension<Repository>,
    Extension(db): Extension<sled::Db>,
    Query(query): Query<LogQuery>,
) -> Response {
) -> Result<Response> {
    let offset = query.offset.unwrap_or(0);

    let reference = format!("refs/heads/{}", query.branch.as_deref().unwrap_or("master"));
@@ -221,12 +250,12 @@

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

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

#[derive(Deserialize)]
@@ -237,7 +266,7 @@
pub async fn handle_info_refs(
    Extension(RepositoryPath(repository_path)): Extension<RepositoryPath>,
    Query(query): Query<SmartGitQuery>,
) -> Response {
) -> Result<Response> {
    // todo: tokio command
    let out = std::process::Command::new("git")
        .arg("http-backend")
@@ -248,13 +277,13 @@
        .output()
        .unwrap();

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

pub async fn handle_git_upload_pack(
    Extension(RepositoryPath(repository_path)): Extension<RepositoryPath>,
    body: Bytes,
) -> Response {
) -> Result<Response> {
    // todo: tokio command
    let mut child = std::process::Command::new("git")
        .arg("http-backend")
@@ -270,7 +299,7 @@
    child.stdin.as_mut().unwrap().write_all(&body).unwrap();
    let out = child.wait_with_output().unwrap();

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

#[derive(Template)]
@@ -284,11 +313,11 @@
    Extension(repo): Extension<Repository>,
    Extension(RepositoryPath(repository_path)): Extension<RepositoryPath>,
    Extension(git): Extension<Arc<Git>>,
) -> Response {
    let open_repo = git.repo(repository_path).await;
    let refs = open_repo.refs().await;
) -> Result<Response> {
    let open_repo = git.repo(repository_path).await?;
    let refs = open_repo.refs().await?;

    into_response(&RefsView { repo, refs })
    Ok(into_response(&RefsView { repo, refs }))
}

#[derive(Template)]
@@ -302,11 +331,11 @@
    Extension(repo): Extension<Repository>,
    Extension(RepositoryPath(repository_path)): Extension<RepositoryPath>,
    Extension(git): Extension<Arc<Git>>,
) -> Response {
    let open_repo = git.clone().repo(repository_path).await;
    let readme = open_repo.readme().await;
) -> Result<Response> {
    let open_repo = git.clone().repo(repository_path).await?;
    let readme = open_repo.readme().await?;

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

#[derive(Template)]
@@ -326,15 +355,15 @@
    Extension(RepositoryPath(repository_path)): Extension<RepositoryPath>,
    Extension(git): Extension<Arc<Git>>,
    Query(query): Query<CommitQuery>,
) -> Response {
    let open_repo = git.repo(repository_path).await;
) -> Result<Response> {
    let open_repo = git.repo(repository_path).await?;
    let commit = if let Some(commit) = query.id {
        open_repo.commit(&commit).await
        open_repo.commit(&commit).await?
    } else {
        Arc::new(open_repo.latest_commit().await)
        Arc::new(open_repo.latest_commit().await?)
    };

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

#[derive(Deserialize)]
@@ -367,7 +396,7 @@
    Extension(ChildPath(child_path)): Extension<ChildPath>,
    Extension(git): Extension<Arc<Git>>,
    Query(query): Query<TreeQuery>,
) -> Response {
) -> Result<Response> {
    #[derive(Template)]
    #[template(path = "repo/tree.html")]
    pub struct TreeView {
@@ -383,15 +412,17 @@
        pub file: FileWithContent,
    }

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

    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 }),
    }
    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)]
@@ -406,27 +437,27 @@
    Extension(RepositoryPath(repository_path)): Extension<RepositoryPath>,
    Extension(git): Extension<Arc<Git>>,
    Query(query): Query<CommitQuery>,
) -> Response {
    let open_repo = git.repo(repository_path).await;
) -> Result<Response> {
    let open_repo = git.repo(repository_path).await?;
    let commit = if let Some(commit) = query.id {
        open_repo.commit(&commit).await
        open_repo.commit(&commit).await?
    } else {
        Arc::new(open_repo.latest_commit().await)
        Arc::new(open_repo.latest_commit().await?)
    };

    into_response(&DiffView { repo, commit })
    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>,
) -> Response {
    let open_repo = git.repo(repository_path).await;
) -> Result<Response> {
    let open_repo = git.repo(repository_path).await?;
    let commit = if let Some(commit) = query.id {
        open_repo.commit(&commit).await
        open_repo.commit(&commit).await?
    } else {
        Arc::new(open_repo.latest_commit().await)
        Arc::new(open_repo.latest_commit().await?)
    };

    let headers = [(
@@ -434,5 +465,5 @@
        HeaderValue::from_static("text/plain"),
    )];

    (headers, commit.diff_plain.clone()).into_response()
    Ok((headers, commit.diff_plain.clone()).into_response())
}