use clap::Parser;
use git2::{DiffOptions, Repository, Error};
use serde::{Serialize, Deserialize};
-use std::collections::{BTreeMap, HashMap};
+use std::collections::{BTreeMap, HashMap, HashSet};
+use std::fs::File;
use std::io::Write;
use std::path::{Path, PathBuf};
use tera::{Context, Filter, Function, Tera, Value, to_value, try_get_value};
#[derive(Serialize)]
struct GitRepo {
name: String,
- metadata: ItsyMetadata,
+ metadata: GitsyMetadata,
history: Vec<GitObject>,
branches: Vec<GitObject>,
tags: Vec<GitObject>,
}
#[derive(Serialize, Default)]
-struct ItsyMetadata {
+struct GitsyMetadata {
full_name: Option<String>,
description: Option<String>,
website: Option<String>,
clone: Option<String>,
- attributes: BTreeMap<String, String>,
+ attributes: BTreeMap<String, toml::Value>,
}
#[derive(Serialize, Default)]
Ok(())
}
-fn parse_repo(repo: &Repository, name: &str) -> Result<GitRepo, Error> {
+fn parse_repo(repo: &Repository, name: &str, metadata: GitsyMetadata) -> Result<GitRepo, Error> {
let mut history: Vec<GitObject> = vec!();
let mut branches: Vec<GitObject> = vec!();
let mut tags: Vec<GitObject> = vec!();
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, "")?;
+ walk_file_tree(&repo, "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, "")?;
+ walk_file_tree(&repo, "HEAD", &mut all_files, 0, true, "")?;
Ok(GitRepo {
name: name.to_string(),
- metadata: Default::default(),
+ metadata,
history,
branches,
tags,
#[derive(Deserialize)]
#[allow(dead_code)]
-struct ItsySettings {
- template_dir: PathBuf,
+struct GitsySettings {
output_dir: PathBuf,
recursive_repo_dirs: Option<Vec<PathBuf>>,
- extra: HashMap<String, toml::Value>,
+ site_name: Option<String>,
+ site_url: Option<String>,
+ site_description: Option<String>,
+ #[serde(rename(deserialize = "gitsy_templates"))]
+ templates: GitsySettingsTemplates,
+ #[serde(rename(deserialize = "gitsy_extra"))]
+ extra: Option<BTreeMap<String, toml::Value>>,
}
+
#[derive(Deserialize)]
-#[allow(dead_code)]
-struct ItsySettingsRepo {
+struct GitsySettingsTemplates {
+ path: PathBuf,
+ header: Option<String>,
+ footer: Option<String>,
+ repo_list: Option<String>,
+ repo_summary: Option<String>,
+ commit: Option<String>,
+ branch: Option<String>,
+ tag: Option<String>,
+ file: Option<String>,
+ dir: Option<String>,
+ error: Option<String>,
+}
+
+#[derive(Deserialize, Default)]
+struct GitsySettingsRepo {
path: PathBuf,
name: Option<String>,
description: Option<String>,
website: Option<String>,
+ attributes: BTreeMap<String, toml::Value>,
+}
+
+use std::hash::{Hash, Hasher};
+impl Hash for GitsySettingsRepo {
+ fn hash<H: Hasher>(&self, state: &mut H) {
+ self.path.hash(state);
+ }
+}
+impl PartialEq for GitsySettingsRepo {
+ fn eq(&self, other: &Self) -> bool {
+ self.path == other.path
+ }
+}
+impl Eq for GitsySettingsRepo {}
+
+fn write_rendered(file: &mut File, rendered: &str, header: Option<&str>, footer: Option<&str>) {
+ if let Some(header) = header {
+ file.write(header.as_bytes()).expect("failed to save rendered html");
+ }
+ file.write(rendered.as_bytes()).expect("failed to save rendered html");
+ if let Some(footer) = footer {
+ file.write(footer.as_bytes()).expect("failed to save rendered html");
+ }
}
fn main() {
// 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.");
+ let settings: GitsySettings = 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 reserved_keys = vec!("gitsy_templates","gitsy_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()) {
// 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!();
+ let mut repo_descriptions: std::collections::HashSet<GitsySettingsRepo> = HashSet::new();
for k in &table_keys {
let v = settings_raw.get(k).unwrap();
- match toml::from_str::<ItsySettingsRepo>(&v.to_string()) {
+ match toml::from_str::<GitsySettingsRepo>(&v.to_string()) {
Ok(mut repo) => {
if repo.name.is_none() {
repo.name = Some(k.clone());
}
- repos.push(repo);
+ repo_descriptions.insert(repo);
+ },
+ Err(e) => {
+ println!("Failed to parse repo [{}]: {:?}", k, e);
},
- _ => {},
}
}
- for repo in &repos {
- println!("Parse repo: {}", repo.name.as_ref().unwrap());
+ match settings.recursive_repo_dirs {
+ Some(dirs) => {
+ for parent in &dirs {
+ for dir in std::fs::read_dir(parent).expect("Repo directory not found.") {
+ let dir = dir.expect("Repo contains invalid entries");
+ repo_descriptions.insert(GitsySettingsRepo {
+ path: dir.path().clone(),
+ ..Default::default()
+ });
+ }
+ }
+ },
+ _ => {},
}
- let mut template_path = settings.template_dir.clone();
+ let mut template_path = settings.templates.path.clone();
template_path.push("**");
template_path.push("*.html");
let mut tera = match Tera::new(template_path.to_str().expect("No template path set!")) {
// Create output directory
let _ = std::fs::create_dir(settings.output_dir.to_str().expect("Output path not set!"));
+ let generated_dt = chrono::offset::Local::now();
+
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");
+ for repo_desc in &repo_descriptions {
+ let dir = &repo_desc.path;
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 path: String = dir.to_string_lossy().to_string();
+ let name: String = dir.file_name().expect("Encountered directory with no 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 metadata = GitsyMetadata {
+ full_name: repo_desc.name.clone(),
+ description: repo_desc.description.clone(),
+ website: repo_desc.website.clone(),
+ clone: None,
+ attributes: repo_desc.attributes.clone(),
+ };
+ let summary = parse_repo(&repo, &name, metadata).expect("Failed to analyze repo HEAD.");
let mut local_ctx = Context::from_serialize(&summary).unwrap();
- match tera.render("summary.html", &local_ctx) {
+ if let Some(extra) = &settings.extra {
+ local_ctx.try_insert("extra", extra).expect("Failed to add extra settings to template engine.");
+ }
+ if let Some(site_name) = &settings.site_name {
+ local_ctx.insert("site_name", site_name);
+ }
+ if let Some(site_url) = &settings.site_url {
+ local_ctx.insert("site_url", site_url);
+ }
+ if let Some(site_description) = &settings.site_description {
+ local_ctx.insert("site_description", site_description);
+ }
+ local_ctx.insert("site_generated_ts", &generated_dt.timestamp());
+ local_ctx.insert("site_generated_offset", &generated_dt.offset().local_minus_utc());
+ let header: Option<String> = match &settings.templates.header {
+ Some(header) => Some(tera.render(header, &local_ctx).expect("Unable to templatize header file")),
+ _ => None,
+ };
+ let footer: Option<String> = match &settings.templates.footer {
+ Some(footer) => Some(tera.render(footer, &local_ctx).expect("Unable to templatize footer file")),
+ _ => None,
+ };
+
+ match tera.render(&settings.templates.repo_summary.as_deref().unwrap_or("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");
+ write_rendered(&mut file, &rendered, header.as_deref(), footer.as_deref());
+ },
+ Err(x) => match x.kind {
+ tera::ErrorKind::TemplateNotFound(_) if settings.templates.repo_summary.is_none() => {},
+ _ => println!("ERROR: {:?}", x),
},
- Err(x) => println!("ERROR: {:?}", x),
}
for branch in &summary.branches {
local_ctx.insert("branch", branch);
- match tera.render("branch.html", &local_ctx) {
+ match tera.render(&settings.templates.branch.as_deref().unwrap_or("branch.html"), &local_ctx) {
Ok(rendered) => {
let mut output_path = settings.output_dir.clone();
output_path.push(&summary.name);
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");
+ write_rendered(&mut file, &rendered, header.as_deref(), footer.as_deref());
},
Err(x) => match x.kind {
- tera::ErrorKind::TemplateNotFound(_) => {},
+ tera::ErrorKind::TemplateNotFound(_) if settings.templates.branch.is_none() => {},
_ => println!("ERROR: {:?}", x),
},
}
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) {
+ match tera.render(&settings.templates.tag.as_deref().unwrap_or("tag.html"), &local_ctx) {
Ok(rendered) => {
let mut output_path = settings.output_dir.clone();
output_path.push(&summary.name);
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");
+ write_rendered(&mut file, &rendered, header.as_deref(), footer.as_deref());
},
Err(x) => match x.kind {
- tera::ErrorKind::TemplateNotFound(_) => {},
+ tera::ErrorKind::TemplateNotFound(_) if settings.templates.tag.is_none() => {},
_ => println!("ERROR: {:?}", x),
},
}
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) {
+ match tera.render(&settings.templates.commit.as_deref().unwrap_or("commit.html"), &local_ctx) {
Ok(rendered) => {
let mut output_path = settings.output_dir.clone();
output_path.push(&summary.name);
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");
+ write_rendered(&mut file, &rendered, header.as_deref(), footer.as_deref());
+ },
+ Err(x) => match x.kind {
+ tera::ErrorKind::TemplateNotFound(_) if settings.templates.commit.is_none() => {},
+ _ => println!("ERROR: {:?}", x),
},
- 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) {
+ match tera.render(&settings.templates.file.as_deref().unwrap_or("file.html"), &local_ctx) {
Ok(rendered) => {
let mut output_path = settings.output_dir.clone();
output_path.push(&summary.name);
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");
+ write_rendered(&mut file, &rendered, header.as_deref(), footer.as_deref());
+ },
+ Err(x) => match x.kind {
+ tera::ErrorKind::TemplateNotFound(_) if settings.templates.file.is_none() => {},
+ _ => println!("ERROR: {:?}", x),
},
- 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) {
+ match tera.render(&settings.templates.dir.as_deref().unwrap_or("dir.html"), &local_ctx) {
Ok(rendered) => {
let mut output_path = settings.output_dir.clone();
output_path.push(&summary.name);
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");
+ write_rendered(&mut file, &rendered, header.as_deref(), footer.as_deref());
+ },
+ Err(x) => match x.kind {
+ tera::ErrorKind::TemplateNotFound(_) if settings.templates.dir.is_none() => {},
+ _ => println!("ERROR: {:?}", x),
},
- Err(x) => println!("ERROR: {:?}", x),
}
local_ctx.remove("files");
}
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) {
+ if let Some(extra) = &settings.extra {
+ global_ctx.try_insert("extra", extra).expect("Failed to add extra settings to template engine.");
+ }
+ if let Some(site_name) = &settings.site_name {
+ global_ctx.insert("site_name", site_name);
+ }
+ if let Some(site_url) = &settings.site_url {
+ global_ctx.insert("site_url", site_url);
+ }
+ if let Some(site_description) = &settings.site_description {
+ global_ctx.insert("site_description", site_description);
+ }
+ global_ctx.insert("site_generated_ts", &generated_dt.timestamp());
+ global_ctx.insert("site_generated_offset", &generated_dt.offset().local_minus_utc());
+ let header: Option<String> = match &settings.templates.header {
+ Some(header) => Some(tera.render(header, &global_ctx).expect("Unable to templatize header file")),
+ _ => None,
+ };
+ let footer: Option<String> = match &settings.templates.footer {
+ Some(footer) => Some(tera.render(footer, &global_ctx).expect("Unable to templatize footer file")),
+ _ => None,
+ };
+
+ match tera.render(&settings.templates.repo_list.as_deref().unwrap_or("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");
+ write_rendered(&mut file, &rendered, header.as_deref(), footer.as_deref());
+ },
+ Err(x) => match x.kind {
+ tera::ErrorKind::TemplateNotFound(_) if settings.templates.repo_list.is_none() => {},
+ _ => println!("ERROR: {:?}", x),
+ },
+ }
+
+ match tera.render(&settings.templates.error.as_deref().unwrap_or("404.html"), &global_ctx) {
+ Ok(rendered) => {
+ let mut output_path = settings.output_dir.clone();
+ output_path.push("404.html");
+ let mut file = std::fs::File::create(output_path.to_str().expect("Output path not set!")).unwrap();
+ write_rendered(&mut file, &rendered, header.as_deref(), footer.as_deref());
+ },
+ Err(x) => match x.kind {
+ tera::ErrorKind::TemplateNotFound(_) if settings.templates.error.is_none() => {},
+ _ => println!("ERROR: {:?}", x),
},
- Err(x) => println!("ERROR: {:?}", x),
}
}
+The page you are seeking does not exist.
-<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>
+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>
-<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 -%}
+<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>
+ {%- endfor -%}
+</pre>
+{% endfor -%}
+{% endfor -%}
-<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>
+<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 -%}
-<html>
- <body style="font-family: monospace;">
- {{file.path}} ({{file.name}}) [{{file.id}}]<br/>
- -------
- <pre style="margin: 0">{{file.contents}}</pre>
- </body>
-</html>
+{{file.path}} ({{file.name}}) [{{file.id}}]<br/>
+-------
+<pre style="margin: 0">{{file.contents}}</pre>
+ </body>
+</html>
+<html>
+ <head>
+ <title>{{site_name}}</title>
+ </head>
+ <body style="font-family: monospace;">
+ Site: {{site_name}}<br/>
+ Generated: {{ts_to_git_timestamp(ts=site_generated_ts, tz=site_generated_offset)}}<br/>
+ Extra settings: {{extra.global_user_defined_vars}}<br/>
+ <hr/>
-<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>
+{% for repo in repos | sort(attribute="name") -%}
+<a href="{{repo.name}}/summary.html">{{ repo.name }}</a> ({{repo.metadata.website}}) [{{repo.metadata.attributes | get(key="some_extra_thing", default="")}}] ({{ts_to_date(ts=repo.history[0].ts_utc, tz=repo.history[0].ts_offset)}})<br/>
+{% endfor -%}
-<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 -%}, {%- endif -%}<span class="commit-ref">{{ref}}</span>{%- endfor -%}</td>
- </tr>
- {% endif -%}
- {% endfor -%}
- </table>
+<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 -%}, {%- 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="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="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>
+<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>
-<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>
+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/>