packages feed

baikai-kit-0.2.0.0: src/Baikai/Kit/Repo.hs

module Baikai.Kit.Repo
  ( KitRepo (..),
    RepoRefresh (..),
    ensureKitRepo,
    PullResult (..),
    pullKitRepo,
  )
where

import Baikai.Kit.Config (KitConfig, kitCacheDir)
import Baikai.Kit.Error (KitError (..))
import Baikai.Prelude
import Control.Exception (IOException, try)
import Data.Text qualified as Text
import System.Directory (createDirectoryIfMissing, doesDirectoryExist, doesFileExist)
import System.Exit (ExitCode (..))
import System.FilePath ((</>))
import System.Process (readProcessWithExitCode)

data PullResult
  = PullSucceeded
  | PullFailed !Text
  deriving stock (Eq, Show)

-- | What happened to the cache on the way to returning it.
data RepoRefresh
  = -- | The cache did not exist and was cloned.
    RepoCloned
  | -- | The cache existed and was pulled.
    RepoPulled
  | -- | The refresh failed and the cached copy is being used as it is;
    --   the 'Text' is git's output. A caller that needs fresh content
    --   (@kit update@) treats this as a failure; the others warn.
    RepoStale !Text
  deriving stock (Eq, Show)

data KitRepo = KitRepo
  { dir :: !FilePath,
    refresh :: !RepoRefresh
  }
  deriving stock (Eq, Generic, Show)

-- | Resolve the kit cache, refreshing it if it is already a checkout and
--   cloning it if it is not. Never prints and never exits: a first clone
--   that fails with no usable cache is 'KitCloneFailed', and a failed
--   refresh over a usable cache is 'RepoStale'.
ensureKitRepo :: KitConfig -> IO (Either KitError KitRepo)
ensureKitRepo config = do
  cacheDir <- kitCacheDir config
  exists <- doesDirectoryExist (cacheDir </> ".git")
  if exists
    then do
      result <- pullKitRepo config cacheDir
      pure . Right $ case result of
        PullSucceeded -> KitRepo {dir = cacheDir, refresh = RepoPulled}
        PullFailed err -> KitRepo {dir = cacheDir, refresh = RepoStale err}
    else do
      cloned <-
        try @IOException $ do
          createDirectoryIfMissing True cacheDir
          readProcessWithExitCode
            "git"
            ["clone", "--depth", "1", Text.unpack (config ^. #repoUrl), cacheDir]
            ""
      case cloned of
        -- A missing `git` binary arrives here as an IOException.
        Left e -> staleOrFailed cacheDir (Text.pack (show e))
        Right (ExitSuccess, _, _) -> pure (Right KitRepo {dir = cacheDir, refresh = RepoCloned})
        Right (ExitFailure _, _, errOut) -> staleOrFailed cacheDir (Text.pack errOut)
  where
    staleOrFailed cacheDir output = do
      manifestExists <- doesFileExist (cacheDir </> "kit.json")
      pure $
        if manifestExists
          then Right KitRepo {dir = cacheDir, refresh = RepoStale output}
          else Left (KitCloneFailed (config ^. #repoUrl) output)

pullKitRepo :: KitConfig -> FilePath -> IO PullResult
pullKitRepo _config cacheDir = do
  result <-
    try @IOException $
      readProcessWithExitCode "git" ["-C", cacheDir, "pull", "--ff-only", "--quiet"] ""
  case result of
    Right (ExitSuccess, _, _) -> pure PullSucceeded
    Right (ExitFailure _, _, errOut) ->
      pure (PullFailed (Text.pack errOut))
    Left e ->
      pure (PullFailed (Text.pack (show e)))