use std::collections::HashMap; use gitbaby::commit::types::{ListCommitsOptions, TreeEntryMode}; use models::git::git_languages; use models::repo::repos; use sea_orm::{ ActiveModelTrait, ColumnTrait, EntityTrait, QueryFilter, QueryOrder, Set, Unchanged, }; use serde::Serialize; use time::OffsetDateTime; use uuid::Uuid; use crate::service::GitService; use crate::share::errors::{GitError, map_baby}; #[derive(Debug, Clone, Serialize, utoipa::ToSchema)] pub struct LanguageStatDto { pub language: String, pub percentage_bp: i32, pub size: i64, pub is_primary: bool, } #[derive(Debug, Clone, Serialize, utoipa::ToSchema)] pub struct ContributorStatDto { pub name: String, pub email: String, pub commits: i64, pub additions: i64, pub deletions: i64, } #[derive(Debug, Clone, Serialize, utoipa::ToSchema)] pub struct CodeActivityAuthorDto { pub name: String, pub email: String, pub commits: i64, pub additions: i64, pub deletions: i64, } #[derive(Debug, Clone, Serialize, utoipa::ToSchema)] pub struct CodeActivityDto { pub authors: Vec, pub total_commits: i64, pub total_additions: i64, pub total_deletions: i64, } pub fn ext_to_language(name: &str) -> Option<&'static str> { let base = name.rsplit('/').next().unwrap_or(name); match base { "Dockerfile" => return Some("Dockerfile"), "Makefile" => return Some("Makefile"), "Gemfile" => return Some("Ruby"), _ => {} } let ext = base.rsplit('.').next()?.to_ascii_lowercase(); let lang = match ext.as_str() { "rs" => "Rust", "go" => "Go", "py" => "Python", "js" => "JavaScript", "ts" => "TypeScript", "tsx" => "TypeScript", "jsx" => "JavaScript", "java" => "Java", "c" | "h" => "C", "cpp" | "cc" | "hpp" => "C++", "cs" => "C#", "php" => "PHP", "rb" => "Ruby", "swift" => "Swift", "kt" | "kts" => "Kotlin", "scala" => "Scala", "md" | "markdown" => "Markdown", "yaml" | "yml" => "YAML", "json" => "JSON", "toml" => "TOML", "xml" => "XML", "html" | "htm" => "HTML", "css" => "CSS", "scss" | "sass" => "SCSS", "sql" => "SQL", "sh" | "bash" | "zsh" => "Shell", "dockerfile" => "Dockerfile", "proto" => "Protocol Buffers", "vue" => "Vue", "lua" => "Lua", "dart" => "Dart", _ => return None, }; Some(lang) } impl GitService { /// Recompute language breakdown from the tree at HEAD and persist into /// `git_language` (percentage in basis points) + `repos.primary_language`. pub async fn analysis_languages( &self, space_name: String, repo_name: String, ) -> Result, GitError> { let ctx = self.context(space_name, repo_name).await?; let baby = self.baby(&ctx); let langs = language_stats(&baby).await?; let total: i64 = langs.values().sum(); let mut rows: Vec<(String, i64)> = langs.into_iter().collect(); rows.sort_by_key(|(_, size)| std::cmp::Reverse(*size)); let db = self.db_write(); git_languages::Entity::delete_many() .filter(git_languages::Column::RepoId.eq(ctx.model.id)) .exec(db) .await?; let now = OffsetDateTime::now_utc(); let primary = rows.first().map(|(name, _)| name.clone()); for (language, size) in &rows { let bp = if total > 0 { ((*size as f64 / total as f64) * 10_000.0).round() as i32 } else { 0 }; git_languages::ActiveModel { id: Set(Uuid::now_v7()), repo_id: Set(ctx.model.id), language: Set(language.clone()), percentage_bp: Set(bp), size: Set(*size), created_at: Set(now), } .insert(db) .await?; } let am = repos::ActiveModel { id: Unchanged(ctx.model.id), primary_language: Set(primary), updated_at: Set(now), ..Default::default() }; am.update(db).await?; let primary_name = rows.first().map(|(n, _)| n.clone()); Ok(rows .into_iter() .map(|(language, size)| { let is_primary = primary_name.as_deref() == Some(language.as_str()); LanguageStatDto { language, percentage_bp: if total > 0 { ((size as f64 / total as f64) * 10_000.0).round() as i32 } else { 0 }, size, is_primary, } }) .collect()) } pub async fn analysis_languages_read( &self, space_name: String, repo_name: String, ) -> Result, GitError> { let ctx = self.context(space_name, repo_name).await?; let rows = git_languages::Entity::find() .filter(git_languages::Column::RepoId.eq(ctx.model.id)) .order_by_desc(git_languages::Column::Size) .all(self.db_read()) .await?; Ok(rows .into_iter() .enumerate() .map(|(i, r)| LanguageStatDto { language: r.language, percentage_bp: r.percentage_bp, size: r.size, is_primary: i == 0, }) .collect()) } pub async fn analysis_contributors( &self, space_name: String, repo_name: String, ) -> Result, GitError> { let ctx = self.context(space_name, repo_name).await?; let commits = self .baby(&ctx) .list_commits(ListCommitsOptions { revision: "HEAD".to_string(), ..Default::default() }) .await .map_err(map_baby)?; let mut map: HashMap<(String, String), i64> = HashMap::new(); for c in &commits { *map.entry((c.author.name.clone(), c.author.email.clone())) .or_default() += 1; } let mut out: Vec = map .into_iter() .map(|((name, email), commits)| ContributorStatDto { name, email, commits, additions: 0, deletions: 0, }) .collect(); out.sort_by_key(|b| std::cmp::Reverse(b.commits)); Ok(out) } pub async fn analysis_code_activity( &self, space_name: String, repo_name: String, branch: Option, ) -> Result { let ctx = self.context(space_name, repo_name).await?; let revision = branch.unwrap_or_else(|| "HEAD".to_string()); let commits = self .baby(&ctx) .list_commits(ListCommitsOptions { revision, ..Default::default() }) .await .map_err(map_baby)?; let mut authors: Vec = Vec::new(); for c in &commits { match authors.iter_mut().find(|a| a.email == c.author.email) { Some(a) => a.commits += 1, None => authors.push(CodeActivityAuthorDto { name: c.author.name.clone(), email: c.author.email.clone(), commits: 1, additions: 0, deletions: 0, }), } } authors.sort_by_key(|b| std::cmp::Reverse(b.commits)); let total = commits.len() as i64; Ok(CodeActivityDto { authors, total_commits: total, total_additions: 0, total_deletions: 0, }) } } pub(crate) async fn language_stats( baby: &gitbaby::GitBaby, ) -> Result, GitError> { let entries = baby .walk_tree( "HEAD", std::path::Path::new(""), gitbaby::tree::types::WalkTreeOptions { recurse: true, show_trees: false, }, ) .await .map_err(map_baby)?; let mut map: HashMap = HashMap::new(); for e in entries { if (e.mode == TreeEntryMode::Blob || e.mode == TreeEntryMode::Exec) && let Some(lang) = ext_to_language(&e.name) { *map.entry(lang.to_string()).or_default() += e.size.unwrap_or(0) as i64; } } Ok(map) }