use std::{borrow::Cow,collections::BTreeMap,path::{Path, PathBuf},sync::Arc,time::Duration,};use arc_swap::ArcSwapOption;use git2::{Oid, Repository, Signature, Sort};use time::OffsetDateTime;pub type RepositoryMetadataList = BTreeMap<Option<String>, Vec<RepositoryMetadata>>;#[derive(Clone)]pub struct Git {commits: moka::future::Cache<Oid, Arc<Commit>>,readme_cache: moka::future::Cache<PathBuf, Arc<str>>,refs: moka::future::Cache<PathBuf, Arc<Refs>>,repository_metadata: Arc<ArcSwapOption<RepositoryMetadataList>>,}impl Default for Git {fn default() -> Self {Self {commits: moka::future::Cache::new(100),readme_cache: moka::future::Cache::new(100),refs: moka::future::Cache::new(100),repository_metadata: Arc::new(ArcSwapOption::default()),}}}impl Git {pub async fn get_commit<'a>(&'a self, repo: PathBuf, commit: &str) -> Arc<Commit> {let commit = Oid::from_str(commit).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();Arc::new(Commit::from(commit))}).await.unwrap()}).await}pub async fn get_refs<'a>(&'a self, repo: PathBuf) -> Arc<Refs> {self.refs.get_with(repo.clone(), async {tokio::task::spawn_blocking(move || {let repo = git2::Repository::open_bare(repo).unwrap();let refs = repo.references().unwrap();let mut built_refs = Refs::default();for ref_ in refs {let ref_ = ref_.unwrap();if ref_.is_branch() {let commit = ref_.peel_to_commit().unwrap();built_refs.branch.push(Branch {name: ref_.shorthand().unwrap().to_string(),commit: commit.into(),});} else if ref_.is_tag() {let commit = ref_.peel_to_commit().unwrap();built_refs.tag.push(Tag {name: ref_.shorthand().unwrap().to_string(),commit: commit.into(),});}}Arc::new(built_refs)}).await.unwrap()}).await}pub async fn get_readme(&self, repo: PathBuf) -> Arc<str> {self.readme_cache.get_with(repo.clone(), async {tokio::task::spawn_blocking(move || {let repo = Repository::open_bare(repo).unwrap();let head = repo.head().unwrap();let commit = head.peel_to_commit().unwrap();let tree = commit.tree().unwrap();let object = tree.get_name("README.md").unwrap().to_object(&repo).unwrap();let blob = object.into_blob().unwrap();Arc::from(String::from_utf8(blob.content().to_vec()).unwrap())}).await.unwrap()}).await}pub async fn get_latest_commit<'a>(&'a self, repo: PathBuf) -> Commit {tokio::task::spawn_blocking(move || {let repo = Repository::open_bare(repo).unwrap();let head = repo.head().unwrap();let commit = head.peel_to_commit().unwrap();Commit::from(commit)}).await.unwrap()}pub async fn fetch_repository_metadata(&self) -> Arc<RepositoryMetadataList> {if let Some(metadata) = self.repository_metadata.load().as_ref() {return Arc::clone(&metadata);}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_commits(&self, repo: PathBuf) -> Vec<Commit> {tokio::task::spawn_blocking(move || {let repo = Repository::open_bare(repo).unwrap();let mut revs = repo.revwalk().unwrap();revs.set_sorting(Sort::TIME).unwrap();revs.push_head().unwrap();let mut commits = Vec::with_capacity(200);for rev in revs.skip(0).take(200) {let rev = rev.unwrap();let commit = repo.find_commit(rev).unwrap();commits.push(commit.into());}commits}).await.unwrap()}}#[derive(Debug, Default)]pub struct Refs {pub branch: Vec<Branch>,pub tag: Vec<Tag>,}#[derive(Debug)]pub struct Branch {pub name: String,pub commit: Commit,}#[derive(Debug)]pub struct Remote {pub name: String,}#[derive(Debug)]pub struct Tag {pub name: String,pub commit: Commit,}#[derive(Debug)]pub struct RepositoryMetadata {pub name: String,pub description: Option<Cow<'static, str>>,pub owner: Option<String>,pub last_modified: Duration,}#[derive(Debug)]pub struct CommitUser {name: String,email: String,email_md5: String,time: String,}impl From<Signature<'_>> for CommitUser {fn from(v: Signature<'_>) -> Self {CommitUser {name: v.name().unwrap().to_string(),email: v.email().unwrap().to_string(),email_md5: format!("{:x}", md5::compute(v.email_bytes())),time: OffsetDateTime::from_unix_timestamp(v.when().seconds()).unwrap().to_string(),}}}impl CommitUser {pub fn name(&self) -> &str {&self.name}pub fn email(&self) -> &str {&self.email}pub fn email_md5(&self) -> &str {&self.email_md5}pub fn time(&self) -> &str {&self.time}}#[derive(Debug)]pub struct Commit {author: CommitUser,committer: CommitUser,oid: String,tree: String,parents: Vec<String>,summary: String,body: String,}impl From<git2::Commit<'_>> for Commit {fn from(commit: git2::Commit<'_>) -> Self {Commit {author: commit.author().into(),committer: commit.committer().into(),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(),}}}impl Commit {pub fn author(&self) -> &CommitUser {&self.author}pub fn committer(&self) -> &CommitUser {&self.committer}pub fn oid(&self) -> &str {&self.oid}pub fn tree(&self) -> &str {&self.tree}pub fn parents(&self) -> impl Iterator<Item = &str> {self.parents.iter().map(String::as_str)}pub fn summary(&self) -> &str {&self.summary}pub fn body(&self) -> &str {&self.body}}fn fetch_repository_metadata_impl(start: &Path,current: &Path,repos: &mut RepositoryMetadataList,) {let dirs = std::fs::read_dir(current).unwrap().map(|v| v.unwrap().path()).filter(|path| path.is_dir());for dir in dirs {let repository = match Repository::open_bare(&dir) {Ok(v) => v,Err(_e) => {fetch_repository_metadata_impl(start, &dir, repos);continue;}};let repo_path = Some(current.strip_prefix(start).unwrap().to_string_lossy().into_owned(),).filter(|v| !v.is_empty());let repos = repos.entry(repo_path).or_default();let description = std::fs::read_to_string(dir.join("description")).map(Cow::Owned).ok();let last_modified = std::fs::metadata(&dir).unwrap().modified().unwrap();let owner = repository.config().unwrap().get_string("gitweb.owner").ok();repos.push(RepositoryMetadata {name: dir.components().last().unwrap().as_os_str().to_string_lossy().into_owned(),description,owner,last_modified: (OffsetDateTime::now_utc() - OffsetDateTime::from(last_modified)).unsigned_abs(),});}}