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

Commit fedeeaf75328d71e97565321d6840a94f5a681ff
Parent: 7e60605df40665cda169c3ecef79ba46a82f1f2f
Author: mcol <mcol@posteo.net>
Date: 2021-11-21 00:06:21 +0000
Committer: mcol <mcol@posteo.net>
Committed: 2021-11-21 00:06:21 +0000

Refactor how files are generate

A few changes:
- instantiate `Repo`s once from index and pass to main generator
- store whole path to repo in `Repo`
- skip commits/files if the file exists, unless -f is passed
- Clean up some related areas of code

src/Templates.hs Modified

@@ -8,6 +8,7 @@
     envIndexTemplate,
     envCommitTemplate,
     envFileTemplate,
+    envForce,
     -- The Template data type and its constructors.
     Template,
     templatePath,
@@ -18,20 +19,20 @@
     generate,
 ) where
 
-import Control.Monad (filterM)
+import Control.Monad (filterM, void)
 import Data.Char (toLower)
 import Data.List (isSuffixOf)
 import Data.Maybe (catMaybes)
 import Data.Text (unpack, Text)
 import qualified Data.HashMap.Strict as HashMap
 import System.Directory (doesFileExist, getDirectoryContents)
-import System.FilePath ((</>), takeFileName)
+import System.FilePath ((</>))
 import System.IO.Error (tryIOError)
 import qualified Text.Ginger.AST as G
 import Text.Ginger.Parse (SourcePos, parseGingerFile, peErrorMessage)
 import Text.Ginger.GVal (GVal)
 import Text.Ginger.Html (htmlSource, Html)
-import Text.Ginger.Run (easyRenderM, Run, RuntimeError)
+import Text.Ginger.Run (easyRenderM, Run)
 
 import Config
 
@@ -83,11 +84,10 @@
     :: FilePath
     -> HashMap.HashMap Text (GVal (Run SourcePos IO Html))
     -> Template
-    -> IO (Either (RuntimeError SourcePos) (GVal (Run SourcePos IO Html)))
+    -> IO ()
 generate output context template = do
-    let target = (output </>) . takeFileName . templatePath $ template
-    writeFile target ""  -- Clear contents of file if it exists
-    easyRenderM (writeTo target) context . templateGinger $ template
+    writeFile output ""  -- Clear contents of file if it exists
+    void $ easyRenderM (writeTo output) context . templateGinger $ template
 
 ----------------------------------------------------------------------------------------
 -- Private -----------------------------------------------------------------------------

src/Repositories.hs Modified

@@ -5,14 +5,12 @@
 
 module Repositories (
     run,
-    getDescription  -- Used by Index.hs::runIndex
 ) where
 
 import Conduit (runConduit, (.|), sinkList)
-import Control.Monad ((<=<))
+import Control.Monad ((<=<), when)
 import Control.Monad.IO.Class (liftIO)
 import Control.Monad.Trans.Reader (ReaderT)
-import Data.Either (fromRight)
 import Data.Tagged
 import Data.Text (pack, unpack, Text, isPrefixOf, stripPrefix, toLower)
 import Data.Text.Encoding (decodeUtf8With)
@@ -20,9 +18,8 @@
 import Data.Maybe (mapMaybe, catMaybes, fromJust)
 import Git
 import Git.Libgit2 (lgFactory, LgRepo)
-import System.Directory (createDirectoryIfMissing)
+import System.Directory (createDirectoryIfMissing, doesFileExist)
 import System.FilePath ((</>), takeFileName)
-import System.IO.Error (tryIOError)
 import Text.Ginger.GVal (GVal, toGVal, ToGVal)
 import Text.Ginger.Html (Html)
 import Text.Ginger.Run (Run)
@@ -30,23 +27,16 @@
 import qualified Data.HashMap.Strict as HashMap
 
 import Config
+import Index
 import Templates
 import Types
 
 {-
-This is the entrypoint that receives the ``Config`` and uses it to map over our
-repositories, reading from them and writing out their web pages using the given
-templates.
+This is the entrypoint maps over our repositories, reading from them and writing out
+their web pages using the loaded templates.
 -}
-run :: Env -> IO ()
-run env = mapM_ (processRepo env) (repoPaths . envConfig $ env)
-
-{-
-Pass the repository's folder, get its description. This is exported so that Index.hs can
-use it too.
--}
-getDescription :: FilePath -> IO Text
-getDescription = fmap (fromRight "") . tryIOError . fmap pack . readFile . (</> "description")
+run :: Env -> [Repo] -> IO ()
+run = mapM_ . processRepo
 
 
 ----------------------------------------------------------------------------------------
@@ -57,41 +47,32 @@
 repository doesn't exist or is unreadable in any way we can forget about it and move on
 (after informing the user of course).
 -}
-processRepo :: Env -> FilePath -> IO ()
-processRepo env path = withRepository lgFactory path $ processRepo' env path
+processRepo :: Env -> Repo -> IO ()
+processRepo env repo = withRepository lgFactory (Index.repoPath repo) $ processRepo' env repo
 
-processRepo' :: Env -> FilePath -> ReaderT LgRepo IO ()
-processRepo' env path = do
-    let name = takeFileName path
+processRepo' :: Env -> Repo -> ReaderT LgRepo IO ()
+processRepo' env repo = do
+    let name = takeFileName . Index.repoPath $ repo
     let output = outputDirectory (envConfig env) </> name
-    liftIO $ createDirectoryIfMissing True output
     resolveReference "HEAD" >>= \case
         Nothing -> liftIO . print $ "gitserve: " <> name <> ": Failed to resolve HEAD."
         Just commitID -> do
             let gitHead = Tagged commitID
-            -- Variables available in the ginger templates: --
 
-            -- description: The description of the repository from repo/description, if
-            -- it exists.
-            description <- liftIO . getDescription $ path
-
-            -- commits: A list of `Commit` objects to HEAD.
+            -- Collect variables available in the ginger templates --
             commits <- getCommits gitHead
-
-            -- tree: A list of `TreeFile` objects at HEAD.
             tree <- getTree gitHead
-
-            -- tags and branches: Lists of `Ref` objects that wrap `Commit` and
-            -- `Refname` (Text).
             tags <- getRefs "refs/tags/"
             branches <- getRefs "refs/heads/"
+            let scope = package env name (repoDescription repo) commits tree tags branches
 
             -- Run the generator --
-            let scope = package env name description commits tree tags branches
-            liftIO . mapM_ (generate output scope) $ envTemplates env
-            liftIO . mapM_ (genTarget output scope $ envCommitTemplate env) $ commits
-            liftIO . mapM_ (genTarget output scope $ envFileTemplate env) $ tree
-            return ()
+            liftIO . mapM_ (genRepo output scope) $ envTemplates env
+            let force = envForce env
+            liftIO . createDirectoryIfMissing True $ output </> "commits"
+            liftIO . createDirectoryIfMissing True $ output </> "files"
+            liftIO . mapM_ (genTarget output scope force $ envCommitTemplate env) $ commits
+            liftIO . mapM_ (genTarget output scope True $ envFileTemplate env) $ tree  -- TODO: detect file changes
 
 {-
 The role of the function above is to gather information about a git repository and
@@ -185,6 +166,14 @@
     dropName (Just _) name = Just name
     dropName Nothing _ = Nothing
 
+genRepo
+    :: FilePath
+    -> HashMap.HashMap Text (GVal (Run SourcePos IO Html))
+    -> Template
+    -> IO ()
+genRepo output scope template = generate output' scope template
+  where
+    output' = (</>) output . takeFileName . templatePath $ template
 
 ----------------------------------------------------------------------------------------
 -- Targets -----------------------------------------------------------------------------
@@ -212,13 +201,15 @@
     :: Target a
     => FilePath
     -> HashMap.HashMap Text (GVal (Run SourcePos IO Html))
+    -> Bool
     -> Maybe Template
     -> a
     -> IO ()
-genTarget _ _ Nothing _ = return ()
-genTarget output scope (Just template) target = do
-    let template' = template { templatePath = identify target }
-    let scope' = scope <> HashMap.fromList [(pack . category $ target, toGVal target)]
-    liftIO $ createDirectoryIfMissing True (output </> category target)
-    generate (output </> category target) scope' template'
-    return ()
+genTarget _ _ _ Nothing _ = return ()
+genTarget output scope force (Just template) target = do
+    let output' = output </> category target </> identify target
+    exists <- doesFileExist output'
+    when (force || not exists) $ do
+        print output'
+        let scope' = scope <> HashMap.fromList [(pack . category $ target, toGVal target)]
+        generate output' scope' template

src/Main.hs Modified

@@ -58,8 +58,8 @@
         else do
             conf <- getConfig (config options)
             env <- loadTemplates (force options) conf
-            runIndex env
-            run env
+            repos <- runIndex env
+            run env repos
 
 currentVersion :: String
 currentVersion = "0.0.0"

src/Index.hs Modified

@@ -5,13 +5,19 @@
 {-# Language InstanceSigs #-}  -- Needed for toGVal type signature
 
 module Index (
-    runIndex
+    runIndex,
+    Repo,
+    repoAsLookup,
+    repoPath,
+    repoDescription,
 ) where
 
 import Data.Default (def)
+import Data.Either (fromRight)
 import Data.Text (pack, Text)
 import System.Directory (createDirectoryIfMissing)
-import System.FilePath (takeFileName)
+import System.FilePath ((</>), takeFileName)
+import System.IO.Error (tryIOError)
 import Text.Ginger.GVal (GVal, toGVal, ToGVal, asText, asHtml, asLookup)
 import Text.Ginger.Html (Html, html)
 import Text.Ginger.Parse (SourcePos)
@@ -19,62 +25,77 @@
 import qualified Data.HashMap.Strict as HashMap
 
 import Config
-import Repositories
 import Templates
 
 {-
 This creates the main index file from the index template, using information from all
 configured respositories.
 -}
-runIndex :: Env -> IO ()
+runIndex :: Env -> IO [Repo]
 runIndex env =
     case envIndexTemplate env of
         Nothing ->
-            return ()
+            return []
         Just template -> do
             let config = envConfig env
             createDirectoryIfMissing True $ outputDirectory config
-            let paths = repoPaths config
-            descriptions <- mapM getDescription paths
-            let repos = zipWith ($) (Repo . takeFileName <$> paths) descriptions
+            let output = (outputDirectory config) </> "index.html"
+            repos <- loadRepos config
             let scope = packageIndex config repos
-            generate (outputDirectory config) scope (template { templatePath = "index.html" })
-            return ()
-
-----------------------------------------------------------------------------------------
--- Private -----------------------------------------------------------------------------
-
-{-
-This packages the variables that are available inside the index template.
--}
-packageIndex
-    :: Config
-    -> [Repo]
-    -> HashMap.HashMap Text (GVal (Run SourcePos IO Html))
-packageIndex config repos = HashMap.fromList
-    [ ("host", toGVal $ host config)
-    , ("repositories", toGVal repos)
-    ]
+            generate output scope template
+            return repos
 
 {-
 The unique variable in the index scope is the list of repositories, which can be looped
 over and each entry has some properties that can be accessed. Those are defined here.
+The type is exported so that within the scope of a single repository, its name and
+description is available.
 -}
 data Repo = Repo
-    { repoName :: FilePath
+    { repoPath :: FilePath
     , repoDescription :: Text
     }
 
 instance ToGVal m Repo where
     toGVal :: Repo -> GVal m
     toGVal repo = def
-        { asHtml = html . pack . show . repoName $ repo
-        , asText = pack . show . repoName $ repo
+        { asHtml = html . pack . show . takeFileName . repoPath $ repo
+        , asText = pack . show . takeFileName . repoPath $ repo
         , asLookup = Just . repoAsLookup $ repo
         }
 
 repoAsLookup :: Repo -> Text -> Maybe (GVal m)
 repoAsLookup repo = \case
-    "name" -> Just . toGVal . repoName $ repo
+    "name" -> Just . toGVal . takeFileName . repoPath $ repo
     "description" -> Just . toGVal . repoDescription $ repo
     _ -> Nothing
+
+
+----------------------------------------------------------------------------------------
+-- Private -----------------------------------------------------------------------------
+
+{-
+This packages the variables that are available inside the index template.
+-}
+packageIndex
+    :: Config
+    -> [Repo]
+    -> HashMap.HashMap Text (GVal (Run SourcePos IO Html))
+packageIndex config repos = HashMap.fromList
+    [ ("host", toGVal $ host config)
+    , ("repositories", toGVal repos)
+    ]
+
+{-
+Get paths along with their descriptions.
+-}
+loadRepos :: Config -> IO [Repo]
+loadRepos config = zipWith ($) (Repo <$> paths) <$> mapM getDescription paths
+  where
+    paths = repoPaths config
+
+{-
+Pass the repository's folder, get its description.
+-}
+getDescription :: FilePath -> IO Text
+getDescription = fmap (fromRight "") . tryIOError . fmap pack . readFile . (</> "description")