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

Commit 343b0d5aff98ca8f204751ae82b4b64277a8fb24
Parent: 0292014748caae952bbc8dd6225680d83c0a5135
Author: mcol <mcol@posteo.net>
Date: 2021-11-25 14:29:19 +0000
Committer: mcol <mcol@posteo.net>
Committed: 2021-11-25 14:29:19 +0000

Redesign templating logic

- replaces most uses of filepath with path and path-io.
- changes expected template dir structure
- lets mutliple index scope templates to be used

Closes #9
Closes #7

templates/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/repo/tree.html Added

@@ -0,0 +1,29 @@
+{% 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/repo/repo-table-header.html.include Added

@@ -0,0 +1,24 @@
+{#- This block is rendered at the top of every page. -#}
+
+<table id="head">
+  <tr>
+    <td width="32px" rowspan="2"><a href="{{ host }}"><img src="{{ host }}/avatar.png" id="avatar"/></a></td>
+    <td rowspan="2"><a href="{{ host }}">{{ host }}</a> / <a href="{{ host }}">code</a> / <a href="{{ host }}/{{ name }}">{{ name }}</a></td>
+    <td class="link"><a href="{{ host }}/posts.html">posts</a></td>
+  </tr>
+  <tr>
+    <td class="link"><a href="{{ host }}/about.html">about</a></td>
+  </tr>
+</table>
+<hr>
+
+<table>
+  <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 %}
+  </td></tr>
+</table>
+<hr>

templates/repo/refs.html Added

@@ -0,0 +1,37 @@
+{% extends "../template.html.include" %}
+
+{%- block title -%}
+{{ name }} - Refs
+{%- endblock -%}
+
+{% block body %}
+{% include  "repo-table-header.html.include" %}
+
+<div id="content">
+  <h2>Branches</h2>
+  <table id="branches">
+    <thead>
+      <tr><td><b>Name</b></td><td colspan="2"><b>Last commit</b></td><td><b>Author</b></td></tr>
+    </thead>
+    <tbody>
+      {% for branch in branches %}
+      <tr><td>{{ branch.name }}</td><td>{{ branch.authored[:16] }}</td><td>{{ branch.title }}</td><td>{{ branch.author }}</td></tr>
+      {% endfor %}
+    </tbody>
+  </table>
+
+  <br>
+
+  <h2>Tags</h2>
+  <table id="tags">
+    <thead>
+      <tr><td><b>Name</b></td><td colspan="2"><b>Last commit</b></td><td><b>Author</b></td></tr>
+    </thead>
+    <tbody>
+      {% for tag in tags %}
+      <tr><td>{{ tag.name }}</td><td>{{ tag.authored[:16] }}</td><td>{{ tag.title }}</td><td>{{ tag.author }}</td></tr>
+      {% endfor %}
+    </tbody>
+  </table>
+</div>
+{% endblock %}

templates/repo/index.html Added

@@ -0,0 +1,27 @@
+{% extends "../template.html.include" %}
+
+{%- block title -%}
+{{ description }}
+{%- endblock -%}
+
+{% block body %}
+{% include "repo-table-header.html.include" %}
+
+<div id="content">
+  <table id="log">
+    <thead>
+      <tr>
+        <td><b>Date</b></td>
+        <td><b>Commit message</b></td>
+        <td><b>Author</b></td>
+      </tr>
+    </thead>
+
+    <tbody>
+      {% for commit in commits[:50] %}
+      <tr><td>{{ commit.authored[:16] }}</td><td><a href="commit/{{ commit.id }}.html">{{ commit.title }}</a></td><td>{{ commit.author }}</td></tr>
+      {% endfor %}
+    </tbody>
+  </table>
+</div>
+{% endblock %}

templates/repo/file.html Added

@@ -0,0 +1,39 @@
+{% 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 %}
+  <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 %}
+{% endif %}
+  </pre>
+</div>
+{% endblock %}

templates/repo/commit.html Added

@@ -0,0 +1,21 @@
+{% 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>
+</div>
+{% endblock %}

templates/repo-table-header.html.include Deleted

@@ -1,24 +0,0 @@
-{#- This block is rendered at the top of every page. -#}
-
-<table id="head">
-  <tr>
-    <td width="32px" rowspan="2"><a href="{{ host }}"><img src="{{ host }}/avatar.png" id="avatar"/></a></td>
-    <td rowspan="2"><a href="{{ host }}">{{ host }}</a> / <a href="{{ host }}">code</a> / <a href="{{ host }}/{{ name }}">{{ name }}</a></td>
-    <td class="link"><a href="{{ host }}/posts.html">posts</a></td>
-  </tr>
-  <tr>
-    <td class="link"><a href="{{ host }}/about.html">about</a></td>
-  </tr>
-</table>
-<hr>
-
-<table>
-  <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 %}
-  </td></tr>
-</table>
-<hr>

templates/refs.html Deleted

@@ -1,37 +0,0 @@
-{% extends "template.html.include" %}
-
-{%- block title -%}
-{{ name }} - Refs
-{%- endblock -%}
-
-{% block body %}
-{% include  "repo-table-header.html.include" %}
-
-<div id="content">
-  <h2>Branches</h2>
-  <table id="branches">
-    <thead>
-      <tr><td><b>Name</b></td><td colspan="2"><b>Last commit</b></td><td><b>Author</b></td></tr>
-    </thead>
-    <tbody>
-      {% for branch in branches %}
-      <tr><td>{{ branch.name }}</td><td>{{ branch.authored[:16] }}</td><td>{{ branch.title }}</td><td>{{ branch.author }}</td></tr>
-      {% endfor %}
-    </tbody>
-  </table>
-
-  <br>
-
-  <h2>Tags</h2>
-  <table id="tags">
-    <thead>
-      <tr><td><b>Name</b></td><td colspan="2"><b>Last commit</b></td><td><b>Author</b></td></tr>
-    </thead>
-    <tbody>
-      {% for tag in tags %}
-      <tr><td>{{ tag.name }}</td><td>{{ tag.authored[:16] }}</td><td>{{ tag.title }}</td><td>{{ tag.author }}</td></tr>
-      {% endfor %}
-    </tbody>
-  </table>
-</div>
-{% endblock %}

templates/index.html Modified

@@ -1,25 +1,26 @@
 {% extends "template.html.include" %}
 
 {%- block title -%}
-{{ description }}
+gitserve - Index
 {%- endblock -%}
 
 {% block body %}
-{% include "repo-table-header.html.include" %}
+<table id="head">
+  <tr><td width="32px" rowspan="2"><a href="{{ host }}"><img src="{{ host }}/avatar.png" id="avatar"/></a></td>
+    <td rowspan="2"><a href="{{ host }}">{{ host }}</a> / <a href="{{ host }}">code</a></td><td class="link"><a href="{{ host }}/posts.html">posts</a></td></tr><tr><td class="link"><a href="{{ host }}/about.html">about</a></td></tr>
+</table>
+
+<hr>
 
 <div id="content">
-  <table id="log">
+  <table id="index">
     <thead>
-      <tr>
-        <td><b>Date</b></td>
-        <td><b>Commit message</b></td>
-        <td><b>Author</b></td>
-      </tr>
+      <tr><td><b>Name</b></td><td><b>Description</b></td><td><b>Last commit</b></td></tr>
     </thead>
 
     <tbody>
-      {% for commit in commits[:50] %}
-      <tr><td>{{ commit.authored[:16] }}</td><td><a href="commit/{{ commit.id }}.html">{{ commit.title }}</a></td><td>{{ commit.author }}</td></tr>
+      {% for repo in repositories %}
+      <tr><td><a href="{{ repo.name }}">{{ repo.name }}</a></td><td>{{ repo.description }}</td><td>{{ repo.updated[:16] }}</td></tr>
       {% endfor %}
     </tbody>
   </table>

templates/index-main.html Deleted

@@ -1,28 +0,0 @@
-{% extends "template.html.include" %}
-
-{%- block title -%}
-gitserve - Index
-{%- endblock -%}
-
-{% block body %}
-<table id="head">
-  <tr><td width="32px" rowspan="2"><a href="{{ host }}"><img src="{{ host }}/avatar.png" id="avatar"/></a></td>
-    <td rowspan="2"><a href="{{ host }}">{{ host }}</a> / <a href="{{ host }}">code</a></td><td class="link"><a href="{{ host }}/posts.html">posts</a></td></tr><tr><td class="link"><a href="{{ host }}/about.html">about</a></td></tr>
-</table>
-
-<hr>
-
-<div id="content">
-  <table id="index">
-    <thead>
-      <tr><td><b>Name</b></td><td><b>Description</b></td><td><b>Last commit</b></td></tr>
-    </thead>
-
-    <tbody>
-      {% for repo in repositories %}
-      <tr><td><a href="{{ repo.name }}">{{ repo.name }}</a></td><td>{{ repo.description }}</td><td>{{ repo.updated[:16] }}</td></tr>
-      {% endfor %}
-    </tbody>
-  </table>
-</div>
-{% endblock %}

templates/file.html Deleted

@@ -1,39 +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 %}
-  <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 %}
-{% endif %}
-  </pre>
-</div>
-{% endblock %}

templates/commit.html Deleted

@@ -1,21 +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>
-</div>
-{% endblock %}

src/Types.hs Modified

@@ -21,7 +21,7 @@
 import Data.Text.Encoding.Error (lenientDecode)
 import Git
 import Git.Libgit2 (LgRepo)
-import System.FilePath (takeFileName)
+import Path (Abs, Dir, Path, dirname, toFilePath)
 import Text.Ginger.GVal (GVal, ToGVal, asBoolean, asHtml, asList, asLookup, asText, toGVal)
 import Text.Ginger.Html (Html, html)
 import Text.Ginger.Parse (SourcePos)
@@ -37,23 +37,27 @@
 as well as in a list of all repositories in the index template.
 -}
 data Repo = Repo
-    { repositoryPath :: FilePath
+    { repositoryPath :: Path Abs Dir
     , repositoryDescription :: Text
     , repositoryHead :: Maybe (Commit LgRepo)
     }
 
+instance ToGVal m (Path b t) where
+    toGVal :: Path b t -> GVal m
+    toGVal = toGVal . toFilePath
+
 instance ToGVal m Repo where
     toGVal :: Repo -> GVal m
     toGVal repo =
         def
-            { asHtml = html . pack . show . takeFileName . repositoryPath $ repo
-            , asText = pack . show . takeFileName . repositoryPath $ repo
+            { asHtml = html . pack . show . dirname . repositoryPath $ repo
+            , asText = pack . show . dirname . repositoryPath $ repo
             , asLookup = Just . repoAsLookup $ repo
             }
 
 repoAsLookup :: Repo -> Text -> Maybe (GVal m)
 repoAsLookup repo = \case
-    "name" -> Just . toGVal . takeFileName . repositoryPath $ repo
+    "name" -> Just . toGVal . dirname . repositoryPath $ repo
     "description" -> Just . toGVal . repositoryDescription $ repo
     "head" -> Just . toGVal . repositoryHead $ repo
     "updated" -> toGVal . show . signatureWhen . commitCommitter <$> repositoryHead repo

src/Templates.hs Modified

@@ -1,36 +1,25 @@
 {-# LANGUAGE LambdaCase #-}
+{-# LANGUAGE OverloadedStrings #-}
 
 module Templates (
-    -- The Env data type and its constructors.
-    Env,
-    envConfig,
-    envTemplates,
-    envIndexTemplate,
-    envCommitTemplate,
-    envFileTemplate,
-    envForce,
-    -- The Template data type and its constructors.
-    Template,
-    templatePath,
-    templateGinger,
-    -- The entrypoint used by main.
-    loadTemplates,
-    -- The core functionality for which templates are used.
+    Env (..),
+    Template (..),
+    loadEnv,
     generate,
 ) where
 
-import Control.Monad (void, (<=<))
+import Control.Monad (join, void, (<=<))
 import Control.Monad.IO.Class (liftIO)
 import Control.Monad.Trans.Reader (ReaderT)
-import Data.Char (toLower)
 import qualified Data.HashMap.Strict as HashMap
-import Data.List (isSuffixOf)
+import Data.List (find)
 import Data.Maybe (catMaybes)
 import Data.Text (Text, unpack)
 import Git.Libgit2 (LgRepo)
-import Path (Abs, Dir, File, Path, dirname, filename, parseAbsDir, toFilePath, (</>))
-import Path.IO (copyDirRecur, copyFile, ensureDir, listDir)
-import System.Directory (makeAbsolute)
+import Path (Abs, Dir, File, Path, Rel, dirname, filename, parseAbsDir, toFilePath, (</>))
+import Path.IO (copyDirRecur, copyFile, doesDirExist, ensureDir, listDir)
+import System.Directory (canonicalizePath)
+import qualified System.FilePath as FP
 import System.IO.Error (tryIOError)
 import qualified Text.Ginger.AST as G
 import Text.Ginger.GVal (GVal)
@@ -38,7 +27,7 @@
 import Text.Ginger.Parse (ParserError (..), SourcePos, parseGingerFile)
 import Text.Ginger.Run (easyContext, runGingerT)
 
-import Config
+import Config (Config (..))
 import Types
 
 {-
@@ -47,136 +36,116 @@
 -}
 data Env = Env
     { envConfig :: Config
-    , envTemplates :: [Template]
-    , envIndexTemplate :: Maybe Template
+    , envIndexTemplates :: [Template]
     , envCommitTemplate :: Maybe Template
     , envFileTemplate :: Maybe Template
+    , envRepoTemplates :: [Template]
+    , envOutputDirectory :: Path Abs Dir
+    , envRepoPaths :: [Path Abs Dir]
+    , envHost :: Text
     , envForce :: Bool
     }
 
 data Template = Template
-    { templatePath :: FilePath
+    { templatePath :: Path Rel File
     , templateGinger :: G.Template SourcePos
     }
 
 {-
 This creates the runtime environment, collecting the config and loading template data
-from file.
+from the template directory.
 -}
-loadTemplates :: Bool -> Config -> IO Env
-loadTemplates force config = do
+loadEnv :: Bool -> Config -> IO Env
+loadEnv force config = do
+    -- First ensure that the output directory exists
+    output <- parseAbsDir <=< canonicalizePath . outputDirectory $ config
+    ensureDir output
+
+    -- Parse repos for env
+    repoPaths' <- mapM (parseAbsDir <=< canonicalizePath) . repoPaths $ config
+
+    -- Find template files, copying the static files as is
+    (dirs, files) <- ls . templateDirectory $ config
+    (_, filesRepo) <- ls $ templateDirectory config FP.</> "repo"
+    copyStaticDirs output dirs
+    copyStaticFiles output files
+
     -- Load files from template directory
-    templateFiles <- getFiles config
-    templates <- mapM loadTemplate (fmap toFilePath templateFiles)
-    -- Scoped templates
-    indexT <- loadTemplate . indexTemplate $ config
-    commitT <- loadTemplate . commitTemplate $ config
-    fileT <- loadTemplate . fileTemplate $ config
+    indexT <- collectTemplates files
+    commitT <- fmap join . mapM loadTemplate . find ((==) "commit.html" . toFilePath . filename) $ filesRepo
+    fileT <- fmap join . mapM loadTemplate . find ((==) "file.html" . toFilePath . filename) $ filesRepo
+    repoT <- collectTemplates . filter (flip notElem ["commit.html", "file.html"] . toFilePath . filename) $ filesRepo
+
     -- Global environment
     return
         Env
             { envConfig = config
-            , envTemplates = catMaybes templates
-            , envIndexTemplate = indexT
+            , envIndexTemplates = indexT
             , envCommitTemplate = commitT
             , envFileTemplate = fileT
+            , envRepoTemplates = repoT
+            , envOutputDirectory = output
+            , envRepoPaths = repoPaths'
+            , envHost = host config
             , envForce = force
             }
+  where
+    collectTemplates :: [Path Abs File] -> IO [Template]
+    collectTemplates = fmap catMaybes . mapM loadTemplate . filter ((==) ".html" . FP.takeExtension . toFilePath)
+
+    ls :: FilePath -> IO ([Path Abs Dir], [Path Abs File])
+    ls dir = do
+        canon <- parseAbsDir =<< canonicalizePath dir
+        exists <- doesDirExist canon
+        if exists
+            then listDir canon
+            else return ([], [])
+
+    copyStaticDirs :: Path Abs Dir -> [Path Abs Dir] -> IO ()
+    copyStaticDirs output = mapM_ (\p -> copyDirRecur p (output </> dirname p)) . filterStaticDirs
+    filterStaticDirs = filter ((/=) "repo" . toFilePath)
+
+    copyStaticFiles :: Path Abs Dir -> [Path Abs File] -> IO ()
+    copyStaticFiles output = mapM_ (\p -> copyFile p (output </> filename p)) . filterStaticFiles
+    filterStaticFiles = filter (flip notElem [".html", ".include"] . FP.takeExtension . toFilePath)
 
 {-
 This is the generator function that receives repository-specific variables and uses
 Ginger to render templates using them.
 -}
 generate ::
-    FilePath ->
+    Path Abs File ->
     HashMap.HashMap Text (GVal RunRepo) ->
     Template ->
     ReaderT LgRepo IO ()
 generate output context template = do
-    liftIO $ writeFile output "" -- Clear contents of file if it exists
-    void . runGingerT (easyContext (writeTo output) context) . templateGinger $ template
-
-----------------------------------------------------------------------------------------
--- Private -----------------------------------------------------------------------------
+    let output' = toFilePath output
+    liftIO $ writeFile output' "" -- Clear contents of file if it exists
+    void . runGingerT (easyContext (writeTo output') context) . templateGinger $ template
+  where
+    -- This function gets the output HTML data and is responsible for saving it to file.
+    writeTo :: FilePath -> Html -> ReaderT LgRepo IO ()
+    writeTo path = liftIO . appendFile path . unpack . htmlSource
 
 {-
 This takes the session's `Config` and maybe returns a loaded template for the
 ``indexTemplate`` setting.
 -}
-loadTemplate :: FilePath -> IO (Maybe Template)
+loadTemplate :: Path Abs File -> IO (Maybe Template)
 loadTemplate path =
-    parseGingerFile includeResolver path >>= \case
-        Right parsed -> return . Just . Template path $ parsed
+    parseGingerFile includeResolver (toFilePath path) >>= \case
+        Right parsed -> return . Just . Template (filename path) $ parsed
         Left err -> do
-            informError err
+            informError (toFilePath path) err
             return Nothing
   where
     -- An attempt at pretty printing the error message.
-    informError :: ParserError -> IO ()
-    informError (ParserError msg Nothing) =
-        putStr $ "Template error: " <> path <> "\n" <> indent msg
-    informError (ParserError msg (Just pos)) =
-        putStrLn $ "Template error: " <> path <> "\n" <> indent (show pos <> "\n" <> msg)
+    informError p (ParserError msg Nothing) =
+        putStr $ "Template error: " <> p <> "\n" <> indent msg
+    informError p (ParserError msg (Just pos)) =
+        putStrLn $ "Template error: " <> p <> "\n" <> indent (show pos <> "\n" <> msg)
     indent = unlines . map (mappend "    ") . lines
 
-{-
-This resolves Ginger's template includes.
--}
-includeResolver :: FilePath -> IO (Maybe String)
-includeResolver path = either (const Nothing) Just <$> tryIOError (readFile path)
-
-{-
-getFiles will look inside the template directory and generate a list of paths to likely
-valid template files and another for static files.
-
-It's not very pretty, but we also copy the static files and folders from here because
-I'm too lazy to work with Path too much.
--}
-getFiles :: Config -> IO [Path Abs File]
-getFiles config = do
-    (dirs, files) <- ls . templateDirectory $ config
-    let files' = filter (not . isSuffixOf "include" . toFilePath) files
-    let templates = filter (isTemplate config) files'
-    let statics = filter (`notElem` templates) files'
-    output <- parseAbsDir <=< makeAbsolute . outputDirectory $ config
-    ensureDir output
-    copyStaticDirs output dirs
-    copyStaticFiles output statics
-    return templates
-  where
-    -- This gets fully qualified paths of the directory's contents
-    ls :: FilePath -> IO ([Path Abs Dir], [Path Abs File])
-    ls = listDir <=< parseAbsDir <=< makeAbsolute
-
-{-
-This is used to filter files in the template directory so that we only try to load HTML
-files.
--}
-isTemplate :: Config -> Path Abs File -> Bool
-isTemplate config = isTemplate' . map toLower . toFilePath
-  where
-    isTemplate' :: FilePath -> Bool
-    isTemplate' p = isSuffixOf "html" p && not (isTargeted config p)
-    isTargeted :: Config -> FilePath -> Bool
-    isTargeted c p = p `elem` [indexTemplate c, commitTemplate c, fileTemplate c]
-
-{-
-These copies static files/folders into the output folder unchanged.
--}
-copyStaticFiles :: Path Abs Dir -> [Path Abs File] -> IO ()
-copyStaticFiles output = mapM_ copy
-  where
-    copy :: Path Abs File -> IO ()
-    copy path = copyFile path $ output </> filename path
-
-copyStaticDirs :: Path Abs Dir -> [Path Abs Dir] -> IO ()
-copyStaticDirs output = mapM_ copy
-  where
-    copy :: Path Abs Dir -> IO ()
-    copy path = copyDirRecur path $ output </> dirname path
-
-{-
-This function gets the output HTML data and is responsible for saving it to file.
--}
-writeTo :: FilePath -> Html -> ReaderT LgRepo IO ()
-writeTo path = liftIO . appendFile path . unpack . htmlSource
+    -- This resolves template 'includes'.
+    includeResolver :: FilePath -> IO (Maybe String)
+    includeResolver p = either (const Nothing) Just <$> tryIOError (readFile p)

src/Repositories.hs Modified

@@ -22,13 +22,13 @@
 import Data.Text.Encoding.Error (lenientDecode)
 import Git
 import Git.Libgit2 (LgRepo, lgFactory)
-import System.Directory (createDirectoryIfMissing, doesFileExist)
-import System.FilePath (takeFileName, (</>))
+import Path (Abs, Dir, File, Path, Rel, dirname, parseRelDir, parseRelFile, toFilePath, (</>))
+import Path.IO (doesFileExist, ensureDir)
+import qualified System.FilePath as FP
 import System.IO.Error (tryIOError)
 import Text.Ginger.GVal (GVal, ToGVal, toGVal)
 
-import Config
-import Templates
+import Templates (Env (..), Template (..), generate)
 import Types
 
 {-
@@ -46,16 +46,15 @@
 -}
 loadRepos :: Env -> IO [Repo]
 loadRepos env = do
+    let paths = envRepoPaths env
     descs <- mapM getDescription paths
-    return $ ($ Nothing) <$> zipWith Repo paths descs
-  where
-    paths = repoPaths . envConfig $ env
+    return . fmap ($ Nothing) . zipWith Repo paths $ descs
 
 {-
 Pass the repository's folder, get its description.
 -}
-getDescription :: FilePath -> IO Text
-getDescription = fmap (fromRight "") . tryIOError . fmap pack . readFile . (</> "description")
+getDescription :: Path Abs Dir -> IO Text
+getDescription = fmap (fromRight "") . tryIOError . fmap pack . readFile . flip FP.combine "description" . toFilePath
 
 {-
 This receives a file path to a single repository and tries to process it. If the
@@ -63,16 +62,16 @@
 (after informing the user of course).
 -}
 processRepo :: Env -> Repo -> IO Repo
-processRepo env repo = withRepository lgFactory (repositoryPath repo) $ processRepo' env repo
+processRepo env repo = withRepository lgFactory (toFilePath . repositoryPath $ repo) $ processRepo' env repo
 
 processRepo' :: Env -> Repo -> ReaderT LgRepo IO Repo
 processRepo' env repo = do
-    let name = takeFileName . repositoryPath $ repo
-    let output = outputDirectory (envConfig env) </> name
+    let name = dirname . repositoryPath $ repo
+    let output = envOutputDirectory env </> name
 
     resolveReference "HEAD" >>= \case
         Nothing -> do
-            liftIO . putStrLn $ "gitserve: " <> name <> ": Failed to resolve HEAD."
+            liftIO . putStrLn $ "gitserve: " <> show name <> ": Failed to resolve HEAD."
             return repo
         Just commitID -> do
             let gitHead = Tagged commitID
@@ -85,12 +84,14 @@
             let scope = package env name (repositoryDescription repo) commits tree tags branches
 
             -- Create the destination folders
-            liftIO . createDirectoryIfMissing True $ output </> "commit"
-            liftIO . createDirectoryIfMissing True $ output </> "file"
+            commitDir <- liftIO . parseRelDir $ "commit"
+            fileDir <- liftIO . parseRelDir $ "file"
+            liftIO . ensureDir $ output </> commitDir
+            liftIO . ensureDir $ output </> fileDir
 
             -- Run the generator --
             let force = envForce env
-            mapM_ (genRepo output scope) $ envTemplates env
+            mapM_ (genRepo output scope) $ envRepoTemplates env
             mapM_ (genTarget output scope force $ envCommitTemplate env) commits
             mapM_ (genTarget output scope True $ envFileTemplate env) tree -- TODO: detect file changes
 
@@ -105,7 +106,7 @@
 -}
 package ::
     Env ->
-    FilePath ->
+    Path Rel Dir ->
     Text ->
     [Commit LgRepo] ->
     [TreeFile] ->
@@ -114,8 +115,8 @@
     HashMap.HashMap Text (GVal RunRepo)
 package env name description commits tree tags branches =
     HashMap.fromList
-        [ ("host", toGVal . host . envConfig $ env)
-        , ("name", toGVal . pack $ name)
+        [ ("host", toGVal . envHost $ env)
+        , ("name", toGVal . pack . init . toFilePath $ name)
         , ("description", toGVal description)
         , ("commits", toGVal . reverse $ commits) -- Could be optimised
         , ("tree", toGVal tree)
@@ -200,13 +201,13 @@
     dropName Nothing _ = Nothing
 
 genRepo ::
-    FilePath ->
+    Path Abs Dir ->
     HashMap.HashMap Text (GVal RunRepo) ->
     Template ->
     ReaderT LgRepo IO ()
 genRepo output scope template = generate output' scope template
   where
-    output' = (</>) output . takeFileName . templatePath $ template
+    output' = output </> templatePath template
 
 ----------------------------------------------------------------------------------------
 -- Targets -----------------------------------------------------------------------------
@@ -221,6 +222,11 @@
 class ToGVal RunRepo a => Target a where
     identify :: a -> FilePath
     category :: a -> FilePath
+    dest :: a -> IO (Path Rel File)
+    dest t = do
+        dir <- parseRelDir . category $ t
+        file <- parseRelFile . identify $ t
+        return $ dir </> file
 
 instance Target (Commit LgRepo) where
     identify = (++ ".html") . show . untag . commitOid
@@ -232,7 +238,7 @@
 
 genTarget ::
     Target a =>
-    FilePath ->
+    Path Abs Dir ->
     HashMap.HashMap Text (GVal RunRepo) ->
     Bool ->
     Maybe Template ->
@@ -240,9 +246,10 @@
     ReaderT LgRepo IO ()
 genTarget _ _ _ Nothing _ = return ()
 genTarget output scope force (Just template) target = do
-    let output' = output </> category target </> identify target
+    outFile <- liftIO . dest $ target
+    let output' = output </> outFile
     exists <- liftIO . doesFileExist $ output'
     when (force || not exists) $ do
-        liftIO . putStrLn $ "Writing " <> output'
+        liftIO . putStrLn $ "Writing " <> toFilePath output'
         let scope' = scope <> HashMap.fromList [(pack . category $ target, toGVal target)]
         generate output' scope' template

src/Main.hs Modified

@@ -8,12 +8,11 @@
 
 import Data.Version (showVersion)
 import qualified Options.Applicative as O
-import System.Directory (createDirectoryIfMissing)
 
-import Config (getConfig, outputDirectory)
+import Config (getConfig)
 import Index (runIndex)
 import Repositories (run)
-import Templates (loadTemplates)
+import Templates (loadEnv)
 
 {-
 Command line options
@@ -63,6 +62,5 @@
         then putStrLn $ "Your gitserve version is: " <> showVersion version
         else do
             conf <- getConfig (config options)
-            createDirectoryIfMissing True $ outputDirectory conf
-            env <- loadTemplates (force options) conf
+            env <- loadEnv (force options) conf
             runIndex env =<< run env

src/Index.hs Modified

@@ -7,49 +7,46 @@
 import Control.Monad (void)
 import qualified Data.HashMap.Strict as HashMap
 import Data.Text (Text, unpack)
-import System.FilePath ((</>))
+import Path (toFilePath)
+import System.FilePath (combine)
 import Text.Ginger.GVal (GVal, toGVal)
 import Text.Ginger.Html (Html, htmlSource)
 import Text.Ginger.Parse (SourcePos)
 import Text.Ginger.Run (Run, easyRenderM)
 
-import Config
-import Templates
+import Templates (Env (..), Template (..))
 import Types
 
 {-
-This creates the main index file from the index template, using information from all
+This creates the main index files from the index templates, using information from all
 configured respositories.
 -}
 runIndex :: Env -> [Repo] -> IO ()
-runIndex env repos =
-    case envIndexTemplate env of
-        Nothing ->
-            return ()
-        Just template -> do
-            putStrLn $ "Writing " <> output
-            writeFile output "" -- Clear contents of file if it exists
-            void $
-                easyRenderM
-                    (appendFile output . unpack . htmlSource)
-                    (packageIndex config repos)
-                    (templateGinger template)
-          where
-            config = envConfig env
-            output = outputDirectory config </> "index.html"
+runIndex env repos = mapM_ (runIndexFile env repos) . envIndexTemplates $ env
 
 ----------------------------------------------------------------------------------------
 -- Private -----------------------------------------------------------------------------
 
+runIndexFile :: Env -> [Repo] -> Template -> IO ()
+runIndexFile env repos template = do
+    let output = combine (toFilePath . envOutputDirectory $ env) . toFilePath . templatePath $ template
+    putStrLn $ "Writing " <> output
+    writeFile output "" -- Clear contents of file if it exists
+    void $
+        easyRenderM
+            (appendFile output . unpack . htmlSource)
+            (packageIndex env repos)
+            (templateGinger template)
+
 {-
-This packages the variables that are available inside the index template.
+This packages the variables that are available inside the index scope.
 -}
 packageIndex ::
-    Config ->
+    Env ->
     [Repo] ->
     HashMap.HashMap Text (GVal (Run SourcePos IO Html))
-packageIndex config repos =
+packageIndex env repos =
     HashMap.fromList
-        [ ("host", toGVal $ host config)
+        [ ("host", toGVal $ envHost env)
         , ("repositories", toGVal repos)
         ]

src/Config.hs Modified

@@ -3,15 +3,8 @@
 {-# LANGUAGE DerivingStrategies #-}
 
 module Config (
-    Config,
-    repoPaths,
-    templateDirectory,
-    indexTemplate,
-    commitTemplate,
-    fileTemplate,
+    Config (..),
     getConfig,
-    outputDirectory,
-    host,
 ) where
 
 import Control.Monad ((<=<))
@@ -22,9 +15,6 @@
 data Config = Config
     { repoPaths :: [FilePath]
     , templateDirectory :: FilePath
-    , indexTemplate :: FilePath
-    , commitTemplate :: FilePath
-    , fileTemplate :: FilePath
     , outputDirectory :: FilePath
     , host :: Text
     }

config.dhall Modified

@@ -14,9 +14,6 @@
 let config =
     { repoPaths = folders
     , templateDirectory = "./templates"
-    , indexTemplate = "./templates/index-main.html"
-    , commitTemplate = "./templates/commit.html"
-    , fileTemplate = "./templates/file.html"
     , outputDirectory = "./output"
     , host = "http://localhost:8000"
     }