Eventually-decentralized project hosting and management platform

[[ 🗃 ^WvWbo vervis ]] :: [📥 Inbox] [📤 Outbox] [🐤 Followers] [🤝 Collaborators] [🛠 Changes]

Clone

HTTPS: darcs clone https://vervis.peers.community/repos/WvWbo

SSH: darcs clone USERNAME@vervis.peers.community:WvWbo

Tags

TODO

src / Vervis /

Darcs.hs

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
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
{- This file is part of Vervis.
 -
 - Written in 2016, 2018, 2019, 2020, 2022
 - by fr33domlover <fr33domlover@riseup.net>.
 -
 - ♡ Copying is an act of love. Please copy, reuse and share.
 -
 - The author(s) have dedicated all copyright and related and neighboring
 - rights to this software to the public domain worldwide. This software is
 - distributed without any warranty.
 -
 - You should have received a copy of the CC0 Public Domain Dedication along
 - with this software. If not, see
 - <http://creativecommons.org/publicdomain/zero/1.0/>.
 -}

module Vervis.Darcs
    ( readSourceView
    --, readWikiView
    , readChangesView
    --, lastChange
    , readPatch
    , writePostApplyHooks
    , canApplyDarcsPatch
    , applyDarcsPatch
    )
where

import Prelude hiding (lookup)

import Control.Applicative ((<|>))
import Control.Monad.IO.Class
import Control.Monad.Trans.Class (lift)
import Control.Monad.Trans.Except
import Darcs.Util.Path
import Darcs.Util.Tree
import Darcs.Util.Tree.Hashed
import Data.Bifunctor
import Data.Bool (bool)
import Data.ByteString (ByteString)
import Data.Foldable hiding (find)
import Data.List.NonEmpty (NonEmpty (..), nonEmpty)
import Data.Maybe (listToMaybe, mapMaybe, fromMaybe)
import Data.Text (Text)
import Data.Text.Encoding (encodeUtf8, decodeUtf8With, decodeUtf8)
import Data.Text.Encoding.Error (strictDecode)
import Data.Time.Clock (UTCTime, getCurrentTime, diffUTCTime)
import Data.Traversable (for)
import Database.Persist
import Development.Darcs.Internal.Hash.Codec
import Development.Darcs.Internal.Hash.Types
import Development.Darcs.Internal.Inventory.Parser
import Development.Darcs.Internal.Inventory.Read
import Development.Darcs.Internal.Inventory.Types
import Development.Darcs.Internal.Patch.Types
import System.Exit
import System.FilePath ((</>))
import System.Process.Typed
import Text.Email.Validate (emailAddress)

import qualified Data.Attoparsec.Text as A
import qualified Data.ByteString as B
import qualified Data.ByteString.Lazy as BL
import qualified Data.ByteString.Base16 as B16 (encode, decode)
import qualified Data.Foldable as F (find)
import qualified Data.List.NonEmpty as NE
import qualified Data.Text as T
import qualified Data.Text.Encoding as TE
import qualified Data.Vector as V (empty)
import qualified Database.Esqueleto as E

import qualified Development.Darcs.Internal.Patch.Parser as P

import Network.FedURI
import Yesod.ActivityPub
import Yesod.Hashids
import Yesod.MonadSite

import Darcs.Local.Repository
import Data.Either.Local (maybeRight)
import Data.EventTime.Local
import Data.List.Local
import Data.List.NonEmpty.Local
import Data.Patch.Local hiding (Patch)
import Data.Text.UTF8.Local (decodeStrict)
import Data.Time.Clock.Local ()
import System.Process.Typed.Local

import qualified Data.Patch.Local as DP
import qualified Data.Text.UTF8.Local as TU

import Vervis.Changes
import Vervis.Foundation
import Vervis.Model
import Vervis.Model.Ident
import Development.PatchMediaType
import Vervis.Path
import Vervis.Readme
import Vervis.Settings
import Vervis.SourceTree

dirToAnchoredPath :: [EntryName] -> AnchoredPath
dirToAnchoredPath = AnchoredPath . map (decodeWhiteName . encodeUtf8)

matchType :: ItemType -> EntryType
matchType TreeType = TypeTree
matchType BlobType = TypeBlob

nameToText :: Name -> Text
nameToText = decodeUtf8With strictDecode . encodeWhiteName

itemToEntry :: Name -> TreeItem IO -> DirEntry
itemToEntry name item = DirEntry (matchType $ itemType item) (nameToText name)

findReadme :: [(Name, TreeItem IO)] -> IO (Maybe (Text, BL.ByteString))
findReadme pairs =
    case F.find (isReadme . nameToText . fst) pairs of
        Nothing           -> return Nothing
        Just (name, item) ->
            case item of
                File (Blob load _hash) -> do
                    content <- load
                    return $ Just (nameToText name, content)
                _ -> return Nothing

itemToSourceView :: EntryName -> TreeItem IO -> IO (SourceView BL.ByteString)
itemToSourceView name (File (Blob load _hash)) = do
    content <- load
    return $ SourceFile $ FileView name content
itemToSourceView name (SubTree tree) = do
    let items = listImmediate tree
    mreadme <- findReadme items
    return $ SourceDir DirectoryView
        { dvName    = Just name
        , dvEntries = map (uncurry itemToEntry) items
        , dvReadme  = mreadme
        }
itemToSourceView _name (Stub _load _hash) = error "supposed to be expanded"

readStubbedTree :: FilePath -> IO (Tree IO)
readStubbedTree path = do
    let darcsDir = path </> "_darcs"
    (msize, hash) <- readPristineRoot darcsDir
    let pristineDir = darcsDir </> "pristine.hashed"
    readDarcsHashed pristineDir (msize, hash)

readSourceView
    :: FilePath
    -- ^ Repository path
    -> [EntryName]
    -- ^ Path in the source tree pointing to a file or directory
    -> IO (Maybe (SourceView Widget))
readSourceView path dir = do
    stubbedTree <- readStubbedTree path
    msv <- if null dir
        then do
            let items = listImmediate stubbedTree
            mreadme <- findReadme items
            return $ Just $ SourceDir DirectoryView
                { dvName    = Nothing
                , dvEntries = map (uncurry itemToEntry) items
                , dvReadme  = mreadme
                }
        else do
            let anch = dirToAnchoredPath dir
            expandedTree <- expandPath stubbedTree anch
            let mitem = find expandedTree anch
            for mitem $ itemToSourceView (last dir)
    return $ renderSources dir <$> msv

{-
readWikiView
    :: (EntryName -> EntryName -> Maybe Text)
    -- ^ Page name predicate. Returns 'Nothing' for a file which isn't a page.
    -- For a page file, returns 'Just' the page name, which is the filename
    -- with some parts possibly removed or added. For example, you may wish to
    -- remove any extensions, replace underscores with spaces and so on.
    -> (EntryName -> Bool)
    -- ^ Main page predicate. This is used to pick a top-level page to display
    -- as the wiki root page.
    -> FilePath
    -- ^ Repository path.
    -> [EntryName]
    -- ^ Path in the source tree pointing to a file. The last component doesn't
    -- have to be the full name of the file though, but it much match the page
    -- predicate for the actual file to be found.
    -> IO (Maybe WikiView)
readWikiView isPage isMain path dir = do
    stubbedTree <- readStubbedTree path
    let (parent, ispage, mfile) =
            if null dir
                then
                    ( []
                    , bool Nothing (Just Nothing) . isMain
                    , Nothing
                    )
                else
                    ( init dir
                    , maybe Nothing (Just . Just) . isPage lst
                    , Just $ decodeWhiteName $ encodeUtf8 lst
                    )
                    where
                    lst = last dir
        anch = dirToAnchoredPath parent
        matchBlob f (n, (File (Blob load _))) = f (nameToText n) load
        matchBlob _ _                         = Nothing
        matchBlob' f (File (Blob load _)) = Just $ f load
        matchBlob' _ _                    = Nothing
        page name load = (,) load . Just <$> ispage name
        matchP = listToMaybe . mapMaybe (matchBlob page) . listImmediate
        matchF t = mfile >>= lookup t >>= matchBlob' (flip (,) Nothing)
    expandedTree <- expandPath stubbedTree anch
    let mpage = case find expandedTree anch of
            Nothing             -> Nothing
            Just (File _)       -> Nothing
            Just (Stub _ _)     -> error "supposed to be expanded"
            Just (SubTree tree) -> matchP tree <|> matchF tree
        mkview Nothing b   = WikiViewRaw b
        mkview (Just mt) b = WikiViewPage mt b
    for mpage $ \ (load, mmtitle) -> mkview mmtitle <$> load
-}

readChangesView
    :: FilePath
    -- ^ Repository path
    -> Int
    -- ^ Offset, i.e. latest patches to skip
    -> Int
    -- ^ Limit, i.e. how many latest patches to take after the offset
    -> IO (Maybe (Int, [LogEntry]))
    -- ^ Total number of changes, and view of the chosen subset
readChangesView path off lim = fmap maybeRight $ runExceptT $ do
    total <- ExceptT $ readLatestInventory path latestInventorySizeP
    let off' = total - off - lim
    ps <- ExceptT $ readLatestInventory path $ latestInventoryPageP off' lim
    now <- lift getCurrentTime
    let toLE (pi, h, _) = LogEntry
            { leAuthor  =
                T.stripEnd $ T.takeWhile (/= '<') $ piAuthor pi
            , leHash    = decodeStrict $ encodePatchInfoHash h
            , leMessage = piTitle pi
            , leTime    =
                ( piTime pi
                , intervalToEventTime $
                  FriendlyConvert $
                  now `diffUTCTime` piTime pi
                )
            }
    return (total, map toLE $ reverse $ snd ps)

lastChange :: FilePath -> UTCTime -> IO (Maybe EventTime)
lastChange path now = fmap maybeRight $ runExceptT $ do
    total <- ExceptT $ readLatestInventory path latestInventorySizeP
    let lim = 1
        off = total - lim
    (_, l) <- ExceptT $ readLatestInventory path $ latestInventoryPageP off lim
    return $ case reverse l of
        []                 -> Never
        (pi, _ih, _ch) : _ ->
            intervalToEventTime $
            FriendlyConvert $
            now `diffUTCTime` piTime pi

data Change
    = AddFile FilePath
    | AddDir FilePath
    | Move FilePath FilePath
    | RemoveFile FilePath
    | RemoveDir FilePath
    | Replace FilePath Text Text Text
    | Binary FilePath ByteString ByteString
    | Pref Text Text Text

splitChange :: P.Change -> Either P.Hunk Change
splitChange = f
    where
    text = decodeUtf8
    path = T.unpack . text
    f (P.EditFile h)          = Left h
    f (P.AddFile p)           = Right $ AddFile (path p)
    f (P.AddDir p)            = Right $ AddDir (path p)
    f (P.Move old new)        = Right $ Move (path old) (path new)
    f (P.RemoveFile p)        = Right $ RemoveFile (path p)
    f (P.RemoveDir p)         = Right $ RemoveDir (path p)
    f (P.Replace p r old new) = Right $ Replace (path p) (text r) (text old) (text new)
    f (P.Binary p old new)    = Right $ Binary (path p) old new
    f (P.Pref pref old new)   = Right $ Pref (text pref) (text old) (text new)

-- | Group hunks by filename, assuming all the hunks for a given file are
-- placed together in the patch file, and in increasing line number order.
groupHunksByFile
    :: NonEmpty (P.Hunk)
    -> NonEmpty (ByteString, NonEmpty (Int, [ByteString], [ByteString]))
groupHunksByFile = groupWithExtract1 P.hunkFile rest
    where
    rest h = (P.hunkLine h, P.hunkRemove h, P.hunkAdd h)

-- | Find groups of consecutive sequences of removes and adds, and turn each
-- such group into a single hunk. This may not actually be necessary, because
-- the small consecutive hunks would be joined later anyway when adding context
-- lines, but I'm still doing this here for consistency. In the "Vervis.Git"
-- module, the hunks are joined like this too.
joinHunks
    :: NonEmpty (Int, [ByteString], [ByteString])
    -> NonEmpty (Bool, Int, Hunk)
joinHunks =
    NE.map (mkHunk . second groupPairs) .
    groupMapBy1 consecutive lineNumber lines
  where
    consecutive (n1, r1, _) (n2, _, _) = n1 + length r1 == n2
    lineNumber (n, _, _) = n
    lines (_, rs, as) = (map decodeUtf8 rs, map decodeUtf8 as)
    mkHunk (line, (adds, pairs, rems)) = (False, line, Hunk adds pairs rems)

-- | Read patch content, both metadata and the actual diff, from a given Darcs
-- repository. Preconditions:
--
-- * The repo's existence has been verified against the DB
-- * The repo dir is assumed to exist. If it doesn't, an exception is thrown.
-- * The repository is assumed to be in a consistent state, all the expected
--   inventory files and patch files and so on are assumed to exist and have
--   the expected format. If not, an exception is thrown.
-- * The hash may or may not be found in the repo. If there's no patch in the
--   repo with the given hash, 'Nothing' is returned.
readPatch :: FilePath -> Text -> IO (Maybe DP.Patch)
readPatch path hash = handle $ runExceptT $ do
    let pih = PatchInfoHash $ fst $ B16.decode $ encodeUtf8 hash
    li <- ExceptT $ readLatestInventory path latestInventoryAllP
    mp <- loop pih (liPatches li) (fst <$> liPrevTag li)
    for mp $ \ (pi, pch) -> do
        (_pir, changes) <-
            ExceptT $ readCompressedPatch path pch (P.patch <* A.endOfInput)
        (an, ae) <-
            ExceptT . pure $ A.parseOnly (author <* A.endOfInput) $ piAuthor pi
        return DP.Patch
            { patchWritten     =
                ( Author
                    { authorName  = an
                    , authorEmail = ae
                    }
                , piTime pi
                )
            , patchCommitted   = Nothing
            , patchTitle       = piTitle pi
            , patchDescription = fromMaybe "" $ piDescription pi
            , patchDiff        =
                let (befores, pairs, afters) = groupEithers $ map splitChange changes
                    befores' = mkedit befores
                    pairs' = map (bimap arrangeHunks mkedit) pairs
                    afters' = arrangeHunks <$> nonEmpty afters
                in  befores' ++ concatMap (NE.toList . uncurry (<>)) pairs' ++ maybe [] NE.toList afters'
            }
  where
    handle a = do
        r <- a
        case r of
            Left e -> fail $ "readPatch failed: " ++ e
            Right mp -> return mp
    lookup' pih ps = case F.find (\ (_pi, pih', _pch) -> pih' == pih) ps of
        Nothing              -> Nothing
        Just (pi, _pih, pch) -> Just (pi, pch)
    loop pih ps mih = case lookup' pih ps of
        Just p  -> return $ Just p
        Nothing -> case mih of
            Nothing -> return Nothing
            Just ih -> do
                i <- ExceptT $ readCompressedInventory path ih earlyInventoryAllP
                case i of
                    Left ei  -> loop pih (eiPatches ei) Nothing
                    Right mi -> loop pih (miPatches mi) (Just $ miPrevious mi)
    email = maybe (fail "invalid email") pure . emailAddress . encodeUtf8
    author = (,)
        <$> (T.stripEnd <$> A.takeWhile1 (/= '<'))
        <*  A.skip (== '<')
        <*> (A.takeWhile1 (/= '>') >>= email)
        <*  A.skip (== '>')
    arrangeHunks = NE.map (mkhunk . second joinHunks) . groupHunksByFile
        where
        mkhunk (file, hunks) =
            EditTextFile (T.unpack $ decodeUtf8 file) V.empty hunks 0 0
    mkedit = fmap mkedit'
        where
        mkedit' (AddFile fp)               = AddTextFile fp 0 []
        mkedit' (AddDir fp)                = AddTextFile fp 0 []
        mkedit' (Move old new)             = MoveFile old 0 new 0
        mkedit' (RemoveFile fp)            = RemoveTextFile fp 0 []
        mkedit' (RemoveDir fp)             = RemoveTextFile fp 0 []
        mkedit' (Replace fp regex old new) = AddTextFile "Replace" 0 [T.concat ["replace ", T.pack fp, " ", regex, " ", old, " ", new]]
        mkedit' (Binary fp old new)        = EditBinaryFile fp (fromIntegral $ B.length old) 0 (fromIntegral $ B.length new) 0
        mkedit' (Pref pref old new)        = AddTextFile "Pref" 0 [T.concat ["changepref ", pref, " ", old, " ", new]]

writePostApplyHooks :: WorkerDB ()
writePostApplyHooks = do
    hook <- asksSite $ appPostApplyHookFile . appSettings
    authority <- asksSite $ renderAuthority . siteInstanceHost
    repos <- selectKeysList [RepoVcs ==. VCSDarcs] []
    for_ repos $ \ repoID -> do
        repoHash <- encodeKeyHashid repoID
        path <- askRepoDir repoHash
        liftIO $
            writeDefaultsFile path hook authority (keyHashidText repoHash)

canApplyDarcsPatch repoPath patch = do
    let input = BL.fromStrict $ TE.encodeUtf8 patch
    exitCode <- runProcess $ setStdin (byteStringInput input) $ proc "darcs" ["apply", "--all", "--no-allow-conflicts", "--dry-run", "--repodir='" ++ repoPath ++ "'"]
    return $ exitCode == ExitSuccess

applyDarcsPatch repoPath patch = do
    let input = BL.fromStrict $ TE.encodeUtf8 patch
    runProcessE "darcs apply" $ setStdin (byteStringInput input) $ proc "darcs" ["apply", "--all", "--no-allow-conflicts", "--repodir='" ++ repoPath ++ "'"]
[See repo JSON]