🐙 Templated web page generator for your git repositories
git clone https://github.com/m-col/gitja
Files | Refs | Readme | License

Commit ba94ae5190871fc07a7e490f90e3b2a3f0984e4b
Parent: 45e5fe4230364612e439938a4067b955ce1ba1b1
Author: Matt Colligan <mcol@posteo.net>
Date: 2026-08-30 23:53:30 +0100
Committer: GitHub <noreply@github.com>
Committed: 2026-08-30 23:53:30 +0100

Generate pages for nested files, not just top-level ones (#33)

* Expose a genuinely flattened file listing (blobs)

Both "tree" and "tree_recursive" (at the top-level package scope, and the
per-file scope on a directory entry) are misleadingly named - neither
actually recurses. "tree" is filtered to top-level entries; "tree_recursive"
is the exact same unfiltered top-level list (a no-op filter, since
top-level entries never contain a path separator); and per-file
`tree`/`tree_recursive` both just return one level of a directory's
immediate children (treeFileGetTreeRecursive literally just returns `fs`
unchanged). There was no existing way to get a real flat, full-depth
listing of a repo's files.

Add `flattenFiles` (Types.hs), which actually recurses into
FolderContents and collects only file/blob leaves, and expose it as a new
top-level `blobs` field in Repositories.hs's `package` scope. Left the
existing `tree`/`tree_recursive` fields untouched, since other templates
(and the readme/license lookup, which searches `tree`) may depend on
their current, if misleadingly-named, behavior.

Each entry's `path` in the flattened list is already the full path
relative to the repo root - the tree walk in Repositories.hs accumulates
this via prependParent as it descends, regardless of how deep a file is
nested.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012RfZTyHvxLyVp4yaWmNvhN

* Actually generate file pages for nested files, not just top-level ones

processRepo' only ever called the file-page generator on the top-level
tree (in both the force and non-force/getUpdatedFiles branches), never on
anything reachable through a directory's FolderContents. So gitja has
never actually written a file.html-equivalent page for anything nested in
a subdirectory, independent of the "blobs" field or any template - the
page itself was never generated, regardless of what a template linked to.

Flatten tree with flattenFiles before both generation paths, so a file's
depth no longer determines whether it gets a page.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012RfZTyHvxLyVp4yaWmNvhN

* Drop comment

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012RfZTyHvxLyVp4yaWmNvhN

* Move flattenFiles into Repositories.hs

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012RfZTyHvxLyVp4yaWmNvhN

* Drop tree_recursive - it never actually recursed

Confirmed by reading the code: at both scopes (top-level package, and
per-file on a directory entry), tree_recursive was identical to tree.
Top-level tree's filter (notElem pathSeparator ...) was a no-op, since
the list it filters only ever contains top-level entries in the first
place - nested entries live inside each directory's own FolderContents,
never flattened into it. Per-file, treeFileGetTreeRecursive just returned
`fs` unchanged - no recursion despite the name. The docs described
tree_recursive as "a list of *all* of the repository's/directory's
contents", which was never actually true.

"tree" is git's own name for a tree object's entries (a git tree is not
itself recursive - that's exactly what it means for git to have separate
tree objects per directory), so keeping that name for "this tree's
entries" is accurate, not a simplification. Dropped the now-pointless
filter/atTop logic along with tree_recursive, and updated
DOCUMENTATION.md's Scopes/Attributes tables to match - "blobs" is
documented there too, since it existed but wasn't previously listed.

This doesn't add per-tree-entry pages for every object type a tree can
contain (blob/tree/commit-as-submodule) - just removes a redundant,
inaccurately-documented field. That's real future work, not this.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012RfZTyHvxLyVp4yaWmNvhN

* Reinstate tree_recursive, properly recursive this time

blobs (leaves only) and tree_recursive (every entry, blobs and trees
alike, at any depth) are different things - collapsing tree_recursive
into blobs lost that distinction. Add flattenTree (Types.hs), the
tree-and-blob equivalent of flattenFiles - keeps every entry including
directories themselves as it descends, equivalent to `git ls-tree -r -t`.
Used for both the top-level "tree_recursive" (package scope, Repositories.hs)
and the per-file "tree_recursive" on a directory entry (treeAsLookup,
Types.hs).

flattenTree lives in Types.hs rather than alongside flattenFiles in
Repositories.hs, since it's needed by treeAsLookup there too and
Repositories.hs already depends on Types.hs (the reverse would be a
cycle).

Updated DOCUMENTATION.md's Scopes/Attributes tables to describe both
tree_recursive and blobs accurately.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012RfZTyHvxLyVp4yaWmNvhN

* Add a per-tree page category, and rename file -> blob

Third special repo/ template, alongside commit.html and the newly-renamed
blob.html: tree.html, generated once per tree (directory) object at any
depth, mirroring how blob.html/commit.html already work per blob/commit.
Uses flattenTrees (Repositories.hs) to walk every tree object including
itself, and treeHref for its output path. Unlike blobs, a tree's own path
never appears in a commit diff, so there's no cheap way to tell whether a
tree needs regenerating from newCommits alone - tree pages are always
regenerated on every run rather than incrementally, which is simpler and
correct rather than risking the exact kind of silent staleness bug the
blob-generation fix earlier addressed.

"file" -> "blob" throughout (envFileTemplate/fileT/fileHref/fileDir ->
envBlobTemplate/blobT/blobHref/blobDir, output moves from file/ to
blob/), matching git's own object vocabulary and freeing up the name
"file" from colliding with anything tree-related.

Updated the three bundled templates (base/docs/stagit) so this doesn't
silently break them: file.html -> blob.html (dropping their now-truly-
unreachable is_directory branches, since blobs are never directories),
the existing once-per-repo file-listing tree.html -> files.html (freeing
the name for the new per-object template), and a real tree.html per-object
page for each, built from what those old is_directory branches were
already trying to do. Every href referencing the old file/ output
directory is updated to blob/, and files.html/tree.html's child links now
route to tree/ or blob/ depending on the entry's own type.

Updated DOCUMENTATION.md's folder structure, scopes and attributes tables
to describe blob/tree/commit as the three special per-object templates,
and the blob/tree attribute table entries as shared (most attributes
apply to both, a few are n/a on one side).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012RfZTyHvxLyVp4yaWmNvhN

* Fix test.sh and rename special templates to foreach.<scope>.html

Fixes the CI failure on the previous commit: test.sh's golden-file test
still expected output under gitja/file/... (the pre-rename output
directory), and test/templates/repo/file.html was never updated to
match the blob rename. Renamed that test template to foreach.blob.html
(see below), updated its dumped "file"/"scope: file" labels to
"blob"/"scope: blob" (the values themselves - paths, hrefs, contents -
are unchanged, since it's the same underlying object under a renamed
scope), moved test/expected/gitja/file/ to test/expected/gitja/blob/
with matching label updates, and fixed test.sh's TESTS array paths.

Also rename the three special per-object templates to
foreach.blob.html/foreach.commit.html/foreach.tree.html (from
blob.html/commit.html/tree.html), across the bundled templates, the test
template, and Env.hs's lookups. Makes it visually obvious in a directory
listing which templates in repo/ run once per repo vs. once per object -
previously "blob.html"/"commit.html"/"tree.html" looked like just three
more arbitrarily-named repo templates alongside index.html/refs.html/etc,
despite being the special-cased ones.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012RfZTyHvxLyVp4yaWmNvhN

* Rename TreeFile to TreeEntry and tree_recursive to entries, add trees field

TreeEntry better reflects that these represent any tree object (blob, tree, or submodule), and entries/trees give the flattened lists clearer, distinct names.

---------

Co-authored-by: Claude <noreply@anthropic.com>

test/test.sh Modified

@@ -36,10 +36,10 @@
     "static/a_nice_file"  # Static folder at top level
     "gitja/index.html"  # Repo scope
     "gitja/commit/0292014748caae952bbc8dd6225680d83c0a5135.html"  # A commit
-    "gitja/file/test.templates.style.css.html"  # A plain text file
+    "gitja/blob/test.templates.style.css.html"  # A plain text file
     "so_called_binary_file"  # Static file at top level that git considers binary
-    "gitja/file/test.templates.so_called_binary_file.html"  # The binary file
-    "gitja/file/github.FUNDING.yml.html"  # HREF drops leading period
+    "gitja/blob/test.templates.so_called_binary_file.html"  # The binary file
+    "gitja/blob/github.FUNDING.yml.html"  # HREF drops leading period
     "gitja/log.html"  # Symbolic link inside repo/
     "gitja/static/another_file"  # Static folder inside repo/
 )

test/templates/repo/foreach.commit.html Added

@@ -0,0 +1,19 @@
+{% extends "../title.html.include" -%}
+{%- block title -%}{{ commit }}{%- endblock -%}
+
+{% block body %}
+scope: commit
+commit: {{ commit }}
+commit.id: {{ commit.id }}
+commit.title: {{ commit.title }}
+commit.body: {{ commit.body }}
+commit.message: {{ commit.message }}
+commit.author: {{ commit.author }}
+commit.committer: {{ commit.committer }}
+commit.author_email: {{ commit.author_email }}
+commit.committer_email: {{ commit.committer_email }}
+commit.authored: {{ commit.authored }}
+commit.committed: {{ commit.committed }}
+commit.encoding: {{ commit.encoding }}
+commit.parent: {{ commit.parent }}
+{% endblock %}

test/templates/repo/foreach.blob.html Added

@@ -0,0 +1,28 @@
+{% extends "../title.html.include" -%}
+{%- block title -%}{{ blob }}{%- endblock -%}
+
+{% block body %}
+scope: blob
+blob: {{ blob }}
+blob.path: {{ blob.path }}
+blob.href: {{ blob.href }}
+blob.contents: {% if blob.is_binary %}(binary)
+{% else %}{{ blob.contents }}{% endif %}
+blob.mode: {{ blob.mode }}
+blob.mode_octal: {{ blob.mode_octal }}
+blob.mode_symbolic: {{ blob.mode_symbolic }}
+if blob.is_directory then "directory" else "file":
+	{%- if blob.is_directory %} "directory"{% else %} "file"{% endif %}
+
+
+blob scope also should have data from repo scope available:
+host: {{ host }}
+repos: {{ repositories }}
+name: {{ name }}
+description: {{ description }}
+commits: (skipped)
+tree: (skipped)
+tags: {{ length(filter(tags, (t) -> t.name == "0.2.0")) }}
+readme.path: {{ readme.path }}
+license.path: {{ license.path }}
+{% endblock %}

test/templates/repo/file.html Deleted

@@ -1,28 +0,0 @@
-{% extends "../title.html.include" -%}
-{%- block title -%}{{ file }}{%- endblock -%}
-
-{% block body %}
-scope: file
-file: {{ file }}
-file.path: {{ file.path }}
-file.href: {{ file.href }}
-file.contents: {% if file.is_binary %}(binary)
-{% else %}{{ file.contents }}{% endif %}
-file.mode: {{ file.mode }}
-file.mode_octal: {{ file.mode_octal }}
-file.mode_symbolic: {{ file.mode_symbolic }}
-if file.is_directory then "directory" else "file":
-	{%- if file.is_directory %} "directory"{% else %} "file"{% endif %}
-
-
-file scope also should have data from repo scope available:
-host: {{ host }}
-repos: {{ repositories }}
-name: {{ name }}
-description: {{ description }}
-commits: (skipped)
-tree: (skipped)
-tags: {{ length(filter(tags, (t) -> t.name == "0.2.0")) }}
-readme.path: {{ readme.path }}
-license.path: {{ license.path }}
-{% endblock %}

test/templates/repo/commit.html Deleted

@@ -1,19 +0,0 @@
-{% extends "../title.html.include" -%}
-{%- block title -%}{{ commit }}{%- endblock -%}
-
-{% block body %}
-scope: commit
-commit: {{ commit }}
-commit.id: {{ commit.id }}
-commit.title: {{ commit.title }}
-commit.body: {{ commit.body }}
-commit.message: {{ commit.message }}
-commit.author: {{ commit.author }}
-commit.committer: {{ commit.committer }}
-commit.author_email: {{ commit.author_email }}
-commit.committer_email: {{ commit.committer_email }}
-commit.authored: {{ commit.authored }}
-commit.committed: {{ commit.committed }}
-commit.encoding: {{ commit.encoding }}
-commit.parent: {{ commit.parent }}
-{% endblock %}

test/expected/gitja/file/test.templates.style.css.html Deleted

@@ -1,21 +0,0 @@
-test/templates/style.css
-scope: file
-file: test/templates/style.css
-file.path: test/templates/style.css
-file.href: test.templates.style.css.html
-file.contents: html
-file.mode: Plain
-file.mode_octal: 00644
-file.mode_symbolic: -rw-r--r--
-if file.is_directory then "directory" else "file": "file"
-
-file scope also should have data from repo scope available:
-host: https://github.com/m-col/gitja
-repos: gitja
-name: gitja
-description: 🐙 Templated web page generator for your git repositories
-commits: (skipped)
-tree: (skipped)
-tags: 1
-readme.path: README.rst
-license.path: LICENSE

test/expected/gitja/file/test.templates.so_called_binary_file.html Deleted

@@ -1,21 +0,0 @@
-test/templates/so_called_binary_file
-scope: file
-file: test/templates/so_called_binary_file
-file.path: test/templates/so_called_binary_file
-file.href: test.templates.so_called_binary_file.html
-file.contents: (binary)
-file.mode: Plain
-file.mode_octal: 00644
-file.mode_symbolic: -rw-r--r--
-if file.is_directory then "directory" else "file": "file"
-
-file scope also should have data from repo scope available:
-host: https://github.com/m-col/gitja
-repos: gitja
-name: gitja
-description: 🐙 Templated web page generator for your git repositories
-commits: (skipped)
-tree: (skipped)
-tags: 1
-readme.path: README.rst
-license.path: LICENSE

test/expected/gitja/file/github.FUNDING.yml.html Deleted

@@ -1,22 +0,0 @@
-.github/FUNDING.yml
-scope: file
-file: .github/FUNDING.yml
-file.path: .github/FUNDING.yml
-file.href: github.FUNDING.yml.html
-file.contents: github: [&quot;m-col&quot;]
-custom: [&quot;https://liberapay.com/mcol&quot;]
-file.mode: Plain
-file.mode_octal: 00644
-file.mode_symbolic: -rw-r--r--
-if file.is_directory then "directory" else "file": "file"
-
-file scope also should have data from repo scope available:
-host: https://github.com/m-col/gitja
-repos: gitja
-name: gitja
-description: 🐙 Templated web page generator for your git repositories
-commits: (skipped)
-tree: (skipped)
-tags: 1
-readme.path: README.rst
-license.path: LICENSE

test/expected/gitja/blob/test.templates.style.css.html Added

@@ -0,0 +1,21 @@
+test/templates/style.css
+scope: blob
+blob: test/templates/style.css
+blob.path: test/templates/style.css
+blob.href: test.templates.style.css.html
+blob.contents: html
+blob.mode: Plain
+blob.mode_octal: 00644
+blob.mode_symbolic: -rw-r--r--
+if blob.is_directory then "directory" else "file": "file"
+
+blob scope also should have data from repo scope available:
+host: https://github.com/m-col/gitja
+repos: gitja
+name: gitja
+description: 🐙 Templated web page generator for your git repositories
+commits: (skipped)
+tree: (skipped)
+tags: 1
+readme.path: README.rst
+license.path: LICENSE

test/expected/gitja/blob/test.templates.so_called_binary_file.html Added

@@ -0,0 +1,21 @@
+test/templates/so_called_binary_file
+scope: blob
+blob: test/templates/so_called_binary_file
+blob.path: test/templates/so_called_binary_file
+blob.href: test.templates.so_called_binary_file.html
+blob.contents: (binary)
+blob.mode: Plain
+blob.mode_octal: 00644
+blob.mode_symbolic: -rw-r--r--
+if blob.is_directory then "directory" else "file": "file"
+
+blob scope also should have data from repo scope available:
+host: https://github.com/m-col/gitja
+repos: gitja
+name: gitja
+description: 🐙 Templated web page generator for your git repositories
+commits: (skipped)
+tree: (skipped)
+tags: 1
+readme.path: README.rst
+license.path: LICENSE

test/expected/gitja/blob/github.FUNDING.yml.html Added

@@ -0,0 +1,22 @@
+.github/FUNDING.yml
+scope: blob
+blob: .github/FUNDING.yml
+blob.path: .github/FUNDING.yml
+blob.href: github.FUNDING.yml.html
+blob.contents: github: [&quot;m-col&quot;]
+custom: [&quot;https://liberapay.com/mcol&quot;]
+blob.mode: Plain
+blob.mode_octal: 00644
+blob.mode_symbolic: -rw-r--r--
+if blob.is_directory then "directory" else "file": "file"
+
+blob scope also should have data from repo scope available:
+host: https://github.com/m-col/gitja
+repos: gitja
+name: gitja
+description: 🐙 Templated web page generator for your git repositories
+commits: (skipped)
+tree: (skipped)
+tags: 1
+readme.path: README.rst
+license.path: LICENSE

templates/stagit/repo/tree.html Deleted

@@ -1,29 +0,0 @@
-{% extends "../template.html.include" %}
-
-{%- block title -%}
-{{ name }} - target
-{%- endblock -%}
-
-{% block body %}
-{% include "repo-table-header.html.include" %}
-
-<div id="content">
-  <table id="files">
-    <thead>
-      <tr>
-        <td><b>Mode</b></td>
-        <td><b>Name</b></td>
-      </tr>
-    </thead>
-
-    <tbody>
-      {% for file in tree %}
-      <tr>
-        <td>{{ file.mode_symbolic }}</td>
-        <td><a href="{{ host }}/{{ name }}/file/{{ file.href }}">{{ file.path }}</a></td>
-      </tr>
-      {% endfor %}
-    </tbody>
-  </table>
-</div>
-{% endblock %}

templates/stagit/repo/repo-table-header.html.include Modified

@@ -16,9 +16,9 @@
   <tr><td><span class="desc">{{ description }}</span></td></tr>
   <tr class="url"><td>git clone <a href="{{ host }}/{{ name }}">{{ host }}/{{ name }}</a></td></tr>
   <tr><td>
-    <a href="{{ host }}/{{ name }}/tree.html">Files</a> | <a href="{{ host }}/{{ name }}/refs.html">Refs</a>
-    {% if readme %} | <a href="{{ host }}/{{ name }}/file/{{ readme.href }}">Readme</a>{% endif %}
-    {% if license %} | <a href="{{ host }}/{{ name }}/file/{{ license.href }}">License</a>{% endif %}
+    <a href="{{ host }}/{{ name }}/files.html">Files</a> | <a href="{{ host }}/{{ name }}/refs.html">Refs</a>
+    {% if readme %} | <a href="{{ host }}/{{ name }}/blob/{{ readme.href }}">Readme</a>{% endif %}
+    {% if license %} | <a href="{{ host }}/{{ name }}/blob/{{ license.href }}">License</a>{% endif %}
   </td></tr>
 </table>
 <hr>

templates/stagit/repo/foreach.tree.html Added

@@ -0,0 +1,35 @@
+{% extends "../template.html.include" %}
+
+{%- block title -%}
+{{ name }} - {{ tree }}
+{%- endblock -%}
+
+{% block body %}
+{% include "repo-table-header.html.include" %}
+
+<div id="content">
+  <p>{{ tree.mode_symbolic }} {{ tree }}</p><hr>
+
+  <table id="files">
+    <thead>
+      <tr>
+        <td><b>Mode</b></td>
+        <td><b>Name</b></td>
+      </tr>
+    </thead>
+
+    <tbody>
+      {% for child in tree.tree %}
+      <tr>
+        <td>{{ child.mode_symbolic }}</td>
+        {% if child.is_directory %}
+        <td><a href="{{ host }}/{{ name }}/tree/{{ child.href }}">{{ child.path }}</a></td>
+        {% else %}
+        <td><a href="{{ host }}/{{ name }}/blob/{{ child.href }}">{{ child.path }}</a></td>
+        {% endif %}
+      </tr>
+      {% endfor %}
+    </tbody>
+  </table>
+</div>
+{% endblock %}

templates/stagit/repo/foreach.commit.html Added

@@ -0,0 +1,46 @@
+{% extends "../template.html.include" %}
+
+{%- block title -%}
+{{ name }} - {{ commit.title }}
+{%- endblock -%}
+
+{% block body %}
+{% include "repo-table-header.html.include" %}
+
+<div id="content">
+  <pre><b>Commit</b> <a href="{{ host }}/{{ name }}/commit/{{ commit.id }}.html">{{ commit.id }}</a>
+<b>Parent:</b> <a href="{{ host }}/{{ name }}/commit/{{ commit.parent }}.html">{{ commit.parent }}</a>
+<b>Author:</b> {{ commit.author }} &lt;<a href="mailto:{{ commit.author_email }}">{{ commit.author_email }}</a>&gt;
+<b>Date:</b> {{ commit.authored }}
+<b>Committer:</b> {{ commit.committer }} &lt;<a href="mailto:{{ commit.committer_email }}">{{ commit.committer_email }}</a>&gt;
+<b>Committed:</b> {{ commit.committed }}
+
+{{ commit.message }}
+</pre>
+
+{%- for diff in commit.diffs -%}
+<div class="diff">
+  <p>
+    <b>{{ diff.new_file }}</b>
+    <span class="status">{{ diff.status }}</span>
+  </p>
+
+  {% if diff.newfile != diff.oldfile %}
+  <p>Renamed from {{ diff.old_file }}</p>
+  {% endif %}
+
+  {%- for hunk in diff.hunks -%}
+  {{ hunk.header }}
+
+  <pre>
+    {%- for line in hunk.lines -%}
+      <span class="{{ line.class }}">{{ line.text }}</span>
+    {%- endfor -%}
+  </pre>
+
+  {%- endfor -%}
+</div>
+{%- endfor -%}
+
+</div>
+{% endblock %}

templates/stagit/repo/foreach.blob.html Added

@@ -0,0 +1,21 @@
+{% extends "../template.html.include" %}
+
+{%- block title -%}
+{{ name }} - {{ blob }}
+{%- endblock -%}
+
+{% block body %}
+{% include "repo-table-header.html.include" %}
+
+<div id="content">
+  <p>{{ blob.mode_symbolic }} {{ blob }}</p><hr>
+
+  {% if blob.is_binary %}
+  (File is binary)
+  {% else %}
+<pre id="blob">{% for line in split(blob.contents, '\n') %}
+<a href="#l{{ loop.index }}" class="line" id="l{{ loop.index }}">{{ printf("%7v", loop.index) }}</a> {{ line }}
+{% endfor %}</pre>
+  {% endif %}
+</div>
+{% endblock %}

templates/stagit/repo/files.html Added

@@ -0,0 +1,33 @@
+{% extends "../template.html.include" %}
+
+{%- block title -%}
+{{ name }} - files
+{%- endblock -%}
+
+{% block body %}
+{% include "repo-table-header.html.include" %}
+
+<div id="content">
+  <table id="files">
+    <thead>
+      <tr>
+        <td><b>Mode</b></td>
+        <td><b>Name</b></td>
+      </tr>
+    </thead>
+
+    <tbody>
+      {% for file in tree %}
+      <tr>
+        <td>{{ file.mode_symbolic }}</td>
+        {% if file.is_directory %}
+        <td><a href="{{ host }}/{{ name }}/tree/{{ file.href }}">{{ file.path }}</a></td>
+        {% else %}
+        <td><a href="{{ host }}/{{ name }}/blob/{{ file.href }}">{{ file.path }}</a></td>
+        {% endif %}
+      </tr>
+      {% endfor %}
+    </tbody>
+  </table>
+</div>
+{% endblock %}

templates/stagit/repo/file.html Deleted

@@ -1,41 +0,0 @@
-{% extends "../template.html.include" %}
-
-{%- block title -%}
-{{ name }} - {{ file }}
-{%- endblock -%}
-
-{% block body %}
-{% include "repo-table-header.html.include" %}
-
-<div id="content">
-  <p>{{ file.mode_symbolic }} {{ file }}</p><hr>
-
-{% if file.is_directory %}
-  <table id="files">
-    <thead>
-      <tr>
-        <td><b>Mode</b></td>
-        <td><b>Name</b></td>
-      </tr>
-    </thead>
-
-    <tbody>
-      {% for child in file.contents %}
-      <tr>
-        <td>{{ child.mode_symbolic }}</td>
-        <td><a href="{{ host }}/{{ name }}/file/{{ child.href }}">{{ child.path }}</a></td>
-      </tr>
-      {% endfor %}
-    </tbody>
-  </table>
-{% else %}
-  {% if file.is_binary %}
-  (File is binary)
-  {% else %}
-<pre id="blob">{% for line in split(file.contents, '\n') %}
-<a href="#l{{ loop.index }}" class="line" id="l{{ loop.index }}">{{ printf("%7v", loop.index) }}</a> {{ line }}
-{% endfor %}</pre>
-  {% endif %}
-{% endif %}
-</div>
-{% endblock %}

templates/stagit/repo/commit.html Deleted

@@ -1,46 +0,0 @@
-{% extends "../template.html.include" %}
-
-{%- block title -%}
-{{ name }} - {{ commit.title }}
-{%- endblock -%}
-
-{% block body %}
-{% include "repo-table-header.html.include" %}
-
-<div id="content">
-  <pre><b>Commit</b> <a href="{{ host }}/{{ name }}/commit/{{ commit.id }}.html">{{ commit.id }}</a>
-<b>Parent:</b> <a href="{{ host }}/{{ name }}/commit/{{ commit.parent }}.html">{{ commit.parent }}</a>
-<b>Author:</b> {{ commit.author }} &lt;<a href="mailto:{{ commit.author_email }}">{{ commit.author_email }}</a>&gt;
-<b>Date:</b> {{ commit.authored }}
-<b>Committer:</b> {{ commit.committer }} &lt;<a href="mailto:{{ commit.committer_email }}">{{ commit.committer_email }}</a>&gt;
-<b>Committed:</b> {{ commit.committed }}
-
-{{ commit.message }}
-</pre>
-
-{%- for diff in commit.diffs -%}
-<div class="diff">
-  <p>
-    <b>{{ diff.new_file }}</b>
-    <span class="status">{{ diff.status }}</span>
-  </p>
-
-  {% if diff.newfile != diff.oldfile %}
-  <p>Renamed from {{ diff.old_file }}</p>
-  {% endif %}
-
-  {%- for hunk in diff.hunks -%}
-  {{ hunk.header }}
-
-  <pre>
-    {%- for line in hunk.lines -%}
-      <span class="{{ line.class }}">{{ line.text }}</span>
-    {%- endfor -%}
-  </pre>
-
-  {%- endfor -%}
-</div>
-{%- endfor -%}
-
-</div>
-{% endblock %}

templates/docs/template.html.include Modified

@@ -23,10 +23,10 @@
       <span id="links">
         <a href="{{ host }}">Home</a> |
         <a href="{{ host }}/{{ name }}/log.html">Log</a> |
-        <a href="{{ host }}/{{ name }}/tree.html">Files</a> |
+        <a href="{{ host }}/{{ name }}/files.html">Files</a> |
         <a href="{{ host }}/{{ name }}/refs.html">Refs</a> |
-        <a href="{{ host }}/{{ name }}/file/{{ readme.href }}">Readme</a> |
-        <a href="{{ host }}/{{ name }}/file/{{ license.href }}">License</a>
+        <a href="{{ host }}/{{ name }}/blob/{{ readme.href }}">Readme</a> |
+        <a href="{{ host }}/{{ name }}/blob/{{ license.href }}">License</a>
       </span>
 
       <p id="tag">

templates/docs/repo/tree.html Deleted

@@ -1,23 +0,0 @@
-{% extends "../template.html.include" %}
-
-{%- block title %}tree{% endblock -%}
-
-{% block body %}
-<table id="files">
-  <thead>
-    <tr>
-      <td><b>Mode</b></td>
-      <td><b>Name</b></td>
-    </tr>
-  </thead>
-
-  <tbody>
-    {% for file in tree %}
-    <tr>
-      <td>{{ file.mode_symbolic }}</td>
-      <td><a href="{{ host }}/{{ name }}/file/{{ file.href }}">{{ file.path }}</a></td>
-    </tr>
-    {% endfor %}
-  </tbody>
-</table>
-{% endblock %}

templates/docs/repo/foreach.tree.html Added

@@ -0,0 +1,29 @@
+{% extends "../template.html.include" %}
+
+{%- block title %}{{ tree }}{% endblock -%}
+
+{% block body %}
+<p>{{ tree.mode_symbolic }} {{ tree }}</p><hr>
+
+<table id="files">
+  <thead>
+    <tr>
+      <td><b>Mode</b></td>
+      <td><b>Name</b></td>
+    </tr>
+  </thead>
+
+  <tbody>
+    {% for child in tree.tree %}
+    <tr>
+      <td>{{ child.mode_symbolic }}</td>
+      {% if child.is_directory %}
+      <td><a href="{{ host }}/{{ name }}/tree/{{ child.href }}">{{ child.path }}</a></td>
+      {% else %}
+      <td><a href="{{ host }}/{{ name }}/blob/{{ child.href }}">{{ child.path }}</a></td>
+      {% endif %}
+    </tr>
+    {% endfor %}
+  </tbody>
+</table>
+{% endblock %}

templates/docs/repo/foreach.commit.html Added

@@ -0,0 +1,40 @@
+{% extends "../template.html.include" %}
+
+{%- block title %}{{ commit.title }}{% endblock -%}
+
+{% block body %}
+<pre><b>Commit</b> <a href="{{ host }}/{{ name }}/commit/{{ commit.id }}.html">{{ commit.id }}</a>
+<b>Parent:</b> <a href="{{ host }}/{{ name }}/commit/{{ commit.parent }}.html">{{ commit.parent }}</a>
+<b>Author:</b> {{ commit.author }} &lt;<a href="mailto:{{ commit.author_email }}">{{ commit.author_email }}</a>&gt;
+<b>Date:</b> {{ commit.authored }}
+<b>Committer:</b> {{ commit.committer }} &lt;<a href="mailto:{{ commit.committer_email }}">{{ commit.committer_email }}</a>&gt;
+<b>Committed:</b> {{ commit.committed }}
+
+{{ commit.message }}
+</pre>
+
+{%- for diff in commit.diffs -%}
+<div class="diff">
+  <p>
+    <b>{{ diff.new_file }}</b>
+    <span class="status">{{ diff.status }}</span>
+  </p>
+
+  {% if diff.newfile != diff.oldfile %}
+  <p>Renamed from {{ diff.old_file }}</p>
+  {% endif %}
+
+  {%- for hunk in diff.hunks -%}
+  {{ hunk.header }}
+
+  <pre>
+    {%- for line in hunk.lines -%}
+      <span class="{{ line.class }}">{{ line.text }}</span>
+    {%- endfor -%}
+  </pre>
+
+  {%- endfor -%}
+</div>
+{%- endfor -%}
+
+{% endblock %}

templates/docs/repo/foreach.blob.html Added

@@ -0,0 +1,15 @@
+{% extends "../template.html.include" %}
+
+{%- block title %}{{ blob }}{% endblock -%}
+
+{% block body %}
+<p>{{ blob.mode_symbolic }} {{ blob }}</p><hr>
+
+{% if blob.is_binary %}
+(File is binary)
+{% else %}
+<pre id="blob">{% for line in split(blob.contents, '\n') %}
+<a href="#l{{ loop.index }}" class="line" id="l{{ loop.index }}">{{ printf("%5v", loop.index) }}</a>  {{ line }}
+{% endfor %}</pre>
+{% endif %}
+{% endblock %}

templates/docs/repo/files.html Added

@@ -0,0 +1,27 @@
+{% extends "../template.html.include" %}
+
+{%- block title %}tree{% endblock -%}
+
+{% block body %}
+<table id="files">
+  <thead>
+    <tr>
+      <td><b>Mode</b></td>
+      <td><b>Name</b></td>
+    </tr>
+  </thead>
+
+  <tbody>
+    {% for file in tree %}
+    <tr>
+      <td>{{ file.mode_symbolic }}</td>
+      {% if file.is_directory %}
+      <td><a href="{{ host }}/{{ name }}/tree/{{ file.href }}">{{ file.path }}</a></td>
+      {% else %}
+      <td><a href="{{ host }}/{{ name }}/blob/{{ file.href }}">{{ file.path }}</a></td>
+      {% endif %}
+    </tr>
+    {% endfor %}
+  </tbody>
+</table>
+{% endblock %}

templates/docs/repo/file.html Deleted

@@ -1,47 +0,0 @@
-{% extends "../template.html.include" %}
-
-{%- block title %}{{ file }}{% endblock -%}
-
-{% block body %}
-<p>{{ file.mode_symbolic }} {{ file }}</p><hr>
-
-{% if file.is_directory %}
-<table id="files">
-  <thead>
-    <tr>
-      <td><b>Mode</b></td>
-      <td><b>Name</b></td>
-    </tr>
-  </thead>
-
-  <tbody>
-    {% for child in file.tree %}
-    <tr>
-      <td>{{ child.mode_symbolic }}</td>
-      <td><a href="{{ host }}/{{ name }}/file/{{ child.href }}">{{ child.path }}</a></td>
-    </tr>
-    {% endfor %}
-  </tbody>
-</table>
-
-  {%- if length(file.contents) == 1 -%}
-    {%- set child = file.contents[0] -%}
-    {% if child.is_binary %}
-    (Child is binary)
-    {% else %}
-<pre id="blob">{% for line in split(child.contents, '\n') %}
-<a href="#l{{ loop.index }}" class="line" id="l{{ loop.index }}">{{ printf("%5v", loop.index) }}</a>  {{ line }}
-{% endfor %}</pre>
-    {% endif %}
-  {%- endif -%}
-
-{% else %}
-  {% if file.is_binary %}
-  (File is binary)
-  {% else %}
-<pre id="blob">{% for line in split(file.contents, '\n') %}
-<a href="#l{{ loop.index }}" class="line" id="l{{ loop.index }}">{{ printf("%5v", loop.index) }}</a>  {{ line }}
-{% endfor %}</pre>
-  {% endif %}
-{% endif %}
-{% endblock %}

templates/docs/repo/commit.html Deleted

@@ -1,40 +0,0 @@
-{% extends "../template.html.include" %}
-
-{%- block title %}{{ commit.title }}{% endblock -%}
-
-{% block body %}
-<pre><b>Commit</b> <a href="{{ host }}/{{ name }}/commit/{{ commit.id }}.html">{{ commit.id }}</a>
-<b>Parent:</b> <a href="{{ host }}/{{ name }}/commit/{{ commit.parent }}.html">{{ commit.parent }}</a>
-<b>Author:</b> {{ commit.author }} &lt;<a href="mailto:{{ commit.author_email }}">{{ commit.author_email }}</a>&gt;
-<b>Date:</b> {{ commit.authored }}
-<b>Committer:</b> {{ commit.committer }} &lt;<a href="mailto:{{ commit.committer_email }}">{{ commit.committer_email }}</a>&gt;
-<b>Committed:</b> {{ commit.committed }}
-
-{{ commit.message }}
-</pre>
-
-{%- for diff in commit.diffs -%}
-<div class="diff">
-  <p>
-    <b>{{ diff.new_file }}</b>
-    <span class="status">{{ diff.status }}</span>
-  </p>
-
-  {% if diff.newfile != diff.oldfile %}
-  <p>Renamed from {{ diff.old_file }}</p>
-  {% endif %}
-
-  {%- for hunk in diff.hunks -%}
-  {{ hunk.header }}
-
-  <pre>
-    {%- for line in hunk.lines -%}
-      <span class="{{ line.class }}">{{ line.text }}</span>
-    {%- endfor -%}
-  </pre>
-
-  {%- endfor -%}
-</div>
-{%- endfor -%}
-
-{% endblock %}

templates/base/template.html.include Modified

@@ -19,13 +19,13 @@
     <span id="clone"><b>git clone <a href="{{ host }}/{{ name }}">{{ host }}/{{ name }}</a></b></span>
     <div class="links">
       <a href="{{ host }}/{{ name }}/index.html">Log</a> |
-      <a href="{{ host }}/{{ name }}/tree.html">Files</a> |
+      <a href="{{ host }}/{{ name }}/files.html">Files</a> |
       <a href="{{ host }}/{{ name }}/refs.html">Refs</a>
       {% if readme %}
-      | <a href="{{ host }}/{{ name }}/file/{{ readme.href }}">{{ readme.name }}</a>
+      | <a href="{{ host }}/{{ name }}/blob/{{ readme.href }}">{{ readme.name }}</a>
       {% endif %}
       {% if license %}
-      | <a href="{{ host }}/{{ name }}/file/{{ license.href }}">{{ license.name }}</a>
+      | <a href="{{ host }}/{{ name }}/blob/{{ license.href }}">{{ license.name }}</a>
       {% endif %}
     </div>
     </p>

templates/base/repo/tree.html Deleted

@@ -1,23 +0,0 @@
-{% extends "../template.html.include" %}
-
-{%- block title %}{{ name }} - Tree{% endblock -%}
-
-{% block content %}
-<table>
-  <thead>
-    <tr>
-      <td><b>Mode</b></td>
-      <td><b>Name</b></td>
-    </tr>
-  </thead>
-
-  <tbody>
-    {% for file in tree %}
-    <tr>
-      <td>{{ file.mode_symbolic }}</td>
-      <td><a href="{{ host }}/{{ name }}/file/{{ file.href }}">{{ file.path }}</a></td>
-    </tr>
-    {% endfor %}
-  </tbody>
-</table>
-{% endblock %}

templates/base/repo/foreach.tree.html Added

@@ -0,0 +1,29 @@
+{% extends "../template.html.include" %}
+
+{%- block title %}{{ name }} - {{ tree }}{% endblock %}
+
+{%- block content %}
+<p>{{ tree.mode_symbolic }} {{ tree }}</p><hr>
+
+<table>
+  <thead>
+    <tr>
+      <td><b>Mode</b></td>
+      <td><b>Name</b></td>
+    </tr>
+  </thead>
+
+  <tbody>
+    {% for child in tree.tree %}
+    <tr>
+      <td>{{ child.mode_symbolic }}</td>
+      {% if child.is_directory %}
+      <td><a href="{{ host }}/{{ name }}/tree/{{ child.href }}">{{ child.path }}</a></td>
+      {% else %}
+      <td><a href="{{ host }}/{{ name }}/blob/{{ child.href }}">{{ child.path }}</a></td>
+      {% endif %}
+    </tr>
+    {% endfor %}
+  </tbody>
+</table>
+{% endblock %}

templates/base/repo/foreach.commit.html Added

@@ -0,0 +1,40 @@
+{% extends "../template.html.include" %}
+
+{%- block title %}{{ name }} - {{ commit.title }}{% endblock %}
+
+{%- block content %}
+<pre><b>Commit</b> <a href="{{ host }}/{{ name }}/commit/{{ commit.id }}.html">{{ commit.id }}</a>
+<b>Parent:</b> <a href="{{ host }}/{{ name }}/commit/{{ commit.parent }}.html">{{ commit.parent }}</a>
+<b>Author:</b> {{ commit.author }} &lt;<a href="mailto:{{ commit.author_email }}">{{ commit.author_email }}</a>&gt;
+<b>Date:</b> {{ commit.authored }}
+<b>Committer:</b> {{ commit.committer }} &lt;<a href="mailto:{{ commit.committer_email }}">{{ commit.committer_email }}</a>&gt;
+<b>Committed:</b> {{ commit.committed }}
+
+{{ commit.message }}
+</pre>
+
+{%- for diff in commit.diffs -%}
+<div class="diff">
+  <p>
+    <b>{{ diff.new_file }}</b>
+    <span class="status">{{ diff.status }}</span>
+  </p>
+
+  {% if diff.newfile != diff.oldfile %}
+  <p>Renamed from {{ diff.old_file }}</p>
+  {% endif %}
+
+  {%- for hunk in diff.hunks -%}
+  {{ hunk.header }}
+
+  <pre>
+    {%- for line in hunk.lines -%}
+      <span class="{{ line.class }}">{{ line.text }}</span>
+    {%- endfor -%}
+  </pre>
+
+  {%- endfor -%}
+</div>
+{%- endfor -%}
+
+{% endblock %}

templates/base/repo/foreach.blob.html Added

@@ -0,0 +1,15 @@
+{% extends "../template.html.include" %}
+
+{%- block title %}{{ name }} - {{ blob }}{% endblock %}
+
+{%- block content %}
+<p>{{ blob.mode_symbolic }} {{ blob }}</p><hr>
+
+{% if blob.is_binary %}
+(File is binary)
+{% else %}
+<pre>{% for line in split(blob.contents, '\n') %}
+<a href="#l{{ loop.index }}" class="line" id="l{{ loop.index }}">{{ printf("%5v", loop.index) }}</a>  {{ line }}
+{% endfor %}</pre>
+{% endif %}
+{% endblock %}

templates/base/repo/files.html Added

@@ -0,0 +1,27 @@
+{% extends "../template.html.include" %}
+
+{%- block title %}{{ name }} - Tree{% endblock -%}
+
+{% block content %}
+<table>
+  <thead>
+    <tr>
+      <td><b>Mode</b></td>
+      <td><b>Name</b></td>
+    </tr>
+  </thead>
+
+  <tbody>
+    {% for file in tree %}
+    <tr>
+      <td>{{ file.mode_symbolic }}</td>
+      {% if file.is_directory %}
+      <td><a href="{{ host }}/{{ name }}/tree/{{ file.href }}">{{ file.path }}</a></td>
+      {% else %}
+      <td><a href="{{ host }}/{{ name }}/blob/{{ file.href }}">{{ file.path }}</a></td>
+      {% endif %}
+    </tr>
+    {% endfor %}
+  </tbody>
+</table>
+{% endblock %}

templates/base/repo/file.html Deleted

@@ -1,35 +0,0 @@
-{% extends "../template.html.include" %}
-
-{%- block title %}{{ name }} - {{ file }}{% endblock %}
-
-{%- block content %}
-<p>{{ file.mode_symbolic }} {{ file }}</p><hr>
-
-{% if file.is_directory %}
-<table>
-  <thead>
-    <tr>
-      <td><b>Mode</b></td>
-      <td><b>Name</b></td>
-    </tr>
-  </thead>
-
-  <tbody>
-    {% for child in file.tree %}
-    <tr>
-      <td>{{ child.mode_symbolic }}</td>
-      <td><a href="{{ host }}/{{ name }}/file/{{ child.href }}">{{ child.path }}</a></td>
-    </tr>
-    {% endfor %}
-  </tbody>
-</table>
-{% else %}
-  {% if file.is_binary %}
-  (File is binary)
-  {% else %}
-<pre>{% for line in split(file.contents, '\n') %}
-<a href="#l{{ loop.index }}" class="line" id="l{{ loop.index }}">{{ printf("%5v", loop.index) }}</a>  {{ line }}
-{% endfor %}</pre>
-{% endif %}
-{% endif %}
-{% endblock %}

templates/base/repo/commit.html Deleted

@@ -1,40 +0,0 @@
-{% extends "../template.html.include" %}
-
-{%- block title %}{{ name }} - {{ commit.title }}{% endblock %}
-
-{%- block content %}
-<pre><b>Commit</b> <a href="{{ host }}/{{ name }}/commit/{{ commit.id }}.html">{{ commit.id }}</a>
-<b>Parent:</b> <a href="{{ host }}/{{ name }}/commit/{{ commit.parent }}.html">{{ commit.parent }}</a>
-<b>Author:</b> {{ commit.author }} &lt;<a href="mailto:{{ commit.author_email }}">{{ commit.author_email }}</a>&gt;
-<b>Date:</b> {{ commit.authored }}
-<b>Committer:</b> {{ commit.committer }} &lt;<a href="mailto:{{ commit.committer_email }}">{{ commit.committer_email }}</a>&gt;
-<b>Committed:</b> {{ commit.committed }}
-
-{{ commit.message }}
-</pre>
-
-{%- for diff in commit.diffs -%}
-<div class="diff">
-  <p>
-    <b>{{ diff.new_file }}</b>
-    <span class="status">{{ diff.status }}</span>
-  </p>
-
-  {% if diff.newfile != diff.oldfile %}
-  <p>Renamed from {{ diff.old_file }}</p>
-  {% endif %}
-
-  {%- for hunk in diff.hunks -%}
-  {{ hunk.header }}
-
-  <pre>
-    {%- for line in hunk.lines -%}
-      <span class="{{ line.class }}">{{ line.text }}</span>
-    {%- endfor -%}
-  </pre>
-
-  {%- endfor -%}
-</div>
-{%- endfor -%}
-
-{% endblock %}

src/Types.hs Modified

@@ -14,7 +14,7 @@
 import Control.Monad.Trans.Reader (ReaderT)
 import Data.ByteString (ByteString)
 import Data.Default (def)
-import Data.Maybe (fromMaybe, listToMaybe)
+import Data.Maybe (listToMaybe)
 import Data.Tagged (untag)
 import qualified Data.Text as T
 import Data.Text.Encoding (decodeUtf8With)
@@ -214,15 +214,16 @@
 
 {-
 Next we have some data used to represent a repository's tree and the different kinds of
-objects contained therein.
+objects contained therein. A TreeEntry is any single entry found in a git tree object -
+a blob, a nested tree, or a commit (submodule reference).
 -}
-data TreeFile = TreeFile
-    { treeFilePath :: T.Text
-    , treeFileContents :: TreeFileContents
-    , treeFileMode :: TreeEntryMode
+data TreeEntry = TreeEntry
+    { treeEntryPath :: T.Text
+    , treeEntryContents :: TreeEntryContents
+    , treeEntryMode :: TreeEntryMode
     }
 
-data TreeFileContents = BinaryContents | FileContents ByteString | FolderContents [TreeFile]
+data TreeEntryContents = BinaryContents | FileContents ByteString | FolderContents [TreeEntry]
 
 data TreeEntryMode = ModeDirectory | ModePlain | ModeExecutable | ModeSymlink | ModeSubmodule
     deriving stock (Show)
@@ -240,7 +241,7 @@
 {-
 This
 -}
-getBlobContents :: Git.BlobOid LgRepo -> ReaderT LgRepo IO TreeFileContents
+getBlobContents :: Git.BlobOid LgRepo -> ReaderT LgRepo IO TreeEntryContents
 getBlobContents oid = do
     repo <- Git.getRepository
     blobPtr <- liftIO mallocForeignPtr
@@ -259,59 +260,63 @@
 GVal implementations for data definitions above, allowing commits to be rendered in
 Ginger templates.
 -}
-instance ToGVal m TreeFile where
-    toGVal :: TreeFile -> GVal m
-    toGVal treefile =
+instance ToGVal m TreeEntry where
+    toGVal :: TreeEntry -> GVal m
+    toGVal treeentry =
         def
-            { asHtml = html . treeFilePath $ treefile
-            , asText = treeFilePath treefile
-            , asLookup = Just . treeAsLookup $ treefile
+            { asHtml = html . treeEntryPath $ treeentry
+            , asText = treeEntryPath treeentry
+            , asLookup = Just . treeAsLookup $ treeentry
             , asBoolean = True -- Used for conditionally checking readme/license template variables.
             }
 
-instance ToGVal m TreeFileContents where
-    toGVal :: TreeFileContents -> GVal m
+instance ToGVal m TreeEntryContents where
+    toGVal :: TreeEntryContents -> GVal m
     toGVal BinaryContents = def
     toGVal (FileContents bytestring) = toGVal bytestring
-    toGVal (FolderContents treeFiles) =
+    toGVal (FolderContents treeEntries) =
         def
-            { asHtml = html . T.pack . show . fmap treeFilePath $ treeFiles
-            , asText = T.pack . show . fmap treeFilePath $ treeFiles
-            , asList = Just . fmap toGVal $ treeFiles
+            { asHtml = html . T.pack . show . fmap treeEntryPath $ treeEntries
+            , asText = T.pack . show . fmap treeEntryPath $ treeEntries
+            , asList = Just . fmap toGVal $ treeEntries
             }
 
-treeAsLookup :: TreeFile -> T.Text -> Maybe (GVal m)
-treeAsLookup treefile = \case
-    "path" -> Just . toGVal . treeFilePath $ treefile
-    "name" -> Just . toGVal . FP.takeFileName . T.unpack . treeFilePath $ treefile
-    "href" -> Just . toGVal . treePathToHref $ treefile
-    "contents" -> Just . toGVal . treeFileContents $ treefile
-    "tree" -> Just . toGVal . treeFileGetTree (treeFilePath treefile) . treeFileContents $ treefile
-    "tree_recursive" -> Just . toGVal . treeFileGetTreeRecursive . treeFileContents $ treefile
-    "mode" -> Just . toGVal . drop 4 . show . treeFileMode $ treefile
-    "mode_octal" -> Just . toGVal . modeToOctal . treeFileMode $ treefile
-    "mode_symbolic" -> Just . toGVal . modeToSymbolic . treeFileMode $ treefile
-    "is_binary" -> Just . toGVal . treeFileIsBinary $ treefile
-    "is_directory" -> Just . toGVal . treeFileIsDirectory $ treefile
+{-
+Recursively descend into a tree, keeping every entry along the way - blobs and trees
+alike - as a single flat list. Equivalent to `git ls-tree -r -t`.
+-}
+flattenTree :: TreeEntry -> [TreeEntry]
+flattenTree treeentry = case treeEntryContents treeentry of
+    FolderContents entries -> treeentry : concatMap flattenTree entries
+    _ -> [treeentry]
+
+treeAsLookup :: TreeEntry -> T.Text -> Maybe (GVal m)
+treeAsLookup treeentry = \case
+    "path" -> Just . toGVal . treeEntryPath $ treeentry
+    "name" -> Just . toGVal . FP.takeFileName . T.unpack . treeEntryPath $ treeentry
+    "href" -> Just . toGVal . treePathToHref $ treeentry
+    "contents" -> Just . toGVal . treeEntryContents $ treeentry
+    "tree" -> Just . toGVal . treeEntryGetTree . treeEntryContents $ treeentry
+    "entries" -> Just . toGVal . concatMap flattenTree . treeEntryGetTree . treeEntryContents $ treeentry
+    "mode" -> Just . toGVal . drop 4 . show . treeEntryMode $ treeentry
+    "mode_octal" -> Just . toGVal . modeToOctal . treeEntryMode $ treeentry
+    "mode_symbolic" -> Just . toGVal . modeToSymbolic . treeEntryMode $ treeentry
+    "is_binary" -> Just . toGVal . treeEntryIsBinary $ treeentry
+    "is_directory" -> Just . toGVal . treeEntryIsDirectory $ treeentry
     _ -> Nothing
   where
-    treeFileGetTree :: T.Text -> TreeFileContents -> [TreeFile]
-    treeFileGetTree parent (FolderContents fs) = filter atTop fs
-      where
-        atTop = notElem FP.pathSeparator . drop 1 . T.unpack . fromMaybe "" . T.stripPrefix parent . treeFilePath
-    treeFileGetTree _ _ = []
-
-    treeFileGetTreeRecursive :: TreeFileContents -> [TreeFile]
-    treeFileGetTreeRecursive (FolderContents fs) = fs
-    treeFileGetTreeRecursive _ = []
-
-    treeFileIsBinary :: TreeFile -> Bool
-    treeFileIsBinary treefile' = case treeFileContents treefile' of
+    -- The entries of this tree object - i.e. this directory's immediate children.
+    treeEntryGetTree :: TreeEntryContents -> [TreeEntry]
+    treeEntryGetTree (FolderContents fs) = fs
+    treeEntryGetTree _ = []
+
+    treeEntryIsBinary :: TreeEntry -> Bool
+    treeEntryIsBinary treeentry' = case treeEntryContents treeentry' of
         BinaryContents -> True
         _ -> False
 
-    treeFileIsDirectory :: TreeFile -> Bool
-    treeFileIsDirectory treefile' = case treeFileContents treefile' of
+    treeEntryIsDirectory :: TreeEntry -> Bool
+    treeEntryIsDirectory treeentry' = case treeEntryContents treeentry' of
         FolderContents _ -> True
         _ -> False
 
@@ -330,10 +335,10 @@
     modeToSymbolic ModeSubmodule = "git-module"
 
 {-
-Get the name of a tree file path's HTML file. Leading periods are dropped.
+Get the name of a tree entry path's HTML file. Leading periods are dropped.
 -}
-treePathToHref :: TreeFile -> T.Text
-treePathToHref = T.dropWhile (== '.') . flip T.append ".html" . T.replace "/" "." . treeFilePath
+treePathToHref :: TreeEntry -> T.Text
+treePathToHref = T.dropWhile (== '.') . flip T.append ".html" . T.replace "/" "." . treeEntryPath
 
 {-
 Data to store information about references: tags and branches.

src/Repositories.hs Modified

@@ -132,9 +132,11 @@
                 withRunInIO \runInIO -> do
                     -- Create the destination folders --
                     commitDir <- (directory </>) <$> parseRelDir "commit"
-                    fileDir <- (directory </>) <$> parseRelDir "file"
+                    blobDir <- (directory </>) <$> parseRelDir "blob"
+                    treeDir <- (directory </>) <$> parseRelDir "tree"
                     ensureDir commitDir
-                    ensureDir fileDir
+                    ensureDir blobDir
+                    ensureDir treeDir
 
                     -- Check which commits are new since the last run --
                     newCommits <- getUpdates commitDir commits
@@ -149,12 +151,21 @@
                     whenJust (envCommitTemplate env) \commitT -> do
                         mapM_ (gen force commitT "commit" commitDir commitHref) newCommits
 
-                    whenJust (envFileTemplate env) \fileT -> do
+                    whenJust (envBlobTemplate env) \blobT -> do
+                        let allBlobs = concatMap flattenFiles tree
                         if force
-                            then mapM_ (gen True fileT "file" fileDir fileHref) tree
+                            then mapM_ (gen True blobT "blob" blobDir blobHref) allBlobs
                             else
-                                let updatedFiles = getUpdatedFiles tree newCommits
-                                 in mapM_ (gen True fileT "file" fileDir fileHref) updatedFiles
+                                let updatedBlobs = getUpdatedFiles allBlobs newCommits
+                                 in mapM_ (gen True blobT "blob" blobDir blobHref) updatedBlobs
+
+                    whenJust (envTreeTemplate env) \treeT -> do
+                        -- A tree's own path never appears in a commit's diff (only
+                        -- the blobs within it do), so unlike blobs, staleness can't
+                        -- be judged by newCommits/getUpdatedFiles - always
+                        -- regenerate every tree page.
+                        let allTrees = concatMap flattenTrees tree
+                        mapM_ (gen True treeT "tree" treeDir treeHref) allTrees
 
                     -- Copy any static files/folders into the output directory --
                     envRepoCopyStatics env directory
@@ -174,7 +185,7 @@
     Path Rel Dir ->
     T.Text ->
     [Commit] ->
-    [TreeFile] ->
+    [TreeEntry] ->
     HashMap.HashMap T.Text (GVal RunRepo)
 package env repos name description commits tree =
     HashMap.fromList
@@ -183,16 +194,18 @@
         , ("name", toGVal . T.pack . init . toFilePath $ name)
         , ("description", toGVal description)
         , ("commits", toGVal commits)
-        , ("tree", toGVal . filter (notElem FP.pathSeparator . T.unpack . treeFilePath) $ tree)
-        , ("tree_recursive", toGVal tree)
+        , ("tree", toGVal tree)
+        , ("entries", toGVal . concatMap flattenTree $ tree)
+        , ("blobs", toGVal . concatMap flattenFiles $ tree)
+        , ("trees", toGVal . concatMap flattenTrees $ tree)
         , ("readme", toGVal . findFile "readme" $ tree)
         , ("license", toGVal . findFile "license" $ tree)
         ]
   where
     -- Find a file in the tree starting with the specified prefix. The prefix is looked
     -- for on the full path, so will only find files in the top level directory.
-    findFile :: T.Text -> [TreeFile] -> Maybe TreeFile
-    findFile prefix = find (T.isPrefixOf prefix . T.toLower . treeFilePath)
+    findFile :: T.Text -> [TreeEntry] -> Maybe TreeEntry
+    findFile prefix = find (T.isPrefixOf prefix . T.toLower . treeEntryPath)
 
 {-
 Collect commit history up to a head.
@@ -298,17 +311,17 @@
 Collect tree information for the given commit. Recurses on directories to list their
 contents.
 -}
-getTree :: Git.CommitOid LgRepo -> ReaderT LgRepo IO [TreeFile]
+getTree :: Git.CommitOid LgRepo -> ReaderT LgRepo IO [TreeEntry]
 getTree = getTree' "" 0 . Git.commitTree <=< Git.lookupCommit
   where
-    getTree' :: Git.TreeFilePath -> Int -> Git.TreeOid LgRepo -> ReaderT LgRepo IO [TreeFile]
+    getTree' :: Git.TreeFilePath -> Int -> Git.TreeOid LgRepo -> ReaderT LgRepo IO [TreeEntry]
     getTree' parent count toid = do
         one <- Git.lookupTree toid
         entries <- Git.listTreeEntries one
         let entries' = fmap (prependParent parent) entries
         contents <- mapM (\x -> getEntryContents x (count + 1)) entries'
         modes <- mapM (getEntryModes . snd) entries'
-        return $ zipWith3 TreeFile (fmap treePaths entries') contents modes
+        return $ zipWith3 TreeEntry (fmap treePaths entries') contents modes
 
     prependParent ::
         Git.TreeFilePath ->
@@ -317,7 +330,7 @@
     prependParent "" pathentry = pathentry
     prependParent parent (path, entry) = (mconcat [parent, "/", path], entry)
 
-    getEntryContents :: (Git.TreeFilePath, Git.TreeEntry LgRepo) -> Int -> ReaderT LgRepo IO TreeFileContents
+    getEntryContents :: (Git.TreeFilePath, Git.TreeEntry LgRepo) -> Int -> ReaderT LgRepo IO TreeEntryContents
     getEntryContents (_, Git.BlobEntry oid _) _ = getBlobContents oid
     getEntryContents (path, Git.TreeEntry oid) count = FolderContents <$> getTree' path count oid
     getEntryContents (_, Git.CommitEntry oid) _ = return . FileContents . B.fromString . show . untag $ oid
@@ -372,10 +385,20 @@
             (return [])
             ((x :) <$> go xs)
 
-getUpdatedFiles :: [TreeFile] -> [Commit] -> [TreeFile]
+flattenFiles :: TreeEntry -> [TreeEntry]
+flattenFiles treeentry = case treeEntryContents treeentry of
+    FolderContents files -> concatMap flattenFiles files
+    _ -> [treeentry]
+
+flattenTrees :: TreeEntry -> [TreeEntry]
+flattenTrees treeentry = case treeEntryContents treeentry of
+    FolderContents files -> treeentry : concatMap flattenTrees files
+    _ -> []
+
+getUpdatedFiles :: [TreeEntry] -> [Commit] -> [TreeEntry]
 getUpdatedFiles [] _ = []
 getUpdatedFiles _ [] = []
-getUpdatedFiles files commits = filter ((`elem` updated) . treeFilePath) files
+getUpdatedFiles files commits = filter ((`elem` updated) . treeEntryPath) files
   where
     updated :: [T.Text]
     updated = fmap (bsToText . diffNewFile) . concatMap commitDiffs $ commits
@@ -425,8 +448,11 @@
 commitHref :: Commit -> FilePath
 commitHref = (++ ".html") . commitHash
 
-fileHref :: TreeFile -> FilePath
-fileHref = T.unpack . treePathToHref
+blobHref :: TreeEntry -> FilePath
+blobHref = T.unpack . treePathToHref
+
+treeHref :: TreeEntry -> FilePath
+treeHref = T.unpack . treePathToHref
 
 {-
 With a dictionary of preloaded values and a function to access additional data, create a

src/Env.hs Modified

@@ -68,7 +68,8 @@
     { envConfig :: Config
     , envIndexTemplates :: [Template]
     , envCommitTemplate :: Maybe Template
-    , envFileTemplate :: Maybe Template
+    , envBlobTemplate :: Maybe Template
+    , envTreeTemplate :: Maybe Template
     , envRepoTemplates :: [Template]
     , envOutput :: Path Abs Dir
     , envRepos :: [Path Abs Dir]
@@ -104,16 +105,17 @@
 
     -- Load files from template directory
     indexT <- collectTemplates files
-    commitT <- findTemplate "commit.html" filesRepo
-    fileT <- findTemplate "file.html" filesRepo
+    commitT <- findTemplate "foreach.commit.html" filesRepo
+    blobT <- findTemplate "foreach.blob.html" filesRepo
+    treeT <- findTemplate "foreach.tree.html" filesRepo
     repoT <-
         collectTemplates
-            . filter (flip notElem ["commit.html", "file.html"] . toFilePath . filename)
+            . filter (flip notElem ["foreach.commit.html", "foreach.blob.html", "foreach.tree.html"] . toFilePath . filename)
             $ filesRepo
 
     -- Exit early if we didn't find any templates
     when
-        ( all null [indexT, repoT] && all isNothing [commitT, fileT]
+        ( all null [indexT, repoT] && all isNothing [commitT, blobT, treeT]
         )
         $ die "No templates were found."
 
@@ -123,7 +125,8 @@
             { envConfig = config
             , envIndexTemplates = indexT
             , envCommitTemplate = commitT
-            , envFileTemplate = fileT
+            , envBlobTemplate = blobT
+            , envTreeTemplate = treeT
             , envRepoTemplates = repoT
             , envOutput = output
             , envRepos = repos

DOCUMENTATION.md Modified

@@ -67,7 +67,7 @@
 otherwise the ginger docs can be very helpful to see what is supported.
 
 Templates are a folder containing a number of ginger template files.  There are
-4 "scopes", each making available a unique set of variables storing information
+5 "scopes", each making available a unique set of variables storing information
 about the git repositories. Each template file has access to a single one of
 these scopes. The structure of the template folder determines the scopes of the
 files contained therein.
@@ -82,8 +82,9 @@
         repo/
             inside_this_folder.html
             two_names_are_special.html
-            file.html
-            commit.html
+            foreach.blob.html
+            foreach.commit.html
+            foreach.tree.html
 
 The top-level folder, here `template`, is that which is specified in the config
 file.
@@ -98,9 +99,14 @@
 information pertaining to a single git repository. The template files contained
 within this folder are parsed and output once per git repository.
 
-The exceptions to this are the two special template files with the names
-"file.html" and "commit.html". These have access to the *file scope* and
-*commit scope* respectively, and are parsed and output once per file or commit.
+The exceptions to this are the three special template files with the names
+"foreach.blob.html", "foreach.commit.html" and "foreach.tree.html" - named to
+make clear that, unlike everything else in "repo/", these are parsed and
+output once *per* blob, commit or tree found anywhere in the repository, at
+any depth, rather than once per repository. These have access to the *blob
+scope*, *commit scope* and *tree scope* respectively (a submodule reference
+is currently treated as a blob whose content is its target commit hash,
+rather than as its own scope).
 
 The resulting folder structure found in `output` will look like this (if
 `repos` only contains gitja):
@@ -112,7 +118,7 @@
         gitja/
             inside_this_folder.html
             two_names_are_special.html
-            file/
+            blob/
                 LICENSE.html
                 Makefile.html
                 ...
@@ -120,6 +126,9 @@
                 0a18f38bb5c398bd192a6268281fc6abefaedd63.html
                 0a7601059956d9c4d395f5d08e8cf48a515d080f.html
                 ...
+            tree/
+                src.html
+                ...
         ...
 
 ### Static files
@@ -145,16 +154,20 @@
 |        | name            | The repository name, taken from its folder name.      |
 |        | description     | The repository's description (see below).             |
 |        | commits         | A list of the repository's commits.                   |
-|        | tree            | A list of the top-level folder's contents.            |
-|        | tree\_recursive | A list of *all* of the repository's contents.         |
+|        | tree            | A list of the repository root tree's entries.         |
+|        | entries         | A flat list of *every* entry (blob or tree) at any depth in the repository, like `git ls-tree -r -t`. |
+|        | blobs           | A flat list of *every* blob (file) in the repository. |
+|        | trees           | A flat list of *every* tree (directory) at any depth in the repository. |
 |        | tags            | A list of the refs corresponding to tags.             |
 |        | branches        | A list of the refs corresponding to branches.         |
 |        | readme          | The repository's readme file, if it has one.          |
 |        | license         | The repository's license file, if it has one.         |
-| File   |                 | *In addition to the variables from the Repo scope...* |
-|        | file            | A single file.                                        |
+| Blob   |                 | *In addition to the variables from the Repo scope...* |
+|        | blob            | A single blob (file).                                 |
 | Commit |                 | *In addition to the variables from the Repo scope...* |
 |        | commit          | A single commit.                                      |
+| Tree   |                 | *In addition to the variables from the Repo scope...* |
+|        | tree            | A single tree (directory) - shadows the repo-scope `tree`, since a tree page is itself scoped to one tree. |
 
 As in [Jinja](https://jinja.palletsprojects.com), a list can be accessed with
 indexing, and attributes can be accessed using a dot notation. For example, a
@@ -170,17 +183,17 @@
 |            | description      | The repository's description (see below).                |
 |            | head             | The current git commit.                                  |
 |            | updated          | The time when the current commit was committed.          |
-| file       | path             | The path the file relative to the repository root.       |
-|            | name             | The name of the file.                                    |
-|            | href             | The name of the HTML file for this file.                 |
-|            | contents         | The file's contents.                                     |
+| blob/tree  | path             | The path relative to the repository root.                |
+|            | name             | The name of the blob or tree.                             |
+|            | href             | The name of the HTML file for this blob or tree.          |
+|            | contents         | The blob's contents (n/a for a tree).                      |
 |            | mode             | Directory, Plain, Executable, Symlink or Submodule.      |
 |            | mode\_octal      | Mode in octal form e.g. "00644" for plain files.         |
 |            | mode\_symbolic   | Mode in symbolic form e.g. ""-rw-r--r--" for plain files.|
 |            | is\_directory    | A boolean, useful for ginger conditionals.               |
 |            | is\_binary       | A boolean, tells you if the contents can be rendered.    |
-|            | tree             | A list of a directory's direct contents.                 |
-|            | tree\_recursive  | A list of *all* of a directory's contents.               |
+|            | tree             | This tree's direct contents (n/a for a blob).             |
+|            | entries          | A flat list of *every* entry (blob or tree) at any depth under this tree (n/a for a blob). |
 | ref        | name             | The tag or branch name.                                  |
 |            | commit           | The commit pointed to by the tag or branch.              |
 | commit     | id               | The SHA of the given commit.                             |