diff --git a/hook-darcs/main.hs b/hook-darcs/main.hs index 1d1de3f..c44191d 100644 --- a/hook-darcs/main.hs +++ b/hook-darcs/main.hs @@ -13,5 +13,7 @@ - . -} +import Vervis.Hook + main :: IO () -main = putStrLn "Hello, I'm the posthook!" +main = postApply diff --git a/src/Vervis/Hook.hs b/src/Vervis/Hook.hs index 95c0113..be200ec 100644 --- a/src/Vervis/Hook.hs +++ b/src/Vervis/Hook.hs @@ -24,9 +24,11 @@ module Vervis.Hook , Push (..) , writeHookConfig , postReceive + , postApply ) where +import Control.Applicative import Control.Exception import Control.Monad import Control.Monad.IO.Class @@ -35,18 +37,21 @@ import Crypto.Random import Data.Aeson import Data.Bifunctor import Data.ByteString (ByteString) +import Data.Char import Data.Git hiding (Commit) import Data.Git.Ref import Data.Git.Types hiding (Commit) import Data.Git.Graph import Data.Git.Harder -import Data.Graph.Inductive.Graph -- (noNodes) +import Data.Graph.Inductive.Graph import Data.Graph.Inductive.Query.Topsort import Data.Int +import Data.Maybe import Data.List.NonEmpty (NonEmpty, nonEmpty) import Data.Text (Text) import Data.Time.Clock import Data.Time.Clock.POSIX +import Data.Time.Format import Data.Word import GHC.Generics import Network.HTTP.Client @@ -58,9 +63,12 @@ import System.FilePath import System.IO import Text.Email.Aeson.Instances () import Text.Email.Validate +import Text.Read +import Text.XML.Light import Time.Types import Yesod.Core.Content +import qualified Data.Attoparsec.Text as A import qualified Data.ByteString as B import qualified Data.ByteString.Base16 as B16 import qualified Data.DList as D @@ -73,6 +81,7 @@ import qualified Data.Text.IO as TIO import Data.KeyFile import Network.FedURI +import Control.Monad.Trans.Except.Local import Data.DList.Local import Data.List.NonEmpty.Local @@ -130,7 +139,7 @@ data Push = Push , pushRepo :: Text , pushBranch :: Maybe Text , pushBefore :: Maybe Text - , pushAfter :: Text + , pushAfter :: Maybe Text , pushInit :: NonEmpty Commit , pushLast :: Maybe (Int, NonEmpty Commit) } @@ -152,6 +161,45 @@ writeHookConfig config = do createDirectoryIfMissing True cachePath 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 sharer repo = do user <- read <$> getEnv "VERVIS_SSH_USER" @@ -199,17 +247,7 @@ reportNewCommits config sharer repo = do nonEmptyE before "No new commits" let commits = NE.map (uncurry makeCommit) historyNew maxCommits = configMaxCommits config - (early, late) <- - 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)) + (early, late) <- splitCommits config commits let push = Push { pushSecret = configSecret config , pushUser = user @@ -217,32 +255,16 @@ reportNewCommits config sharer repo = do , pushRepo = repo , pushBranch = Just branch , pushBefore = old <$ moldRef - , pushAfter = new + , pushAfter = Just new , pushInit = early , pushLast = late } - 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) + sendPush config manager push case result of Left e -> TIO.hPutStrLn stderr $ "HOOK ERROR: " <> e Right _resp -> return () loop user manager git where - adaptErr :: HttpException -> Text - adaptErr = T.pack . displayException - consHeader n b r = r { requestHeaders = (n, b) : requestHeaders r } parseRef t = if t == nullRef then return Nothing @@ -296,3 +318,113 @@ postReceive = do [s, r] -> return (T.pack s, T.pack r) _ -> die "Unexpected number of arguments" 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 , 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 diff --git a/src/Vervis/Ssh.hs b/src/Vervis/Ssh.hs index 366147f..1b63b1a 100644 --- a/src/Vervis/Ssh.hs +++ b/src/Vervis/Ssh.hs @@ -251,6 +251,8 @@ runAction repoDir _wantReply action = can <- canPushTo sharer repo if can then whenDarcsRepoExists True repoPath $ do + pid <- authId <$> askAuthDetails + liftIO $ setEnv "VERVIS_SSH_USER" (show $ fromSqlKey pid) execute "darcs" ["apply", "--all", "--repodir", repoPath] return ARProcess else return $ ARFail "You can't push to this repository" diff --git a/src/Web/ActivityPub.hs b/src/Web/ActivityPub.hs index 2f0cefc..77434e3 100644 --- a/src/Web/ActivityPub.hs +++ b/src/Web/ActivityPub.hs @@ -1052,7 +1052,7 @@ data Push u = Push , pushCommitsTotal :: Int , pushTarget :: LocalURI , pushHashBefore :: Maybe Text - , pushHashAfter :: Text + , pushHashAfter :: Maybe Text } parsePush :: UriMode u => Authority u -> Object -> Parser (Push u) @@ -1064,7 +1064,7 @@ parsePush a o = do <*> c .: "totalItems" <*> withAuthorityO a (o .: "target") <*> o .:? "hashBefore" - <*> o .: "hashAfter" + <*> o .:? "hashAfter" encodePush :: UriMode u => Authority u -> Push u -> Series 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 <> "hashBefore" .=? before - <> "hashAfter" .= after + <> "hashAfter" .=? after where objectList items = listEncoding (pairs . toSeries a) (NE.toList items) diff --git a/vervis.cabal b/vervis.cabal index d5dddfa..c389a26 100644 --- a/vervis.cabal +++ b/vervis.cabal @@ -367,6 +367,9 @@ library , warp -- for encoding and decoding of crypto public keys , x509 + -- for parsing darcs apply's changes XML from env var in + -- the vervis post-apply hook program + , xml , xss-sanitize , yaml , yesod