In darcs post-apply hook, send a Push object to Vervis
This commit is contained in:
parent
6cb86ebbf1
commit
59ce05694e
5 changed files with 174 additions and 35 deletions
|
@ -13,5 +13,7 @@
|
||||||
- <http://creativecommons.org/publicdomain/zero/1.0/>.
|
- <http://creativecommons.org/publicdomain/zero/1.0/>.
|
||||||
-}
|
-}
|
||||||
|
|
||||||
|
import Vervis.Hook
|
||||||
|
|
||||||
main :: IO ()
|
main :: IO ()
|
||||||
main = putStrLn "Hello, I'm the posthook!"
|
main = postApply
|
||||||
|
|
|
@ -24,9 +24,11 @@ module Vervis.Hook
|
||||||
, Push (..)
|
, Push (..)
|
||||||
, writeHookConfig
|
, writeHookConfig
|
||||||
, postReceive
|
, postReceive
|
||||||
|
, postApply
|
||||||
)
|
)
|
||||||
where
|
where
|
||||||
|
|
||||||
|
import Control.Applicative
|
||||||
import Control.Exception
|
import Control.Exception
|
||||||
import Control.Monad
|
import Control.Monad
|
||||||
import Control.Monad.IO.Class
|
import Control.Monad.IO.Class
|
||||||
|
@ -35,18 +37,21 @@ import Crypto.Random
|
||||||
import Data.Aeson
|
import Data.Aeson
|
||||||
import Data.Bifunctor
|
import Data.Bifunctor
|
||||||
import Data.ByteString (ByteString)
|
import Data.ByteString (ByteString)
|
||||||
|
import Data.Char
|
||||||
import Data.Git hiding (Commit)
|
import Data.Git hiding (Commit)
|
||||||
import Data.Git.Ref
|
import Data.Git.Ref
|
||||||
import Data.Git.Types hiding (Commit)
|
import Data.Git.Types hiding (Commit)
|
||||||
import Data.Git.Graph
|
import Data.Git.Graph
|
||||||
import Data.Git.Harder
|
import Data.Git.Harder
|
||||||
import Data.Graph.Inductive.Graph -- (noNodes)
|
import Data.Graph.Inductive.Graph
|
||||||
import Data.Graph.Inductive.Query.Topsort
|
import Data.Graph.Inductive.Query.Topsort
|
||||||
import Data.Int
|
import Data.Int
|
||||||
|
import Data.Maybe
|
||||||
import Data.List.NonEmpty (NonEmpty, nonEmpty)
|
import Data.List.NonEmpty (NonEmpty, nonEmpty)
|
||||||
import Data.Text (Text)
|
import Data.Text (Text)
|
||||||
import Data.Time.Clock
|
import Data.Time.Clock
|
||||||
import Data.Time.Clock.POSIX
|
import Data.Time.Clock.POSIX
|
||||||
|
import Data.Time.Format
|
||||||
import Data.Word
|
import Data.Word
|
||||||
import GHC.Generics
|
import GHC.Generics
|
||||||
import Network.HTTP.Client
|
import Network.HTTP.Client
|
||||||
|
@ -58,9 +63,12 @@ import System.FilePath
|
||||||
import System.IO
|
import System.IO
|
||||||
import Text.Email.Aeson.Instances ()
|
import Text.Email.Aeson.Instances ()
|
||||||
import Text.Email.Validate
|
import Text.Email.Validate
|
||||||
|
import Text.Read
|
||||||
|
import Text.XML.Light
|
||||||
import Time.Types
|
import Time.Types
|
||||||
import Yesod.Core.Content
|
import Yesod.Core.Content
|
||||||
|
|
||||||
|
import qualified Data.Attoparsec.Text as A
|
||||||
import qualified Data.ByteString as B
|
import qualified Data.ByteString as B
|
||||||
import qualified Data.ByteString.Base16 as B16
|
import qualified Data.ByteString.Base16 as B16
|
||||||
import qualified Data.DList as D
|
import qualified Data.DList as D
|
||||||
|
@ -73,6 +81,7 @@ import qualified Data.Text.IO as TIO
|
||||||
import Data.KeyFile
|
import Data.KeyFile
|
||||||
import Network.FedURI
|
import Network.FedURI
|
||||||
|
|
||||||
|
import Control.Monad.Trans.Except.Local
|
||||||
import Data.DList.Local
|
import Data.DList.Local
|
||||||
import Data.List.NonEmpty.Local
|
import Data.List.NonEmpty.Local
|
||||||
|
|
||||||
|
@ -130,7 +139,7 @@ data Push = Push
|
||||||
, pushRepo :: Text
|
, pushRepo :: Text
|
||||||
, pushBranch :: Maybe Text
|
, pushBranch :: Maybe Text
|
||||||
, pushBefore :: Maybe Text
|
, pushBefore :: Maybe Text
|
||||||
, pushAfter :: Text
|
, pushAfter :: Maybe Text
|
||||||
, pushInit :: NonEmpty Commit
|
, pushInit :: NonEmpty Commit
|
||||||
, pushLast :: Maybe (Int, NonEmpty Commit)
|
, pushLast :: Maybe (Int, NonEmpty Commit)
|
||||||
}
|
}
|
||||||
|
@ -152,6 +161,45 @@ writeHookConfig config = do
|
||||||
createDirectoryIfMissing True cachePath
|
createDirectoryIfMissing True cachePath
|
||||||
encodeFile (cachePath </> hookConfigFileName) config
|
encodeFile (cachePath </> hookConfigFileName) config
|
||||||
|
|
||||||
|
splitCommits
|
||||||
|
:: Monad m
|
||||||
|
=> Config
|
||||||
|
-> NonEmpty a
|
||||||
|
-> ExceptT Text m (NonEmpty a, Maybe (Int, NonEmpty a))
|
||||||
|
splitCommits config commits =
|
||||||
|
if length commits <= maxCommits
|
||||||
|
then return (commits, Nothing)
|
||||||
|
else do
|
||||||
|
let half = maxCommits `div` 2
|
||||||
|
middle = length commits - 2 * half
|
||||||
|
(e, r) = NE.splitAt half commits
|
||||||
|
l = drop middle r
|
||||||
|
eNE <- nonEmptyE e "early is empty"
|
||||||
|
lNE <- nonEmptyE l "late is empty"
|
||||||
|
return (eNE, Just (middle, lNE))
|
||||||
|
where
|
||||||
|
maxCommits = configMaxCommits config
|
||||||
|
|
||||||
|
sendPush :: Config -> Manager -> Push -> ExceptT Text IO (Response ())
|
||||||
|
sendPush config manager push = do
|
||||||
|
let uri :: ObjURI Dev
|
||||||
|
uri =
|
||||||
|
ObjURI
|
||||||
|
(Authority "localhost" (Just $ configPort config))
|
||||||
|
(LocalURI "/post-receive")
|
||||||
|
req <- requestFromURI $ uriFromObjURI uri
|
||||||
|
let req' =
|
||||||
|
setRequestCheckStatus $
|
||||||
|
consHeader hContentType typeJson $
|
||||||
|
req { method = "POST"
|
||||||
|
, requestBody = RequestBodyLBS $ encode push
|
||||||
|
}
|
||||||
|
ExceptT $ first adaptErr <$> try (httpNoBody req' manager)
|
||||||
|
where
|
||||||
|
adaptErr :: HttpException -> Text
|
||||||
|
adaptErr = T.pack . displayException
|
||||||
|
consHeader n b r = r { requestHeaders = (n, b) : requestHeaders r }
|
||||||
|
|
||||||
reportNewCommits :: Config -> Text -> Text -> IO ()
|
reportNewCommits :: Config -> Text -> Text -> IO ()
|
||||||
reportNewCommits config sharer repo = do
|
reportNewCommits config sharer repo = do
|
||||||
user <- read <$> getEnv "VERVIS_SSH_USER"
|
user <- read <$> getEnv "VERVIS_SSH_USER"
|
||||||
|
@ -199,17 +247,7 @@ reportNewCommits config sharer repo = do
|
||||||
nonEmptyE before "No new commits"
|
nonEmptyE before "No new commits"
|
||||||
let commits = NE.map (uncurry makeCommit) historyNew
|
let commits = NE.map (uncurry makeCommit) historyNew
|
||||||
maxCommits = configMaxCommits config
|
maxCommits = configMaxCommits config
|
||||||
(early, late) <-
|
(early, late) <- splitCommits config commits
|
||||||
if length commits <= maxCommits
|
|
||||||
then return (commits, Nothing)
|
|
||||||
else do
|
|
||||||
let half = maxCommits `div` 2
|
|
||||||
middle = length commits - 2 * half
|
|
||||||
(e, r) = NE.splitAt half commits
|
|
||||||
l = drop middle r
|
|
||||||
eNE <- nonEmptyE e "early is empty"
|
|
||||||
lNE <- nonEmptyE l "late is empty"
|
|
||||||
return (eNE, Just (middle, lNE))
|
|
||||||
let push = Push
|
let push = Push
|
||||||
{ pushSecret = configSecret config
|
{ pushSecret = configSecret config
|
||||||
, pushUser = user
|
, pushUser = user
|
||||||
|
@ -217,32 +255,16 @@ reportNewCommits config sharer repo = do
|
||||||
, pushRepo = repo
|
, pushRepo = repo
|
||||||
, pushBranch = Just branch
|
, pushBranch = Just branch
|
||||||
, pushBefore = old <$ moldRef
|
, pushBefore = old <$ moldRef
|
||||||
, pushAfter = new
|
, pushAfter = Just new
|
||||||
, pushInit = early
|
, pushInit = early
|
||||||
, pushLast = late
|
, pushLast = late
|
||||||
}
|
}
|
||||||
uri :: ObjURI Dev
|
sendPush config manager push
|
||||||
uri =
|
|
||||||
ObjURI
|
|
||||||
(Authority "localhost" (Just $ configPort config))
|
|
||||||
(LocalURI "/post-receive")
|
|
||||||
req <- requestFromURI $ uriFromObjURI uri
|
|
||||||
let req' =
|
|
||||||
setRequestCheckStatus $
|
|
||||||
consHeader hContentType typeJson $
|
|
||||||
req { method = "POST"
|
|
||||||
, requestBody = RequestBodyLBS $ encode push
|
|
||||||
}
|
|
||||||
ExceptT $
|
|
||||||
first adaptErr <$> try (httpNoBody req' manager)
|
|
||||||
case result of
|
case result of
|
||||||
Left e -> TIO.hPutStrLn stderr $ "HOOK ERROR: " <> e
|
Left e -> TIO.hPutStrLn stderr $ "HOOK ERROR: " <> e
|
||||||
Right _resp -> return ()
|
Right _resp -> return ()
|
||||||
loop user manager git
|
loop user manager git
|
||||||
where
|
where
|
||||||
adaptErr :: HttpException -> Text
|
|
||||||
adaptErr = T.pack . displayException
|
|
||||||
consHeader n b r = r { requestHeaders = (n, b) : requestHeaders r }
|
|
||||||
parseRef t =
|
parseRef t =
|
||||||
if t == nullRef
|
if t == nullRef
|
||||||
then return Nothing
|
then return Nothing
|
||||||
|
@ -296,3 +318,113 @@ postReceive = do
|
||||||
[s, r] -> return (T.pack s, T.pack r)
|
[s, r] -> return (T.pack s, T.pack r)
|
||||||
_ -> die "Unexpected number of arguments"
|
_ -> die "Unexpected number of arguments"
|
||||||
reportNewCommits config sharer repo
|
reportNewCommits config sharer repo
|
||||||
|
|
||||||
|
reportNewPatches :: Config -> Text -> Text -> IO ()
|
||||||
|
reportNewPatches config sharer repo = do
|
||||||
|
user <- read <$> getEnv "VERVIS_SSH_USER"
|
||||||
|
manager <- newManager defaultManagerSettings
|
||||||
|
melem <- parseXMLDoc <$> getEnv "DARCS_PATCHES_XML"
|
||||||
|
result <- runExceptT $ do
|
||||||
|
push <- ExceptT . pure . runExcept $ do
|
||||||
|
elem <- fromMaybeE melem "parseXMLDoc failed"
|
||||||
|
children <- nonEmptyE (elChildren elem) "No patches"
|
||||||
|
patches <- traverse xml2patch children
|
||||||
|
(early, late) <- splitCommits config patches
|
||||||
|
return Push
|
||||||
|
{ pushSecret = configSecret config
|
||||||
|
, pushUser = user
|
||||||
|
, pushSharer = sharer
|
||||||
|
, pushRepo = repo
|
||||||
|
, pushBranch = Nothing
|
||||||
|
, pushBefore = Nothing
|
||||||
|
, pushAfter = Nothing
|
||||||
|
, pushInit = early
|
||||||
|
, pushLast = late
|
||||||
|
}
|
||||||
|
sendPush config manager push
|
||||||
|
case result of
|
||||||
|
Left e -> dieT $ "Post-apply hook error: " <> e
|
||||||
|
Right _resp -> return ()
|
||||||
|
where
|
||||||
|
dieT err = TIO.hPutStrLn stderr err >> exitFailure
|
||||||
|
xml2patch elem = do
|
||||||
|
unless (elName elem == QName "patch" Nothing Nothing) $
|
||||||
|
throwE $
|
||||||
|
"Expected <patch>, found: " <> T.pack (show $ elName elem)
|
||||||
|
(name, email) <- do
|
||||||
|
t <- T.pack <$> findAttrE "author" elem
|
||||||
|
parseOnlyE authorP t "author"
|
||||||
|
date <- do
|
||||||
|
s <- findAttrE "date" elem
|
||||||
|
case parseTimeM False defaultTimeLocale "%Y%m%d%H%M%S" s of
|
||||||
|
Nothing -> throwE $ "Date parsing failed: " <> T.pack s
|
||||||
|
Just t -> return t
|
||||||
|
hash <- do
|
||||||
|
t <- T.pack <$> findAttrE "hash" elem
|
||||||
|
unless (T.length t == 40) $
|
||||||
|
throwE $ "Expected a hash string of length 40, got: " <> t
|
||||||
|
return t
|
||||||
|
|
||||||
|
inverted <- do
|
||||||
|
s <- findAttrE "inverted" elem
|
||||||
|
readMaybeE s $ "Unrecognized inverted value: " <> T.pack s
|
||||||
|
when inverted $ throwE $ "Found inverted patch " <> hash
|
||||||
|
|
||||||
|
title <- T.pack . strContent <$> findChildE "name" elem
|
||||||
|
description <- do
|
||||||
|
t <- T.pack . strContent <$> findChildE "comment" elem
|
||||||
|
parseOnlyE commentP t "comment"
|
||||||
|
|
||||||
|
return Commit
|
||||||
|
{ commitWritten = (Author name email, date)
|
||||||
|
, commitCommitted = Nothing
|
||||||
|
, commitHash = hash
|
||||||
|
, commitTitle = title
|
||||||
|
, commitDescription = description
|
||||||
|
}
|
||||||
|
where
|
||||||
|
readMaybeE s e = fromMaybeE (readMaybe s) e
|
||||||
|
findAttrE q e =
|
||||||
|
let ms = findAttr (QName q Nothing Nothing) e
|
||||||
|
in fromMaybeE ms $ "Couldn't find attr \"" <> T.pack q <> "\""
|
||||||
|
findChildE q e =
|
||||||
|
case findChildren (QName q Nothing Nothing) e of
|
||||||
|
[] -> throwE $ "No children named " <> T.pack q
|
||||||
|
[c] -> return c
|
||||||
|
_ -> throwE $ "Multiple children named " <> T.pack q
|
||||||
|
authorP = (,)
|
||||||
|
<$> (T.stripEnd <$> A.takeWhile1 (/= '<'))
|
||||||
|
<* A.skip (== '<')
|
||||||
|
<*> (A.takeWhile1 (/= '>') >>= emailP)
|
||||||
|
<* A.skip (== '>')
|
||||||
|
where
|
||||||
|
emailP
|
||||||
|
= maybe (fail "Invalid email") pure
|
||||||
|
. emailAddress
|
||||||
|
. TE.encodeUtf8
|
||||||
|
commentP
|
||||||
|
= A.string "Ignore-this: "
|
||||||
|
*> A.takeWhile1 isHexDigit
|
||||||
|
*> (fromMaybe T.empty <$>
|
||||||
|
optional (A.endOfLine *> A.endOfLine *> A.takeText)
|
||||||
|
)
|
||||||
|
parseOnlyE p t n =
|
||||||
|
case A.parseOnly (p <* A.endOfInput) t of
|
||||||
|
Left e ->
|
||||||
|
throwE $ T.concat ["Parsing ", n, " failed: ", T.pack e]
|
||||||
|
Right a -> return a
|
||||||
|
|
||||||
|
postApply :: IO ()
|
||||||
|
postApply = do
|
||||||
|
cachePath <- getVervisCachePath
|
||||||
|
config <- do
|
||||||
|
mc <- decodeFileStrict' $ cachePath </> hookConfigFileName
|
||||||
|
case mc of
|
||||||
|
Nothing -> die "Parsing hook config failed"
|
||||||
|
Just c -> return c
|
||||||
|
args <- getArgs
|
||||||
|
(sharer, repo) <-
|
||||||
|
case args of
|
||||||
|
[s, r] -> return (T.pack s, T.pack r)
|
||||||
|
_ -> die "Unexpected number of arguments"
|
||||||
|
reportNewPatches config sharer repo
|
||||||
|
|
|
@ -251,6 +251,8 @@ runAction repoDir _wantReply action =
|
||||||
can <- canPushTo sharer repo
|
can <- canPushTo sharer repo
|
||||||
if can
|
if can
|
||||||
then whenDarcsRepoExists True repoPath $ do
|
then whenDarcsRepoExists True repoPath $ do
|
||||||
|
pid <- authId <$> askAuthDetails
|
||||||
|
liftIO $ setEnv "VERVIS_SSH_USER" (show $ fromSqlKey pid)
|
||||||
execute "darcs" ["apply", "--all", "--repodir", repoPath]
|
execute "darcs" ["apply", "--all", "--repodir", repoPath]
|
||||||
return ARProcess
|
return ARProcess
|
||||||
else return $ ARFail "You can't push to this repository"
|
else return $ ARFail "You can't push to this repository"
|
||||||
|
|
|
@ -1052,7 +1052,7 @@ data Push u = Push
|
||||||
, pushCommitsTotal :: Int
|
, pushCommitsTotal :: Int
|
||||||
, pushTarget :: LocalURI
|
, pushTarget :: LocalURI
|
||||||
, pushHashBefore :: Maybe Text
|
, pushHashBefore :: Maybe Text
|
||||||
, pushHashAfter :: Text
|
, pushHashAfter :: Maybe Text
|
||||||
}
|
}
|
||||||
|
|
||||||
parsePush :: UriMode u => Authority u -> Object -> Parser (Push u)
|
parsePush :: UriMode u => Authority u -> Object -> Parser (Push u)
|
||||||
|
@ -1064,7 +1064,7 @@ parsePush a o = do
|
||||||
<*> c .: "totalItems"
|
<*> c .: "totalItems"
|
||||||
<*> withAuthorityO a (o .: "target")
|
<*> withAuthorityO a (o .: "target")
|
||||||
<*> o .:? "hashBefore"
|
<*> o .:? "hashBefore"
|
||||||
<*> o .: "hashAfter"
|
<*> o .:? "hashAfter"
|
||||||
|
|
||||||
encodePush :: UriMode u => Authority u -> Push u -> Series
|
encodePush :: UriMode u => Authority u -> Push u -> Series
|
||||||
encodePush a (Push lateCommits earlyCommits total target before after)
|
encodePush a (Push lateCommits earlyCommits total target before after)
|
||||||
|
@ -1076,7 +1076,7 @@ encodePush a (Push lateCommits earlyCommits total target before after)
|
||||||
)
|
)
|
||||||
<> "target" .= ObjURI a target
|
<> "target" .= ObjURI a target
|
||||||
<> "hashBefore" .=? before
|
<> "hashBefore" .=? before
|
||||||
<> "hashAfter" .= after
|
<> "hashAfter" .=? after
|
||||||
where
|
where
|
||||||
objectList items = listEncoding (pairs . toSeries a) (NE.toList items)
|
objectList items = listEncoding (pairs . toSeries a) (NE.toList items)
|
||||||
|
|
||||||
|
|
|
@ -367,6 +367,9 @@ library
|
||||||
, warp
|
, warp
|
||||||
-- for encoding and decoding of crypto public keys
|
-- for encoding and decoding of crypto public keys
|
||||||
, x509
|
, x509
|
||||||
|
-- for parsing darcs apply's changes XML from env var in
|
||||||
|
-- the vervis post-apply hook program
|
||||||
|
, xml
|
||||||
, xss-sanitize
|
, xss-sanitize
|
||||||
, yaml
|
, yaml
|
||||||
, yesod
|
, yesod
|
||||||
|
|
Loading…
Reference in a new issue