fbc3c223a6
This caused git to complain that filter-process failed and kill it with signal 15. Because it wrote an extra flushPkt for an empty file, which git did not expect, and so git saw an unexpected response to the next request. Luckily, filter-process is only used by default in v9 and up, and v8 is still the default. Also, git had to be updating an empty file, followed by another file, which is a fairly unlikely situation. And git restarts filter-process after this happens and uses it to filter the rest of the files. So this isn't a crippling bug. Sponsored-by: Luke Shumaker on Patreon
170 lines
5.3 KiB
Haskell
170 lines
5.3 KiB
Haskell
{- git long-running filter process
|
|
-
|
|
- As documented in git's gitattributes(5) and
|
|
- Documentation/technical/long-running-process-protocol.txt
|
|
-
|
|
- Copyright 2021 Joey Hess <id@joeyh.name>
|
|
-
|
|
- Licensed under the GNU AGPL version 3 or higher.
|
|
-}
|
|
|
|
{-# LANGUAGE OverloadedStrings #-}
|
|
|
|
module Git.FilterProcess (
|
|
WelcomeMessage(..),
|
|
Version(..),
|
|
Capability(..),
|
|
longRunningProcessHandshake,
|
|
longRunningFilterProcessHandshake,
|
|
FilterRequest(..),
|
|
getFilterRequest,
|
|
respondFilterRequest,
|
|
) where
|
|
|
|
import Common
|
|
import Git.PktLine
|
|
|
|
import qualified Data.ByteString as B
|
|
|
|
{- This is a message like "git-filter-client" or "git-filter-server" -}
|
|
data WelcomeMessage = WelcomeMessage PktLine
|
|
deriving (Show)
|
|
|
|
{- Configuration message, eg "foo=bar" -}
|
|
data ConfigValue = ConfigValue String String
|
|
deriving (Show, Eq)
|
|
|
|
encodeConfigValue :: ConfigValue -> PktLine
|
|
encodeConfigValue (ConfigValue k v) = stringPktLine (k <> "=" <> v)
|
|
|
|
decodeConfigValue :: PktLine -> Maybe ConfigValue
|
|
decodeConfigValue pktline =
|
|
let t = pktLineToString pktline
|
|
(k, v) = break (== '=') t
|
|
in if null v
|
|
then Nothing
|
|
else Just $ ConfigValue k (drop 1 v)
|
|
|
|
extractConfigValue :: [ConfigValue] -> String -> Maybe String
|
|
extractConfigValue [] _ = Nothing
|
|
extractConfigValue (ConfigValue k v:cs) wantk
|
|
| k == wantk = Just v
|
|
| otherwise = extractConfigValue cs wantk
|
|
|
|
data Version = Version Int
|
|
deriving (Show, Eq)
|
|
|
|
encodeVersion :: Version -> PktLine
|
|
encodeVersion (Version n) = encodeConfigValue $ ConfigValue "version" (show n)
|
|
|
|
decodeVersion :: PktLine -> Maybe Version
|
|
decodeVersion pktline = decodeConfigValue pktline >>= \case
|
|
ConfigValue "version" v -> Version <$> readish v
|
|
_ -> Nothing
|
|
|
|
data Capability = Capability String
|
|
deriving (Show, Eq)
|
|
|
|
encodeCapability :: Capability -> PktLine
|
|
encodeCapability (Capability c) = encodeConfigValue $
|
|
ConfigValue "capability" c
|
|
|
|
decodeCapability :: PktLine -> Maybe Capability
|
|
decodeCapability pktline = decodeConfigValue pktline >>= \case
|
|
ConfigValue "capability" c -> Just $ Capability c
|
|
_ -> Nothing
|
|
|
|
longRunningProcessHandshake
|
|
:: (WelcomeMessage -> Maybe WelcomeMessage)
|
|
-> ([Version] -> [Version])
|
|
-> ([Capability] -> [Capability])
|
|
-> IO (Either String ())
|
|
longRunningProcessHandshake respwelcomemessage filterversions filtercapabilities =
|
|
readUntilFlushPkt >>= \case
|
|
[] -> protoerr "no welcome message"
|
|
(welcomemessage:versions) ->
|
|
checkwelcomemessage welcomemessage $
|
|
checkversion versions $ do
|
|
capabilities <- readUntilFlushPkt
|
|
checkcapabilities capabilities success
|
|
where
|
|
protoerr msg = return $ Left $ "git protocol error: " ++ msg
|
|
success = return (Right ())
|
|
|
|
checkwelcomemessage welcomemessage cont =
|
|
case respwelcomemessage (WelcomeMessage welcomemessage) of
|
|
Nothing -> protoerr "unsupported welcome message"
|
|
Just (WelcomeMessage welcomemessage') -> do
|
|
writePktLine stdout welcomemessage'
|
|
cont
|
|
|
|
checkversion versions cont = do
|
|
let versions' = filterversions (mapMaybe decodeVersion versions)
|
|
if null versions'
|
|
then protoerr "unsupported protocol version"
|
|
else do
|
|
forM_ versions' $ \v ->
|
|
writePktLine stdout $ encodeVersion v
|
|
writePktLine stdout flushPkt
|
|
cont
|
|
|
|
checkcapabilities capabilities cont = do
|
|
let capabilities' = filtercapabilities (mapMaybe decodeCapability capabilities)
|
|
if null capabilities'
|
|
then protoerr "unsupported protocol capabilities"
|
|
else do
|
|
forM_ capabilities' $ \c ->
|
|
writePktLine stdout $ encodeCapability c
|
|
writePktLine stdout flushPkt
|
|
cont
|
|
|
|
longRunningFilterProcessHandshake :: IO (Either String ())
|
|
longRunningFilterProcessHandshake =
|
|
longRunningProcessHandshake respwelcomemessage filterversions filtercapabilities
|
|
where
|
|
respwelcomemessage (WelcomeMessage w)
|
|
| pktLineToString w == "git-filter-client" =
|
|
Just $ WelcomeMessage $ stringPktLine "git-filter-server"
|
|
| otherwise = Nothing
|
|
filterversions = filter (== Version 2)
|
|
-- Delay capability is not implemented, so filter it out.
|
|
filtercapabilities = filter (`elem` [Capability "smudge", Capability "clean"])
|
|
|
|
data FilterRequest = Smudge FilePath | Clean FilePath
|
|
deriving (Show, Eq)
|
|
|
|
{- Waits for the next FilterRequest to be received. Does not read
|
|
- the content to be filtered, which is what gets sent subsequent to the
|
|
- FilterRequest. Use eg readUntilFlushPkt to read it, before calling
|
|
- respondFilterRequest. -}
|
|
getFilterRequest :: IO (Maybe FilterRequest)
|
|
getFilterRequest = do
|
|
ps <- readUntilFlushPkt
|
|
let cs = mapMaybe decodeConfigValue ps
|
|
case (extractConfigValue cs "command", extractConfigValue cs "pathname") of
|
|
(Just command, Just pathname)
|
|
| command == "smudge" -> return $ Just $ Smudge pathname
|
|
| command == "clean" -> return $ Just $ Clean pathname
|
|
| otherwise -> return Nothing
|
|
_ -> return Nothing
|
|
|
|
{- Send a response to a FilterRequest, consisting of the filtered content. -}
|
|
respondFilterRequest :: B.ByteString -> IO ()
|
|
respondFilterRequest b = do
|
|
writePktLine stdout $ encodeConfigValue $ ConfigValue "status" "success"
|
|
writePktLine stdout flushPkt
|
|
send b
|
|
-- The protocol allows for another list of ConfigValues to be sent
|
|
-- here, but we don't use it. Send another flushPkt to terminate
|
|
-- the empty list.
|
|
writePktLine stdout flushPkt
|
|
where
|
|
send b' =
|
|
let (pktline, rest) = encodePktLine b'
|
|
in do
|
|
if isFlushPkt pktline
|
|
then return ()
|
|
else writePktLine stdout pktline
|
|
case rest of
|
|
Just b'' -> send b''
|
|
Nothing -> writePktLine stdout flushPkt
|