functional prototype: TOML settings, CLI args, templated repo output

This commit is contained in:
Trevor Bentley
2023-01-10 01:13:35 +01:00
parent 9b64c36b06
commit 1951074e8f
11 changed files with 2036 additions and 11 deletions
Generated
+1076
View File
File diff suppressed because it is too large Load Diff
+5 -1
View File
@@ -11,7 +11,11 @@ repository = "https://github.com/mrmekon/itsy-gitsy"
readme = "README.md" readme = "README.md"
license = "GPL-3.0-or-later" license = "GPL-3.0-or-later"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies] [dependencies]
chrono = "0.4.23"
clap = { version="4.0.32", features=["derive"] }
git2 = "0.15.0" git2 = "0.15.0"
serde = { version = "1.0.152", features = ["derive"] }
tera = "1.17.1"
toml = "0.5.10"
+12
View File
@@ -0,0 +1,12 @@
template_dir = "templates/"
output_dir = "gen/"
[a_repo]
path = "repos/connectr"
name = "connectr"
[another_repo]
path = "repos/fruitbasket"
[extra]
thingo = 1
+768 -10
View File
@@ -1,16 +1,774 @@
use git2::{Commit, DiffOptions, ObjectType, Repository, Signature, Time}; use chrono::{
use git2::{DiffFormat, Error, Pathspec}; DateTime,
offset::FixedOffset,
naive::NaiveDateTime,
};
use clap::Parser;
use git2::{DiffOptions, Repository, Error};
use serde::{Serialize, Deserialize};
use std::collections::{BTreeMap, HashMap};
use std::io::Write;
use std::path::{Path, PathBuf};
use tera::{Context, Filter, Function, Tera, Value, to_value, try_get_value};
fn run() -> Result<(), Error> { fn ts_to_date(ts: i64, offset: Option<i64>, format: Option<String>) -> String {
let path = "."; let offset = offset.unwrap_or(0);
let repo = Repository::open(path)?; let dt = NaiveDateTime::from_timestamp_opt(ts + offset, 0).expect("Invalid timestamp");
let mut revwalk = repo.revwalk()?; let dt_tz: DateTime<FixedOffset> = DateTime::from_local(dt, FixedOffset::east_opt(offset as i32).expect("Invalid timezone"));
revwalk.set_sorting(git2::Sort::TOPOLOGICAL)?; match format {
revwalk.push_head()?; Some(f) => dt_tz.format(&f).to_string(),
None => dt_tz.format("%Y-%m-%d").to_string(),
}
}
fn ts_to_git_timestamp(ts: i64, offset: Option<i64>) -> String {
let offset = offset.unwrap_or(0);
let dt = chrono::naive::NaiveDateTime::from_timestamp_opt(ts + offset, 0).expect("invalid timestamp");
let dt_tz: DateTime<FixedOffset> = DateTime::from_local(dt, FixedOffset::east_opt(offset as i32).expect("Invalid timezone"));
dt_tz.format("%a %b %e %T %Y %z").to_string()
}
fn first_line(msg: &[u8]) -> String {
let message = String::from_utf8_lossy(msg);
message.lines().next().unwrap_or("[no commit message]").to_owned()
}
#[derive(Serialize)]
struct GitRepo {
name: String,
metadata: ItsyMetadata,
history: Vec<GitObject>,
branches: Vec<GitObject>,
tags: Vec<GitObject>,
root_files: Vec<GitFile>,
all_files: Vec<GitFile>,
commits: BTreeMap<String, GitObject>,
}
#[derive(Serialize, Default)]
struct ItsyMetadata {
full_name: Option<String>,
description: Option<String>,
website: Option<String>,
clone: Option<String>,
attributes: BTreeMap<String, String>,
}
#[derive(Serialize, Default)]
struct GitAuthor {
name: Option<String>,
email: Option<String>,
}
#[derive(Serialize, Default)]
struct GitObject {
full_hash: String,
short_hash: String,
ts_utc: i64,
ts_offset: i64,
author: GitAuthor,
committer: GitAuthor,
parents: Vec<String>,
ref_name: Option<String>,
alt_refs: Vec<String>,
tagged_id: Option<String>,
tree_id: Option<String>,
summary: Option<String>,
message: Option<String>,
stats: Option<GitStats>,
diff: Option<GitDiffCommit>,
}
#[derive(Serialize, Default)]
struct GitStats {
files: usize,
additions: usize,
deletions: usize,
}
#[derive(Serialize, Deserialize, Clone)]
struct GitFile {
id: String,
name: String,
path: String,
mode: i32,
kind: String,
is_binary: bool,
size: usize,
contents: Option<String>,
}
#[derive(Serialize, Default)]
struct GitDiffCommit {
files: Vec<GitDiffFile>,
file_count: usize,
additions: usize,
deletions: usize,
}
#[derive(Serialize, Default)]
struct GitDiffFile {
oldfile: String,
newfile: String,
basefile: String,
oldid: String,
newid: String,
extra: String,
additions: usize,
deletions: usize,
hunks: Vec<GitDiffHunk>
}
#[derive(Serialize, Default)]
struct GitDiffHunk {
context: String,
lines: Vec<GitDiffLine>,
}
#[derive(Serialize)]
struct GitDiffLine {
kind: &'static str,
prefix: &'static str,
text: String,
}
fn walk_file_tree(repo: &git2::Repository, rev: &str, files: &mut Vec<GitFile>,
depth: usize, recurse: bool, prefix: &str) -> Result<(), Error> {
let obj = repo.revparse_single(rev)?;
let tree = obj.peel_to_tree()?;
for entry in tree.iter() {
let name = prefix.to_string() + entry.name().unwrap_or_default();
let kind = match entry.kind() {
Some(git2::ObjectType::Tree) => "dir",
Some(git2::ObjectType::Blob) => "file",
Some(git2::ObjectType::Commit) => "submodule",
_ => "unknown",
};
let mut is_binary = false;
let mut size = 0;
if let Ok(blob) = repo.find_blob(entry.id()) {
is_binary = blob.is_binary();
size = blob.content().len();
}
files.push(GitFile {
id: entry.id().to_string(),
name: name.clone(),
path: match depth {
0 => name.to_string(),
_ => format!("{}/{}", prefix, name),
},
kind: kind.to_string(),
mode: entry.filemode(),
is_binary,
size,
contents: None,
});
if recurse && entry.kind() == Some(git2::ObjectType::Tree) {
let prefix = name + "/";
walk_file_tree(repo, &entry.id().to_string(), files, depth+1, true, &prefix)?;
}
}
Ok(()) Ok(())
} }
fn parse_repo(repo: &Repository, name: &str) -> Result<GitRepo, Error> {
let mut history: Vec<GitObject> = vec!();
let mut branches: Vec<GitObject> = vec!();
let mut tags: Vec<GitObject> = vec!();
let mut commits: BTreeMap<String, GitObject> = BTreeMap::new();
let mut revwalk = repo.revwalk()?;
revwalk.set_sorting(git2::Sort::TOPOLOGICAL)?;
revwalk.push_head()?;
for oid in revwalk {
let oid = oid?;
commits.insert(oid.to_string(), parse_commit(repo, &oid.to_string())?);
let commit = repo.find_commit(oid)?;
let obj = repo.revparse_single(&commit.id().to_string())?;
let full_hash = commit.id().to_string();
let short_hash = obj.short_id()?.as_str().unwrap_or_default().to_string();
let mut parents: Vec<String> = vec!();
let a = if commit.parents().len() == 1 {
let parent = commit.parent(0)?;
parents.push(parent.id().to_string());
Some(parent.tree()?)
} else {
None
};
let b = commit.tree()?;
let mut diffopts = DiffOptions::new();
let diff = repo.diff_tree_to_tree(a.as_ref(), Some(&b), Some(&mut diffopts))?;
let stats = diff.stats()?;
let stats = GitStats {
files: stats.files_changed(),
additions: stats.insertions(),
deletions: stats.deletions(),
};
// TODO: is it acceptable to iterate over all references for
// every commit? Is there another way? Should probably cache
// all of the ref IDs in memory.
let mut alt_refs = vec!();
for refr in repo.references()? {
let refr = refr?;
if let Some(target) = refr.target() {
if target == commit.id() {
// TODO: save these
if let Some(name) = refr.shorthand() {
alt_refs.push(name.to_string());
}
}
}
}
history.push(GitObject {
full_hash,
short_hash,
ts_utc: commit.author().when().seconds(),
ts_offset: (commit.author().when().offset_minutes() as i64) * 60,
parents,
ref_name: None,
alt_refs,
author: GitAuthor {
name: commit.author().name().map(|x| x.to_owned()),
email: commit.author().email().map(|x| x.to_owned()),
},
summary: Some(first_line(commit.message_bytes())),
stats: Some(stats),
..Default::default()
});
}
for branch in repo.branches(None)? {
let (branch, _branch_type) = branch?;
let refr = branch.get();
let name = branch.name()?.unwrap_or("[unnamed]");
let obj = repo.revparse_single(name)?;
// Only show direct references, skip symbolic aliases. Maybe
// this is a bad idea?
match refr.kind() {
Some(k) if k == git2::ReferenceType::Symbolic => continue,
_ => {},
}
let commit = repo.find_commit(obj.id())?;
let full_hash = obj.id().to_string();
let short_hash = obj.short_id()?.as_str().unwrap_or_default().to_string();
branches.push(GitObject {
full_hash,
short_hash,
ts_utc: commit.author().when().seconds(),
ts_offset: (commit.author().when().offset_minutes() as i64) * 60,
parents: vec!(),
ref_name: Some(name.to_string()),
author: GitAuthor {
name: commit.author().name().map(|x| x.to_owned()),
email: commit.author().email().map(|x| x.to_owned()),
},
committer: GitAuthor {
name: commit.committer().name().map(|x| x.to_owned()),
email: commit.committer().email().map(|x| x.to_owned()),
},
summary: Some(first_line(commit.message_bytes())),
message: commit.message().map(|x| x.to_string()),
..Default::default()
});
}
for tag in repo.tag_names(None)?.iter() {
let tag = tag.unwrap_or("[unnamed]");
let obj = repo.revparse_single(tag)?;
let commit = repo.find_tag(obj.id())?;
let full_hash = obj.id().to_string();
let short_hash = obj.short_id()?.as_str().unwrap_or_default().to_string();
let (ts, tz) = match commit.tagger() {
Some(t) => (t.when().seconds(), (t.when().offset_minutes() as i64) * 60),
_ => (0, 0),
};
let (author,email) = match commit.tagger() {
Some(t) => (t.name().map(|x| x.to_owned()),
t.email().map(|x| x.to_owned())),
_ => (None, None),
};
let summary = match commit.message_bytes() {
Some(m) => Some(first_line(m)),
_ => None,
};
tags.push(GitObject {
full_hash,
short_hash,
ts_utc: ts,
ts_offset: tz,
ref_name: Some(tag.to_string()),
author: GitAuthor {
name: author,
email,
},
tagged_id: Some(commit.target_id().to_string()),
message: commit.message().map(|x| x.to_string()),
summary,
..Default::default()
});
}
let mut root_files: Vec<GitFile> = vec!();
let mut all_files: Vec<GitFile> = vec!();
walk_file_tree(&repo, "origin/HEAD", &mut root_files, 0, false, "")?;
// TODO: maybe this should be optional? Walking the whole tree
// could be slow on huge repos.
walk_file_tree(&repo, "origin/HEAD", &mut all_files, 0, true, "")?;
Ok(GitRepo {
name: name.to_string(),
metadata: Default::default(),
history,
branches,
tags,
root_files,
all_files,
commits,
})
}
fn parse_commit(repo: &Repository, refr: &str) -> Result<GitObject, Error> {
let obj = repo.revparse_single(refr)?;
let commit = repo.find_commit(obj.id())?;
let mut parents: Vec<String> = vec!();
let a = match commit.parents().len() {
x if x == 1 => {
let parent = commit.parent(0).unwrap();
parents.push(parent.id().to_string());
Some(parent.tree()?)
},
x if x > 1 => {
for parent in commit.parents() {
parents.push(parent.id().to_string());
}
None
},
_ => {
None
},
};
let b = commit.tree()?;
let mut diffopts = DiffOptions::new();
let diff = repo.diff_tree_to_tree(a.as_ref(), Some(&b), Some(&mut diffopts))?;
let stats = diff.stats()?;
let mut commit_diff: GitDiffCommit = GitDiffCommit {
file_count: stats.files_changed(),
additions: stats.insertions(),
deletions: stats.deletions(),
..Default::default()
};
let files: std::rc::Rc<std::cell::RefCell<Vec<GitDiffFile>>> = std::rc::Rc::new(std::cell::RefCell::new(vec!()));
diff.foreach(
&mut |file, _progress| {
let mut file_diff: GitDiffFile = Default::default();
file_diff.newfile = match file.status() {
git2::Delta::Deleted => "/dev/null".to_owned(),
_ => file.new_file().path().map(|x| "b/".to_string() + &x.to_string_lossy()).unwrap_or("/dev/null".to_string()),
};
file_diff.oldfile = match file.status() {
git2::Delta::Added => "/dev/null".to_owned(),
_ => file.old_file().path().map(|x| "a/".to_string() + &x.to_string_lossy()).unwrap_or("/dev/null".to_string()),
};
file_diff.basefile = match file.status() {
git2::Delta::Added => file.new_file().path().map(|x| x.to_string_lossy().to_string()).unwrap_or("/dev/null".to_string()),
_ => file.old_file().path().map(|x| x.to_string_lossy().to_string()).unwrap_or("/dev/null".to_string()),
};
file_diff.oldid = file.old_file().id().to_string();
file_diff.newid = file.new_file().id().to_string();
files.borrow_mut().push(file_diff);
true
},
None, // TODO: handle binary files?
Some(&mut |_file, hunk| {
let mut files = files.borrow_mut();
let file_diff: &mut GitDiffFile = files.last_mut().expect("Diff hunk not associated with a file!");
let mut hunk_diff: GitDiffHunk = Default::default();
hunk_diff.context = String::from_utf8_lossy(hunk.header()).to_string();
file_diff.hunks.push(hunk_diff);
true
}),
Some(&mut |_file, _hunk, line| {
let mut files = files.borrow_mut();
let file_diff: &mut GitDiffFile = files.last_mut().expect("Diff hunk not associated with a file!");
let hunk_diff: &mut GitDiffHunk = file_diff.hunks.last_mut().expect("Diff line not associated with a hunk!");
let (kind, prefix) = match line.origin() {
' ' => ("ctx", " "),
'-' => ("del", "-"),
'+' => ("add", "+"),
_ => ("other", " "),
};
match line.origin() {
'-' => file_diff.deletions += 1,
'+' => file_diff.additions += 1,
_ => {},
}
let line_diff = GitDiffLine {
text: String::from_utf8_lossy(line.content()).to_string(),
kind,
prefix,
};
hunk_diff.lines.push(line_diff);
true
})
)?;
match std::rc::Rc::try_unwrap(files) {
Ok(files) => {
let files: Vec<GitDiffFile> = files.into_inner();
commit_diff.files = files;
},
Err(_) => {},
}
let tree = obj.peel_to_tree()?;
let summary = GitObject {
full_hash: obj.id().to_string(),
short_hash: obj.short_id()?.as_str().unwrap_or_default().to_string(),
ts_utc: commit.author().when().seconds(),
ts_offset: (commit.author().when().offset_minutes() as i64) * 60,
tagged_id: None,
tree_id: Some(tree.id().to_string()),
parents,
ref_name: None,
alt_refs: vec!(),
author: GitAuthor {
name: commit.author().name().map(|x| x.to_string()),
email: commit.author().email().map(|x| x.to_string()),
},
committer: GitAuthor {
name: commit.committer().name().map(|x| x.to_string()),
email: commit.committer().email().map(|x| x.to_string()),
},
summary: Some(first_line(commit.message_bytes())),
message: commit.message().map(|x| x.to_string()),
stats: None,
diff: Some(commit_diff),
};
Ok(summary)
}
fn fill_file_contents(repo: &Repository, file: &GitFile) -> Result<GitFile, Error> {
let mut file = file.clone();
if file.kind == "file" {
let blob = repo.find_blob(git2::Oid::from_str(&file.id)?)?;
file.contents = match blob.is_binary() {
false => Some(String::from_utf8_lossy(blob.content()).to_string()),
true => Some(format!("[Binary data ({} bytes)]", blob.content().len())),
};
}
Ok(file)
}
fn dir_listing(repo: &Repository, file: &GitFile) -> Result<Vec<GitFile>, Error> {
let mut files: Vec<GitFile> = vec!();
walk_file_tree(&repo, &file.id, &mut files, 0, false, "")?;
Ok(files)
}
struct FileFilter;
impl Filter for FileFilter {
fn filter(&self, value: &Value, _args: &HashMap<String, Value>
) -> Result<Value, tera::Error> {
let file_list: Vec<GitFile> = try_get_value!("only_files", "value", Vec<GitFile>, value);
let file_list: Vec<GitFile> = file_list.iter().filter_map(|x| match x.kind.as_str() {
"file" => Some(x.clone()),
_ => None,
}).collect();
Ok(to_value(file_list).unwrap())
}
}
struct DirFilter;
impl Filter for DirFilter {
fn filter(&self, value: &Value, _args: &HashMap<String, Value>
) -> Result<Value, tera::Error> {
let file_list: Vec<GitFile> = try_get_value!("only_dirs", "value", Vec<GitFile>, value);
let file_list: Vec<GitFile> = file_list.iter().filter_map(|x| match x.kind.as_str() {
"dir" => Some(x.clone()),
_ => None,
}).collect();
Ok(to_value(file_list).unwrap())
}
}
struct TsDateFn;
impl Function for TsDateFn {
fn call(&self, args: &HashMap<String, Value>) -> Result<Value, tera::Error> {
let ts: Option<i64> = match args.get("ts") {
Some(ts) => match tera::from_value(ts.clone()) {
Ok(ts) => Some(ts),
_ => None,
},
_ => None,
};
let ts = ts.expect("ts_to_date missing a `ts` argument");
let tz: Option<i64> = match args.get("tz") {
Some(tz) => match tera::from_value(tz.clone()) {
Ok(tz) => Some(tz),
_ => None,
},
_ => None,
};
let fmt: Option<String> = match args.get("fmt") {
Some(fmt) => match tera::from_value(fmt.clone()) {
Ok(fmt) => Some(fmt),
_ => None,
},
_ => None,
};
Ok(to_value(ts_to_date(ts, tz, fmt)).unwrap())
}
}
struct TsTimestampFn;
impl Function for TsTimestampFn {
fn call(&self, args: &HashMap<String, Value>) -> Result<Value, tera::Error> {
let ts: Option<i64> = match args.get("ts") {
Some(ts) => match tera::from_value(ts.clone()) {
Ok(ts) => Some(ts),
_ => None,
},
_ => None,
};
let ts = ts.expect("ts_to_git_timestamp missing a `ts` argument");
let tz: Option<i64> = match args.get("tz") {
Some(tz) => match tera::from_value(tz.clone()) {
Ok(tz) => Some(tz),
_ => None,
},
_ => None,
};
Ok(to_value(ts_to_git_timestamp(ts, tz)).unwrap())
}
}
#[derive(Parser, Debug)]
#[command(author, version, about, long_about = None)]
struct CliArgs {
#[arg(short, long, value_name = "FILE")]
config: Option<PathBuf>,
}
#[derive(Deserialize)]
#[allow(dead_code)]
struct ItsySettings {
template_dir: PathBuf,
output_dir: PathBuf,
recursive_repo_dirs: Option<Vec<PathBuf>>,
extra: HashMap<String, toml::Value>,
}
#[derive(Deserialize)]
#[allow(dead_code)]
struct ItsySettingsRepo {
path: PathBuf,
name: Option<String>,
description: Option<String>,
website: Option<String>,
}
fn main() { fn main() {
println!("Hello, world!"); let cli = CliArgs::parse();
run().expect("run failed"); let config_path = cli.config.as_deref().unwrap_or(Path::new("config.toml"));
// Parse the known settings directly into their struct
let toml = std::fs::read_to_string(config_path).expect(&format!("Configuration file not found: {}", config_path.display()));
let settings: ItsySettings = toml::from_str(&toml).expect("Configuration file is invalid.");
// Get a list of all remaining TOML "tables" in the file.
// These are the user-supplied individual repositories.
let reserved_keys = vec!("repos","extra");
let settings_raw: HashMap<String, toml::Value> = toml::from_str(&toml).expect("blah");
let table_keys: Vec<String> = settings_raw.iter().filter_map(|x| match x.1.is_table() {
true => match reserved_keys.contains(&x.0.as_str()) {
false => Some(x.0.clone()),
true => None,
},
false => None
}).collect();
// Try to convert each unknown "table" into a repo struct, and
// save the ones that are successful. If no repo name is
// specified, use the TOML table name.
let mut repos: Vec<ItsySettingsRepo> = vec!();
for k in &table_keys {
let v = settings_raw.get(k).unwrap();
match toml::from_str::<ItsySettingsRepo>(&v.to_string()) {
Ok(mut repo) => {
if repo.name.is_none() {
repo.name = Some(k.clone());
}
repos.push(repo);
},
_ => {},
}
}
for repo in &repos {
println!("Parse repo: {}", repo.name.as_ref().unwrap());
}
let mut template_path = settings.template_dir.clone();
template_path.push("**");
template_path.push("*.html");
let mut tera = match Tera::new(template_path.to_str().expect("No template path set!")) {
Ok(t) => t,
Err(e) => {
println!("Parsing error(s): {}", e);
::std::process::exit(1);
}
};
tera.register_filter("only_files", FileFilter{});
tera.register_filter("only_dirs", DirFilter{});
tera.register_function("ts_to_date", TsDateFn{});
tera.register_function("ts_to_git_timestamp", TsTimestampFn{});
// Create output directory
let _ = std::fs::create_dir(settings.output_dir.to_str().expect("Output path not set!"));
let mut repos: Vec<GitRepo> = vec!();
for dir in std::fs::read_dir(std::path::Path::new("repos")).expect("Repo directory not found.") {
let dir = dir.expect("Repo contains invalid entries");
match dir.metadata() {
Ok(m) if m.is_dir() => {},
_ => continue,
}
let path: String = dir.path().to_string_lossy().to_string();
let name: String = dir.file_name().to_string_lossy().to_string();
let repo = Repository::open(path).expect("Unable to find git repository.");
let summary = parse_repo(&repo, &name).expect("Failed to analyze repo HEAD.");
let mut local_ctx = Context::from_serialize(&summary).unwrap();
match tera.render("summary.html", &local_ctx) {
Ok(rendered) => {
let mut output_path = settings.output_dir.clone();
output_path.push(&name);
let _ = std::fs::create_dir(output_path.to_str().expect("Output path not set!"));
output_path.push("summary.html");
let mut file = std::fs::File::create(output_path.to_str().expect("Output path not set!")).unwrap();
file.write(rendered.as_bytes()).expect("failed to save rendered html");
},
Err(x) => println!("ERROR: {:?}", x),
}
for branch in &summary.branches {
local_ctx.insert("branch", branch);
match tera.render("branch.html", &local_ctx) {
Ok(rendered) => {
let mut output_path = settings.output_dir.clone();
output_path.push(&summary.name);
output_path.push("branch");
let _ = std::fs::create_dir(output_path.to_str().expect("Output path not set!"));
output_path.push(format!("{}.html", branch.full_hash));
let mut file = std::fs::File::create(output_path.to_str().expect("Output path not set!")).unwrap();
file.write(rendered.as_bytes()).expect("failed to save rendered html");
},
Err(x) => match x.kind {
tera::ErrorKind::TemplateNotFound(_) => {},
_ => println!("ERROR: {:?}", x),
},
}
local_ctx.remove("branch");
}
for tag in &summary.tags {
local_ctx.insert("tag", tag);
if let Some(commit) = summary.commits.get(tag.tagged_id.as_ref().unwrap()) {
local_ctx.insert("commit", &commit);
}
match tera.render("tag.html", &local_ctx) {
Ok(rendered) => {
let mut output_path = settings.output_dir.clone();
output_path.push(&summary.name);
output_path.push("tag");
let _ = std::fs::create_dir(output_path.to_str().expect("Output path not set!"));
output_path.push(format!("{}.html", tag.full_hash));
let mut file = std::fs::File::create(output_path.to_str().expect("Output path not set!")).unwrap();
file.write(rendered.as_bytes()).expect("failed to save rendered html");
},
Err(x) => match x.kind {
tera::ErrorKind::TemplateNotFound(_) => {},
_ => println!("ERROR: {:?}", x),
},
}
local_ctx.remove("tag");
local_ctx.remove("commit");
}
for (_id, commit) in &summary.commits {
local_ctx.try_insert("commit", &commit).expect("Failed to add commit to template engine.");
match tera.render("commit.html", &local_ctx) {
Ok(rendered) => {
let mut output_path = settings.output_dir.clone();
output_path.push(&summary.name);
output_path.push("commit");
let _ = std::fs::create_dir(output_path.to_str().expect("Output path not set!"));
output_path.push(format!("{}.html", commit.full_hash));
let mut file = std::fs::File::create(output_path.to_str().expect("Output path not set!")).unwrap();
file.write(rendered.as_bytes()).expect("failed to save rendered html");
},
Err(x) => println!("ERROR: {:?}", x),
}
local_ctx.remove("commit");
}
for file in summary.all_files.iter().filter(|x| x.kind == "file") {
let file = fill_file_contents(&repo, &file).expect("Failed to parse file.");
local_ctx.try_insert("file", &file).expect("Failed to add file to template engine.");
match tera.render("file.html", &local_ctx) {
Ok(rendered) => {
let mut output_path = settings.output_dir.clone();
output_path.push(&summary.name);
output_path.push("file");
let _ = std::fs::create_dir(output_path.to_str().expect("Output path not set!"));
output_path.push(format!("{}.html", file.id));
let mut file = std::fs::File::create(output_path.to_str().expect("Output path not set!")).unwrap();
file.write(rendered.as_bytes()).expect("failed to save rendered html");
},
Err(x) => println!("ERROR: {:?}", x),
}
local_ctx.remove("file");
}
for dir in summary.all_files.iter().filter(|x| x.kind == "dir") {
let listing = dir_listing(&repo, &dir).expect("Failed to parse file.");
local_ctx.try_insert("files", &listing).expect("Failed to add dir to template engine.");
match tera.render("dir.html", &local_ctx) {
Ok(rendered) => {
let mut output_path = settings.output_dir.clone();
output_path.push(&summary.name);
output_path.push("dir");
let _ = std::fs::create_dir(output_path.to_str().expect("Output path not set!"));
output_path.push(format!("{}.html", dir.id));
let mut file = std::fs::File::create(output_path.to_str().expect("Output path not set!")).unwrap();
file.write(rendered.as_bytes()).expect("failed to save rendered html");
},
Err(x) => println!("ERROR: {:?}", x),
}
local_ctx.remove("files");
}
repos.push(summary);
}
let mut global_ctx = Context::new();
global_ctx.try_insert("repos", &repos).expect("Failed to add repo to template engine.");
match tera.render("repos.html", &global_ctx) {
Ok(rendered) => {
let mut output_path = settings.output_dir.clone();
output_path.push("repos.html");
let mut file = std::fs::File::create(output_path.to_str().expect("Output path not set!")).unwrap();
file.write(rendered.as_bytes()).expect("failed to save rendered html");
},
Err(x) => println!("ERROR: {:?}", x),
}
} }
+11
View File
@@ -0,0 +1,11 @@
<html>
<body style="font-family: monospace;">
branch: {{branch.ref_name}}<br/>
hash: {{branch.full_hash}} ({{branch.short_hash}})<br/>
author: {{branch.author.name}}<br/>
committer: {{branch.committer.name}}<br/>
date: {{ts_to_date(ts=branch.ts_utc, tz=branch.ts_offset)}}<br/>
summary: {{branch.summary}}<br/>
<pre>{{branch.message}}</pre>
</body>
</html>
+31
View File
@@ -0,0 +1,31 @@
<html>
<body style="font-family: monospace;">
<strong>
commit: {{commit.full_hash}}<br/>
author: {{commit.author.name}}<br/>
committer: {{commit.committer.name}}<br/>
parent: {% if commit.parents | length > 0 -%}<a href="{{commit.parents | first}}.html">{{commit.parents | first}}</a>{%-endif-%}<br/>
</strong>
<br/>
<pre style="margin: 0;">{{commit.message}}</pre>
{% for file in commit.diff.files -%}
<br/>
<strong>
diff --git a/{{file.basefile}} b/{{file.basefile}}<br/>
line changes: +{{file.additions}}/-{{file.deletions}}<br/>
index {{file.oldid | truncate(length=7,end="")}}..{{file.newid | truncate(length=7,end="")}}<br/>
--- {{file.oldfile}}<br/>
+++ {{file.newfile}}
</strong>
{% for hunk in file.hunks -%}
<pre><strong>{{hunk.context}}</strong>
{%- for line in hunk.lines -%}
{%- if line.kind in ["del","add"] -%}<span style="color: {%- if line.kind == "del" -%}bb0000{%- else -%}00aa00{% endif %}">{%- endif -%}
{{line.prefix}}{{line.text}}
{%- if line.kind in ["del","add"] -%}</span>{%- endif -%}
{%- endfor -%}
</pre>
{% endfor -%}
{% endfor -%}
</body>
</html>
+21
View File
@@ -0,0 +1,21 @@
<html>
<body style="font-family: monospace;">
<table class="files">
<tr>
<th>File</th>
<th>ID</th>
<th>Type</th>
<th>Mode</th>
<th>Size</th>
</tr>
{% for file in files -%}
<tr class="file">
<td class="file-name"><a href="../{{file.kind}}/{{file.id}}.html">{{file.name}}</a></td>
<td class="file-id">{{file.id}}</td>
<td class="file-type">{{file.kind}} ({{file.is_binary}})</td>
<td class="file-mode">{{file.mode}}</td>
<td class="file-size">{{file.size}}</td>
</tr>
{% endfor -%}
</body>
</html>
+7
View File
@@ -0,0 +1,7 @@
<html>
<body style="font-family: monospace;">
{{file.path}} ({{file.name}}) [{{file.id}}]<br/>
-------
<pre style="margin: 0">{{file.contents}}</pre>
</body>
</html>
+7
View File
@@ -0,0 +1,7 @@
<html>
<body style="font-family: monospace;">
{% for repo in repos -%}
<a href="{{repo.name}}/summary.html">{{ repo.name }}</a> ({{ts_to_date(ts=repo.history[0].ts_utc, tz=repo.history[0].ts_offset)}})<br/>
{% endfor -%}
</body>
</html>
+83
View File
@@ -0,0 +1,83 @@
<html>
<body style="font-family: monospace;">
<table class="commits">
<tr>
<th>Commit ID</th>
<th>Message</th>
<th>Author</th>
<th>Date</th>
<th>Diff</th>
<th>Refs</th>
</tr>
{% for entry in history -%}
{% if loop.index0 < 250 -%}
<tr class="commit">
<td class="oid"><a href="commit/{{entry.full_hash}}.html">{{entry.short_hash}}</a></td>
<td class="commit-msg" style="font-family: sans-serif;">{{entry.summary}}</td>
<td class="author" style="font-family: sans-serif;">{{entry.author.name}}</td>
<td class="date">{{ts_to_date(ts=entry.ts_utc, tz=entry.ts_offset)}}</td>
<td class="diff">{{entry.stats.files}} (+{{entry.stats.additions}}/-{{entry.stats.deletions}})</td>
<td class="refs">{%- for ref in entry.alt_refs -%}{%- if loop.index0 > 0 -%},&nbsp; {%- endif -%}<span class="commit-ref">{{ref}}</span>{%- endfor -%}</td>
</tr>
{% endif -%}
{% endfor -%}
</table>
<table class="branches">
<tr>
<th>Branch</th>
<th>Commit ID</th>
<th>Message</th>
<th>Author</th>
<th>Date</th>
</tr>
{% for entry in branches -%}
<tr class="branch">
<td class="name"><a href="branch/{{entry.full_hash}}.html">{{entry.ref_name}}</a></td>
<td class="oid">{{entry.short_hash}}</td>
<td class="commit-msg" style="font-family: sans-serif;">{{entry.summary}}</td>
<td class="author">{{entry.author.name}}</td>
<td class="date">{{ts_to_date(ts=entry.ts_utc, tz=entry.ts_offset)}}</td>
</tr>
{% endfor -%}
</table>
<table class="tags">
<tr>
<th>Tag</th>
<th>Commit ID</th>
<th>Message</th>
<th>Author</th>
<th>Date</th>
</tr>
{% for entry in tags -%}
<tr class="tag">
<td class="name"><a href="tag/{{entry.full_hash}}.html">{{entry.ref_name}}</a></td>
<td class="oid">{{entry.short_hash}}</td>
<td class="commit-msg" style="font-family: sans-serif;">{{entry.summary}}</td>
<td class="author">{{entry.author.name}}</td>
<td class="date">{{ts_to_date(ts=entry.ts_utc, tz=entry.ts_offset)}}</td>
</tr>
{% endfor -%}
</table>
<table class="files">
<tr>
<th>File</th>
<th>ID</th>
<th>Type</th>
<th>Mode</th>
<th>Size</th>
</tr>
{% for file in root_files -%}
<tr class="file">
<td class="name"><a href="{{file.kind}}/{{file.id}}.html">{{file.name}}</a></td>
<td class="id">{{file.id}}</td>
<td class="type">{{file.kind}} ({{file.is_binary}})</td>
<td class="mode">{{file.mode}}</td>
<td class="size">{{file.size}}</td>
</tr>
{% endfor -%}
</table>
</body>
</html>
+15
View File
@@ -0,0 +1,15 @@
<html>
<body style="font-family: monospace;">
branch: {{tag.ref_name}}<br/>
hash: {{tag.full_hash}} ({{tag.short_hash}})<br/>
author: {{tag.author.name}}<br/>
committer: {{tag.committer.name}}<br/>
date: {{ts_to_date(ts=tag.ts_utc, tz=tag.ts_offset)}}<br/>
summary: {{tag.summary}}<br/>
<pre>{{tag.message}}</pre>
<br/>
commit: {%- if commit -%}<a href="../commit/{{commit.full_hash}}.html">{{commit.full_hash}}</a>
<pre>{{commit.message}}</pre>
{%-else-%}{{tag.tagged_id}}{%-endif-%}<br/>
</body>
</html>