use crate::config::*;
use crate::repo::*;
use chrono::{DateTime, Utc};
use git2::Repository;
use gleisbau::{
graph::Builder as GraphBuilder,
print::{format::CommitFormat, unicode::print_unicode},
settings::{
BranchOrder, BranchSettings, BranchSettingsDef, Characters, MergePatterns, Settings,
},
};
use rayon::prelude::*;
use std::ffi::OsStr;
use std::hash::{DefaultHasher, Hash, Hasher};
use std::path::Path;
use std::{cmp::Reverse, fs};
use syntect::highlighting::Color;
use syntect::highlighting::ThemeSet;
use syntect::html::highlighted_html_for_string;
use syntect::parsing::SyntaxSet;
use tokio::time::Duration;
pub struct AppState {
pub simple_repos: Vec<SimpleRepo>,
pub repos: Vec<Repo>,
pub files: Vec<FileInfo>,
pub last_load: DateTime<Utc>,
pub config: Config,
}
impl AppState {
pub fn get_simple_repos(&mut self) -> Vec<SimpleRepo> {
let reload_time = self.last_load + Duration::from_secs(self.config.load_duration);
if Utc::now() > reload_time {
self.last_load = Utc::now();
self.simple_repos = load_simple_repos(self.config.clone());
}
self.simple_repos.clone()
}
pub fn get_repo(&mut self, name: String) -> Option<&Repo> {
if let Some(simple_repo) = self.simple_repos.iter().find(|x| x.name == name) {
let existing = self
.repos
.iter()
.find(|x| x.name == simple_repo.name)
.map(|r| r.name.clone());
let mut reload = false;
if let Some(repo_name) = existing {
if let Some(repo) = self.repos.iter().find(|x| x.name == repo_name) {
let reload_time =
repo.last_load + Duration::from_secs(self.config.load_duration);
if Utc::now() > reload_time {
reload = true;
}
}
} else {
reload = true;
}
if reload {
if let Some(reloaded) = load_repo(name.clone(), self.config.clone()) {
if let Some(pos) = self.repos.iter().position(|x| x.name == name) {
self.repos.swap_remove(pos);
}
self.repos.push(reloaded);
return self.repos.iter().find(|x| x.name == name);
} else {
return None;
}
} else {
return self.repos.iter().find(|x| x.name == simple_repo.name);
}
}
None
}
pub fn get_file(&mut self, file_id: String, repo_name: String) -> Option<&FileInfo> {
let reload;
if let Some(file) = self.files.iter().find(|x| x.id == file_id) {
let file_reload_time = file.last_load + Duration::from_secs(self.config.load_duration);
if Utc::now() > file_reload_time {
reload = true;
} else {
reload = false;
}
} else {
reload = true;
}
if reload {
if let Some(reloaded) = load_file(file_id.clone(), repo_name, self.config.clone()) {
if let Some(pos) = self.files.iter().position(|x| x.id == file_id) {
self.files.swap_remove(pos);
}
self.files.push(reloaded);
return self.files.iter().find(|x| x.id == file_id);
} else {
return None;
}
} else {
return self.files.iter().find(|x| x.id == file_id);
}
}
}
pub fn load_simple_repos(config: Config) -> Vec<SimpleRepo> {
tracing::debug!("Reloading simple repos...");
let git_dir = config.git_dir.clone();
let private_repos = config.private_repos.clone();
let read = match fs::read_dir(&git_dir) {
Ok(r) => r,
Err(_) => return Vec::new(),
};
let entries: Vec<_> = read
.filter_map(Result::ok)
.filter(|entry| entry.path().is_dir())
.filter_map(|entry| {
let path = entry.path();
let name = path
.file_name()
.and_then(|n| n.to_str())
.map(|s| s.strip_suffix(".git").unwrap_or(s).to_string())
.unwrap_or_default();
if private_repos.contains(&name) {
return None;
}
Repository::open(&path).ok()
})
.collect();
let mut repos: Vec<SimpleRepo> = entries
.into_par_iter()
.map(|repo| {
let name;
// check if repo is repo_name/.git/, happens when the path is the code and not the source
if repo
.path()
.file_name()
.unwrap_or_default()
.to_str()
.unwrap_or_default()
== ".git"
{
name = repo
.path()
.parent()
.and_then(|p| p.file_name())
.and_then(|n| n.to_str())
.map(|s| s.strip_suffix(".git").unwrap_or(s).to_string())
.unwrap_or_default();
} else {
name = repo
.path()
.file_name()
.and_then(|n| n.to_str())
.map(|s| s.strip_suffix(".git").unwrap_or(s).to_string())
.unwrap_or_default();
}
let lastcommittime = repo
.head()
.and_then(|h| h.peel_to_commit())
.map(|c| {
let timestamp = c.time().seconds();
DateTime::from_timestamp(timestamp, 0).unwrap_or_default()
})
.unwrap_or_else(|_| DateTime::default());
let mut desc = String::new();
let mut files = Vec::new();
if let Ok(commit) = repo.head().and_then(|h| h.peel_to_commit()) {
if let Ok(tree) = commit.tree() {
let _ = tree.walk(git2::TreeWalkMode::PreOrder, |root, entry| {
if entry.kind() == Some(git2::ObjectType::Blob) {
let full_path = if root.is_empty() {
entry.name().unwrap_or("").to_string()
} else {
format!("{}{}", root, entry.name().unwrap_or(""))
};
if let Ok(obj) = entry.to_object(&repo) {
if let Some(blob) = obj.as_blob() {
let mut s = DefaultHasher::new();
full_path.hash(&mut s);
files.push(File {
path: full_path,
size: blob.size(),
id: format!("{}", s.finish()),
});
match entry.name().unwrap() {
"DESCRIPTION" => {
desc = str::from_utf8(blob.content())
.unwrap_or_default()
.to_string();
}
_ => {}
}
}
}
}
git2::TreeWalkResult::Ok
});
}
}
SimpleRepo {
name,
desc,
lastcommit: lastcommittime,
}
})
.collect();
repos.sort_unstable_by_key(|x| Reverse(x.lastcommit));
tracing::debug!("Finished loading repos");
repos
}
pub fn load_repo(name: String, config: Config) -> Option<Repo> {
tracing::debug!("Reloading repo {}...", name);
let git_dir = config.git_dir.clone();
let read = match fs::read_dir(&git_dir) {
Ok(r) => r,
Err(e) => {
tracing::error!("Failed to read git dir {}: {}", git_dir, e);
return None;
}
};
for entry in read.filter_map(Result::ok) {
let path = entry.path();
if !path.is_dir() {
continue;
}
let repo = match Repository::open(&path) {
Ok(r) => r,
Err(e) => {
tracing::error!("Failed to open repo at {}: {}", path.display(), e);
continue;
}
};
let repo_name = path
.file_name()
.and_then(|n| n.to_str())
.map(|s| s.strip_suffix(".git").unwrap_or(s).to_string())
.unwrap_or_default();
if config.private_repos.contains(&repo_name) {
continue;
}
if repo_name != name {
continue;
}
tracing::debug!("Found matching repo: {}", repo_name);
let branches = repo.branches(Some(git2::BranchType::Local)).unwrap();
let mut branches_out: Vec<Branch> = branches
.filter_map(|res| match res {
Ok((branch, _)) => Some(branch),
Err(_) => None,
})
.filter_map(|branch| {
let name = branch.name().ok().flatten().map(|s| s.to_string())?;
let reference = branch.get();
let oid = reference.target()?;
let commit = repo.find_commit(oid).ok()?;
let author = commit.author();
Some(Branch {
name: name,
lastcommit: DateTime::from_timestamp(commit.time().seconds(), 0)
.unwrap_or_default(),
author: match author.name() {
Some(n) => n.to_string(),
None => String::new(),
},
})
})
.collect();
branches_out.sort_unstable_by_key(|x| Reverse(x.lastcommit));
let mut readme_text = String::new();
let mut desc = String::new();
let mut files = Vec::new();
if let Ok(commit) = repo.head().and_then(|h| h.peel_to_commit()) {
if let Ok(tree) = commit.tree() {
let _ = tree.walk(git2::TreeWalkMode::PreOrder, |root, entry| {
if entry.kind() == Some(git2::ObjectType::Blob) {
let full_path = if root.is_empty() {
entry.name().unwrap_or("").to_string()
} else {
format!("{}{}", root, entry.name().unwrap_or(""))
};
if let Ok(obj) = entry.to_object(&repo) {
if let Some(blob) = obj.as_blob() {
let mut s = DefaultHasher::new();
full_path.hash(&mut s);
files.push(File {
path: full_path,
size: blob.size(),
id: format!("{}", s.finish()),
});
match entry.name().unwrap() {
"README.md" => {
readme_text = str::from_utf8(blob.content())
.unwrap_or_default()
.to_string();
}
"DESCRIPTION" => {
desc = str::from_utf8(blob.content())
.unwrap_or_default()
.to_string();
}
_ => {}
}
}
}
}
git2::TreeWalkResult::Ok
});
}
}
let mut readme_html = String::new();
let parser = pulldown_cmark::Parser::new(&readme_text);
pulldown_cmark::html::push_html(&mut readme_html, parser);
let mut git_graph = String::new();
let wrapping = Some((None, Some(0), Some(8)));
let style = Characters::round();
let settings = Settings {
reverse_commit_order: false,
debug: false,
colored: true,
compact: false,
include_remote: false,
format: CommitFormat::Short,
wrapping,
characters: style,
branch_order: BranchOrder::ShortestFirst(true),
branches: BranchSettings::from(BranchSettingsDef::git_flow())
.map_err(|err| err.to_string())
.unwrap(),
merge_patterns: MergePatterns::default(),
};
let graph_builder = GraphBuilder::new()
.with_settings(&settings)
.with_repository(repo)
.with_max_count(config.git_graph_count);
if let Ok(graph) = graph_builder.build() {
if let Ok((g_lines, t_lines, _indices)) = print_unicode(&graph, &settings) {
for (g_line, t_line) in g_lines.iter().zip(t_lines.iter()) {
git_graph += &format!(
" {} {}<br>",
ansi_to_html::convert(&g_line).unwrap_or_default(),
ansi_to_html::convert(&t_line).unwrap_or_default()
);
}
};
};
tracing::debug!("Finished loading repo");
let found_repo = Repo {
name: repo_name,
desc: desc,
readme: readme_html,
branches: branches_out,
commit_graph: git_graph,
files: files,
last_load: Utc::now(),
};
return Some(found_repo);
}
None
}
pub fn load_file(file_id: String, repo: String, config: Config) -> Option<FileInfo> {
tracing::debug!("Reloading file {}...", file_id);
let git_dir = config.git_dir.clone();
let read = match fs::read_dir(&git_dir) {
Ok(r) => r,
Err(e) => {
tracing::error!("Failed to read git dir {}: {}", git_dir, e);
return None;
}
};
for entry in read.filter_map(Result::ok) {
let path = entry.path();
if !path.is_dir() {
continue;
}
let repo_path = match Repository::open(&path) {
Ok(r) => r,
Err(e) => {
tracing::error!("Failed to open repo at {}: {}", path.display(), e);
continue;
}
};
let repo_name = path
.file_name()
.and_then(|n| n.to_str())
.map(|s| s.strip_suffix(".git").unwrap_or(s).to_string())
.unwrap_or_default();
if repo_name != repo {
continue;
}
if let Ok(commit) = repo_path.head().and_then(|h| h.peel_to_commit()) {
if let Ok(tree) = commit.tree() {
let mut result = None;
let mut ext = String::new();
let _ = tree.walk(git2::TreeWalkMode::PreOrder, |root, entry| {
if entry.kind() == Some(git2::ObjectType::Blob) {
let full_path = if root.is_empty() {
entry.name().unwrap_or("").to_string()
} else {
format!("{}{}", root, entry.name().unwrap_or(""))
};
if let Ok(obj) = entry.to_object(&repo_path) {
if let Some(blob) = obj.as_blob() {
let mut s = DefaultHasher::new();
full_path.hash(&mut s);
let file_id_str = format!("{}", s.finish());
if file_id_str == file_id {
result =
Some(String::from_utf8_lossy(blob.content()).to_string());
ext = Path::new(&full_path)
.extension()
.and_then(OsStr::to_str)
.unwrap_or("txt")
.to_string();
}
}
}
}
git2::TreeWalkResult::Ok
});
if let Some(content) = result {
let ss = SyntaxSet::load_defaults_newlines();
let sr = ss
.find_syntax_by_extension(&ext)
.unwrap_or(ss.find_syntax_plain_text());
let ts = ThemeSet::load_defaults();
let mut theme = ts.themes["base16-eighties.dark"].clone();
theme.settings.background = Some(Color {
r: (0),
g: (0),
b: (0),
a: (0),
});
let html = highlighted_html_for_string(&content, &ss, &sr, &theme)
.unwrap_or(content.clone());
tracing::debug!("Finished loading file");
return Some(FileInfo {
id: file_id,
html_content: html,
last_load: Utc::now(),
});
}
}
}
}
None
}