From f3142420736b58f814f89572e30a7ee8961cf059 Mon Sep 17 00:00:00 2001
From: Jordan Doyle <jordan@doyle.la>
Date: Sat, 09 Jul 2022 00:02:09 +0100
Subject: [PATCH] Cleanup git API

---
 Cargo.lock           |   1 +
 Cargo.toml           |   1 +
 src/git.rs           | 240 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++------------------------
 src/main.rs          |   3 +--
 src/methods/index.rs |   2 +-
 src/methods/repo.rs  |  73 ++++++++++++++++++++++++++++++++++++++-----------------------------------
 6 files changed, 179 insertions(+), 141 deletions(-)

diff --git a/Cargo.lock b/Cargo.lock
index c750fb4..fec37a7 100644
--- a/Cargo.lock
+++ a/Cargo.lock
@@ -1611,6 +1611,7 @@
  "humantime",
  "md5",
  "moka",
+ "parking_lot",
  "path-clean",
  "serde",
  "syntect",
diff --git a/Cargo.toml b/Cargo.toml
index 2577af8..ce79190 100644
--- a/Cargo.toml
+++ a/Cargo.toml
@@ -18,6 +18,7 @@
 md5 = "0.7"
 moka = { version = "0.9", features = ["future"] }
 path-clean = "0.1"
+parking_lot = "0.12"
 serde = { version = "1.0", features = ["derive"] }
 syntect = "5"
 time = "0.3"
diff --git a/src/git.rs b/src/git.rs
index 27a6315..6551bf1 100644
--- a/src/git.rs
+++ a/src/git.rs
@@ -11,22 +11,25 @@
     DiffFormat, DiffLineType, DiffOptions, DiffStatsFormat, ObjectType, Oid, Repository, Signature,
 };
 use moka::future::Cache;
+use parking_lot::Mutex;
 use syntect::html::{ClassStyle, ClassedHTMLGenerator};
 use syntect::parsing::SyntaxSet;
 use time::OffsetDateTime;
+use tracing::instrument;
 
 pub type RepositoryMetadataList = BTreeMap<Option<String>, Vec<RepositoryMetadata>>;
 
-#[derive(Clone)]
 pub struct Git {
     commits: Cache<Oid, Arc<Commit>>,
     readme_cache: Cache<PathBuf, Option<Arc<str>>>,
     refs: Cache<PathBuf, Arc<Refs>>,
-    repository_metadata: Arc<ArcSwapOption<RepositoryMetadataList>>,
+    repository_metadata: ArcSwapOption<RepositoryMetadataList>,
+    syntax_set: SyntaxSet,
 }
 
-impl Default for Git {
-    fn default() -> Self {
+impl Git {
+    #[instrument(skip(syntax_set))]
+    pub fn new(syntax_set: SyntaxSet) -> Self {
         Self {
             commits: Cache::builder()
                 .time_to_live(Duration::from_secs(10))
@@ -40,68 +43,100 @@
                 .time_to_live(Duration::from_secs(10))
                 .max_capacity(100)
                 .build(),
-            repository_metadata: Arc::new(ArcSwapOption::default()),
+            repository_metadata: ArcSwapOption::default(),
+            syntax_set,
         }
     }
 }
 
 impl Git {
-    pub async fn get_commit<'a>(
-        &'a self,
-        repo: PathBuf,
-        commit: &str,
-        syntax_set: Arc<SyntaxSet>,
-    ) -> Arc<Commit> {
-        let commit = Oid::from_str(commit).unwrap();
+    #[instrument(skip(self))]
+    pub async fn repo(self: Arc<Self>, repo_path: PathBuf) -> Arc<OpenRepository> {
+        let repo = tokio::task::spawn_blocking({
+            let repo_path = repo_path.clone();
+            move || git2::Repository::open(repo_path).unwrap()
+        })
+        .await
+        .unwrap();
 
-        self.commits
-            .get_with(commit, async {
-                tokio::task::spawn_blocking(move || {
-                    let repo = Repository::open_bare(repo).unwrap();
-                    let commit = repo.find_commit(commit).unwrap();
-                    let (diff_output, diff_stats) =
-                        fetch_diff_and_stats(&repo, &commit, &syntax_set);
+        Arc::new(OpenRepository {
+            git: self,
+            cache_key: repo_path,
+            repo: Mutex::new(repo),
+        })
+    }
 
-                    let mut commit = Commit::from(commit);
-                    commit.diff_stats = diff_stats;
-                    commit.diff = diff_output;
+    #[instrument(skip(self))]
+    pub async fn fetch_repository_metadata(&self) -> Arc<RepositoryMetadataList> {
+        if let Some(metadata) = self.repository_metadata.load().as_ref() {
+            return Arc::clone(metadata);
+        }
 
-                    Arc::new(commit)
-                })
-                .await
-                .unwrap()
-            })
-            .await
+        let start = Path::new("../test-git").canonicalize().unwrap();
+
+        let repos = tokio::task::spawn_blocking(move || {
+            let mut repos: RepositoryMetadataList = RepositoryMetadataList::new();
+            fetch_repository_metadata_impl(&start, &start, &mut repos);
+            repos
+        })
+        .await
+        .unwrap();
+
+        let repos = Arc::new(repos);
+        self.repository_metadata.store(Some(repos.clone()));
+
+        repos
     }
+}
 
-    pub async fn get_tag(&self, repo: PathBuf, tag_name: &str) -> DetailedTag {
-        let repo = Repository::open_bare(repo).unwrap();
-        let tag = repo
-            .find_reference(&format!("refs/tags/{tag_name}"))
-            .unwrap()
-            .peel_to_tag()
-            .unwrap();
-        let tag_target = tag.target().unwrap();
-
-        let tagged_object = match tag_target.kind() {
-            Some(ObjectType::Commit) => Some(TaggedObject::Commit(tag_target.id().to_string())),
-            Some(ObjectType::Tree) => Some(TaggedObject::Tree(tag_target.id().to_string())),
-            None | Some(_) => None,
-        };
+pub struct OpenRepository {
+    git: Arc<Git>,
+    cache_key: PathBuf,
+    repo: Mutex<git2::Repository>,
+}
 
-        DetailedTag {
-            name: tag_name.to_string(),
-            tagger: tag.tagger().map(Into::into),
-            message: tag.message().unwrap().to_string(),
-            tagged_object,
-        }
+impl OpenRepository {
+    #[instrument(skip(self))]
+    pub async fn tag_info(self: Arc<Self>, tag_name: &str) -> DetailedTag {
+        let reference = format!("refs/tags/{tag_name}");
+        let tag_name = tag_name.to_string();
+
+        tokio::task::spawn_blocking(move || {
+            let repo = self.repo.lock();
+
+            let tag = repo
+                .find_reference(&reference)
+                .unwrap()
+                .peel_to_tag()
+                .unwrap();
+            let tag_target = tag.target().unwrap();
+
+            let tagged_object = match tag_target.kind() {
+                Some(ObjectType::Commit) => Some(TaggedObject::Commit(tag_target.id().to_string())),
+                Some(ObjectType::Tree) => Some(TaggedObject::Tree(tag_target.id().to_string())),
+                None | Some(_) => None,
+            };
+
+            DetailedTag {
+                name: tag_name,
+                tagger: tag.tagger().map(Into::into),
+                message: tag.message().unwrap().to_string(),
+                tagged_object,
+            }
+        })
+        .await
+        .unwrap()
     }
 
-    pub async fn get_refs(&self, repo: PathBuf) -> Arc<Refs> {
-        self.refs
-            .get_with(repo.clone(), async {
+    #[instrument(skip(self))]
+    pub async fn refs(self: Arc<Self>) -> Arc<Refs> {
+        let git = self.git.clone();
+
+        git.refs
+            .get_with(self.cache_key.clone(), async move {
                 tokio::task::spawn_blocking(move || {
-                    let repo = git2::Repository::open_bare(repo).unwrap();
+                    let repo = self.repo.lock();
+
                     let ref_iter = repo.references().unwrap();
 
                     let mut built_refs = Refs::default();
@@ -134,33 +169,34 @@
             .await
     }
 
-    pub async fn get_readme(&self, repo: PathBuf) -> Option<Arc<str>> {
+    #[instrument(skip(self))]
+    pub async fn readme(self: Arc<Self>) -> Option<Arc<str>> {
         const README_FILES: &[&str] = &["README.md", "README", "README.txt"];
+
+        let git = self.git.clone();
 
-        self.readme_cache
-            .get_with(repo.clone(), async {
+        git.readme_cache
+            .get_with(self.cache_key.clone(), async move {
                 tokio::task::spawn_blocking(move || {
-                    let repo = Repository::open_bare(repo).unwrap();
+                    let repo = self.repo.lock();
+
                     let head = repo.head().unwrap();
                     let commit = head.peel_to_commit().unwrap();
                     let tree = commit.tree().unwrap();
-
-                    for file in README_FILES {
-                        let object = if let Some(o) = tree.get_name(file) {
-                            o
-                        } else {
-                            continue;
-                        };
-
-                        let object = object.to_object(&repo).unwrap();
-                        let blob = object.into_blob().unwrap();
-
-                        return Some(Arc::from(
-                            String::from_utf8(blob.content().to_vec()).unwrap(),
-                        ));
-                    }
 
-                    None
+                    let blob = README_FILES
+                        .iter()
+                        .map(|file| tree.get_name(file))
+                        .next()
+                        .flatten()?
+                        .to_object(&repo)
+                        .unwrap()
+                        .into_blob()
+                        .unwrap();
+
+                    Some(Arc::from(
+                        String::from_utf8(blob.content().to_vec()).unwrap(),
+                    ))
                 })
                 .await
                 .unwrap()
@@ -168,12 +204,15 @@
             .await
     }
 
-    pub async fn get_latest_commit(&self, repo: PathBuf, syntax_set: Arc<SyntaxSet>) -> Commit {
+    #[instrument(skip(self))]
+    pub async fn latest_commit(self: Arc<Self>) -> Commit {
         tokio::task::spawn_blocking(move || {
-            let repo = Repository::open_bare(repo).unwrap();
+            let repo = self.repo.lock();
+
             let head = repo.head().unwrap();
             let commit = head.peel_to_commit().unwrap();
-            let (diff_output, diff_stats) = fetch_diff_and_stats(&repo, &commit, &syntax_set);
+            let (diff_output, diff_stats) =
+                fetch_diff_and_stats(&repo, &commit, &self.git.syntax_set);
 
             let mut commit = Commit::from(commit);
             commit.diff_stats = diff_stats;
@@ -184,39 +223,45 @@
         .unwrap()
     }
 
-    pub async fn fetch_repository_metadata(&self) -> Arc<RepositoryMetadataList> {
-        if let Some(metadata) = self.repository_metadata.load().as_ref() {
-            return Arc::clone(metadata);
-        }
+    #[instrument(skip(self))]
+    pub async fn commit(self: Arc<Self>, commit: &str) -> Arc<Commit> {
+        let commit = Oid::from_str(commit).unwrap();
 
-        let start = Path::new("../test-git").canonicalize().unwrap();
+        let git = self.git.clone();
 
-        let repos = tokio::task::spawn_blocking(move || {
-            let mut repos: RepositoryMetadataList = RepositoryMetadataList::new();
-            fetch_repository_metadata_impl(&start, &start, &mut repos);
-            repos
-        })
-        .await
-        .unwrap();
+        git.commits
+            .get_with(commit, async move {
+                tokio::task::spawn_blocking(move || {
+                    let repo = self.repo.lock();
 
-        let repos = Arc::new(repos);
-        self.repository_metadata.store(Some(repos.clone()));
+                    let commit = repo.find_commit(commit).unwrap();
+                    let (diff_output, diff_stats) =
+                        fetch_diff_and_stats(&repo, &commit, &self.git.syntax_set);
 
-        repos
+                    let mut commit = Commit::from(commit);
+                    commit.diff_stats = diff_stats;
+                    commit.diff = diff_output;
+
+                    Arc::new(commit)
+                })
+                .await
+                .unwrap()
+            })
+            .await
     }
 
-    pub async fn get_commits(
-        &self,
-        repo: PathBuf,
+    #[instrument(skip(self))]
+    pub async fn commits(
+        self: Arc<Self>,
         branch: Option<&str>,
         offset: usize,
     ) -> (Vec<Commit>, Option<usize>) {
-        const AMOUNT: usize = 200;
+        const LIMIT: usize = 200;
 
         let ref_name = branch.map(|branch| format!("refs/heads/{}", branch));
 
         tokio::task::spawn_blocking(move || {
-            let repo = Repository::open_bare(repo).unwrap();
+            let repo = self.repo.lock();
             let mut revs = repo.revwalk().unwrap();
 
             if let Some(ref_name) = ref_name.as_deref() {
@@ -227,7 +272,7 @@
 
             let mut commits: Vec<Commit> = revs
                 .skip(offset)
-                .take(AMOUNT + 1)
+                .take(LIMIT + 1)
                 .map(|rev| {
                     let rev = rev.unwrap();
                     repo.find_commit(rev).unwrap().into()
@@ -385,6 +430,7 @@
     }
 }
 
+#[instrument(skip(repo, commit, syntax_set))]
 fn fetch_diff_and_stats(
     repo: &git2::Repository,
     commit: &git2::Commit<'_>,
@@ -413,6 +459,7 @@
     (diff_output, diff_stats)
 }
 
+#[instrument(skip(diff, syntax_set))]
 fn format_diff(diff: &git2::Diff<'_>, syntax_set: &SyntaxSet) -> String {
     let mut diff_output = String::new();
 
@@ -462,6 +509,7 @@
     diff_output
 }
 
+#[instrument(skip(repos))]
 fn fetch_repository_metadata_impl(
     start: &Path,
     current: &Path,
diff --git a/src/main.rs b/src/main.rs
index a6a4711..4892d22 100644
--- a/src/main.rs
+++ a/src/main.rs
@@ -43,8 +43,7 @@
         .route("/highlight.css", get(static_css(css)))
         .fallback(methods::repo::service.into_service())
         .layer(layer_fn(LoggingMiddleware))
-        .layer(Extension(Git::default()))
-        .layer(Extension(Arc::new(syntax_set)));
+        .layer(Extension(Arc::new(Git::new(syntax_set))));
 
     axum::Server::bind(&"127.0.0.1:3333".parse().unwrap())
         .serve(app.into_make_service_with_connect_info::<std::net::SocketAddr>())
diff --git a/src/methods/index.rs b/src/methods/index.rs
index 7711e4f..7281242 100644
--- a/src/methods/index.rs
+++ a/src/methods/index.rs
@@ -7,7 +7,7 @@
 use crate::{git::RepositoryMetadataList, Git};
 
 #[allow(clippy::unused_async)]
-pub async fn handle(Extension(git): Extension<Git>) -> Html<String> {
+pub async fn handle(Extension(git): Extension<Arc<Git>>) -> Html<String> {
     #[derive(Template)]
     #[template(path = "index.html")]
     pub struct View {
diff --git a/src/methods/repo.rs b/src/methods/repo.rs
index e3a62df..bbaf83c 100644
--- a/src/methods/repo.rs
+++ a/src/methods/repo.rs
@@ -14,7 +14,6 @@
 };
 use path_clean::PathClean;
 use serde::Deserialize;
-use syntect::parsing::SyntaxSet;
 use tower::{util::BoxCloneService, Service};
 
 use super::filters;
@@ -100,7 +99,7 @@
 pub async fn handle_tag(
     Extension(repo): Extension<Repository>,
     Extension(RepositoryPath(repository_path)): Extension<RepositoryPath>,
-    Extension(git): Extension<Git>,
+    Extension(git): Extension<Arc<Git>>,
     Query(query): Query<TagQuery>,
 ) -> Html<String> {
     #[derive(Template)]
@@ -110,7 +109,8 @@
         tag: DetailedTag,
     }
 
-    let tag = git.get_tag(repository_path, &query.name).await;
+    let open_repo = git.repo(repository_path).await;
+    let tag = open_repo.tag_info(&query.name).await;
 
     Html(View { repo, tag }.render().unwrap())
 }
@@ -127,7 +127,7 @@
 pub async fn handle_log(
     Extension(repo): Extension<Repository>,
     Extension(RepositoryPath(repository_path)): Extension<RepositoryPath>,
-    Extension(git): Extension<Git>,
+    Extension(git): Extension<Arc<Git>>,
     Query(query): Query<LogQuery>,
 ) -> Html<String> {
     #[derive(Template)]
@@ -139,12 +139,9 @@
         branch: Option<String>,
     }
 
-    let (commits, next_offset) = git
-        .get_commits(
-            repository_path,
-            query.branch.as_deref(),
-            query.offset.unwrap_or(0),
-        )
+    let open_repo = git.repo(repository_path).await;
+    let (commits, next_offset) = open_repo
+        .commits(query.branch.as_deref(), query.offset.unwrap_or(0))
         .await;
 
     Html(
@@ -163,7 +160,7 @@
 pub async fn handle_refs(
     Extension(repo): Extension<Repository>,
     Extension(RepositoryPath(repository_path)): Extension<RepositoryPath>,
-    Extension(git): Extension<Git>,
+    Extension(git): Extension<Arc<Git>>,
 ) -> Html<String> {
     #[derive(Template)]
     #[template(path = "repo/refs.html")]
@@ -172,7 +169,8 @@
         refs: Arc<Refs>,
     }
 
-    let refs = git.get_refs(repository_path).await;
+    let open_repo = git.repo(repository_path).await;
+    let refs = open_repo.refs().await;
 
     Html(View { repo, refs }.render().unwrap())
 }
@@ -181,7 +179,7 @@
 pub async fn handle_about(
     Extension(repo): Extension<Repository>,
     Extension(RepositoryPath(repository_path)): Extension<RepositoryPath>,
-    Extension(git): Extension<Git>,
+    Extension(git): Extension<Arc<Git>>,
 ) -> Html<String> {
     #[derive(Template)]
     #[template(path = "repo/about.html")]
@@ -190,7 +188,8 @@
         readme: Option<Arc<str>>,
     }
 
-    let readme = git.get_readme(repository_path).await;
+    let open_repo = git.clone().repo(repository_path).await;
+    let readme = open_repo.readme().await;
 
     Html(View { repo, readme }.render().unwrap())
 }
@@ -204,8 +203,7 @@
 pub async fn handle_commit(
     Extension(repo): Extension<Repository>,
     Extension(RepositoryPath(repository_path)): Extension<RepositoryPath>,
-    Extension(git): Extension<Git>,
-    Extension(syntax_set): Extension<Arc<SyntaxSet>>,
+    Extension(git): Extension<Arc<Git>>,
     Query(query): Query<CommitQuery>,
 ) -> Html<String> {
     #[derive(Template)]
@@ -215,18 +213,14 @@
         pub commit: Arc<Commit>,
     }
 
-    Html(
-        View {
-            repo,
-            commit: if let Some(commit) = query.id {
-                git.get_commit(repository_path, &commit, syntax_set).await
-            } else {
-                Arc::new(git.get_latest_commit(repository_path, syntax_set).await)
-            },
-        }
-        .render()
-        .unwrap(),
-    )
+    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)
+    };
+
+    Html(View { repo, commit }.render().unwrap())
 }
 
 #[allow(clippy::unused_async)]
@@ -244,8 +238,7 @@
 pub async fn handle_diff(
     Extension(repo): Extension<Repository>,
     Extension(RepositoryPath(repository_path)): Extension<RepositoryPath>,
-    Extension(git): Extension<Git>,
-    Extension(syntax_set): Extension<Arc<SyntaxSet>>,
+    Extension(git): Extension<Arc<Git>>,
     Query(query): Query<CommitQuery>,
 ) -> Html<String> {
     #[derive(Template)]
@@ -255,16 +248,12 @@
         pub commit: Arc<Commit>,
     }
 
-    Html(
-        View {
-            repo,
-            commit: if let Some(commit) = query.id {
-                git.get_commit(repository_path, &commit, syntax_set).await
-            } else {
-                Arc::new(git.get_latest_commit(repository_path, syntax_set).await)
-            },
-        }
-        .render()
-        .unwrap(),
-    )
+    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)
+    };
+
+    Html(View { repo, commit }.render().unwrap())
 }
--
rgit 0.1.4