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

Commit 8612b175af634ee9ed541d6d6a8d828853b37dd7
Parent: 535752da0f1c9ebb0cebdf415637558756b0538d
Author: mcol <mcol@posteo.net>
Date: 2021-12-04 13:38:44 +0300
Committer: mcol <mcol@posteo.net>
Committed: 2021-12-04 13:38:44 +0300

Shuffle Env-related code with Config-related code into Env module

src/Templates.hs Modified

@@ -2,44 +2,22 @@
 {-# LANGUAGE OverloadedStrings #-}
 
 module Templates (
-    Env (..),
     Template (..),
-    loadEnv,
+    loadTemplate,
     generate,
 ) where
 
-import Control.Monad (filterM, join, when, (<=<))
-import Control.Monad.Extra (findM)
 import Control.Monad.IO.Class (liftIO)
 import Control.Monad.Trans.Reader (ReaderT)
 import qualified Data.HashMap.Strict as HashMap
 import Data.IORef (modifyIORef', newIORef, readIORef)
-import Data.Maybe (catMaybes, fromMaybe, isNothing)
 import qualified Data.Text as T
 import qualified Data.Text.Lazy.Builder as TB
 import qualified Data.Text.Lazy.IO as T
 import Git.Libgit2 (LgRepo)
-import Path (Abs, Dir, File, Path, Rel, dirname, filename, parseAbsDir, toFilePath, (</>))
-import Path.IO (
-    copyDirRecur,
-    copyFile,
-    doesDirExist,
-    ensureDir,
-    forgivingAbsence,
-    ignoringAbsence,
-    isSymlink,
-    listDir,
- )
-import System.Directory (
-    canonicalizePath,
-    createDirectoryLink,
-    createFileLink,
-    getSymbolicLinkTarget,
-    pathIsSymbolicLink,
-    removeFile,
- )
-import System.Exit (die)
-import qualified System.FilePath as FP
+import Path (Abs, File, Path, Rel, filename, toFilePath)
+import Path.IO (ignoringAbsence)
+import System.Directory (removeFile)
 import System.IO.Error (tryIOError)
 import qualified Text.Ginger.AST as G
 import Text.Ginger.GVal (GVal)
@@ -47,109 +25,34 @@
 import Text.Ginger.Parse (ParserError (..), SourcePos, parseGingerFile)
 import Text.Ginger.Run (easyContext, runGingerT)
 
-import Config (Config (..))
 import Types
 
-{-
-The Env data type represents all of the program's state, including user configuration
-and loaded template data. This can be accessed as immutable global state at any point.
--}
-data Env = Env
-    { envConfig :: Config
-    , envIndexTemplates :: [Template]
-    , envCommitTemplate :: Maybe Template
-    , envFileTemplate :: Maybe Template
-    , envRepoTemplates :: [Template]
-    , envOutput :: Path Abs Dir
-    , envRepos :: [Path Abs Dir]
-    , envHost :: T.Text
-    , envQuiet :: Bool
-    , envForce :: Bool
-    }
-
 data Template = Template
     { templatePath :: Path Rel File
     , templateGinger :: G.Template SourcePos
     }
 
 {-
-This creates the runtime environment, collecting the config and loading template data
-from the template directory.
+This tries to load a `Template` from the given file path.
 -}
-loadEnv :: Bool -> Bool -> Config -> IO Env
-loadEnv quiet force config = do
-    -- First ensure that the output directory exists
-    output <- parseAbsDir <=< canonicalizePath . confOutput $ config
-    ensureDir output
-
-    -- Parse repos for env
-    repos <-
-        if confScan config
-            then do
-                ps <- fmap concat . mapM (fmap fst . ls) . confRepos $ config
-                return . filter ((/=) ".git" . toFilePath . dirname) $ ps
-            else mapM (parseAbsDir <=< canonicalizePath) . confRepos $ config
-
-    -- Find template files, copying the static files as is
-    (dirs, files) <- ls . confTemplate $ config
-    (_, filesRepo) <- ls $ confTemplate config FP.</> "repo"
-    copyStaticDirs output dirs
-    copyStaticFiles output files
-
-    -- Load files from template directory
-    indexT <- collectTemplates files
-    commitT <- findTemplate "commit.html" filesRepo
-    fileT <- findTemplate "file.html" filesRepo
-    repoT <-
-        collectTemplates
-            . filter (flip notElem ["commit.html", "file.html"] . toFilePath . filename)
-            $ filesRepo
-
-    -- Exit early if we didn't find any templates
-    when
-        ( null indexT
-            && isNothing commitT
-            && isNothing fileT
-            && null repoT
-        )
-        $ die "No templates were found."
-
-    -- App environment
-    return
-        Env
-            { envConfig = config
-            , envIndexTemplates = indexT
-            , envCommitTemplate = commitT
-            , envFileTemplate = fileT
-            , envRepoTemplates = repoT
-            , envOutput = output
-            , envRepos = repos
-            , envHost = confHost config
-            , envQuiet = quiet
-            , envForce = force
-            }
+loadTemplate :: Path Abs File -> IO (Maybe Template)
+loadTemplate path =
+    parseGingerFile includeResolver (toFilePath path) >>= \case
+        Right parsed -> return . Just . Template (filename path) $ parsed
+        Left err -> do
+            informError (toFilePath path) err
+            return Nothing
   where
-    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 ([], [])
-
-    collectTemplates :: [Path Abs File] -> IO [Template]
-    collectTemplates = fmap catMaybes . mapM loadTemplate <=< filterM isMatch
-      where
-        isMatch p = do
-            ((FP.takeExtension . toFilePath $ p) == ".html" &&)
-                <$> (fmap not . pathIsSymbolicLink . toFilePath $ p)
+    -- An attempt at pretty printing the error message.
+    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
 
-    findTemplate :: FilePath -> [Path Abs File] -> IO (Maybe Template)
-    findTemplate name = fmap join . mapM loadTemplate <=< findM isMatch
-      where
-        isMatch p =
-            ((toFilePath . filename $ p) == name &&)
-                <$> (fmap not . pathIsSymbolicLink . toFilePath $ p)
+    -- This resolves template 'includes'.
+    includeResolver :: FilePath -> IO (Maybe String)
+    includeResolver p = either (const Nothing) Just <$> tryIOError (readFile p)
 
 {-
 This is the generator function that receives repository-specific variables and uses
@@ -171,73 +74,3 @@
     runGingerT (easyContext emit context) . templateGinger $ template
     result <- liftIO . readIORef $ content
     liftIO . T.writeFile output' . TB.toLazyText $ result
-
-{-
-This takes the session's `Config` and maybe returns a loaded template for the
-``indexTemplate`` setting.
--}
-loadTemplate :: Path Abs File -> IO (Maybe Template)
-loadTemplate path =
-    parseGingerFile includeResolver (toFilePath path) >>= \case
-        Right parsed -> return . Just . Template (filename path) $ parsed
-        Left err -> do
-            informError (toFilePath path) err
-            return Nothing
-  where
-    -- An attempt at pretty printing the error message.
-    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 template 'includes'.
-    includeResolver :: FilePath -> IO (Maybe String)
-    includeResolver p = either (const Nothing) Just <$> tryIOError (readFile p)
-
-{-
-The logic for copying static files and folders. Any file or folder in the
-``confTemplate`` is considered static if:
-
-- it is a symbolic link, or
-- it does not end in ".html" or ".include".
-
-Symbolic links are not followed and are copied as is. This means that a symbolic link
-from `confTemplate/link.html` to `gitserve/index.html` will be copied, keeping the
-link intact, resulting in a symbolic link at `output/link.html` essentially
-pointing to `output/gitserve/index.html`.
--}
-copyStaticDirs :: Path Abs Dir -> [Path Abs Dir] -> IO ()
-copyStaticDirs output = mapM_ copy
-  where
-    copy :: Path Abs Dir -> IO ()
-    copy p = do
-        let isRepo = (toFilePath . dirname $ p) == "repo/"
-        isLink <- pathIsSymbolicLink . toFilePath $ p
-        when (not isRepo || isLink) $ do
-            let output' = output </> dirname p
-            let fp = FP.dropTrailingPathSeparator . toFilePath $ p
-            if isLink
-                then do
-                    target <- getSymbolicLinkTarget fp
-                    createDirectoryLink target . FP.dropTrailingPathSeparator . toFilePath $ output'
-                else copyDirRecur p output'
-
-copyStaticFiles :: Path Abs Dir -> [Path Abs File] -> IO ()
-copyStaticFiles output = mapM_ copy
-  where
-    copy :: Path Abs File -> IO ()
-    copy p = do
-        let fp = toFilePath p
-        let isTemplate = FP.takeExtension fp `elem` [".html", ".include"]
-        isLink <- pathIsSymbolicLink fp
-        when (not isTemplate || isLink) $ do
-            let output' = output </> filename p
-            if isLink
-                then do
-                    target <- getSymbolicLinkTarget fp
-                    maybeExists <- forgivingAbsence . isSymlink $ output'
-                    let exists = fromMaybe False maybeExists
-                    when exists . removeFile . toFilePath $ output'
-                    createFileLink target . toFilePath $ output'
-                else copyFile p output'

src/Repositories.hs Modified

@@ -30,7 +30,8 @@
 import qualified System.FilePath as FP
 import Text.Ginger.GVal (GVal, ToGVal, toGVal)
 
-import Templates (Env (..), Template (..), generate)
+import Env (Env (..))
+import Templates (Template (..), generate)
 import Types
 
 {-

src/Main.hs Modified

@@ -17,10 +17,9 @@
 import qualified System.Directory as D
 import System.FilePath (takeDirectory, (</>))
 
-import Config (getConfig)
+import Env (getConfig, loadEnv)
 import Index (runIndex)
 import Repositories (run)
-import Templates (loadEnv)
 
 {-
 Command line options

src/Index.hs Modified

@@ -14,7 +14,8 @@
 import Text.Ginger.Parse (SourcePos)
 import Text.Ginger.Run (Run, easyRenderM)
 
-import Templates (Env (..), Template (..))
+import Env (Env (..))
+import Templates (Template (..))
 import Types
 
 {-

src/Env.hs Added

@@ -0,0 +1,206 @@
+{-# LANGUAGE DataKinds #-}
+{-# LANGUAGE DeriveGeneric #-}
+{-# LANGUAGE DerivingVia #-}
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE TypeOperators #-}
+
+module Env (
+    Config (..),
+    getConfig,
+    Env (..),
+    loadEnv,
+    generate,
+) where
+
+import Control.Monad (filterM, join, when, (<=<))
+import Control.Monad.Extra (findM)
+import Data.Maybe (catMaybes, fromMaybe, isNothing)
+import Data.Text (pack)
+import qualified Data.Text as T
+import Dhall
+import Dhall.Deriving
+import Path (Abs, Dir, File, Path, dirname, filename, parseAbsDir, toFilePath, (</>))
+import Path.IO (
+    copyDirRecur,
+    copyFile,
+    doesDirExist,
+    ensureDir,
+    forgivingAbsence,
+    isSymlink,
+    listDir,
+ )
+import System.Directory (
+    canonicalizePath,
+    createDirectoryLink,
+    createFileLink,
+    getSymbolicLinkTarget,
+    makeAbsolute,
+    pathIsSymbolicLink,
+    removeFile,
+ )
+import System.Exit (die)
+import qualified System.FilePath as FP
+
+import Templates (Template (..), generate, loadTemplate)
+
+{-
+The Config data type represents the configuration options available in the config file.
+Each record of the type, without the 'conf' prefix, is an option.
+-}
+data Config = Config
+    { confRepos :: [FilePath]
+    , confScan :: Bool
+    , confTemplate :: FilePath
+    , confOutput :: FilePath
+    , confHost :: Text
+    }
+    deriving stock (Generic)
+    deriving
+        (FromDhall)
+        via Codec (Field (CamelCase <<< DropPrefix "conf")) Config
+
+getConfig :: String -> IO Config
+getConfig = input auto . pack <=< makeAbsolute
+
+{-
+The Env data type represents all of the program's state, including user configuration
+and loaded template data. This can be accessed as immutable global state at any point.
+-}
+data Env = Env
+    { envConfig :: Config
+    , envIndexTemplates :: [Template]
+    , envCommitTemplate :: Maybe Template
+    , envFileTemplate :: Maybe Template
+    , envRepoTemplates :: [Template]
+    , envOutput :: Path Abs Dir
+    , envRepos :: [Path Abs Dir]
+    , envHost :: T.Text
+    , envQuiet :: Bool
+    , envForce :: Bool
+    }
+
+{-
+This creates the runtime environment, collecting the config and loading template data
+from the template directory.
+-}
+loadEnv :: Bool -> Bool -> Config -> IO Env
+loadEnv quiet force config = do
+    -- First ensure that the output directory exists
+    output <- parseAbsDir <=< canonicalizePath . confOutput $ config
+    ensureDir output
+
+    -- Parse repos for env
+    repos <-
+        if confScan config
+            then do
+                ps <- fmap concat . mapM (fmap fst . ls) . confRepos $ config
+                return . filter ((/=) ".git" . toFilePath . dirname) $ ps
+            else mapM (parseAbsDir <=< canonicalizePath) . confRepos $ config
+
+    -- Find template files, copying the static files as is
+    (dirs, files) <- ls . confTemplate $ config
+    (_, filesRepo) <- ls $ confTemplate config FP.</> "repo"
+    copyStaticDirs output dirs
+    copyStaticFiles output files
+
+    -- Load files from template directory
+    indexT <- collectTemplates files
+    commitT <- findTemplate "commit.html" filesRepo
+    fileT <- findTemplate "file.html" filesRepo
+    repoT <-
+        collectTemplates
+            . filter (flip notElem ["commit.html", "file.html"] . toFilePath . filename)
+            $ filesRepo
+
+    -- Exit early if we didn't find any templates
+    when
+        ( null indexT
+            && isNothing commitT
+            && isNothing fileT
+            && null repoT
+        )
+        $ die "No templates were found."
+
+    -- App environment
+    return
+        Env
+            { envConfig = config
+            , envIndexTemplates = indexT
+            , envCommitTemplate = commitT
+            , envFileTemplate = fileT
+            , envRepoTemplates = repoT
+            , envOutput = output
+            , envRepos = repos
+            , envHost = confHost config
+            , envQuiet = quiet
+            , envForce = force
+            }
+  where
+    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 ([], [])
+
+    collectTemplates :: [Path Abs File] -> IO [Template]
+    collectTemplates = fmap catMaybes . mapM loadTemplate <=< filterM isMatch
+      where
+        isMatch p = do
+            ((FP.takeExtension . toFilePath $ p) == ".html" &&)
+                <$> (fmap not . pathIsSymbolicLink . toFilePath $ p)
+
+    findTemplate :: FilePath -> [Path Abs File] -> IO (Maybe Template)
+    findTemplate name = fmap join . mapM loadTemplate <=< findM isMatch
+      where
+        isMatch p =
+            ((toFilePath . filename $ p) == name &&)
+                <$> (fmap not . pathIsSymbolicLink . toFilePath $ p)
+
+{-
+The logic for copying static files and folders. Any file or folder in the
+``confTemplate`` is considered static if:
+
+- it is a symbolic link, or
+- it does not end in ".html" or ".include".
+
+Symbolic links are not followed and are copied as is. This means that a symbolic link
+from `confTemplate/link.html` to `gitserve/index.html` will be copied, keeping the
+link intact, resulting in a symbolic link at `output/link.html` essentially
+pointing to `output/gitserve/index.html`.
+-}
+copyStaticDirs :: Path Abs Dir -> [Path Abs Dir] -> IO ()
+copyStaticDirs output = mapM_ copy
+  where
+    copy :: Path Abs Dir -> IO ()
+    copy p = do
+        let isRepo = (toFilePath . dirname $ p) == "repo/"
+        isLink <- pathIsSymbolicLink . toFilePath $ p
+        when (not isRepo || isLink) $ do
+            let output' = output </> dirname p
+            let fp = FP.dropTrailingPathSeparator . toFilePath $ p
+            if isLink
+                then do
+                    target <- getSymbolicLinkTarget fp
+                    createDirectoryLink target . FP.dropTrailingPathSeparator . toFilePath $ output'
+                else copyDirRecur p output'
+
+copyStaticFiles :: Path Abs Dir -> [Path Abs File] -> IO ()
+copyStaticFiles output = mapM_ copy
+  where
+    copy :: Path Abs File -> IO ()
+    copy p = do
+        let fp = toFilePath p
+        let isTemplate = FP.takeExtension fp `elem` [".html", ".include"]
+        isLink <- pathIsSymbolicLink fp
+        when (not isTemplate || isLink) $ do
+            let output' = output </> filename p
+            if isLink
+                then do
+                    target <- getSymbolicLinkTarget fp
+                    maybeExists <- forgivingAbsence . isSymlink $ output'
+                    let exists = fromMaybe False maybeExists
+                    when exists . removeFile . toFilePath $ output'
+                    createFileLink target . toFilePath $ output'
+                else copyFile p output'

src/Config.hs Deleted

@@ -1,31 +0,0 @@
--- Required by Dhall
-{-# LANGUAGE DataKinds #-}
-{-# LANGUAGE DeriveGeneric #-}
-{-# LANGUAGE DerivingVia #-}
-{-# LANGUAGE TypeOperators #-}
-
-module Config (
-    Config (..),
-    getConfig,
-) where
-
-import Control.Monad ((<=<))
-import Data.Text (pack)
-import Dhall
-import Dhall.Deriving
-import System.Directory (makeAbsolute)
-
-data Config = Config
-    { confRepos :: [FilePath]
-    , confScan :: Bool
-    , confTemplate :: FilePath
-    , confOutput :: FilePath
-    , confHost :: Text
-    }
-    deriving stock (Generic)
-    deriving
-        (FromDhall)
-        via Codec (Field (CamelCase <<< DropPrefix "conf")) Config
-
-getConfig :: String -> IO Config
-getConfig = input auto . pack <=< makeAbsolute

gitserve.cabal Modified

@@ -17,7 +17,7 @@
 executable gitserve
   hs-source-dirs:      src
   main-is:             Main.hs
-  other-modules:       Config, Index, Repositories, Templates, Types, Paths_gitserve
+  other-modules:       Index, Env, Repositories, Templates, Types, Paths_gitserve
   default-language:    Haskell2010
   build-depends:       base >= 4.7 && < 5
                      , conduit