summary history branches tags files
commit:6ab59fc4e9b6339afd863da66c7d02fc7405cd4b
author:Trevor Bentley
committer:Trevor Bentley
date:Wed Jan 11 19:39:14 2023 +0100
parents:cfa809e39efa355824db4b0db1835a914012c367
add configurable limits
diff --git a/settings.toml b/settings.toml
line changes: +332/-9
index 3208f93..7c75188
--- a/settings.toml
+++ b/settings.toml
@@ -1,26 +1,283 @@
+###############################################################################
+##
+## Global, site-wide configuration settings
+##
+###############################################################################
+
+# A friendly name for the generated site.
+#
+# This is accessible in all templates.
 site_name = "Trevor's Repos"
+
+# The URL where this generated site is hosted.
+#
+# This is accessible in all templates.
 site_url = "https://git.trevorbentley.com"
+
+# A description of the generated site.
+#
+# This is accessible in all templates.
 site_description = "A bunch of git repos in a stupid format."
 
+# List of directories, each of which contains Git repositories to
+# index.
+#
+# For each directory listed here, Itsy-Gitsy imports all of the
+# immediate subdirectories as repositories.  Each repository is
+# imported with its name set to the name of the subdirectory it is in.
+#
+# Every repository imported in this way has the global setting from
+# this file applied.  It is not possible to apply per-repository
+# settings for bulk-imported repositories.  Repositories that require
+# specific configuration can be explicitly specified later in this
+# file.
+recursive_repo_dirs = ["repos/"]
+
+# List of files to copy to the site's `global_assets` directory.
+#
+# The files in this list are copied unmodified, and with the same name, to the
+# configured `global_asset` output directory.  Use for including global,
+# site-wide resources, such as stylesheets, images, and icons.
+#
+# Currently does NOT support globbing or directories.  Each file must be listed
+# individually.
+#
+# This can also be set per-repository, in which case the `repo_assets` output
+# directory is used.
+asset_files = ["test.html"]
+
+# Whether to render Markdown files in repos into HTML.
+#
+# Rendering Markdown can make site generation take more time.  It has potential
+# security implications, as user-generated input (files in the repo) are
+# formatted into HTML and served unescaped.  Use with caution.
+#
+# This can also be set per-repository.
 render_markdown = true
+
+# Whether render files with syntax highlighting, as HTML.
+#
+# Applying syntax highlighting is CPU-intensive, and greatly increases site
+# generation time.  It has potential security implications, as user-generated
+# input (files in the repo) are formatted into HTML and served unescaped.  Use
+# with caution.
+#
+# This can also be set per-repository.
 syntax_highlight = true
+
+# Which theme to use for syntax highlighting colors.
+#
+# The default themes from Syntect are available, namely:
+#
+# - `base16-ocean.light`
+# - `base16-ocean.dark`
+# - `base16-eighties.dark`
+# - `base16-mocha.dark`
+# - `InspiredGithub`
+# - `Solarized (dark)`
+# - `Solarized (light)`
+#
 syntax_highlight_theme = "base16-ocean.light"
 
-asset_files = ["test.html"]
+# Limits maximum number of history items (i.e. git log) to parse.
+#
+# After the limit is reached, no more history items will be processed or stored
+# in memory, and no more commits will be output to disk.  For large
+# repositories, this can increase processing speed and decrease memory and disk
+# usage.
+#
+# This can also be set per-repository.
+limit_history    = 500
 
-recursive_repo_dirs = ["repos/"]
+# Limits maximum number of commits to output.
+#
+# When the limit is reached, no more commits are written to disk.  Similar to
+# `limit_history`, but the entire git log is still read into RAM and provided
+# to the templates.
+#
+# This can also be set per-repository.
+limit_commits    = 500
 
+# Limits maximum number of branches to parse.
+#
+# When the limit is reached, no more branches are parsed in memory or provided
+# to the `branch` template.
+#
+# Since branch order is unsorted and non-deterministic, the most sensible
+# values for this are very large, or 0 (to disable branches).
+#
+# This can also be set per-repository.
+limit_branches   = 500
+
+# Limits maximum number of tags to parse.
+#
+# When the limit is reached, no more tags are parsed in memory or provided to
+# the `tag` template.
+#
+# Since tag order is unsorted and non-deterministic, the most sensible values
+# for this are very large, or 0 (to disable tags).
+#
+# This can also be set per-repository.
+limit_tags       = 500
+
+# Limits directory depth to traverse when parsing files.
+#
+# Limits the number of directories traversed when enumerating files in the
+# `all_files` entry, which is passed as part of the repository to each
+# template.  This can help reduce RAM usage, and potentially disk usage, for
+# repositories with a very large number of files or directories.
+#
+# Set to 0 to disable both `all_files` and `root_files`, i.e. do not parse any
+# file listing.  If set to 1 or greater, only applies to `all_files`.
+#
+# This can also be set per-repository.
+limit_tree_depth = 20
+
+# Limits size of files in repo with content previews.
+#
+# Only non-binary files smaller than this limit will have their contents
+# provided to the `file` template.  Large files are still processed, but do not
+# include the text contents.
+#
+# This can also be set per-repository.
+limit_file_size  = 2097152
+
+# Limits size of a single produced repository.
+#
+# Limits the size of a generated repository preview.  This is a low-precision
+# limit, terminating generation wherever it happens to be, and may lead to many
+# dead links in the final output.
+#
+# This limit is not strict: generation is terminated after the first file that
+# exceeds this limit.  The output size might somewhat overflow this limit.
+# Also, static repository assets specified in this configuration are NOT
+# included in this limit.
+#
+# This is intended merely as a safety mechanism to prevent massive run-away
+# disk usage.
+#
+# This can also be set per-repository.
+limit_repo_size  = 52428800
+
+# Limits total size of all output.
+#
+# Limits the size of the total run: the sum of all repositories.  This is a
+# low-precision limit, terminating generation wherever it happens to be, and
+# may lead to many dead links or entirely missing repositories.
+#
+# This limit is not strict: generation is terminated after the first file that
+# exceeds this limit.  The output size might somewhat overflow this limit.
+# Also, static repository assets specified in this configuration are NOT
+# included in this limit.
+#
+# This is intended merely as a safety mechanism to prevent massive run-away
+# disk usage.
+#
+# This can also be set per-repository.
+limit_total_size = 524288000
+
+
+
+###############################################################################
+##
+## Subsection specifying which files to use as templates.
+##
+## The individual templates are relative to the `path` directory.  Each
+## template is a single file using the Tera template engine's format for text
+## substitution.
+##
+###############################################################################
 [gitsy_templates]
+
+# Path to a folder containing Tera templates.
+#
+# All files with a .html extension found under this directory, and its
+# immediate children directories, are imported into the Tera template engine.
 path = "templates/"
+
+# Template responsible for the list of repositories.
+#
+# This template is evaluated with the list of all configured repositories.  It
+# is intended for providing an overview of the available repositories, but the
+# full details of each repository are also included.
+#
+# This template executes one time.
 repo_list    = "repos.html"
+
+# Template responsible for summarizing a single repository.
+#
+# This template is evaluated with a single parsed repository, with all repo
+# data (commits, branches, etc) available.
+#
+# This template executes one time per repository.
 repo_summary = "summary.html"
+
+# Template responsible for displaying a single commit.
+#
+# Called once per parsed commit, with both the whole repository and the current
+# commit available to the template.
+#
+# This template executes many times.
 commit       = "commit.html"
+
+# Template responsible for displaying a single branch.
+#
+# Called once per parsed branch, with both the whole repository and the current
+# branch available to the template.
+#
+# This template executes many times.
 branch       = "branch.html"
+
+# Template responsible for displaying a single tag.
+#
+# Called once per parsed tag, with both the whole repository and the current
+# tag available to the template.
+#
+# This template executes many times.
 tag          = "tag.html"
+
+# Template responsible for displaying a single file.
+#
+# Called once per parsed file, with both the whole repository and the current
+# file available to the template.
+#
+# This template executes many times.
 file         = "file.html"
+
+# Template responsible for displaying a single directory.
+#
+# Called once per parsed directory, with both the whole repository and the
+# current directory available to the template.
+#
+# This template executes many times.
 dir          = "dir.html"
+
+# Template responsible for displaying a site-wide error.
+#
+# Intended for 404 (page not found) errors.  If used, you must configure your
+# webserver to redirect HTTP errors to the generated error page.
+#
+# This template executes one time.
 error        = "404.html"
 
+
+
+###############################################################################
+##
+## Subsection specifying names of output files/directories.
+##
+## Each entry pairs with one of the entries in `gitsy_templates` above,
+## specifying the directories and filenames for the rendered output, after the
+## template engine has performed its substitutions.
+##
+## All outputs are relative to the `path` directory.
+##
+## There are currently two supported variables for paths:
+##
+## * "%REPO%" -- replaced with the name of the currently processing repository
+## * "%ID%" -- replaced with the ID of the currently processing object
+##
+###############################################################################
 [gitsy_outputs]
 path = "gen/"
 repo_list     = "repos.html"
@@ -32,18 +289,84 @@ file          = "%REPO%/file/%ID%.html"
 syntax_css    = "%REPO%/file/syntax.css"
 dir           = "%REPO%/dir/%ID%.html"
 error         = "404.html"
+
+# Output directory for files specified in global `asset_files`.
+#
+# Each input file is copied to this directory unmodified.
 global_assets = "assets/"
+
+# Output directory for files specified in per-repo `asset_files`.
+#
+# Each input file is copied to this directory unmodified.
 repo_assets   = "%REPO%/assets/"
 
+
+
+###############################################################################
+##
+## Subsection for arbitrary, global user data.
+##
+## These variables are available to every page template.  Add extra metadata
+## that you want available site-wide here.
+##
+###############################################################################
 [gitsy_extra]
-global_user_defined_vars = "whatever"
-these_can_also_be_numbers = 5
-or_bools = true
+generated_by = "Itsy-Gitsy"
+#global_user_defined_var = "whatever"
+#these_can_also_be_numbers = 5
+#or_bools = true
+#or_other_toml_types = {like_dicts = "yep, those too"}
+
+
 
+###############################################################################
+##
+## Individual repository configurations.
+##
+###############################################################################
+
+# The section name is used as the repository name if no `name` attribute is
+# provided.
 [circadian]
+# Path to the Git repository.
 path = "more_repos/circadian"
+
+# Name of this repository, if different from the section name.
+name = "circadian"
+
+# A description of this repository.
+description = "A repository full of code"
+
+# URL of website associated with this repository.
 website = "https://circadian.trevorbentley.com"
-attributes = {some_extra_thing = "user defined", visible = false, number_of_bananas = 3}
-render_markdown = false
-syntax_highlighting = false
-asset_files = ["LICENSE"]
+
+# Dictionary of arbitrary, user-defined attributes associated with the repo.
+#
+# Specifying as an inline-table requires all keys to be on one line.
+#
+# Available in all repo-specific page templates.
+#
+attributes = { status = "active", type = "daemon" }
+
+# Per-repository settings, same as the global versions described above:
+
+#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"]
+
+# An alternative way to specify the user-defined attributes.
+#
+# This method allows keys to be on their own lines.
+#
+#[circadian.attributes]
+#status = "active"
+#type = "daemon"

diff --git a/src/main.rs b/src/main.rs
line changes: +135/-42
index cc5ad0b..67208dc
--- a/src/main.rs
+++ b/src/main.rs
@@ -25,12 +25,16 @@ use syntect::{
 
 // TODO:
 //
-//   * specify limits
+//   * verbose output
+//   * pagination
+//   * remote repositories
 //   * extra metadata for recursive repo listings?
 //   * all relative paths should be relative to settings.toml
 //   * basic, light, dark, and fancy default themes
+//   * split into modules
+//   * parallelize output generation
 //   * automated tests
-//   * documentation
+//   * documentation + examples
 //
 
 fn ts_to_date(ts: i64, offset: Option<i64>, format: Option<String>) -> String {
@@ -117,6 +121,7 @@ struct GitFile {
     kind: String,
     is_binary: bool,
     size: usize,
+    tree_depth: usize,
     contents: Option<String>,
     contents_safe: bool,
     contents_preformatted: bool,
@@ -157,7 +162,7 @@ struct GitDiffLine {
 }
 
 fn walk_file_tree(repo: &git2::Repository, rev: &str, files: &mut Vec<GitFile>,
-                  depth: usize, recurse: bool, prefix: &str) -> Result<(), Error> {
+                  depth: usize, max_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() {
@@ -186,30 +191,41 @@ fn walk_file_tree(repo: &git2::Repository, rev: &str, files: &mut Vec<GitFile>,
             mode: entry.filemode(),
             is_binary,
             size,
+            tree_depth: depth,
             contents: None,
             contents_safe: false,
             contents_preformatted: true,
         });
-        if recurse && entry.kind() == Some(git2::ObjectType::Tree) {
+        if recurse && depth < (max_depth - 1) && entry.kind() == Some(git2::ObjectType::Tree) {
             let prefix = name + "/";
-            walk_file_tree(repo, &entry.id().to_string(), files, depth+1, true, &prefix)?;
+            walk_file_tree(repo, &entry.id().to_string(), files,
+                           depth+1, max_depth, true, &prefix)?;
         }
     }
     Ok(())
 }
 
-fn parse_repo(repo: &Repository, name: &str, metadata: GitsyMetadata) -> Result<GitRepo, Error> {
+fn parse_repo(repo: &Repository, name: &str, settings: &GitsySettingsRepo, 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 commits: BTreeMap<String, GitObject> = BTreeMap::new();
+    let mut commit_count = 0;
+    let mut history_count = 0;
+    let mut branch_count = 0;
+    let mut tag_count = 0;
 
     let mut revwalk = repo.revwalk()?;
     revwalk.set_sorting(git2::Sort::TOPOLOGICAL)?;
     revwalk.push_head()?;
     for oid in revwalk {
         let oid = oid?;
+        if commit_count >= settings.limit_commits.unwrap_or(usize::MAX) ||
+            history_count >= settings.limit_history.unwrap_or(usize::MAX) {
+                break;
+        }
         commits.insert(oid.to_string(), parse_commit(repo, &oid.to_string())?);
+        commit_count += 1;
         let commit = repo.find_commit(oid)?;
         let obj = repo.revparse_single(&commit.id().to_string())?;
         let full_hash = commit.id().to_string();
@@ -249,25 +265,35 @@ fn parse_repo(repo: &Repository, name: &str, metadata: GitsyMetadata) -> Result<
             }
         }
 
-        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()
-        });
+        if history_count < settings.limit_history.unwrap_or(usize::MAX) {
+            // TODO: this is basically a duplicate of the commit
+            // array, and really should be pointers to that array
+            // instead.  But it's not a quick task to switch to
+            // self-referential data structures in Rust.
+            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()
+            });
+            history_count += 1;
+        }
     }
 
     for branch in repo.branches(None)? {
+        if branch_count >= settings.limit_branches.unwrap_or(usize::MAX) {
+            break;
+        }
         let (branch, _branch_type) = branch?;
         let refr = branch.get();
         let name = branch.name()?.unwrap_or("[unnamed]");
@@ -300,8 +326,12 @@ fn parse_repo(repo: &Repository, name: &str, metadata: GitsyMetadata) -> Result<
             message: commit.message().map(|x| x.to_string()),
             ..Default::default()
         });
+        branch_count += 1;
     }
     for tag in repo.tag_names(None)?.iter() {
+        if tag_count >= settings.limit_tags.unwrap_or(usize::MAX) {
+            break;
+        }
         let tag = tag.unwrap_or("[unnamed]");
         let obj = repo.revparse_single(tag)?;
         let commit = repo.find_tag(obj.id())?;
@@ -335,14 +365,18 @@ fn parse_repo(repo: &Repository, name: &str, metadata: GitsyMetadata) -> Result<
             summary,
             ..Default::default()
         });
+        tag_count += 1;
     }
 
     let mut root_files: Vec<GitFile> = vec!();
     let mut all_files: Vec<GitFile> = vec!();
-    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, "HEAD", &mut all_files, 0, true, "")?;
+    let max_depth = settings.limit_tree_depth.unwrap_or(usize::MAX);
+    if max_depth > 0 {
+        walk_file_tree(&repo, "HEAD", &mut root_files, 0, usize::MAX, false, "")?;
+        // TODO: maybe this should be optional?  Walking the whole tree
+        // could be slow on huge repos.
+        walk_file_tree(&repo, "HEAD", &mut all_files, 0, max_depth, true, "")?;
+    }
 
     Ok(GitRepo {
         name: name.to_string(),
@@ -543,7 +577,7 @@ fn fill_file_contents(repo: &Repository, file: &GitFile, settings: &GitsySetting
 
 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, "")?;
+    walk_file_tree(&repo, &file.id, &mut files, 0, usize::MAX, false, "")?;
     Ok(files)
 }
 
@@ -646,6 +680,14 @@ struct GitsySettings {
     templates: GitsySettingsTemplates,
     #[serde(rename(deserialize = "gitsy_outputs"))]
     outputs: GitsySettingsOutputs,
+    limit_history: Option<usize>,
+    limit_commits: Option<usize>,
+    limit_branches: Option<usize>,
+    limit_tags: Option<usize>,
+    limit_tree_depth: Option<usize>,
+    limit_file_size: Option<usize>,
+    limit_repo_size: Option<usize>,
+    limit_total_size: Option<usize>,
     render_markdown: Option<bool>,
     syntax_highlight: Option<bool>,
     syntax_highlight_theme: Option<String>,
@@ -751,7 +793,15 @@ struct GitsySettingsRepo {
     render_markdown: Option<bool>,
     syntax_highlight: Option<bool>,
     syntax_highlight_theme: Option<String>,
-    attributes: BTreeMap<String, toml::Value>,
+    attributes: Option<BTreeMap<String, toml::Value>>,
+    limit_history: Option<usize>,
+    limit_commits: Option<usize>,
+    limit_branches: Option<usize>,
+    limit_tags: Option<usize>,
+    limit_tree_depth: Option<usize>,
+    limit_file_size: Option<usize>,
+    limit_repo_size: Option<usize>,
+    limit_total_size: Option<usize>,
 }
 
 use std::hash::{Hash, Hasher};
@@ -767,11 +817,12 @@ impl PartialEq for GitsySettingsRepo {
 }
 impl Eq for GitsySettingsRepo {}
 
-fn write_rendered(path: &str, rendered: &str) {
+fn write_rendered(path: &str, rendered: &str) -> usize {
     let mut file = File::create(path)
         .expect(&format!("Unable to write to output path: {}", path));
     file.write(rendered.as_bytes())
         .expect(&format!("Failed to save rendered html to path: {}", path));
+    rendered.as_bytes().len()
 }
 
 fn main() {
@@ -823,6 +874,14 @@ fn main() {
                         render_markdown: settings.render_markdown.clone(),
                         syntax_highlight: settings.syntax_highlight.clone(),
                         syntax_highlight_theme: settings.syntax_highlight_theme.clone(),
+                        limit_history: settings.limit_history.clone(),
+                        limit_commits: settings.limit_commits.clone(),
+                        limit_branches: settings.limit_branches.clone(),
+                        limit_tags: settings.limit_tags.clone(),
+                        limit_tree_depth: settings.limit_tree_depth.clone(),
+                        limit_file_size: settings.limit_file_size.clone(),
+                        limit_repo_size: settings.limit_repo_size.clone(),
+                        limit_total_size: settings.limit_total_size.clone(),
                         ..Default::default()
                     });
                 }
@@ -846,13 +905,25 @@ fn main() {
     tera.register_function("ts_to_date", TsDateFn{});
     tera.register_function("ts_to_git_timestamp", TsTimestampFn{});
 
+    macro_rules! size_check {
+        ($settings:ident, $cur:ident, $total:expr) => {
+            if $cur > $settings.limit_repo_size.unwrap_or(usize::MAX) {
+                break;
+            }
+            if $total + $cur > $settings.limit_total_size.unwrap_or(usize::MAX) {
+                break;
+            }
+        }
+    }
+
     // Create output directory
     let _ = std::fs::create_dir(settings.outputs.path.to_str().expect("Output path invalid."));
 
     let generated_dt = chrono::offset::Local::now();
-
+    let mut total_bytes = 0;
     let mut repos: Vec<GitRepo> = vec!();
     for repo_desc in &repo_descriptions {
+        let mut repo_bytes = 0;
         let dir = &repo_desc.path;
         match dir.metadata() {
             Ok(m) if m.is_dir() => {},
@@ -866,9 +937,9 @@ fn main() {
             description: repo_desc.description.clone(),
             website: repo_desc.website.clone(),
             clone: None,
-            attributes: repo_desc.attributes.clone(),
+            attributes: repo_desc.attributes.clone().unwrap_or_default(),
         };
-        let summary = parse_repo(&repo, &name, metadata).expect("Failed to analyze repo HEAD.");
+        let summary = parse_repo(&repo, &name, &repo_desc, metadata).expect("Failed to analyze repo HEAD.");
 
         let mut local_ctx = Context::from_serialize(&summary).unwrap();
         if let Some(extra) = &settings.extra {
@@ -888,7 +959,7 @@ fn main() {
 
         match tera.render(&settings.templates.repo_summary.as_deref().unwrap_or("summary.html"), &local_ctx) {
             Ok(rendered) => {
-                write_rendered(&settings.outputs.repo_summary(Some(&summary), None), &rendered);
+                repo_bytes += write_rendered(&settings.outputs.repo_summary(Some(&summary), None), &rendered);
             },
             Err(x) => match x.kind {
                 tera::ErrorKind::TemplateNotFound(_) if settings.templates.repo_summary.is_none() => {},
@@ -897,10 +968,11 @@ fn main() {
         }
 
         for branch in &summary.branches {
+            size_check!(repo_desc, repo_bytes, total_bytes);
             local_ctx.insert("branch", branch);
             match tera.render(&settings.templates.branch.as_deref().unwrap_or("branch.html"), &local_ctx) {
                 Ok(rendered) => {
-                    write_rendered(&settings.outputs.branch(Some(&summary), Some(branch)), &rendered);
+                    repo_bytes += write_rendered(&settings.outputs.branch(Some(&summary), Some(branch)), &rendered);
                 },
                 Err(x) => match x.kind {
                     tera::ErrorKind::TemplateNotFound(_) if settings.templates.branch.is_none() => {},
@@ -911,13 +983,14 @@ fn main() {
         }
 
         for tag in &summary.tags {
+            size_check!(repo_desc, repo_bytes, total_bytes);
             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(&settings.templates.tag.as_deref().unwrap_or("tag.html"), &local_ctx) {
                 Ok(rendered) => {
-                    write_rendered(&settings.outputs.tag(Some(&summary), Some(tag)), &rendered);
+                    repo_bytes += write_rendered(&settings.outputs.tag(Some(&summary), Some(tag)), &rendered);
                 },
                 Err(x) => match x.kind {
                     tera::ErrorKind::TemplateNotFound(_) if settings.templates.tag.is_none() => {},
@@ -929,10 +1002,11 @@ fn main() {
         }
 
         for (_id, commit) in &summary.commits {
+            size_check!(repo_desc, repo_bytes, total_bytes);
             local_ctx.try_insert("commit", &commit).expect("Failed to add commit to template engine.");
             match tera.render(&settings.templates.commit.as_deref().unwrap_or("commit.html"), &local_ctx) {
                 Ok(rendered) => {
-                    write_rendered(&settings.outputs.commit(Some(&summary), Some(commit)), &rendered);
+                    repo_bytes += write_rendered(&settings.outputs.commit(Some(&summary), Some(commit)), &rendered);
                 },
                 Err(x) => match x.kind {
                     tera::ErrorKind::TemplateNotFound(_) if settings.templates.commit.is_none() => {},
@@ -954,15 +1028,19 @@ fn main() {
                                       .unwrap_or("base16-ocean.light")).expect("Invalid syntax highlighting theme specified.");
             let css: String = css_for_theme_with_class_style(theme, syntect::html::ClassStyle::Spaced)
                 .expect("Invalid syntax highlighting theme specified.");
-            write_rendered(&settings.outputs.syntax_css(Some(&summary), None), css.as_str());
+            repo_bytes += write_rendered(&settings.outputs.syntax_css(Some(&summary), None), css.as_str());
         }
 
         for file in summary.all_files.iter().filter(|x| x.kind == "file") {
-            let file = fill_file_contents(&repo, &file, &repo_desc).expect("Failed to parse file.");
+            size_check!(repo_desc, repo_bytes, total_bytes);
+            let file = match file.size < repo_desc.limit_file_size.unwrap_or(usize::MAX) {
+                true => fill_file_contents(&repo, &file, &repo_desc).expect("Failed to parse file."),
+                false => file.clone(),
+            };
             local_ctx.try_insert("file", &file).expect("Failed to add file to template engine.");
             match tera.render(&settings.templates.file.as_deref().unwrap_or("file.html"), &local_ctx) {
                 Ok(rendered) => {
-                    write_rendered(&settings.outputs.file(Some(&summary), Some(&file)), &rendered);
+                    repo_bytes += write_rendered(&settings.outputs.file(Some(&summary), Some(&file)), &rendered);
                 },
                 Err(x) => match x.kind {
                     tera::ErrorKind::TemplateNotFound(_) if settings.templates.file.is_none() => {},
@@ -973,11 +1051,15 @@ fn main() {
         }
 
         for dir in summary.all_files.iter().filter(|x| x.kind == "dir") {
+            size_check!(repo_desc, repo_bytes, total_bytes);
+            if dir.tree_depth >= repo_desc.limit_tree_depth.unwrap_or(usize::MAX) - 1 {
+                continue;
+            }
             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(&settings.templates.dir.as_deref().unwrap_or("dir.html"), &local_ctx) {
                 Ok(rendered) => {
-                    write_rendered(&settings.outputs.dir(Some(&summary), Some(dir)), &rendered);
+                    repo_bytes += write_rendered(&settings.outputs.dir(Some(&summary), Some(dir)), &rendered);
                 },
                 Err(x) => match x.kind {
                     tera::ErrorKind::TemplateNotFound(_) if settings.templates.dir.is_none() => {},
@@ -998,10 +1080,16 @@ fn main() {
                 std::fs::copy(&src_file, &dst_file)
                     .expect(&format!("Failed to copy repo asset file: {} ({})",
                                      src_file.display(), repo_desc.name.as_deref().unwrap_or_default()));
+                if let Ok(meta) = std::fs::metadata(dst_file) {
+                    repo_bytes += meta.len() as usize;
+                }
             }
         }
 
         repos.push(summary);
+        println!("Wrote repo: {} ({} bytes)", name, repo_bytes);
+        total_bytes += repo_bytes;
+        size_check!(repo_desc, repo_bytes, total_bytes);
     }
 
     let mut global_ctx = Context::new();
@@ -1023,7 +1111,7 @@ fn main() {
 
     match tera.render(&settings.templates.repo_list.as_deref().unwrap_or("repos.html"), &global_ctx) {
         Ok(rendered) => {
-            write_rendered(&settings.outputs.repo_list(None, None), &rendered);
+            total_bytes += write_rendered(&settings.outputs.repo_list(None, None), &rendered);
         },
         Err(x) => match x.kind {
             tera::ErrorKind::TemplateNotFound(_) if settings.templates.repo_list.is_none() => {},
@@ -1033,7 +1121,7 @@ fn main() {
 
     match tera.render(&settings.templates.error.as_deref().unwrap_or("404.html"), &global_ctx) {
         Ok(rendered) => {
-            write_rendered(&settings.outputs.error(None, None), &rendered);
+            total_bytes += write_rendered(&settings.outputs.error(None, None), &rendered);
         },
         Err(x) => match x.kind {
             tera::ErrorKind::TemplateNotFound(_) if settings.templates.error.is_none() => {},
@@ -1050,6 +1138,11 @@ fn main() {
                           .expect(&format!("Failed to copy asset file: {}", src_file.display())));
             std::fs::copy(&src_file, &dst_file)
                 .expect(&format!("Failed to copy asset file: {}", src_file.display()));
+            if let Ok(meta) = std::fs::metadata(dst_file) {
+                total_bytes += meta.len() as usize;
+            }
         }
     }
+
+    println!("Total bytes written: {}", total_bytes);
 }

diff --git a/templates/footer.html b/templates/footer.html
line changes: +1/-1
index f29b415..9d302a7
--- a/templates/footer.html
+++ b/templates/footer.html
@@ -1,2 +1,2 @@
 <hr/>
-Generated by Itsy-Gitsy on {{ts_to_git_timestamp(ts=site_generated_ts, tz=site_generated_offset)}}<br/>
+Generated by {{extra | get(key="generated_by", default="Itsy-Gitsy")}} on {{ts_to_git_timestamp(ts=site_generated_ts, tz=site_generated_offset)}}<br/>

diff --git a/templates/header.html b/templates/header.html
line changes: +1/-1
index 24dc11d..3968b07
--- a/templates/header.html
+++ b/templates/header.html
@@ -1,3 +1,3 @@
 Site: {{site_name}}<br/>
-Extra settings: {{extra.global_user_defined_vars}}<br/>
+Description: {{site_description}}<br/>
 <hr/>

diff --git a/templates/repos.html b/templates/repos.html
line changes: +1/-1
index fa0a79d..70b1eb6
--- a/templates/repos.html
+++ b/templates/repos.html
@@ -2,6 +2,6 @@
 
 {% block content %}
 {% 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/>
+<a href="{{repo.name}}/summary.html">{{ repo.name }}</a> ({{repo.metadata.website}}) [{{repo.metadata.attributes | get(key="status", default="unknown")}}] ({{ts_to_date(ts=repo.history[0].ts_utc, tz=repo.history[0].ts_offset)}})<br/>
 {% endfor -%}
 {% endblock content %}