use crate::{code_count, AppState}; use actix_web::{get, web, HttpResponse, Responder}; use anyhow::Result; use askama::Template; use chrono::offset::Utc; use chrono::{DateTime, NaiveDateTime}; use git2::Repository; use log::{error, warn}; use serde::Deserialize; #[derive(Template)] #[template(path = "user_repos.html")] struct UserReposHtml<'a> { username: String, repos: Vec<RepoMetadata>, language_colors: &'static code_count::LANGUAGE_COLORS, default_color: &'a &'static str, } struct RepoMetadata { name: String, description: String, languages: Option<Vec<code_count::LanguageStats>>, last_commit_date: String, last_commit_timestamp: i64, last_commit_message: String, number_of_commits: usize, total_loc: usize, } fn unix_time_to_date(time: i64) -> Option<String> { let naive = NaiveDateTime::from_timestamp_opt(time, 0)?; let datetime: DateTime<Utc> = DateTime::from_utc(naive, Utc); Some(format!("{}", datetime.format("%m/%d/%Y"))) } fn get_commit_time(commit: &git2::Commit) -> String { unix_time_to_date(commit.time().seconds()).unwrap_or(String::from("N/A")) } fn count_repo_commits(repo: &Repository) -> Result<usize> { let mut revwalk = repo.revwalk()?; revwalk.push_head()?; revwalk.set_sorting(git2::Sort::TIME | git2::Sort::TOPOLOGICAL)?; let number_of_commits = revwalk.fold(0, |acc, curr| { let rev = match curr { Ok(x) => x, _ => { return acc; } }; if repo.find_commit(rev).is_ok() { acc + 1 } else { acc } }); Ok(number_of_commits) } #[derive(Deserialize)] pub struct ReposQuery { pub order: Option<String>, pub by: Option<String>, } #[get("/~{username}")] pub(crate) async fn get_repos( app_data: web::Data<AppState>, path: web::Path<String>, q: web::Query<ReposQuery>, ) -> impl Responder { let username = path.into_inner(); if let Some(response) = crate::guard_username_configured(&username, &app_data) { return response; } let dir_entries = match std::fs::read_dir(format!("{}/{}", &app_data.config.repos_dir, username)) { Ok(x) => x, _ => { return HttpResponse::InternalServerError() .body(format!("Unable to get repos for {}", username)) } }; let mut repo_entries: Vec<RepoMetadata> = vec![]; for dir_entry in dir_entries { if !dir_entry.is_ok() { continue; } let dir_entry = dir_entry.unwrap(); let file_type = match dir_entry.file_type() { Ok(x) => x, _ => continue, }; if !file_type.is_dir() { continue; } let os_file_name = dir_entry.file_name(); let repo_name = match os_file_name.to_str() { Some(x) => x, _ => continue, }; let repo = match Repository::open(&format!( "{}/{}/{}", &app_data.config.repos_dir, username, repo_name )) { Ok(x) => x, _ => continue, }; if !repo.head().is_ok() { continue; } let repo_head = repo.head().unwrap(); let commit = match repo_head.peel_to_commit() { Ok(commit) => commit, _ => continue, }; let commit_str = commit.id().to_string(); let mut line_counts = match app_data.line_counts.lock() { Ok(x) => x, Err(x) => { warn!("failed getting lock. Skipping repo {}", x); continue; } }; let mut languages = None; let mut total_loc = 0; let mut number_of_commits = 0; if let Some((repo_language_stats, num_commits)) = line_counts.get(&commit_str) { languages = Some(repo_language_stats.languages.clone()); total_loc = repo_language_stats.total_loc; number_of_commits = *num_commits; } else if let Ok(repo_language_stats) = code_count::get_repo_languages(&repo, &username, repo_name) { let commits_count = match count_repo_commits(&repo) { Ok(x) => x, Err(x) => { error!("error counting commits for {}: {:?}", repo_name, x); continue; } }; line_counts.insert(commit_str, (repo_language_stats.clone(), commits_count)); languages = Some(repo_language_stats.languages); total_loc = repo_language_stats.total_loc; number_of_commits = commits_count; }; drop(line_counts); repo_entries.push(RepoMetadata { name: repo_name.to_string(), description: app_data.config.get_repo_description(&username, repo_name), last_commit_date: get_commit_time(&commit), last_commit_timestamp: commit.time().seconds(), last_commit_message: commit.summary().unwrap_or("").to_string(), number_of_commits, languages, total_loc, }); } let mut order_desc = true; if q.order == Some("asc".to_string()) { order_desc = false; } let qby = q.by.clone().unwrap_or(String::from("")); match qby.as_str() { "loc" => { if order_desc { repo_entries.sort_by_key(|a| -(a.total_loc as i32)); } else { repo_entries.sort_by_key(|a| a.total_loc); } } "name" => { if order_desc { repo_entries.sort_by(|a, b| b.name.cmp(&a.name)); } else { repo_entries.sort_by(|a, b| a.name.cmp(&b.name)); } } "commits" => { if order_desc { repo_entries.sort_by_key(|a| -(a.number_of_commits as i32)); } else { repo_entries.sort_by_key(|a| a.number_of_commits); } } _ => { if order_desc { repo_entries.sort_by_key(|a| -a.last_commit_timestamp); } else { repo_entries.sort_by_key(|a| a.last_commit_timestamp); } } }; let rendered = UserReposHtml { username, repos: repo_entries, language_colors: &code_count::LANGUAGE_COLORS, default_color: &"654321", } .render(); match rendered { Ok(content) => HttpResponse::Ok().content_type("text/html").body(content), _ => HttpResponse::InternalServerError().body("Error rendering templates listing"), } }