Skip to content
Clone
ed3111f
281 lines 8.7 KB Rust
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<CodeActivityAuthorDto>,
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<Vec<LanguageStatDto>, 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<Vec<LanguageStatDto>, 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<Vec<ContributorStatDto>, 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<ContributorStatDto> = 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<String>,
) -> Result<CodeActivityDto, GitError> {
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<CodeActivityAuthorDto> = 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<HashMap<String, i64>, 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<String, i64> = 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)
}