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
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
|
module Expression (
DeclaredRef(..),
StaticRef',
WatchedRef(..),
WatchedRef',
RevisionExpression'(..), RevisionExpression, DeclaredRevisionExpression,
RangeExpression'(..), RangeExpression, DeclaredRangeExpression,
CommitRange(..),
evaluateDeclaredRevision, evaluateDeclaredRange,
evaluateRevision, evaluateRange,
getRangeCommits, getAddedRangeCommits,
isRangeExpressionDynamic,
isRevisionExpressionDynamic,
parseRangeExpression,
) where
import Control.Concurrent.STM
import Control.Monad
import Control.Monad.IO.Class
import Data.Bifunctor
import Data.Char
import Data.Function
import Data.Kind
import Data.List
import Data.List.NonEmpty qualified as NE
import Data.Maybe
import Data.Proxy
import Data.Text (Text)
import Data.Text qualified as T
import Data.Void
import Text.Megaparsec
import Text.Megaparsec.Char
import System.FilePath.Glob as Glob
import System.IO.Error
import Job.Types
import Output
import Repo
data DeclaredRef
= TextRef Text
| TagRef Pattern (Tag Commit -> Tag Commit -> Tag Commit)
type family StaticRef' d :: Type where
StaticRef' Declared = DeclaredRef
StaticRef' Evaluated = Commit
data WatchedRef
= BranchRef (STM (Maybe Commit))
| WatchedTagRef (WatchedTags (Tag Commit))
type family WatchedRef' d :: Type where
WatchedRef' Declared = DeclaredRef
WatchedRef' Evaluated = WatchedRef
type family StaticTags' d :: Type where
StaticTags' Declared = Pattern
StaticTags' Evaluated = [ Tag Commit ]
data WatchedTags a = WatchedTags
{ wtPattern :: Pattern
, wtUpdate :: Tag Commit -> a -> a
, wtValue :: TVar a
, wtChan :: TChan (Tag Commit)
}
type family WatchedTags' d :: Type where
WatchedTags' Declared = Pattern
WatchedTags' Evaluated = WatchedTags [ Tag Commit ]
data RevisionExpression' d
= StaticRef (StaticRef' d)
| WatchedRef (WatchedRef' d)
| ModifiedRevision Text (RevisionExpression' d)
type RevisionExpression = RevisionExpression' Evaluated
type DeclaredRevisionExpression = RevisionExpression' Declared
watchedRevision :: DeclaredRevisionExpression -> DeclaredRevisionExpression
watchedRevision = \case
StaticRef r -> WatchedRef r
WatchedRef r -> WatchedRef r
ModifiedRevision m r -> ModifiedRevision m (watchedRevision r)
data RangeExpression' d
= SingleRevisionExpression (RevisionExpression' d)
| RangeExpression (RevisionExpression' d) {- tip -} (RevisionExpression' d) {- except -}
| StaticTagsExpression (StaticTags' d)
| WatchedTagsExpression (WatchedTags' d)
type RangeExpression = RangeExpression' Evaluated
type DeclaredRangeExpression = RangeExpression' Declared
watchedRange :: DeclaredRangeExpression -> DeclaredRangeExpression
watchedRange = \case
SingleRevisionExpression rev -> SingleRevisionExpression (watchedRevision rev)
RangeExpression r1 r2 -> RangeExpression (watchedRevision r1) (watchedRevision r2)
StaticTagsExpression pat -> WatchedTagsExpression pat
WatchedTagsExpression pat -> WatchedTagsExpression pat
data CommitRef = CommitRef Text {- suffix -} CommitId
deriving (Eq)
data CommitRange
= EmptyCommitRange
| CommitRange CommitRef {- tip -} CommitRef {- except -}
| CommitSet [ CommitRef ]
deriving (Eq)
textCommitRef :: CommitRef -> Text
textCommitRef (CommitRef suffix cid) = textCommitId cid <> suffix
evaluateDeclaredRevision :: (MonadIO m, MonadFail m, MonadOutput m) => Repo -> DeclaredRevisionExpression -> m RevisionExpression
evaluateDeclaredRevision repo = \case
StaticRef (TextRef ref) -> StaticRef <$> readCommit repo ref
StaticRef (TagRef pat sel) -> do
filter (Glob.match pat . T.unpack . tagTag) <$> listTags repo >>= \case
[] -> fail $ "no tag matching " <> show (decompile pat) <> " found"
ts -> return $ StaticRef $ tagObject $ foldl1' sel ts
WatchedRef (TextRef ref) -> do
watched <- watchBranch repo ref
output <- getOutput
outputEvent output $ TestMessage $ "watch-branch-started " <> ref
return $ WatchedRef $ BranchRef watched
WatchedRef (TagRef wtPattern wtUpdate) -> do
wtChan <- watchTags repo
value <- filter (Glob.match wtPattern . T.unpack . tagTag) <$> listTags repo >>= \case
[] -> fail $ "no tag matching " <> show (decompile wtPattern) <> " found"
ts -> return $ foldl1' wtUpdate ts
wtValue <- liftIO $ newTVarIO value
return $ WatchedRef $ WatchedTagRef WatchedTags {..}
ModifiedRevision suffix rev -> ModifiedRevision suffix <$> evaluateDeclaredRevision repo rev
evaluateDeclaredRange :: (MonadIO m, MonadFail m, MonadOutput m) => Repo -> DeclaredRangeExpression -> m RangeExpression
evaluateDeclaredRange repo = \case
SingleRevisionExpression a ->
SingleRevisionExpression <$> evaluateDeclaredRevision repo a
RangeExpression a b ->
RangeExpression <$> evaluateDeclaredRevision repo a <*> evaluateDeclaredRevision repo b
StaticTagsExpression pat ->
StaticTagsExpression . filter (Glob.match pat . T.unpack . tagTag) <$> listTags repo
WatchedTagsExpression wtPattern -> do
wtChan <- watchTags repo
wtValue <- liftIO . newTVarIO . filter (Glob.match wtPattern . T.unpack . tagTag) =<< listTags repo
let wtUpdate = (:)
return $ WatchedTagsExpression WatchedTags {..}
evaluateRevision :: RevisionExpression -> STM (Maybe CommitRef)
evaluateRevision = \case
StaticRef commit -> return $ Just $ CommitRef "" $ commitId commit
WatchedRef (BranchRef getter) -> fmap (CommitRef "" . commitId) <$> getter
WatchedRef (WatchedTagRef wt) -> Just . CommitRef "" . commitId . tagObject <$> updateWatchedTags wt
ModifiedRevision suffix rev ->
fmap (\(CommitRef suffix' ref) -> CommitRef (suffix' <> suffix) ref) <$> evaluateRevision rev
evaluateRange :: RangeExpression -> STM CommitRange
evaluateRange = \case
SingleRevisionExpression revExpr -> do
rev <- evaluateRevision revExpr
return $ fromMaybe EmptyCommitRange $ CommitSet . (: []) <$> rev
RangeExpression tipExpr exceptExpr -> do
tip <- evaluateRevision tipExpr
except <- evaluateRevision exceptExpr
return $ fromMaybe EmptyCommitRange $ CommitRange <$> tip <*> except
StaticTagsExpression tags -> do
return $ CommitSet $ map (CommitRef "" . commitId . tagObject) tags
WatchedTagsExpression wt -> do
CommitSet . map (CommitRef "" . commitId . tagObject) <$> updateWatchedTags wt
getCommitIdFromRef :: MonadIO m => Repo -> CommitRef -> m (Maybe CommitId)
getCommitIdFromRef repo = liftIO . fmap (fmap commitId) . tryReadCommit repo . textCommitRef
getRangeCommits :: MonadIO m => Repo -> CommitRange -> m [ Commit ]
getRangeCommits repo = \case
EmptyCommitRange -> do
return []
CommitRange tip except -> do
tipId <- getCommitIdFromRef repo tip
excId <- getCommitIdFromRef repo except
listCommitsFrom repo (catMaybes [ tipId ]) (catMaybes [ excId ])
CommitSet refs -> do
return . catMaybes . map (either (const Nothing) Just) =<<
mapM (liftIO . tryIOError . readCommitId repo) . catMaybes =<<
mapM (getCommitIdFromRef repo) refs
getAddedRangeCommits :: MonadIO m => Repo -> CommitRange -> CommitRange -> m [ Commit ]
getAddedRangeCommits _ _ EmptyCommitRange = return []
getAddedRangeCommits repo EmptyCommitRange r = getRangeCommits repo r
getAddedRangeCommits repo (CommitRange ar br) (CommitRange ar' br') = do
a <- getCommitIdFromRef repo ar
b <- getCommitIdFromRef repo br
a' <- getCommitIdFromRef repo ar'
b' <- getCommitIdFromRef repo br'
a'b <- fmap (map commitId) $ mergeBase repo $ catMaybes [ a', b ]
(++)
<$> listCommitsFrom repo a'b (catMaybes [ b' ]) -- added by moving B to B'
<*> listCommitsFrom repo (catMaybes [ a' ]) (catMaybes [ a, b, b' ]) -- added by moving A to A'
getAddedRangeCommits repo prev cur = do
prevList <- getRangeCommits repo prev
curList <- getRangeCommits repo cur
return $ filter (not . (`elem` prevList)) curList
isRangeExpressionDynamic :: RangeExpression -> Bool
isRangeExpressionDynamic = \case
SingleRevisionExpression x -> isRevisionExpressionDynamic x
RangeExpression x y -> isRevisionExpressionDynamic x || isRevisionExpressionDynamic y
StaticTagsExpression {} -> False
WatchedTagsExpression {} -> True
isRevisionExpressionDynamic :: RevisionExpression -> Bool
isRevisionExpressionDynamic = \case
StaticRef _ -> False
WatchedRef _ -> True
ModifiedRevision _ e -> isRevisionExpressionDynamic e
updateWatchedTags :: WatchedTags a -> STM a
updateWatchedTags wt@WatchedTags {..} = do
tryReadTChan wtChan >>= \case
Nothing -> readTVar wtValue
Just tag -> do
when (Glob.match wtPattern $ T.unpack $ tagTag tag) $ do
writeTVar wtValue . (wtUpdate tag) =<< readTVar wtValue
updateWatchedTags wt
type ExprParser = Parsec Void Text
parseRangeExpression :: Text -> Either String DeclaredRangeExpression
parseRangeExpression = bimap errorBundlePretty id . runParser parser ""
where
parser :: ExprParser DeclaredRangeExpression
parser = do
range <- parseRange
eof
return range
parseRange :: ExprParser DeclaredRangeExpression
parseRange = do
range <- parseRevision
choice
[ do
SingleRevisionExpression rev1 <- return range
_ <- string ".."
SingleRevisionExpression rev2 <- parseRevision
return $ RangeExpression rev2 rev1
, do
return range
]
parseRevision :: ExprParser DeclaredRangeExpression
parseRevision = do
tok <- fmap T.concat $ some $ choice
[ takeWhile1P Nothing (\x -> isAlphaNum x || x `elem` [ '_', '-', '/' ])
, try $ string "." <* notFollowedBy (char '.')
]
range <- choice
[ between (char '(') (char ')') $ do
case tok of
"watch" -> watchedRange <$> parseRange
"tags" -> StaticTagsExpression <$> parseTagPattern
"highesttag" -> do
pat <- parseTagPattern
return $ SingleRevisionExpression $ StaticRef $ TagRef pat higherTag
_ -> unexpected $ Tokens $ NE.fromList $ chunkToTokens @Text Proxy tok
, do
return $ SingleRevisionExpression $ StaticRef $ TextRef tok
]
choice
[ do
SingleRevisionExpression rev <- return range
suffix <- takeWhile1P Nothing (\x -> isNumber x || x `elem` [ '~', '^', '@', '{', '}' ])
return $ SingleRevisionExpression $ ModifiedRevision suffix rev
, do
return range
]
parseTagPattern = compile . T.unpack <$> takeWhile1P Nothing (\x -> isAlphaNum x || x `elem` [ '_', '-', '.', '[', ']', '*', '?' ])
higherTag :: Tag a -> Tag a -> Tag a
higherTag t t'
| components t < components t' = t'
| otherwise = t
where
components = map component . T.groupBy ((==) `on` isDigit) . tagTag
component s
| Just ( c, _ ) <- T.uncons s, isDigit c
= Left $ read @Integer $ T.unpack s
| otherwise
= Right s
|