Viewing:
use anyhow::{anyhow, Result};
use askama::Template;
use chrono::offset::Utc;
use comrak::{
adapters::{HeadingAdapter, HeadingMeta},
markdown_to_html_with_plugins, ComrakOptions, ComrakPlugins,
};
use git2::{BlameHunk, Repository};
use slug;
use std::path::Path;
use std::str;
use crate::file;
const MINUTE: i64 = 60; // e.g. 60 seconds
const TWO_MINUTES: i64 = MINUTE * 2;
const HOUR: i64 = MINUTE * 60;
const TWO_HOURS: i64 = HOUR * 2; // 7_200
const DAY: i64 = HOUR * 24; // 86_400
const TWO_DAYS: i64 = DAY * 2; // 172_800
const WEEK: i64 = HOUR * 24; // 604_800
const TWO_WEEKS: i64 = HOUR * 24; // 1_209_600
const MONTH: i64 = DAY * 30; // 2_592_000
const TWO_MONTH: i64 = MONTH * 2; // 5_184_000
const YEAR: i64 = MONTH * 12; // 31_104_000
const TWO_YEAR: i64 = YEAR * 2; // 62_208_000
fn timestamp_to_human_relative_time(time: i64) -> String {
let now = Utc::now().timestamp();
let diff = now - time;
match diff {
i64::MIN..=-1 => String::from("Timezone issue or in the future"),
0..=59 => String::from("Seconds ago"),
MINUTE..=119 => String::from("1 minute ago"),
TWO_MINUTES..=3_599 => format!("{} minutes ago", diff / MINUTE),
HOUR..=7_199 => String::from("1 hour ago"),
TWO_HOURS..=86_399 => format!("{} hours ago", diff / HOUR),
DAY..=171_999 => String::from("Yesterday"),
TWO_DAYS..=604_799 => format!("{} days ago", diff / DAY),
WEEK..=1_209_599 => String::from("last week"),
TWO_WEEKS..=2_591_999 => format!("{} weeks ago", diff / WEEK),
MONTH..=5_183_999 => String::from("one month ago"),
TWO_MONTH..=31_103_999 => format!("{} months ago", diff / MONTH),
YEAR..=62_207_999 => String::from("1 year ago"),
TWO_YEAR..=i64::MAX => format!("{} years ago", diff / YEAR),
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Template)]
#[template(path = "repo_tree.html")]
struct FileTree {
name: String,
// in seconds
last_touched: String,
view_link: String,
is_dir: bool,
path: String,
children: Box<Vec<FileTree>>,
}
#[derive(Template)]
#[template(path = "repo_index.html")]
struct RepoIndexHtml<'a, 'b, 'c> {
repo_name: String,
repo_tree: &'a FileTree,
repo_owner: &'c str,
readme: Option<String>,
description: &'b str,
clone_with: &'b str,
}
#[derive(Template)]
#[template(path = "repo_folder.html")]
struct RepoFolderHtml<'a, 'b> {
repo_name: String,
repo_tree: &'a FileTree,
folder: &'b str,
breadcrumbs: Vec<file::Breadcrumb>,
}
impl FileTree {
fn insert(&mut self, new_entry_parent_name: &str, entry: FileTree) -> Result<()> {
if !self.is_dir {
return Err(anyhow!("can not insert entry into file"));
}
if new_entry_parent_name == self.path {
self.children.push(entry);
return Ok(());
} else if new_entry_parent_name.starts_with(&self.path) {
for folder_content in self.children.iter_mut() {
if folder_content
.insert(new_entry_parent_name, entry.clone())
.is_ok()
{
return Ok(());
};
}
}
Err(anyhow!("could not insert"))
}
}
struct CustomHeadingAdapter;
impl HeadingAdapter for CustomHeadingAdapter {
fn enter(&self, heading: &HeadingMeta) -> String {
let id = slug::slugify(&heading.content);
format!("<h{} id=\"{}\">", heading.level, id)
}
fn exit(&self, heading: &HeadingMeta) -> String {
let id = slug::slugify(&heading.content);
format!(
"<a class=\"ReadmeLink\" href=\"#{}\" title=\"link to {}\">📎</a></h{}>",
id, heading.content, heading.level
)
}
}
fn get_latest_commit_time_for_file(repo: &Repository, file: &str) -> i64 {
let blame = repo.blame_file(Path::new(file), None);
match blame {
Ok(blame) => blame
.iter()
.reduce(|acc: BlameHunk, curr| {
curr.final_commit_id();
let curr_when = curr.final_signature().when();
if acc.final_signature().when() > curr_when {
acc
} else {
curr
}
})
.unwrap()
.final_signature()
.when()
.seconds(),
_ => -1,
}
}
fn get_file_tree(
repo: &Repository,
repo_owner: &str,
repo_name: &str,
folder: &str,
max_depth: usize,
) -> Result<FileTree> {
let tree = if folder == "" {
let head = repo.head()?;
head.peel_to_tree()?
} else {
repo.revparse_single(&format!("HEAD:{}", folder))?
.peel_to_tree()?
};
let mut render_entries = FileTree {
path: if folder == "" {
folder.to_string()
} else {
format!("{}/", folder)
},
name: String::from(""),
last_touched: String::from(""),
view_link: String::from("/"),
is_dir: true,
children: Box::new(vec![]),
};
tree.walk(
git2::TreeWalkMode::PreOrder,
|relative_parent_directory, entry| {
let parent_directory = if folder == "" {
relative_parent_directory.to_string()
} else {
format!("{}/{}", folder, relative_parent_directory)
};
let kind = match entry.kind() {
Some(x) => x,
_ => return git2::TreeWalkResult::Skip,
};
if kind != git2::ObjectType::Tree && kind != git2::ObjectType::Blob {
return git2::TreeWalkResult::Skip;
};
let entry_name = entry.name().unwrap_or("Name not understood");
let mut last_touched = String::from("");
let is_dir = kind == git2::ObjectType::Tree;
if !is_dir {
let commit_time = get_latest_commit_time_for_file(
repo,
&format!("{}{}", parent_directory, entry_name),
);
last_touched = timestamp_to_human_relative_time(commit_time);
}
let file_path = format!(
"{}{}{}",
parent_directory,
entry_name,
if is_dir { "/" } else { "" }
);
let view_link = format!("/~{}/{}/tree/{}", repo_owner, repo_name, file_path);
let new_entry = FileTree {
path: file_path,
name: entry_name.to_string(),
last_touched,
view_link,
is_dir: kind == git2::ObjectType::Tree,
children: Box::new(vec![]),
};
if let Err(err) = render_entries.insert(&parent_directory, new_entry) {
println!(
"error inserting file, this should never happen {} {:?}",
parent_directory, err
);
};
let depth = parent_directory.split('/').collect::<Vec<_>>().len();
if depth >= max_depth {
git2::TreeWalkResult::Skip
} else {
git2::TreeWalkResult::Ok
}
},
)?;
Ok(render_entries)
}
pub fn render_repo_index(
repo: &Repository,
repo_owner: &str,
repo_name: &str,
repo_config: Option<&crate::RepoConfig>,
folder_depth: usize,
) -> Result<String> {
let entries = get_file_tree(repo, repo_owner, repo_name, "", folder_depth)?;
let mut readme = None;
if let Ok(obj) = repo.revparse_single("HEAD:README.md") {
if obj.kind() == Some(git2::ObjectType::Blob) {
if let Some(blob) = obj.as_blob() {
if let Ok(blob_str) = str::from_utf8(blob.content()) {
let adapter = CustomHeadingAdapter;
let mut plugins = ComrakPlugins::default();
plugins.render.heading_adapter = Some(&adapter);
readme = Some(markdown_to_html_with_plugins(
blob_str,
&ComrakOptions::default(),
&plugins,
))
}
}
}
} else {
println!("no rev parse")
}
Ok(RepoIndexHtml {
repo_name: repo_name.clone().to_string(),
repo_tree: &entries,
repo_owner,
readme,
description: &repo_config
.and_then(|config| Some(&config.description))
.unwrap_or(&"".to_string()),
clone_with: &repo_config
.and_then(|config| config.clone_with.as_ref())
.unwrap_or(&"".to_string()),
}
.render()?)
}
pub fn render_repo_folder(
repo: &Repository,
repo_owner: &str,
repo_name: &str,
folder_depth: usize,
folder: &str,
) -> Result<String> {
let entries = get_file_tree(repo, repo_owner, repo_name, folder, folder_depth)?;
Ok(RepoFolderHtml {
repo_name: repo_name.clone().to_string(),
repo_tree: &entries,
folder,
breadcrumbs: file::get_file_breadcrumbs(repo_owner, repo_name, &entries.path),
}
.render()?)
}