1{- git diff-tree interface
2 -
3 - Copyright 2012-2020 Joey Hess <id@joeyh.name>
4 -
5 - Licensed under the GNU AGPL version 3 or higher.
6 -}
7
8module Git.DiffTree (
9	DiffTreeItem(..),
10	isDiffOf,
11	diffTree,
12	diffTreeRecursive,
13	diffIndex,
14	diffWorkTree,
15	diffFiles,
16	diffLog,
17	commitDiff,
18	parseDiffRaw,
19) where
20
21import qualified Data.ByteString as B
22import qualified Data.ByteString.Lazy as L
23import qualified Data.Attoparsec.ByteString.Lazy as A
24import qualified Data.Attoparsec.ByteString.Char8 as A8
25
26import Common
27import Git
28import Git.Sha
29import Git.Command
30import Git.FilePath
31import Git.DiffTreeItem
32import qualified Git.Filename
33import qualified Git.Ref
34import Utility.Attoparsec
35
36{- Checks if the DiffTreeItem modifies a file with a given name
37 - or under a directory by that name. -}
38isDiffOf :: DiffTreeItem -> TopFilePath -> Bool
39isDiffOf diff f =
40	let f' = getTopFilePath f
41	in if B.null f'
42		then True -- top of repo contains all
43		else f' `dirContains` getTopFilePath (file diff)
44
45{- Diffs two tree Refs. -}
46diffTree :: Ref -> Ref -> Repo -> IO ([DiffTreeItem], IO Bool)
47diffTree src dst = getdiff (Param "diff-tree")
48	[Param (fromRef src), Param (fromRef dst), Param "--"]
49
50{- Diffs two tree Refs, recursing into sub-trees -}
51diffTreeRecursive :: Ref -> Ref -> Repo -> IO ([DiffTreeItem], IO Bool)
52diffTreeRecursive src dst = getdiff (Param "diff-tree")
53	[Param "-r", Param (fromRef src), Param (fromRef dst), Param "--"]
54
55{- Diffs between a tree and the index. Does nothing if there is not yet a
56 - commit in the repository. -}
57diffIndex :: Ref -> Repo -> IO ([DiffTreeItem], IO Bool)
58diffIndex ref = diffIndex' ref [Param "--cached"]
59
60{- Diffs between a tree and the working tree. Does nothing if there is not
61 - yet a commit in the repository, or if the repository is bare. -}
62diffWorkTree :: Ref -> Repo -> IO ([DiffTreeItem], IO Bool)
63diffWorkTree ref repo =
64	ifM (Git.Ref.headExists repo)
65		( diffIndex' ref [] repo
66		, return ([], return True)
67		)
68
69diffIndex' :: Ref -> [CommandParam] -> Repo -> IO ([DiffTreeItem], IO Bool)
70diffIndex' ref params repo =
71	ifM (Git.Ref.headExists repo)
72		( getdiff (Param "diff-index")
73			( params ++ [Param $ fromRef ref] ++ [Param "--"] )
74			repo
75		, return ([], return True)
76		)
77
78{- Diff between the index and work tree. -}
79diffFiles :: [CommandParam] -> Repo -> IO ([DiffTreeItem], IO Bool)
80diffFiles = getdiff (Param "diff-files")
81
82{- Runs git log in --raw mode to get the changes that were made in
83 - a particular commit to particular files. The output format
84 - is adjusted to be the same as diff-tree --raw._-}
85diffLog :: [CommandParam] -> Repo -> IO ([DiffTreeItem], IO Bool)
86diffLog params = getdiff (Param "log")
87	(Param "-n1" : Param "--no-abbrev" : Param "--pretty=format:" : params)
88
89{- Uses git show to get the changes made by a commit.
90 -
91 - Does not support merge commits, and will fail on them. -}
92commitDiff :: Sha -> Repo -> IO ([DiffTreeItem], IO Bool)
93commitDiff ref = getdiff (Param "show")
94	[ Param "--no-abbrev", Param "--pretty=", Param "--raw", Param (fromRef ref) ]
95
96getdiff :: CommandParam -> [CommandParam] -> Repo -> IO ([DiffTreeItem], IO Bool)
97getdiff command params repo = do
98	(diff, cleanup) <- pipeNullSplit ps repo
99	return (parseDiffRaw diff, cleanup)
100  where
101	ps =
102		command :
103		Param "-z" :
104		Param "--raw" :
105		Param "--no-renames" :
106		Param "-l0" :
107		params
108
109{- Parses --raw output used by diff-tree and git-log. -}
110parseDiffRaw :: [L.ByteString] -> [DiffTreeItem]
111parseDiffRaw l = go l
112  where
113	go [] = []
114	go (info:f:rest) = case A.parse (parserDiffRaw (L.toStrict f)) info of
115		A.Done _ r -> r : go rest
116		A.Fail _ _ err -> error $ "diff-tree parse error: " ++ err
117	go (s:[]) = error $ "diff-tree parse error near \"" ++ decodeBL s ++ "\""
118
119-- :<srcmode> SP <dstmode> SP <srcsha> SP <dstsha> SP <status>
120--
121-- May be prefixed with a newline, which git log --pretty=format
122-- adds to the first line of the diff, even with -z.
123parserDiffRaw :: RawFilePath -> A.Parser DiffTreeItem
124parserDiffRaw f = DiffTreeItem
125	<$ A.option '\n' (A8.char '\n')
126	<* A8.char ':'
127	<*> octal
128	<* A8.char ' '
129	<*> octal
130	<* A8.char ' '
131	<*> (maybe (fail "bad srcsha") return . extractSha =<< nextword)
132	<* A8.char ' '
133	<*> (maybe (fail "bad dstsha") return . extractSha =<< nextword)
134	<* A8.char ' '
135	<*> A.takeByteString
136	<*> pure (asTopFilePath $ fromInternalGitPath $ Git.Filename.decode f)
137  where
138	nextword = A8.takeTill (== ' ')
139