1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
|
module Git
( module Git.CommitHash,
getCommitHashes,
getRootDir,
getChangedFilesOf,
Commit (..),
Author (..),
getCommitOf,
readTextFileOfText,
readTextFileOfBS,
resolveRef,
getCommitsBetween,
diffOf,
)
where
import Control.Exception (IOException, catch, throw, throwIO)
import Data.Binary (Binary)
import Data.Binary.Instances ()
import Data.ByteString.Lazy qualified as LB
import Data.List.NonEmpty (NonEmpty)
import Data.List.NonEmpty qualified as N
import Data.Maybe (fromMaybe)
import Data.Text qualified as T
import Data.Text.Encoding qualified as T
import Data.Text.Lazy qualified as LT
import Data.Text.Lazy.Encoding qualified as LT
import Data.Text.Lazy.IO qualified as LT
import Data.Time.Clock (UTCTime, getCurrentTime)
import Exception qualified as E
import GHC.Generics (Generic)
import Git.CommitHash
import Patch qualified as A
import Process (proc, sh, sh_)
import Text.Printf (printf)
getCommitHashes :: IO (NonEmpty T.Text)
getCommitHashes = fromMaybe (throw E.NoCommits) . N.nonEmpty . reverse . T.lines . T.decodeUtf8 . LB.toStrict <$> sh "git log --format=%H"
getRootDir :: IO FilePath
getRootDir = T.unpack . stripTrailingNL . T.decodeUtf8 . LB.toStrict <$> sh (proc "git rev-parse --show-toplevel")
where
stripTrailingNL s = fromMaybe s $ T.stripSuffix "\n" s
getChangedFilesOf :: CommitHash -> IO [FilePath]
getChangedFilesOf WorkingTree = do
map T.unpack . T.lines . T.decodeUtf8 . LB.toStrict
<$> sh "git ls-files --modified"
getChangedFilesOf (Commit hash) = do
map T.unpack . T.lines . T.decodeUtf8 . LB.toStrict
<$> sh (proc "git show -p --name-only --format= %" hash)
data Commit = Commit'
{ commitHash :: CommitHash,
date :: UTCTime,
author :: Author
}
deriving (Show, Generic, Binary, Eq)
data Author = Author
{ name :: T.Text,
email :: T.Text
}
deriving (Show, Generic, Binary, Eq)
getCommitOf :: CommitHash -> IO Commit
getCommitOf commitHash@WorkingTree = do
date <- getCurrentTime
authorName <- T.decodeUtf8 . LB.toStrict <$> sh "git config user.name"
authorEmail <- T.decodeUtf8 . LB.toStrict <$> sh "git config user.email"
pure
Commit'
{ author = Author authorName authorEmail,
..
}
getCommitOf commitHash@(Commit hash) = do
( T.splitOn "\NUL" . head . T.lines . T.decodeUtf8 . LB.toStrict
<$> sh
( proc
"git show --quiet --format=%%ai%%x00%%ae%%x00%%an %"
hash
)
)
>>= \case
rawDate : authorEmail : authorName : _ ->
let date = read (T.unpack rawDate)
in pure
Commit'
{ author = Author authorName authorEmail,
..
}
_ -> throwIO E.NoCommits
readTextFileOfText :: CommitHash -> FilePath -> IO LT.Text
readTextFileOfText = readTextFileOf LT.readFile LT.decodeUtf8
readTextFileOfBS :: CommitHash -> FilePath -> IO LB.ByteString
readTextFileOfBS = readTextFileOf LB.readFile id
readTextFileOf :: (FilePath -> IO a) -> (LB.ByteString -> a) -> CommitHash -> FilePath -> IO a
readTextFileOf readFile _ WorkingTree filePath =
catch
(readFile filePath)
(\(_ :: IOException) -> throwIO (E.CannotReadFile filePath))
readTextFileOf _ decode (Commit hash) filePath =
catch
(decode <$> sh (proc "git show %:%" hash filePath))
(\(_ :: E.ProcessException) -> throwIO (E.CannotReadFile (printf "%s:%s" hash filePath)))
resolveRef :: T.Text -> IO CommitHash
resolveRef =
fmap (Commit . T.strip . T.decodeUtf8 . LB.toStrict)
. sh
. proc "git rev-parse %"
-- | `getCommitsBetween prevCommit commit` returns the commits from `prevCommit` to `commit`. The result excludes `prevCommit`, but includes `commit`.
--
-- If `prevCommit` is not an ancestor of `commit`, this functions throws `NoAncestor commit prevCommit`.
getCommitsBetween :: CommitHash -> CommitHash -> IO [CommitHash]
getCommitsBetween WorkingTree commit@(Commit _) =
throwIO (E.NoAncestor WorkingTree commit)
getCommitsBetween WorkingTree WorkingTree = pure [WorkingTree]
getCommitsBetween prevCommit WorkingTree =
fmap (++ [WorkingTree]) . getCommitsBetween prevCommit
=<< resolveRef "HEAD"
getCommitsBetween prevCommit@(Commit prevHash) commit@(Commit hash) = do
catch
(sh_ (proc "git merge-base --is-ancestor % %" prevHash hash))
(\(_ :: E.ProcessException) -> throwIO (E.NoAncestor commit prevCommit))
map (Commit . T.strip) . T.lines . T.decodeUtf8 . LB.toStrict
<$> sh (proc "git log --format=%%H %..%" prevHash hash)
diffOf :: CommitHash -> CommitHash -> IO A.Patch
diffOf prevHash hash =
A.parse . T.decodeUtf8 . LB.toStrict
<$> sh (proc "git diff % %" (toTextUnsafe prevHash) (toTextUnsafe hash))
|