{-| Functions for shelling out to @git@ to work with git repositories. -} module FeedGipeda.GitShell ( isRepositoryRoot , fetch , allCommits , firstCommit , remoteRepo , showHead , sync , SHA ) where import Control.Monad (void) import Data.Char (isSpace) import Data.Functor import Data.Maybe (listToMaybe) import Data.Set (Set) import qualified Data.Set as Set import FeedGipeda.Prelude import FeedGipeda.Repo (Repo) import qualified FeedGipeda.Repo as Repo import System.Directory (createDirectoryIfMissing) import System.Exit (ExitCode (..)) import System.Process (readProcessWithExitCode, showCommandForUser) type SHA = String formatGitArgs :: Maybe FilePath -> String -> [String] -> [String] formatGitArgs local command args = case local of Nothing -> command : args Just l -> ["-C", l, command] ++ args git :: Maybe FilePath -> String -> [String] -> IO (ExitCode, String, String) git local command args = readProcessWithExitCode "git" (formatGitArgs local command args) "" gitLoggingErrors :: Maybe FilePath -> String -> [String] -> IO (Maybe String) gitLoggingErrors repo command args = do (exitCode, stdout, stderr) <- git repo command args case exitCode of ExitSuccess -> return (Just stdout) ExitFailure code -> do logWarn (showCommandForUser "git" (formatGitArgs repo command args)) logWarn "stdout:" logWarn stdout logWarn "stderr:" logWarn stderr return Nothing isRepositoryRoot :: FilePath -> IO Bool isRepositoryRoot path = do (_, stdout, _)<- git (Just path) "rev-parse" ["--git-dir"] -- testing for ".git" and "." (bare repo) should be good enough. (return . maybe False (`elem` [".git", "."]) . listToMaybe . lines) stdout mirror :: Repo -> FilePath -> IO () mirror repo path = void (gitLoggingErrors Nothing "clone" ["--mirror", "--quiet", Repo.uri repo, path]) remoteRepo :: FilePath -> IO Repo remoteRepo path = do ret <- gitLoggingErrors (Just path) "ls-remote" ["--get-url", "origin"] case ret of Nothing -> return (Repo.unsafeFromString "https://error.org/err") -- If this ever happens, all bets are off Just stdout -> return (Repo.unsafeFromString (init stdout)) -- strip the \n with init fetch :: FilePath -> IO () fetch path = void (gitLoggingErrors (Just path) "fetch" ["--quiet"]) {-| @sync repo@ tries to fetch updates from the remote @repo@ or creates a mirror of @repo@ if there isn't already a local clone present. -} sync :: Repo -> IO () sync repo = do path <- Repo.cloneDir repo hasClone <- isRepositoryRoot path if hasClone then fetch path else do createDirectoryIfMissing True path mirror repo path allCommits :: FilePath -> IO (Set SHA) allCommits path = Set.fromList <$> gitLogImpl path [] firstCommit :: FilePath -> IO (Maybe SHA) firstCommit path = listToMaybe <$> gitLogImpl path ["--reverse"] showHead :: FilePath -> FilePath -> IO (Maybe String) showHead repo file = do (exitCode, stdout, stderr) <- git (Just repo) "show" ["HEAD:" ++ file] case exitCode of ExitSuccess -> return (Just stdout) ExitFailure _ -> return Nothing gitLogImpl :: FilePath -> [String] -> IO [SHA] gitLogImpl path args = do ret <- gitLoggingErrors (Just path) "log" ("--format=format:%H" : args) case ret of Nothing -> return [] Just stdout -> return (filter (not . null) (lines stdout))