bake-0.5: src/Development/Bake/Git.hs
{-# LANGUAGE ViewPatterns #-}
module Development.Bake.Git(
SHA1, fromSHA1, toSHA1, ovenGit,
gitPatchExtra, gitInit
) where
import Development.Bake.Core.Type
import General.Extra
import Development.Shake.Command
import Control.Monad.Extra
import Data.List.Extra
import General.HTML
import System.Directory.Extra
import System.FilePath
import Data.Maybe
import Data.Tuple.Extra
import Data.Char
import Data.Hashable
import Data.Monoid
import Prelude
data SHA1 = SHA1 Int String deriving (Show,Eq) -- a number of leading primes, followed by a valid SHA1
-- | Convert a SHA1 obtained from Git into a SHA1. Only done by ovenInit or ovenUpdate
toSHA1 :: String -> SHA1
toSHA1 x = checkSHA1 x $ SHA1 0 x
fromSHA1 :: SHA1 -> String
fromSHA1 (SHA1 _ x) = x
instance Stringy SHA1 where
stringyFrom x = checkSHA1 b $ SHA1 (length a) b
where (a,b) = span (== '\'') x
stringyTo (SHA1 primes sha) = replicate primes '\'' ++ sha
stringyPretty (SHA1 primes sha) = replicate primes '\'' ++ take 7 sha
-- either returns the second argument, or raises an error
checkSHA1 :: String -> a -> a
checkSHA1 x res
| length x /= 40 = error $ "SHA1 for Git must be 40 characters long, got " ++ show x
| not $ all (`elem` "0123456789abcdef") x = error $ "SHA1 for Git must be all lower case hex, got " ++ show x
| otherwise = res
-- | Modify an 'Oven' to work with the Git version control system.
-- Requires the name of the repo (e.g. @https:\/\/github.com\/ndmitchell\/bake.git@)
-- and the name of a branch (e.g. @master@). You can optionally give a path fragment
-- which is used to clone into.
ovenGit :: String -> String -> Maybe FilePath -> Oven () () test -> Oven SHA1 SHA1 test
ovenGit repo branch (fromMaybe "." -> path) o = o
{ovenInit = gitInit repo branch
,ovenUpdate = gitUpdate
,ovenPrepare = \s ps -> do gitCheckout s ps; ovenPrepare o () $ map (const ()) ps
,ovenPatchExtra = \s p -> gitPatchExtra s p =<< gitInitMirror
,ovenSupersede = \_ _ -> False
}
where
gitSafe dir = do
time_ $ cmd (Cwd dir) "git config user.email" ["https://github.com/ndmitchell/bake"]
time_ $ cmd (Cwd dir) "git config user.name" ["Bake Continuous Integration"]
-- initialise the mirror, or make it up to date
gitInitMirror = traced "gitInitMirror" $ do
-- make sure we descend one directory, since bake writes .bake.name
mirror <- fmap (</> "mirror") $ createDir "../bake-git" [repo]
createDirectoryIfMissing True mirror
-- see http://blog.plataformatec.com.br/2013/05/how-to-properly-mirror-a-git-repository/
ready <- doesFileExist $ mirror </> "HEAD"
if ready then
time_ $ cmd (Cwd mirror) "git fetch --prune"
else do
time_ $ cmd (Cwd mirror) "git clone --mirror" [repo] "."
gitSafe mirror
return mirror
gitUpdate s ps = traced "gitUpdate" $ do
gitCheckout s ps
Stdout x <- time $ cmd (Cwd path) "git rev-parse" [branch]
time_ $ cmd (Cwd path) "git push" [repo] [branch ++ ":" ++ branch]
return $ toSHA1 $ trim x
gitCheckout s ps = traced "gitCheckout" $ do
createDirectoryIfMissing True path
mirror <- gitInitMirror
unlessM (doesDirectoryExist $ path </>".git") $ do
time_ $ cmd (Cwd path) "git init"
gitSafe path
time_ $ cmd (Cwd path) "git remote add origin" [(if path == "." then "" else "../") ++ mirror]
time_ $ cmd (Cwd path) "git fetch"
time_ $ cmd (Cwd path) "git reset" -- to unwedge a previous merge conflict
time_ $ cmd (Cwd path) "git checkout" [branch]
time_ $ cmd (Cwd path) "git reset --hard" ["origin/" ++ branch]
Stdout x <- time $ cmd (Cwd path) "git rev-parse HEAD"
when (trim x /= fromSHA1 s) $ error $
"The branch " ++ branch ++ " changed SHA1 independently of bake.\n" ++
"Expected value: " ++ fromSHA1 s ++ "\n" ++
"But has become: " ++ trim x
forM_ ps $ \p ->
time_ $ cmd (Cwd path) "git merge" (fromSHA1 p)
gitInit :: String -> String -> IO SHA1
gitInit repo branch = traced "gitInit" $ do
Stdout hash <- time $ cmd "git ls-remote" [repo] [branch]
case words $ concat $ takeEnd 1 $ lines hash of
[] -> error "Couldn't find branch"
x:xs -> return $ toSHA1 $ trim x
traced :: String -> IO a -> IO a
traced msg act = do
putStrLn $ "% GIT: Begin " ++ msg
res <- act
putStrLn $ "% GIT: Finish " ++ msg
return res
---------------------------------------------------------------------
-- DIFF UTILITIES
gitPatchExtra :: SHA1 -> Maybe SHA1 -> FilePath -> IO (String, String)
gitPatchExtra s Nothing dir = do
Stdout full <- time $ cmd (Cwd dir) "git log --no-merges -n10 --pretty=format:%s" [fromSHA1 s]
Stdout count <- time $ cmd (Cwd dir) "git rev-list --count" [fromSHA1 s]
let summary = takeWhile (/= '\n') full
return (renderHTML $ do str_ $ count ++ " patches"; br_; str_ summary
,renderHTML $ pre_ $ str_ full)
gitPatchExtra s (Just p) dir = do
Stdout diff <- time $ cmd (Cwd dir)
"git diff" [fromSHA1 s ++ "..." ++ fromSHA1 p]
Stdout stat <- time $ cmd (Cwd dir)
"git diff --stat" [fromSHA1 s ++ "..." ++ fromSHA1 p]
Stdout log <- time $ cmd (Cwd dir)
"git log --no-merges -n1 --pretty=format:%s" [fromSHA1 p]
return (renderHTML $ do str_ $ reduceStat stat; br_; str_ $ take 120 $ takeWhile (/= '\n') log
,renderHTML $ pre_ $ do prettyStat stat; str_ "\n"; prettyDiff diff)
reduceStat :: String -> String
reduceStat = commasLimit 3 . map trim . map (takeWhile (/= '|')) . dropEnd 1 . lines
diff :: FilePath -> String
diff x = "diff:" ++ show (abs $ hash x)
-- |
-- > src/Paths.hs | 11 ++
-- > src/Test.hs | 258 ++++++++++++------------
-- > travis.hs | 4 +-
-- > 28 files changed, 1612 insertions(+), 1302 deletions(-)
prettyStat :: String -> HTML
prettyStat = unlines_ . maybe [] (uncurry snoc . (map f *** str_)) . unsnoc . map trimStart . lines
where
f x = a__ [href_ $ "#" ++ diff a] (str_ a) <> str_ b <> g c
where (ab,c) = break (== '|') x
(a,b) = spanEnd isSpace ab
g x@('+':_) = span__ [class_ "green"] (str_ a) <> g b
where (a,b) = span (== '+') x
g x@('-':_) = span__ [class_ "red"] (str_ a) <> g b
where (a,b) = span (== '-') x
g (x:xs) = str_ [x] <> g xs
g [] = mempty
-- |
-- > diff --git a/bake.cabal b/bake.cabal
-- > index 1aa1251..785cecc 100755
-- > --- a/bake.cabal
-- > +++ b/bake.cabal
-- > @@ -1,7 +1,7 @@
-- > cabal-version: >= 1.10
-- > build-type: Simple
-- > name: bake
-- > -version: 0.1
-- > +version: 0.2
prettyDiff :: String -> HTML
prettyDiff = unlines_ . map f . lines
where
f x | "diff --git " `isPrefixOf` x =
let files = [y | ab:'/':y <- drop 2 $ words x, ab `elem` "ab"] in
a__ (take 1 [name_ $ diff y | y <- files]) mempty <>
b_ (str_ x)
f x | any (`isPrefixOf` x) ["index ","--- ","+++ "] = b_ $ str_ x
f xs@('+':_) = span__ [class_ "green"] $ str_ xs
f xs@('-':_) = span__ [class_ "red"] $ str_ xs
f xs = str_ xs