packages feed

update-nix-fetchgit-0.2.4: src/Update/Nix/FetchGit/Prefetch.hs

module Update.Nix.FetchGit.Prefetch
  ( NixPrefetchGitOutput(..)
  , nixPrefetchGit
  , nixPrefetchUrl
  , getGitFullName
  , getGitRevision
  , getGitHubRevisionDate
  , Revision(..)
  ) where

import           Control.Monad                  ( guard )
import           Control.Monad.IO.Class         ( MonadIO(liftIO) )
import           Data.Aeson                     ( FromJSON
                                                , decode
                                                )
import           Data.ByteString.Lazy.UTF8      ( fromString )
import           Data.Text                      ( Text
                                                , pack
                                                , unpack
                                                )
import qualified Data.Text                     as T
import           Data.Time                      ( Day )
import           GHC.Generics
import           GitHub.REST
import           System.Exit                    ( ExitCode(..) )
import           System.Process                 ( readProcessWithExitCode )
import           Update.Nix.FetchGit.Types
import           Update.Nix.FetchGit.Utils
import           Update.Nix.FetchGit.Warning


-- | The type of nix-prefetch-git's output
data NixPrefetchGitOutput = NixPrefetchGitOutput
  { url    :: Text
  , rev    :: Text
  , sha256 :: Text
  , date   :: Text
  }
  deriving (Show, Generic, FromJSON)

-- | Run nix-prefetch-git
nixPrefetchGit
  :: [Text] -- ^ Extra arguments for nix-prefetch-git
  -> Text   -- ^ The URL to prefetch
  -> M NixPrefetchGitOutput
nixPrefetchGit extraArgs prefetchURL = do
  (exitCode, nsStdout, nsStderr) <- liftIO $ readProcessWithExitCode
    "nix-prefetch-git"
    (map unpack extraArgs ++ [unpack prefetchURL])
    ""
  case exitCode of
    ExitFailure e -> refute1 (NixPrefetchGitFailed e (pack nsStderr))
    ExitSuccess   -> pure ()
  note (InvalidPrefetchGitOutput (pack nsStdout)) (decode (fromString nsStdout))

-- | Run nix-prefetch-url --unpack
nixPrefetchUrl
  :: [Text] -- ^ Extra arguments for nix-prefetch-url
  -> Text   -- ^ The URL to prefetch
  -> M Text -- The sha256 output
nixPrefetchUrl extraArgs prefetchURL = do
  (exitCode, nsStdout, nsStderr) <- liftIO $ readProcessWithExitCode
    "nix-prefetch-url"
    ("--unpack" : map unpack extraArgs ++ [unpack prefetchURL])
    ""
  case exitCode of
    ExitFailure e -> refute1 (NixPrefetchUrlFailed e (pack nsStderr))
    ExitSuccess   -> pure ()
  note (InvalidPrefetchUrlOutput (pack nsStdout))
       (parseSHA256 (T.strip . T.pack $ nsStdout))

newtype Revision = Revision { unRevision :: Text }

-- | Discover if this ref is a branch or a tag
--
-- >>> runM _ $ getGitFullName "https://github.com/expipiplus1/update-nix-fetchgit" (Revision "0.1.0.0")
-- Right "refs/tags/0.1.0.0"
--
-- >>> runM _ $ getGitFullName "https://github.com/expipiplus1/update-nix-fetchgit" (Revision "joe-fetchTarball")
-- Right "refs/heads/joe-fetchTarball"
getGitFullName
  :: Text
  -- ^ git repo location
  -> Revision
  -- ^ branch or tag name
  -> M Text
  -- ^ Full name, i.e. with refs/heads/ or refs/tags/
getGitFullName repo revision = do
  gitLsRemotes repo revision >>= \case
    Just (_hash, name) -> pure name
    Nothing            -> refute1 $ NoSuchRef (unRevision revision)

-- | Return a tag or a hash
getGitRevision
  :: Text
  -- ^ git repo location
  -> Revision
  -- ^ branch or tag name
  -> M Text
getGitRevision repo revision = do
  gitLsRemotes repo revision >>= \case
    Just (hash, name) | Just tag <- stripPrefix "refs/tags/" name -> pure tag
                      | otherwise -> pure hash
    Nothing -> refute1 $ NoSuchRef (unRevision revision)

-- | Run git ls-remote --sort=-v:refname and return the first match if any
gitLsRemotes :: Text -> Revision -> M (Maybe (Text, Text))
gitLsRemotes repo revision = do
  (exitCode, nsStdout, nsStderr) <- liftIO $ readProcessWithExitCode
    "git"
    [ "ls-remote"
    , "--sort=-v:refname"
    , T.unpack repo
    , T.unpack (unRevision revision)
    ]
    ""
  case exitCode of
    ExitFailure e -> refute1 (NixPrefetchGitFailed e (pack nsStderr))
    ExitSuccess   -> pure ()
  let stdoutText = T.pack nsStdout
  case fmap T.words . T.lines $ stdoutText of
    []               -> pure Nothing
    [hash, name] : _ -> pure $ Just (hash, name)
    _                -> refute1 (InvalidGitLsRemoteOutput stdoutText)

getGitHubRevisionDate :: Text -> Text -> Revision -> M Day
getGitHubRevisionDate owner repo revision = do
  dateString <- runGitHubT ghState $ do
    ref <- queryGitHub GHEndpoint
      { method       = GET
      , endpoint     = "/repos/:owner/:repo/commits/:ref"
      , endpointVals = [ "owner" := owner
                       , "repo" := repo
                       , "ref" := unRevision revision
                       ]
      , ghData       = []
      }
    pure $ ref .: "commit" .: "committer" .: "date"
  fromEither $ parseISO8601DateToDay dateString

ghState :: GitHubState
ghState = GitHubState { token      = Nothing
                      , userAgent  = "expipiplus1/update-nix-fetchgit"
                      , apiVersion = "v3"
                      }

----------------------------------------------------------------
-- Utils
----------------------------------------------------------------

parseSHA256 :: Text -> Maybe Text
parseSHA256 t = do
  guard (base32Length == T.length t)
  guard (T.all (`elem` base32Chars) t)
  pure t
 where
  base32Chars    = "0123456789abcdfghijklmnpqrsvwxyz" :: String
  sha256HashSize = 32
  base32Length   = (sha256HashSize * 8 - 1) `quot` 5 + 1

stripPrefix :: Text -> Text -> Maybe Text
stripPrefix p t =
  if p `T.isPrefixOf` t then Just $ T.drop (T.length p) t else Nothing