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(-)
@@ -1611,6 +1611,7 @@
"humantime",
"md5",
"moka",
"parking_lot",
"path-clean",
"serde",
"syntect",
@@ -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"
@@ -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,
@@ -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>())
@@ -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 {
@@ -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())
}