{- git diff-tree interface - - Copyright 2012-2020 Joey Hess - - Licensed under the GNU AGPL version 3 or higher. -} module Git.DiffTree ( DiffTreeItem(..), isDiffOf, diffTree, diffTreeRecursive, diffIndex, diffWorkTree, diffFiles, diffLog, commitDiff, parseDiffRaw, ) where import qualified Data.ByteString as B import qualified Data.ByteString.Lazy as L import qualified Data.Attoparsec.ByteString.Lazy as A import qualified Data.Attoparsec.ByteString.Char8 as A8 import Common import Git import Git.Sha import Git.Command import Git.FilePath import Git.DiffTreeItem import qualified Git.Filename import qualified Git.Ref import Utility.Attoparsec {- Checks if the DiffTreeItem modifies a file with a given name - or under a directory by that name. -} isDiffOf :: DiffTreeItem -> TopFilePath -> Bool isDiffOf diff f = let f' = getTopFilePath f in if B.null f' then True -- top of repo contains all else f' `dirContains` getTopFilePath (file diff) {- Diffs two tree Refs. -} diffTree :: Ref -> Ref -> Repo -> IO ([DiffTreeItem], IO Bool) diffTree src dst = getdiff (Param "diff-tree") [Param (fromRef src), Param (fromRef dst), Param "--"] {- Diffs two tree Refs, recursing into sub-trees -} diffTreeRecursive :: Ref -> Ref -> Repo -> IO ([DiffTreeItem], IO Bool) diffTreeRecursive src dst = getdiff (Param "diff-tree") [Param "-r", Param (fromRef src), Param (fromRef dst), Param "--"] {- Diffs between a tree and the index. Does nothing if there is not yet a - commit in the repository. -} diffIndex :: Ref -> Repo -> IO ([DiffTreeItem], IO Bool) diffIndex ref = diffIndex' ref [Param "--cached"] {- Diffs between a tree and the working tree. Does nothing if there is not - yet a commit in the repository, or if the repository is bare. -} diffWorkTree :: Ref -> Repo -> IO ([DiffTreeItem], IO Bool) diffWorkTree ref repo = ifM (Git.Ref.headExists repo) ( diffIndex' ref [] repo , return ([], return True) ) diffIndex' :: Ref -> [CommandParam] -> Repo -> IO ([DiffTreeItem], IO Bool) diffIndex' ref params repo = ifM (Git.Ref.headExists repo) ( getdiff (Param "diff-index") ( params ++ [Param $ fromRef ref] ++ [Param "--"] ) repo , return ([], return True) ) {- Diff between the index and work tree. -} diffFiles :: [CommandParam] -> Repo -> IO ([DiffTreeItem], IO Bool) diffFiles = getdiff (Param "diff-files") {- Runs git log in --raw mode to get the changes that were made in - a particular commit to particular files. The output format - is adjusted to be the same as diff-tree --raw._-} diffLog :: [CommandParam] -> Repo -> IO ([DiffTreeItem], IO Bool) diffLog params = getdiff (Param "log") (Param "-n1" : Param "--no-abbrev" : Param "--pretty=format:" : params) {- Uses git show to get the changes made by a commit. - - Does not support merge commits, and will fail on them. -} commitDiff :: Sha -> Repo -> IO ([DiffTreeItem], IO Bool) commitDiff ref = getdiff (Param "show") [ Param "--no-abbrev", Param "--pretty=", Param "--raw", Param (fromRef ref) ] getdiff :: CommandParam -> [CommandParam] -> Repo -> IO ([DiffTreeItem], IO Bool) getdiff command params repo = do (diff, cleanup) <- pipeNullSplit ps repo return (parseDiffRaw diff, cleanup) where ps = command : Param "-z" : Param "--raw" : Param "--no-renames" : Param "-l0" : params {- Parses --raw output used by diff-tree and git-log. -} parseDiffRaw :: [L.ByteString] -> [DiffTreeItem] parseDiffRaw l = go l where go [] = [] go (info:f:rest) = case A.parse (parserDiffRaw (L.toStrict f)) info of A.Done _ r -> r : go rest A.Fail _ _ err -> error $ "diff-tree parse error: " ++ err go (s:[]) = error $ "diff-tree parse error near \"" ++ decodeBL s ++ "\"" -- : SP SP SP SP -- -- May be prefixed with a newline, which git log --pretty=format -- adds to the first line of the diff, even with -z. parserDiffRaw :: RawFilePath -> A.Parser DiffTreeItem parserDiffRaw f = DiffTreeItem <$ A.option '\n' (A8.char '\n') <* A8.char ':' <*> octal <* A8.char ' ' <*> octal <* A8.char ' ' <*> (maybe (fail "bad srcsha") return . extractSha =<< nextword) <* A8.char ' ' <*> (maybe (fail "bad dstsha") return . extractSha =<< nextword) <* A8.char ' ' <*> A.takeByteString <*> pure (asTopFilePath $ fromInternalGitPath $ Git.Filename.decode f) where nextword = A8.takeTill (== ' ')