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"),
}
}