{-# LANGUAGE CPP #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ConstraintKinds #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE ViewPatterns #-}
{-# OPTIONS_GHC -fno-warn-orphans #-}
{-# OPTIONS_GHC -fno-warn-name-shadowing #-}
module Main where
import Control.Monad
import Control.Monad.IO.Class (MonadIO(..))
import Data.Char
import Data.Foldable hiding (concat, concatMap, elem)
import Data.List
import Data.Maybe
import Data.Tagged
import qualified Data.Text as T
#if MIN_VERSION_shelly(1, 0, 0)
import Data.Text (Text)
import qualified Data.Text as TL hiding (tails)
#else
import Data.Text.Lazy (Text)
import qualified Data.Text.Lazy as TL hiding (tails)
#endif
import Git hiding (Options)
import Git.Libgit2 (lgFactory, withLibGitDo)
import Options.Applicative
import Prelude hiding (FilePath, null)
import Shelly
toStrict :: TL.Text -> T.Text
#if MIN_VERSION_shelly(1, 0, 0)
toStrict = id
#else
toStrict = TL.toStrict
#endif
fromStrict :: T.Text -> TL.Text
#if MIN_VERSION_shelly(1, 0, 0)
fromStrict = id
#else
fromStrict = TL.fromStrict
#endif
instance Read FilePath
data Options = Options
{ verbose :: Bool
, dryRun :: Bool
, remote :: Maybe String
, args :: [String]
}
options :: Parser Options
options = Options
<$> switch (short 'v' <> long "verbose" <> help "Display info")
<*> switch (short 'n' <> long "dry-run" <> help "Dry run")
<*> argument (Just . Just) (value Nothing)
<*> arguments Just hidden
volume :: Options -> Sh a -> Sh a
volume opts = if verbose opts then verbosely else silently
sh :: MonadIO m => Options -> Sh a -> m a
sh opts = shelly . volume opts
git :: [Text] -> Sh Text
git = run "git"
git_ :: [Text] -> Sh ()
git_ = run_ "git"
main :: IO ()
main = withLibGitDo $ execParser opts >>= pushToGitHub
where
opts = info (helper <*> options)
(fullDesc <> progDesc desc <> header hdr)
hdr = "git-gpush 1.0.0 - push to GitHub with smarter behavior"
desc = "\nDescription goes here."
pushToGitHub :: Options -> IO ()
pushToGitHub opts = do
gd <- fromText . TL.init
<$> sh opts (run "git" ["rev-parse", "--git-dir"])
remoteName <- case remote opts of
Nothing -> sh opts $ getRemoteName gd
Just x -> return (TL.pack x)
remoteHead <-
head . TL.words . TL.init
<$> sh opts (git [ "ls-remote", remoteName, "HEAD" ])
withRepository lgFactory gd $ do
cref <- fromJust <$> resolveReference "HEAD"
hc <- lookupCommit cref
rcoid <- Tagged <$> parseOid (toStrict remoteHead)
objs <- missingObjects (Just rcoid) (commitOid hc)
for_ objs $ \obj -> case obj of
Git.CommitObjOid coid -> do
commit <- lookupCommit coid
sh opts $ processCommitTags (commitLog commit)
_ -> return ()
unless (dryRun opts) $
sh opts $ git_ $ [ "push", remoteName ] <> map TL.pack (args opts)
getRemoteName :: FilePath -> Sh Text
getRemoteName gd = do
mref <- liftIO $ withRepository lgFactory gd $ lookupReference "HEAD"
case mref of
Nothing -> error "Could not find HEAD"
Just (Reference _ (RefObj _)) ->
error "Cannot push from a detached HEAD"
Just (Reference _ (RefSymbolic (fromStrict -> branch)))
| "refs/heads/" `TL.isPrefixOf` branch ->
TL.init <$> git [ "config"
, "branch." <> base branch <> ".remote" ]
| otherwise ->
error "Cannot push from a branch outside refs/heads"
where
base = TL.drop (TL.length "refs/heads/")
data CommitTag
= ConfirmTag
{ confirmIssue :: Text
, confirmReport :: Maybe Text
}
deriving (Eq, Show)
findTags :: Text -> [CommitTag]
findTags = concatMap go . tails . map norm . TL.words
where
go [] = []
go [_] = []
go ["confirm", TL.splitOn ":" -> [issue]] =
[ ConfirmTag (TL.tail issue) Nothing | isIssue issue ]
go ["confirm", TL.splitOn ":" -> [issue, report]] =
[ ConfirmTag (TL.tail issue) (Just report) | isIssue issue ]
go _ = []
isIssue issue = not (TL.null issue) && TL.head issue == '#'
norm = TL.filter $ \c -> c `elem` "#:_-" || isDigit c || isLetter c
processCommitTags :: T.Text -> Sh ()
processCommitTags msg =
for_ (findTags (fromStrict msg)) $ \tag -> case tag of
ConfirmTag issue mreport -> do
mreport' <- case mreport of
x@(Just _) -> return x
Nothing -> errExit False $ do
who <- git [ "config", "fpco.directreport" ]
ec <- lastExitCode
return $ if ec == 0
then Just (TL.init who)
else Nothing
-- jww (2013-04-28): Instead of using ghi here, we should use the
-- github library, after reading in ghi.token.
run_ "ghi"
$ [ "edit" ]
<> [ x | x <- [ "-u", fromJust mreport' ], isJust mreport' ]
<> [ issue ]
run_ "ghi" [ "comment"
, "-m", "Please confirm that this has been fixed."
, issue ]
run_ "ghi" [ "label", issue, "-a", "need confirmation" ]
-- Main.hs (git-gpush) ends here