packages feed

dojang-0.1.0: src/Dojang/Commands/Reflect.hs

{-# LANGUAGE OverloadedRecordDot #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TemplateHaskell #-}

module Dojang.Commands.Reflect (reflect) where

import Control.Monad (filterM, forM, forM_, unless)
import Control.Monad.IO.Class (MonadIO (liftIO))
import Data.List (isPrefixOf, nub)
import System.Exit (ExitCode (..), exitWith)
import System.IO (stderr)

import Control.Monad.Logger (logDebug, logDebugSH)
import Control.Monad.Reader (asks)
import Data.Text (pack)
import System.Directory.OsPath (makeAbsolute)
import System.OsPath (OsPath, splitDirectories, takeDirectory)

import Dojang.App (App, AppEnv (manifestFile), ensureContext)
import Dojang.Commands
  ( Admonition (..)
  , codeStyleFor
  , die'
  , dieWithErrors
  , pathStyleFor
  , printStderr
  , printStderr'
  )
import Dojang.Commands.Status (printWarnings)
import Dojang.ExitCodes
  ( fileNotFoundError
  , fileNotRoutedError
  , ignoredFileError
  , sourceCannotBeTargetError
  )
import Dojang.MonadFileSystem (MonadFileSystem (..))
import Dojang.Types.Context
  ( Context (..)
  , FileCorrespondence (..)
  , FileDeltaKind (..)
  , FileEntry (..)
  , FileStat (..)
  , RouteState (..)
  , getRouteState
  , makeCorrespondWithDestination
  )
import Dojang.Types.Repository (Repository (..))


reflect
  :: (MonadFileSystem i, MonadIO i) => Bool -> [OsPath] -> App i ExitCode
reflect force paths = do
  ctx <- ensureContext
  nonExistents <- filterM (fmap not . exists) paths
  pathStyle <- pathStyleFor stderr
  unless (null nonExistents) $ do
    dieWithErrors
      fileNotFoundError
      ["No such file: " <> pathStyle p <> "." | p <- nonExistents]
  absPaths <- liftIO $ mapM makeAbsolute paths
  $(logDebugSH) (absPaths :: [OsPath])
  sourcePath' <- liftIO $ makeAbsolute ctx.repository.sourcePath
  let sourcePathPrefix = splitDirectories sourcePath'
  $(logDebugSH) sourcePathPrefix
  let overlappedPaths =
        [ p
        | p <- absPaths
        , sourcePathPrefix `isPrefixOf` splitDirectories p
        ]
  $(logDebugSH) overlappedPaths
  unless (null overlappedPaths) $ do
    dieWithErrors
      sourceCannotBeTargetError
      [ "Cannot reflect "
        <> pathStyle p
        <> " because it is a file inside the repository."
      | p <- overlappedPaths
      ]
  codeStyle <- codeStyleFor stderr
  warningLists <- forM absPaths $ \absPath -> do
    (state, ws) <- getRouteState ctx absPath
    case state of
      NotRouted -> do
        manifestFile' <- asks (.manifestFile)
        printWarnings ws
        printStderr'
          Error
          ("File " <> pathStyle absPath <> " is not routed.")
        printStderr'
          Hint
          ("Add a route for it in " <> pathStyle manifestFile' <> ".")
        liftIO $ exitWith fileNotRoutedError
      Routed _ -> do
        return ws
      Ignored name pattern -> do
        routeName' <- decodePath name
        if force
          then do
            printStderr' Note
              $ "File "
              <> pathStyle absPath
              <> " is ignored due to pattern "
              <> codeStyle (pack $ show pattern)
              <> " (route name: "
              <> codeStyle (pack routeName')
              <> "), but reflect it anyway as you enforced it using "
              <> codeStyle "-f"
              <> "/"
              <> codeStyle "--force"
              <> " option."
            return ws
          else do
            printWarnings ws
            printStderr' Error
              $ "File "
              <> pathStyle absPath
              <> " is ignored due to pattern "
              <> codeStyle (pack $ show pattern)
              <> " (route name: "
              <> codeStyle (pack routeName')
              <> ")."
            printStderr'
              Hint
              $ "You can reflect it anyway by enforcing it using "
              <> codeStyle "-f"
              <> "/"
              <> codeStyle "--force"
              <> " option."
            liftIO $ exitWith ignoredFileError
  files <- forM absPaths $ \p -> do
    (f, ws) <- makeCorrespondWithDestination ctx p
    printWarnings $ nub ws
    case f of
      Nothing ->
        die' fileNotRoutedError ("File " <> pathStyle p <> " is not routed.")
      Just c -> return c
  let conflicts = filterConflicts files
  $(logDebugSH) conflicts
  unless (force || null conflicts) $ do
    printWarnings $ nub $ concat warningLists
    dieWithErrors
      sourceCannotBeTargetError
      [ "Cannot reflect "
        <> pathStyle c.destination.path
        <> ", since "
        <> pathStyle c.source.path
        <> " is also changed."
      | c <- conflicts
      ]
  forM_ files $ \c -> do
    if c.sourceDelta == Unchanged && c.destinationDelta == Unchanged
      then
        printStderr'
          Note
          ( "File "
              <> pathStyle c.destination.path
              <> " is skipped, since it is the same as file "
              <> pathStyle c.source.path
              <> "."
          )
      else do
        printStderr
          $ "Reflect "
          <> pathStyle c.destination.path
          <> " to "
          <> pathStyle c.source.path
          <> "..."
        cleanup c.intermediate
        copy c.destination c.intermediate
        cleanup c.source
        copy c.intermediate c.source
  printWarnings $ nub $ concat warningLists
  return ExitSuccess


cleanup :: (MonadFileSystem i, MonadIO i) => FileEntry -> App i ()
cleanup fileEntry = do
  path <- decodePath fileEntry.path
  case fileEntry.stat of
    Missing -> return ()
    Directory -> do
      $(logDebug) $ "Remove directory recursively: " <> pack path
      removeDirectoryRecursively fileEntry.path
    _ -> do
      $(logDebug) $ "Remove file: " <> pack path
      removeFile fileEntry.path


copy :: (MonadFileSystem i, MonadIO i) => FileEntry -> FileEntry -> App i ()
copy from to = do
  from' <- decodePath from.path
  to' <- decodePath to.path
  case from.stat of
    Directory -> do
      $(logDebug) $ "Create directory recursively: " <> pack to'
      createDirectories to.path
    _ -> do
      let parent = takeDirectory to.path
      parent' <- decodePath parent
      $(logDebug) $ "Create directory recursively: " <> pack parent'
      createDirectories parent
      $(logDebug) $ "Copy file: " <> pack from' <> " -> " <> pack to'
      copyFile from.path to.path


filterConflicts
  :: [FileCorrespondence]
  -> [FileCorrespondence]
filterConflicts corresponds = [c | c <- corresponds, c.sourceDelta /= Unchanged]