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

-rw-r--r-- src/Env.hs


      1 {-# LANGUAGE DataKinds #-}
      2 {-# LANGUAGE DeriveGeneric #-}
      3 {-# LANGUAGE DerivingVia #-}
      4 {-# LANGUAGE OverloadedStrings #-}
      5 {-# LANGUAGE TypeOperators #-}
      6 
      7 module Env (
      8     Config (..),
      9     getConfig,
     10     Env (..),
     11     loadEnv,
     12 ) where
     13 
     14 import Control.Monad (filterM, join, when, (<=<))
     15 import Control.Monad.Extra (findM)
     16 import Data.Maybe (catMaybes, fromMaybe, isNothing)
     17 import qualified Data.Text as T
     18 import Dhall
     19 import Dhall.Deriving
     20 import Path (Abs, Dir, File, Path, dirname, filename, parseAbsDir, toFilePath, (</>))
     21 import Path.IO (
     22     copyDirRecur,
     23     copyFile,
     24     doesDirExist,
     25     ensureDir,
     26     forgivingAbsence,
     27     isSymlink,
     28     listDir,
     29  )
     30 import System.Directory (
     31     canonicalizePath,
     32     createDirectoryLink,
     33     createFileLink,
     34     getSymbolicLinkTarget,
     35     makeAbsolute,
     36     pathIsSymbolicLink,
     37     removeFile,
     38  )
     39 import System.Exit (die)
     40 import qualified System.FilePath as FP
     41 
     42 import Templates (Template (..), loadTemplate)
     43 
     44 {-
     45 The Config data type represents the configuration options available in the config file.
     46 Each record of the type, without the 'conf' prefix, is an option.
     47 -}
     48 data Config = Config
     49     { confRepos :: [FilePath]
     50     , confScan :: Bool
     51     , confTemplate :: FilePath
     52     , confOutput :: FilePath
     53     , confHost :: Text
     54     }
     55     deriving stock (Generic)
     56     deriving
     57         (FromDhall)
     58         via Codec (Field (CamelCase <<< DropPrefix "conf")) Config
     59 
     60 getConfig :: String -> IO Config
     61 getConfig = input auto . T.pack <=< makeAbsolute
     62 
     63 {-
     64 The Env data type represents all of the program's state, including user configuration
     65 and loaded template data. This can be accessed as immutable global state at any point.
     66 -}
     67 data Env = Env
     68     { envConfig :: Config
     69     , envIndexTemplates :: [Template]
     70     , envCommitTemplate :: Maybe Template
     71     , envBlobTemplate :: Maybe Template
     72     , envTreeTemplate :: Maybe Template
     73     , envRepoTemplates :: [Template]
     74     , envOutput :: Path Abs Dir
     75     , envRepos :: [Path Abs Dir]
     76     , envHost :: T.Text
     77     , envQuiet :: Bool
     78     , envForce :: Bool
     79     , envRepoCopyStatics :: Path Abs Dir -> IO ()
     80     }
     81 
     82 {-
     83 This creates the runtime environment, collecting the config and loading template data
     84 from the template directory.
     85 -}
     86 loadEnv :: Bool -> Bool -> Config -> IO Env
     87 loadEnv quiet force config = do
     88     -- First ensure that the output directory exists
     89     output <- parseAbsDir <=< canonicalizePath . confOutput $ config
     90     ensureDir output
     91 
     92     -- Parse repos for env
     93     repos <-
     94         if confScan config
     95             then do
     96                 ps <- fmap concat . mapM (fmap fst . ls) . confRepos $ config
     97                 return . filter ((/=) ".git" . toFilePath . dirname) $ ps
     98             else mapM (parseAbsDir <=< canonicalizePath) . confRepos $ config
     99 
    100     -- Find template files, copying the static files as is
    101     (dirs, files) <- ls . confTemplate $ config
    102     (dirsRepo, filesRepo) <- ls $ confTemplate config FP.</> "repo"
    103     copyStaticDirs dirs output
    104     copyStaticFiles files output
    105 
    106     -- Load files from template directory
    107     indexT <- collectTemplates files
    108     commitT <- findTemplate "foreach.commit.html" filesRepo
    109     blobT <- findTemplate "foreach.blob.html" filesRepo
    110     treeT <- findTemplate "foreach.tree.html" filesRepo
    111     repoT <-
    112         collectTemplates
    113             . filter (flip notElem ["foreach.commit.html", "foreach.blob.html", "foreach.tree.html"] . toFilePath . filename)
    114             $ filesRepo
    115 
    116     -- Exit early if we didn't find any templates
    117     when
    118         ( all null [indexT, repoT] && all isNothing [commitT, blobT, treeT]
    119         )
    120         $ die "No templates were found."
    121 
    122     -- App environment
    123     return
    124         Env
    125             { envConfig = config
    126             , envIndexTemplates = indexT
    127             , envCommitTemplate = commitT
    128             , envBlobTemplate = blobT
    129             , envTreeTemplate = treeT
    130             , envRepoTemplates = repoT
    131             , envOutput = output
    132             , envRepos = repos
    133             , envHost = confHost config
    134             , envQuiet = quiet
    135             , envForce = force
    136             , envRepoCopyStatics = \p -> copyStaticDirs dirsRepo p >> copyStaticFiles filesRepo p
    137             }
    138   where
    139     ls :: FilePath -> IO ([Path Abs Dir], [Path Abs File])
    140     ls dir = do
    141         canon <- parseAbsDir =<< canonicalizePath dir
    142         exists <- doesDirExist canon
    143         if exists
    144             then listDir canon
    145             else return ([], [])
    146 
    147     collectTemplates :: [Path Abs File] -> IO [Template]
    148     collectTemplates = fmap catMaybes . mapM loadTemplate <=< filterM isMatch
    149       where
    150         isMatch p = do
    151             ((FP.takeExtension . toFilePath $ p) == ".html" &&)
    152                 <$> (fmap not . pathIsSymbolicLink . toFilePath $ p)
    153 
    154     findTemplate :: FilePath -> [Path Abs File] -> IO (Maybe Template)
    155     findTemplate name = fmap join . mapM loadTemplate <=< findM isMatch
    156       where
    157         isMatch p =
    158             ((toFilePath . filename $ p) == name &&)
    159                 <$> (fmap not . pathIsSymbolicLink . toFilePath $ p)
    160 
    161 {-
    162 The logic for copying static files and folders. Any file or folder in the
    163 ``confTemplate`` is considered static if:
    164 
    165 - it is a symbolic link, or
    166 - it does not end in ".html" or ".include".
    167 
    168 Symbolic links are not followed and are copied as is. This means that a symbolic link
    169 from `confTemplate/link.html` to `gitja/index.html` will be copied, keeping the link
    170 intact, resulting in a symbolic link at `output/link.html` essentially pointing to
    171 `output/gitja/index.html`.
    172 -}
    173 copyStaticDirs :: [Path Abs Dir] -> Path Abs Dir -> IO ()
    174 copyStaticDirs dirs output = mapM_ copy dirs
    175   where
    176     copy :: Path Abs Dir -> IO ()
    177     copy p = do
    178         let isRepo = (toFilePath . dirname $ p) == "repo/"
    179         isLink <- pathIsSymbolicLink . toFilePath $ p
    180         when (not isRepo || isLink) $ do
    181             let output' = output </> dirname p
    182             let fp = FP.dropTrailingPathSeparator . toFilePath $ p
    183             if isLink
    184                 then do
    185                     target <- getSymbolicLinkTarget fp
    186                     createDirectoryLink target . FP.dropTrailingPathSeparator . toFilePath $ output'
    187                 else copyDirRecur p output'
    188 
    189 copyStaticFiles :: [Path Abs File] -> Path Abs Dir -> IO ()
    190 copyStaticFiles files output = mapM_ copy files
    191   where
    192     copy :: Path Abs File -> IO ()
    193     copy p = do
    194         let fp = toFilePath p
    195         let isTemplate = FP.takeExtension fp `elem` [".html", ".include"]
    196         isLink <- pathIsSymbolicLink fp
    197         when (not isTemplate || isLink) $ do
    198             let output' = output </> filename p
    199             if isLink
    200                 then do
    201                     target <- getSymbolicLinkTarget fp
    202                     maybeExists <- forgivingAbsence . isSymlink $ output'
    203                     let exists = fromMaybe False maybeExists
    204                     when exists . removeFile . toFilePath $ output'
    205                     createFileLink target . toFilePath $ output'
    206                 else copyFile p output'
    207