summary history branches tags files
commit:273d62c645cf34089bf5c0b7868016a4532e1776
author:Trevor Bentley
committer:Trevor Bentley
date:Thu Jan 12 20:19:40 2023 +0100
parents:c60b1d26a062c4c4acf7e0055f272da757b9412c
support paginated history, branches, tags
diff --git a/config.toml b/config.toml
line changes: +96/-11
index b9035fc..bc59d03
--- a/config.toml
+++ b/config.toml
@@ -46,6 +46,48 @@
 # directory is used.
 #asset_files = []
 
+# Whether to split history output into pages
+#
+# If non-zero and a `history` template is specified, the history
+# output is generated several times, each with the `history` variable
+# in the template engine containing the next `paginate_history` number
+# of entries.
+#
+# A `page` variable is available in the template, which contains the
+# current, next, previous, and total number of pages.
+#
+# Use the "%PAGE%" variable in the `history` output variable to
+# substitute the page number into the produced filename.
+paginate_history = 50
+
+# Whether to split branches output into pages
+#
+# If non-zero and a `branches` template is specified, the branches
+# output is generated several times, each with the `branches` variable
+# in the template engine containing the next `paginate_branches`
+# number of entries.
+#
+# A `page` variable is available in the template, which contains the
+# current, next, previous, and total number of pages.
+#
+# Use the "%PAGE%" variable in the `branches` output variable to
+# substitute the page number into the produced filename.
+paginate_branches = 50
+
+# Whether to split tags output into pages
+#
+# If non-zero and a `tags` template is specified, the tags output is
+# generated several times, each with the `tags` variable in the
+# template engine containing the next `paginate_tags` number of
+# entries.
+#
+# A `page` variable is available in the template, which contains the
+# current, next, previous, and total number of pages.
+#
+# Use the "%PAGE%" variable in the `tags` output variable to
+# substitute the page number into the produced filename.
+paginate_tags = 50
+
 # Whether to render Markdown files in repos into HTML.
 #
 # Rendering Markdown can make site generation take more time.  It has potential
@@ -215,6 +257,18 @@ repo_list    = "repos.html"
 # This template executes one time per repository.
 repo_summary = "summary.html"
 
+# Template responsible for displaying the commit history.
+#
+# This template is evaluated with the same data as `repo_summary`.  If
+# the `paginate_history` setting is non-zero, this may be called
+# several times with the `history` template variable reduced to the
+# requested page size, and with a `page` template variable provided to
+# identify the current, previous, and next pages.
+#
+# This template executes at least one time per repository, or several
+# times if paginated.
+history      = "history.html"
+
 # Template responsible for displaying a single commit.
 #
 # Called once per parsed commit, with both the whole repository and the current
@@ -223,6 +277,18 @@ repo_summary = "summary.html"
 # This template executes many times.
 commit       = "commit.html"
 
+# Template responsible for displaying the repo branches.
+#
+# This template is evaluated with the same data as `repo_summary`.  If
+# the `paginate_branches` setting is non-zero, this may be called
+# several times with the `branches` template variable reduced to the
+# requested page size, and with a `page` template variable provided to
+# identify the current, previous, and next pages.
+#
+# This template executes at least one time per repository, or several
+# times if paginated.
+branches     = "branches.html"
+
 # Template responsible for displaying a single branch.
 #
 # Called once per parsed branch, with both the whole repository and the current
@@ -231,6 +297,18 @@ commit       = "commit.html"
 # This template executes many times.
 branch       = "branch.html"
 
+# Template responsible for displaying the repo tags.
+#
+# This template is evaluated with the same data as `repo_summary`.  If
+# the `paginate_tags` setting is non-zero, this may be called several
+# times with the `tags` template variable reduced to the requested
+# page size, and with a `page` template variable provided to identify
+# the current, previous, and next pages.
+#
+# This template executes at least one time per repository, or several
+# times if paginated.
+tags         = "tags.html"
+
 # Template responsible for displaying a single tag.
 #
 # Called once per parsed tag, with both the whole repository and the current
@@ -279,6 +357,7 @@ error        = "404.html"
 ##
 ## * "%REPO%" -- replaced with the name of the currently processing repository
 ## * "%ID%" -- replaced with the ID of the currently processing object
+## * "%PAGE%" -- replaced with the current page number if output is paginated
 ##
 ## All except `path` are optional.  If not specified, sensible defaults will be
 ## used.
@@ -288,8 +367,11 @@ error        = "404.html"
 path = "rendered/"
 repo_list     = "repos.html"
 repo_summary  = "%REPO%/summary.html"
+history       = "%REPO%/history%PAGE%.html"
 commit        = "%REPO%/commit/%ID%.html"
+branches      = "%REPO%/branches%PAGE%.html"
 branch        = "%REPO%/branch/%ID%.html"
+tags          = "%REPO%/tags%PAGE%.html"
 tag           = "%REPO%/tag/%ID%.html"
 file          = "%REPO%/file/%ID%.html"
 syntax_css    = "%REPO%/file/syntax.css"
@@ -366,18 +448,21 @@ generated_by = "Itsy-Gitsy"
 
 # Per-repository settings, same as the global versions described above:
 
-#render_markdown = false
-#syntax_highlighting = false
+#render_markdown        = false
+#syntax_highlighting    = false
 #syntax_highlight_theme = "base16-ocean.dark"
-#limit_history    = 500
-#limit_commits    = 500
-#limit_branches   = 500
-#limit_tags       = 500
-#limit_tree_depth = 20
-#limit_file_size  = 2097152
-#limit_repo_size  = 52428800
-#limit_total_size = 524288000
-#asset_files = ["LICENSE"]
+#paginate_history       = 50
+#paginate_branches      = 50
+#paginate_tags          = 50
+#limit_history          = 500
+#limit_commits          = 500
+#limit_branches         = 500
+#limit_tags             = 500
+#limit_tree_depth       = 20
+#limit_file_size        = 2097152
+#limit_repo_size        = 52428800
+#limit_total_size       = 524288000
+#asset_files            = ["LICENSE"]
 
 # An alternative way to specify the user-defined attributes.
 #

diff --git a/src/generate.rs b/src/generate.rs
line changes: +76/-1
index 966e3a5..e0c79a4
--- a/src/generate.rs
+++ b/src/generate.rs
@@ -3,7 +3,7 @@ use crate::{
     git::{dir_listing, parse_repo, GitFile, GitRepo, GitsyMetadata},
     loud, louder, loudest, normal, normal_noln,
     settings::{GitsyCli, GitsyRepoDescriptions, GitsySettings, GitsySettingsRepo},
-    template::{DirFilter, FileFilter, TsDateFn, TsTimestampFn},
+    template::{DirFilter, FileFilter, Pagination, TsDateFn, TsTimestampFn},
     util::GitsyError,
 };
 use git2::{Error, Repository};
@@ -316,6 +316,31 @@ impl GitsyGenerator {
                 }
             }
 
+            if let Some(templ_file) = self.settings.templates.branches.as_deref() {
+                let mut paged_ctx = local_ctx.clone();
+                paged_ctx.remove("branches");
+                let pages = summary
+                    .branches
+                    .chunks(self.settings.paginate_branches());
+                let page_count = pages.len();
+                for (idx, page) in pages.enumerate() {
+                    let pagination =
+                        Pagination::new(idx + 1, page_count, &self.settings.outputs.branches(Some(&summary), None));
+                    paged_ctx.insert("page", &pagination.with_relative_paths());
+                    paged_ctx.insert("branches", &page);
+                    match tera.render(templ_file, &paged_ctx) {
+                        Ok(rendered) => {
+                            repo_bytes += GitsyGenerator::write_rendered(&pagination.cur_page, &rendered);
+                        }
+                        Err(x) => match x.kind {
+                            _ => error!("ERROR: {:?}", x),
+                        },
+                    }
+                    paged_ctx.remove("page");
+                    paged_ctx.remove("branches");
+                }
+            }
+
             for branch in &summary.branches {
                 size_check!(repo_desc, repo_bytes, total_bytes, break);
                 local_ctx.insert("branch", branch);
@@ -335,6 +360,31 @@ impl GitsyGenerator {
                 local_ctx.remove("branch");
             }
 
+            if let Some(templ_file) = self.settings.templates.tags.as_deref() {
+                let mut paged_ctx = local_ctx.clone();
+                paged_ctx.remove("tags");
+                let pages = summary
+                    .tags
+                    .chunks(self.settings.paginate_tags());
+                let page_count = pages.len();
+                for (idx, page) in pages.enumerate() {
+                    let pagination =
+                        Pagination::new(idx + 1, page_count, &self.settings.outputs.tags(Some(&summary), None));
+                    paged_ctx.insert("page", &pagination.with_relative_paths());
+                    paged_ctx.insert("tags", &page);
+                    match tera.render(templ_file, &paged_ctx) {
+                        Ok(rendered) => {
+                            repo_bytes += GitsyGenerator::write_rendered(&pagination.cur_page, &rendered);
+                        }
+                        Err(x) => match x.kind {
+                            _ => error!("ERROR: {:?}", x),
+                        },
+                    }
+                    paged_ctx.remove("page");
+                    paged_ctx.remove("tags");
+                }
+            }
+
             for tag in &summary.tags {
                 size_check!(repo_desc, repo_bytes, total_bytes, break);
                 local_ctx.insert("tag", tag);
@@ -360,6 +410,31 @@ impl GitsyGenerator {
                 local_ctx.remove("commit");
             }
 
+            if let Some(templ_file) = self.settings.templates.history.as_deref() {
+                let mut paged_ctx = local_ctx.clone();
+                paged_ctx.remove("history");
+                let pages = summary
+                    .history
+                    .chunks(self.settings.paginate_history());
+                let page_count = pages.len();
+                for (idx, page) in pages.enumerate() {
+                    let pagination =
+                        Pagination::new(idx + 1, page_count, &self.settings.outputs.history(Some(&summary), None));
+                    paged_ctx.insert("page", &pagination.with_relative_paths());
+                    paged_ctx.insert("history", &page);
+                    match tera.render(templ_file, &paged_ctx) {
+                        Ok(rendered) => {
+                            repo_bytes += GitsyGenerator::write_rendered(&pagination.cur_page, &rendered);
+                        }
+                        Err(x) => match x.kind {
+                            _ => error!("ERROR: {:?}", x),
+                        },
+                    }
+                    paged_ctx.remove("page");
+                    paged_ctx.remove("history");
+                }
+            }
+
             for (_id, commit) in &summary.commits {
                 size_check!(repo_desc, repo_bytes, total_bytes, break);
                 local_ctx

diff --git a/src/main.rs b/src/main.rs
line changes: +0/-1
index 4bdac3e..7f2659e
--- a/src/main.rs
+++ b/src/main.rs
@@ -9,7 +9,6 @@ use settings::{GitsyCli, GitsySettings};
 
 // TODO:
 //
-//   * pagination
 //   * basic, light, dark, and fancy default themes
 //   * better error propagation
 //   * automated tests

diff --git a/src/settings.rs b/src/settings.rs
line changes: +54/-11
index 09a34c9..c2df4fc
--- a/src/settings.rs
+++ b/src/settings.rs
@@ -70,8 +70,11 @@ pub struct GitsySettingsTemplates {
     pub path: PathBuf,
     pub repo_list: Option<String>,
     pub repo_summary: Option<String>,
+    pub history: Option<String>,
     pub commit: Option<String>,
+    pub branches: Option<String>,
     pub branch: Option<String>,
+    pub tags: Option<String>,
     pub tag: Option<String>,
     pub file: Option<String>,
     pub dir: Option<String>,
@@ -84,8 +87,11 @@ pub struct GitsySettingsOutputs {
     pub cloned_repos: Option<String>,
     pub repo_list: Option<String>,
     pub repo_summary: Option<String>,
+    pub history: Option<String>,
     pub commit: Option<String>,
+    pub branches: Option<String>,
     pub branch: Option<String>,
+    pub tags: Option<String>,
     pub tag: Option<String>,
     pub file: Option<String>,
     pub dir: Option<String>,
@@ -140,18 +146,22 @@ macro_rules! output_path_fn {
 }
 //step_map_first!(boil_in_wort, Boil, Wort, |b: &Boil| { b.wort_start() });
 
+#[rustfmt::skip]
 impl GitsySettingsOutputs {
-    output_path_fn!(repo_list, GitObject, full_hash, false, "repos.html");
-    output_path_fn!(repo_summary, GitObject, full_hash, false, "%REPO%/summary.html");
-    output_path_fn!(commit, GitObject, full_hash, false, "%REPO%/commit/%ID%.html");
-    output_path_fn!(branch, GitObject, full_hash, false, "%REPO%/branch/%ID%.html");
-    output_path_fn!(tag, GitObject, full_hash, false, "%REPO%/tag/%ID%.html");
-    output_path_fn!(file, GitFile, id, false, "%REPO%/file/%ID%.html");
-    output_path_fn!(syntax_css, GitObject, full_hash, false, "%REPO%/file/syntax.css");
-    output_path_fn!(dir, GitFile, id, false, "%REPO%/dir/%ID%.html");
-    output_path_fn!(error, GitObject, full_hash, false, "404.html");
-    output_path_fn!(global_assets, GitObject, full_hash, true, "assets/");
-    output_path_fn!(repo_assets, GitObject, full_hash, true, "%REPO%/assets/");
+    output_path_fn!(repo_list,       GitObject, full_hash, false, "repos.html");
+    output_path_fn!(repo_summary,    GitObject, full_hash, false, "%REPO%/summary.html");
+    output_path_fn!(history,         GitObject, full_hash, false, "%REPO%/history%PAGE%.html");
+    output_path_fn!(commit,          GitObject, full_hash, false, "%REPO%/commit/%ID%.html");
+    output_path_fn!(branches,        GitObject, full_hash, false, "%REPO%/branches%PAGE%.html");
+    output_path_fn!(branch,          GitObject, full_hash, false, "%REPO%/branch/%ID%.html");
+    output_path_fn!(tags,            GitObject, full_hash, false, "%REPO%/tags%PAGE%.html");
+    output_path_fn!(tag,             GitObject, full_hash, false, "%REPO%/tag/%ID%.html");
+    output_path_fn!(file,            GitFile,   id,        false, "%REPO%/file/%ID%.html");
+    output_path_fn!(syntax_css,      GitObject, full_hash, false, "%REPO%/file/syntax.css");
+    output_path_fn!(dir,             GitFile,   id,        false, "%REPO%/dir/%ID%.html");
+    output_path_fn!(error,           GitObject, full_hash, false, "404.html");
+    output_path_fn!(global_assets,   GitObject, full_hash, true,  "assets/");
+    output_path_fn!(repo_assets,     GitObject, full_hash, true,  "%REPO%/assets/");
 }
 
 #[derive(Clone, Deserialize, Default, Debug)]
@@ -165,6 +175,9 @@ pub struct GitsySettingsRepo {
     pub syntax_highlight: Option<bool>,
     pub syntax_highlight_theme: Option<String>,
     pub attributes: Option<BTreeMap<String, toml::Value>>,
+    pub paginate_history: Option<usize>,
+    pub paginate_branches: Option<usize>,
+    pub paginate_tags: Option<usize>,
     pub limit_history: Option<usize>,
     pub limit_commits: Option<usize>,
     pub limit_branches: Option<usize>,
@@ -201,6 +214,9 @@ pub struct GitsySettings {
     pub templates: GitsySettingsTemplates,
     #[serde(rename(deserialize = "gitsy_outputs"))]
     pub outputs: GitsySettingsOutputs,
+    pub paginate_history: Option<usize>,
+    pub paginate_branches: Option<usize>,
+    pub paginate_tags: Option<usize>,
     pub limit_history: Option<usize>,
     pub limit_commits: Option<usize>,
     pub limit_branches: Option<usize>,
@@ -265,6 +281,9 @@ impl GitsySettings {
                     global_to_repo!(settings, repo, render_markdown);
                     global_to_repo!(settings, repo, syntax_highlight);
                     global_to_repo!(settings, repo, syntax_highlight_theme);
+                    global_to_repo!(settings, repo, paginate_history);
+                    global_to_repo!(settings, repo, paginate_branches);
+                    global_to_repo!(settings, repo, paginate_tags);
                     global_to_repo!(settings, repo, limit_history);
                     global_to_repo!(settings, repo, limit_commits);
                     global_to_repo!(settings, repo, limit_branches);
@@ -294,6 +313,9 @@ impl GitsySettings {
                             render_markdown: settings.render_markdown.clone(),
                             syntax_highlight: settings.syntax_highlight.clone(),
                             syntax_highlight_theme: settings.syntax_highlight_theme.clone(),
+                            paginate_history: settings.paginate_history.clone(),
+                            paginate_branches: settings.paginate_branches.clone(),
+                            paginate_tags: settings.paginate_tags.clone(),
                             limit_history: settings.limit_history.clone(),
                             limit_commits: settings.limit_commits.clone(),
                             limit_branches: settings.limit_branches.clone(),
@@ -311,4 +333,25 @@ impl GitsySettings {
         }
         (settings, repo_descriptions)
     }
+
+    pub fn paginate_history(&self) -> usize {
+        match self.paginate_history.unwrap_or(usize::MAX) {
+            x if x == 0 => usize::MAX,
+            x => x,
+        }
+    }
+
+    pub fn paginate_branches(&self) -> usize {
+        match self.paginate_branches.unwrap_or(usize::MAX) {
+            x if x == 0 => usize::MAX,
+            x => x,
+        }
+    }
+
+    pub fn paginate_tags(&self) -> usize {
+        match self.paginate_tags.unwrap_or(usize::MAX) {
+            x if x == 0 => usize::MAX,
+            x => x,
+        }
+    }
 }

diff --git a/src/template.rs b/src/template.rs
line changes: +96/-0
index 48efbb4..64d0232
--- a/src/template.rs
+++ b/src/template.rs
@@ -1,6 +1,8 @@
 use crate::git::GitFile;
 use chrono::{naive::NaiveDateTime, offset::FixedOffset, DateTime};
+use serde::Serialize;
 use std::collections::HashMap;
+use std::path::PathBuf;
 use tera::{to_value, try_get_value, Filter, Function, Value};
 
 fn ts_to_date(ts: i64, offset: Option<i64>, format: Option<String>) -> String {
@@ -105,3 +107,97 @@ impl Function for TsTimestampFn {
         Ok(to_value(ts_to_git_timestamp(ts, tz)).unwrap())
     }
 }
+
+#[derive(Serialize)]
+pub struct Pagination {
+    pub pages: usize,
+    pub page_idx: usize,
+    pub page_str: String,
+    pub cur_page: String,
+    pub next_page: Option<String>,
+    pub prev_page: Option<String>,
+}
+impl Pagination {
+    pub fn new(cur: usize, total: usize, url_template: &str) -> Self {
+        let digits = total.to_string().len().max(2);
+        let next = match cur + 1 <= total {
+            true => Some(cur + 1),
+            false => None,
+        };
+        let prev = match cur <= 1 {
+            true => None,
+            false => Some(cur - 1),
+        };
+        let cur_str = match cur <= 1 {
+            true => String::new(),
+            false => format!("{:0w$}", cur, w = digits),
+        };
+        let next_str = match next.unwrap_or(0) <= 1 {
+            true => String::new(),
+            false => format!("{:0w$}", next.unwrap_or(0), w = digits),
+        };
+        let prev_str = match prev.unwrap_or(0) <= 1 {
+            true => String::new(),
+            false => format!("{:0w$}", prev.unwrap_or(0), w = digits),
+        };
+        let cur_page = url_template.replace("%PAGE%", &cur_str);
+        let next_page = match next {
+            Some(_) => Some(url_template.replace("%PAGE%", &next_str)),
+            _ => None,
+        };
+        let prev_page = match prev {
+            Some(_) => Some(url_template.replace("%PAGE%", &prev_str)),
+            _ => None,
+        };
+        Pagination {
+            pages: total,
+            page_idx: cur,
+            page_str: cur_str.clone(),
+            cur_page,
+            next_page,
+            prev_page,
+        }
+    }
+
+    pub fn with_relative_paths(&self) -> Self {
+        let cur_page = {
+            let path = PathBuf::from(&self.cur_page);
+            path.file_name()
+                .expect(&format!("Invalid output filename: {}", self.cur_page))
+                .to_string_lossy()
+                .to_string()
+        };
+        let next_page = match &self.next_page {
+            Some(p) => {
+                let path = PathBuf::from(p);
+                Some(
+                    path.file_name()
+                        .expect(&format!("Invalid output filename: {}", p))
+                        .to_string_lossy()
+                        .to_string(),
+                )
+            }
+            _ => None,
+        };
+        let prev_page = match &self.prev_page {
+            Some(p) => {
+                let path = PathBuf::from(p);
+                Some(
+                    path.file_name()
+                        .expect(&format!("Invalid output filename: {}", p))
+                        .to_string_lossy()
+                        .to_string(),
+                )
+            }
+            _ => None,
+        };
+        Pagination {
+            pages: self.pages,
+            page_idx: self.page_idx,
+            page_str: self.page_str.clone(),
+            cur_page,
+            next_page,
+            prev_page,
+        }
+    }
+}

diff --git a/templates/branches.html b/templates/branches.html
line changes: +23/-0
index 0000000..db98fea
--- /dev/null
+++ b/templates/branches.html
@@ -0,0 +1,23 @@
+{% extends "base.html" %}
+
+{% block content %}
+<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>
+{% if page.prev_page %}<a href="{{ page.prev_page }}">PREV</a>{% endif %} &nbsp; &nbsp; &nbsp; <a href="{{ page.cur_page }}">CUR</a> &nbsp; &nbsp; &nbsp; {%if page.next_page %}<a href="{{ page.next_page }}">NEXT</a>{% endif %}<br>
+{% endblock content %}

diff --git a/templates/history.html b/templates/history.html
line changes: +27/-0
index 0000000..5b950cd
--- /dev/null
+++ b/templates/history.html
@@ -0,0 +1,27 @@
+{% extends "base.html" %}
+
+{% block content %}
+<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>
+{% if page.prev_page %}<a href="{{ page.prev_page }}">PREV</a>{% endif %} &nbsp; &nbsp; &nbsp; <a href="{{ page.cur_page }}">CUR</a> &nbsp; &nbsp; &nbsp; {%if page.next_page %}<a href="{{ page.next_page }}">NEXT</a>{% endif %}<br>
+{% endblock content %}

diff --git a/templates/summary.html b/templates/summary.html
line changes: +9/-1
index 4d81e08..e016439
--- a/templates/summary.html
+++ b/templates/summary.html
@@ -1,6 +1,10 @@
 {% extends "base.html" %}
 
 {% block content %}
+<a href="history.html">Commit history</a><br>
+<a href="branches.html">All branches</a><br>
+<a href="tags.html">All tags</a><br>
+<br/>
 <table class="commits">
   <tr>
     <th>Commit ID</th>
@@ -11,7 +15,7 @@
     <th>Refs</th>
   </tr>
   {% for entry in history -%}
-  {% if loop.index0 < 250  -%}
+  {% if loop.index0 < 10  -%}
                       <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>
@@ -33,6 +37,7 @@
     <th>Date</th>
   </tr>
   {% for entry in branches -%}
+  {% if loop.index0 < 10  -%}
   <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>
@@ -40,6 +45,7 @@
     <td class="author">{{entry.author.name}}</td>
     <td class="date">{{ts_to_date(ts=entry.ts_utc, tz=entry.ts_offset)}}</td>
   </tr>
+  {% endif -%}
   {% endfor -%}
 </table>
 
@@ -52,6 +58,7 @@
     <th>Date</th>
   </tr>
   {% for entry in tags -%}
+  {% if loop.index0 < 10  -%}
   <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>
@@ -59,6 +66,7 @@
     <td class="author">{{entry.author.name}}</td>
     <td class="date">{{ts_to_date(ts=entry.ts_utc, tz=entry.ts_offset)}}</td>
   </tr>
+  {% endif -%}
   {% endfor -%}
 </table>
 

diff --git a/templates/tags.html b/templates/tags.html
line changes: +23/-0
index 0000000..f846bcb
--- /dev/null
+++ b/templates/tags.html
@@ -0,0 +1,23 @@
+{% extends "base.html" %}
+
+{% block content %}
+<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>
+{% if page.prev_page %}<a href="{{ page.prev_page }}">PREV</a>{% endif %} &nbsp; &nbsp; &nbsp; <a href="{{ page.cur_page }}">CUR</a> &nbsp; &nbsp; &nbsp; {%if page.next_page %}<a href="{{ page.next_page }}">NEXT</a>{% endif %}<br>
+{% endblock content %}