🏡 index : ~doyle/rgit.git

author Jordan Doyle <jordan@doyle.la> 2022-07-09 0:02:09.0 +01:00:00
committer Jordan Doyle <jordan@doyle.la> 2022-07-09 0:02:12.0 +01:00:00
commit
f3142420736b58f814f89572e30a7ee8961cf059 [patch]
tree
ea5280bbe63ee47225e192986ca6b8380fbb22bc
parent
b001a0330123c11b68e4fdca334f52c629321e36
download
f3142420736b58f814f89572e30a7ee8961cf059.tar.gz

Cleanup git API



Diff

 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())
}