aboutsummaryrefslogtreecommitdiffstats
path: root/src/Store/Store.hs
blob: b55c56db12c8c71920ea2590912dc3e89cb2c7ce (plain)
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
module Store.Store
  ( StoreM,
    withStore,
    withCommit,
    listFiles,
    listAllFiles,
    readFile,
    writeFile,
    deleteFile,
    commit,
  )
where

import Bindings.Libgit2 qualified as B
import Control.Arrow (first)
import Control.Exception (Exception, finally, throw)
import Control.Monad (when)
import Control.Monad.Catch (MonadCatch, MonadMask, MonadThrow, throwM)
import Control.Monad.Reader (MonadReader, ReaderT, ask, runReaderT)
import Control.Monad.State (MonadState, StateT, evalStateT, get, modify)
import Control.Monad.Trans (MonadIO, MonadTrans, lift, liftIO)
import Control.Monad.Trans.Resource (MonadResource, runResourceT)
import Data.Aeson qualified as J
import Data.ByteString qualified as B
import Data.ByteString.Lazy qualified as LB
import Data.ByteString.UTF8 qualified as B
import Data.List (isPrefixOf, isSuffixOf, sort)
import Data.Tagged (Tagged (Tagged), untag)
import Data.Text qualified as T
import Data.Text.Encoding qualified as T
import Data.Time (getCurrentTimeZone, utcToZonedTime)
import Data.Time.Clock (getCurrentTime)
import Foreign
import Git qualified as G
import Git.Libgit2 qualified as GB
import System.FilePath (addTrailingPathSeparator, hasDrive, joinDrive, makeRelative, normalise, splitPath)
import Text.Printf (printf)
import Prelude hiding (readFile, writeFile)

newtype StoreT m a = StoreT
  { runStoreT :: StateT State (ReaderT Env m) a
  }
  deriving
    ( Applicative,
      Functor,
      Monad,
      MonadReader Env,
      MonadState State,
      MonadCatch,
      MonadThrow,
      MonadIO,
      MonadMask,
      MonadResource
    )

data Env = Env
  { repo :: GB.LgRepo,
    ref :: G.RefName
  }

data State = State
  { cid :: G.CommitOid GB.LgRepo,
    tid :: G.TreeOid GB.LgRepo
  }

instance MonadTrans StoreT where
  lift = StoreT . lift . lift

type StoreM = StoreT IO

withStore :: FilePath -> G.RefName -> StoreM a -> IO a
withStore repoPath ref action = do
  repo <- G.openRepository GB.lgFactory G.defaultRepositoryOptions {G.repoPath}
  (cid, tid) <- G.runRepository GB.lgFactory repo do
    Just cid <- fmap Tagged <$> G.resolveReference ref
    tid <- (.commitTree) <$> G.lookupCommit cid
    pure (cid, tid)

  runReaderT
    (evalStateT (runStoreT action) (State {cid, tid}))
    (Env {repo, ref})
    `finally` G.runRepository GB.lgFactory repo G.closeRepository

withCommit :: G.CommitOid GB.LgRepo -> StoreM a -> StoreM a
withCommit cid action = do
  Env {repo, ref} <- ask
  liftIO do
    tid <- G.runRepository GB.lgFactory repo do
      (.commitTree) <$> G.lookupCommit cid
    runReaderT
      (evalStateT (runStoreT action) (State {cid, tid}))
      (Env {repo, ref})

listDirectory :: FilePath -> StoreM [FilePath]
listDirectory dir' = do
  State {tid} <- get
  Env {repo} <- ask
  lift $ G.runRepository GB.lgFactory repo $ do
    let dir = normalise $ if hasDrive dir' then dir' else joinDrive "/" dir'
        n = length (splitPath dir)
    tree <- G.lookupTree tid
    sort
      . map (makeRelative dir)
      . filter ((== n + 1) . length . splitPath)
      . filter (isPrefixOf (addTrailingPathSeparator dir))
      . map fst
      . map
        ( \e ->
            case snd e of
              G.BlobEntry _ _ -> e
              G.CommitEntry _ -> error "XXX commit entry"
              G.TreeEntry _ -> first addTrailingPathSeparator e
        )
      . map (first (("/" <>) . B.toString))
      <$> G.listTreeEntries tree

listFiles :: FilePath -> StoreM [FilePath]
listFiles =
  fmap (filter (not . (isSuffixOf "/"))) . listDirectory

listAllFiles :: StoreM [FilePath]
listAllFiles = do
  State {tid} <- get
  Env {repo} <- ask
  lift $ G.runRepository GB.lgFactory repo $ do
    tree <- G.lookupTree tid
    filter (not . isSuffixOf "/")
      . sort
      . map fst
      . map
        ( \e ->
            case snd e of
              G.BlobEntry _ _ -> e
              G.CommitEntry _ -> error "XXX commit entry"
              G.TreeEntry _ -> first addTrailingPathSeparator e
        )
      . map (first (("/" <>) . B.toString))
      <$> G.listTreeEntries tree

data DoesNotExist = DoesNotExist String FilePath
  deriving (Show)

instance Exception DoesNotExist

data InappropriateType = InappropriateType String FilePath
  deriving (Show)

instance Exception InappropriateType

data DecodeException = DecodeException String
  deriving (Show)

instance Exception DecodeException

class Readable a where
  readFile :: FilePath -> StoreM a

instance Readable T.Text where
  readFile = readFile' G.catBlobUtf8

instance Readable B.ByteString where
  readFile = readFile' G.catBlob

instance Readable LB.ByteString where
  readFile = readFile' G.catBlobLazy

instance Readable J.Value where
  readFile = fmap (either (throw . DecodeException) id . J.eitherDecode) . readFile

readFile' ::
  (G.BlobOid GB.LgRepo -> ReaderT GB.LgRepo IO a) ->
  FilePath ->
  StoreM a
readFile' cat (makeRelative "/" -> fp) = do
  State {tid} <- get
  Env {repo} <- ask
  lift $ G.runRepository GB.lgFactory repo do
    tree <- G.lookupTree tid
    maybe
      (throwM (DoesNotExist "readFile" fp))
      ( \e ->
          case e of
            G.BlobEntry bid _ -> cat bid
            G.CommitEntry _ -> error "XXX commit entry"
            G.TreeEntry _ -> throwM (InappropriateType "readFile" fp)
      )
      =<< G.treeEntry tree (B.fromString fp)

writeFile :: FilePath -> LB.ByteString -> StoreM ()
writeFile (B.fromString . makeRelative "/" -> fp) v = do
  State {tid} <- get
  Env {repo} <- ask
  tid' <- lift $ G.runRepository GB.lgFactory repo do
    bid <- G.createBlobUtf8 (T.decodeUtf8 (LB.toStrict v))
    G.mutateTreeOid tid do
      G.putBlob fp bid
  modify $ \s -> s {tid = tid'}

deleteFile :: FilePath -> StoreM ()
deleteFile (B.fromString . makeRelative "/" -> fp) = do
  State {tid} <- get
  Env {repo} <- ask
  tid' <- lift $ G.runRepository GB.lgFactory repo do
    G.mutateTreeOid tid do
      G.dropEntry fp
      G.currentTreeOid
  modify $ \s -> s {tid = tid'}

commit :: StoreM ()
commit = do
  State {cid, tid} <- get
  Env {repo, ref} <- ask
  now <- lift (utcToZonedTime <$> getCurrentTimeZone <*> getCurrentTime)
  let sig = G.Signature "author" "email" now
  cid' <-
    lift $ runResourceT $ G.runRepository GB.lgFactory repo do
      cid' <-
        G.commitOid
          <$> G.createCommit [cid] tid sig sig "auto-commit" (Just ref)
      when (ref == "HEAD") $ reset cid'
      pure cid'
  modify $ \s -> s {cid = cid'}

reset :: MonadIO m => GB.CommitOid -> ReaderT GB.LgRepo m ()
reset cid = do
  repo <- GB.getRepository
  liftIO $ withForeignPtr (GB.repoObj repo) $ \repoPtr -> do
    withForeignPtr (GB.getOid (untag cid)) $ \oidPtr -> do
      alloca $ \cidPtr' -> do
        exitCode <- B.c'git_object_lookup cidPtr' repoPtr oidPtr B.c'GIT_OBJ_COMMIT
        when (exitCode /= 0) do
          error (printf "unknown commit %s (%d)" (show cid) (fromIntegral exitCode :: Int))
        cidPtr <- peek cidPtr'
        exitCode <- B.c'git_reset repoPtr cidPtr B.c'GIT_RESET_HARD
        when (exitCode /= 0) do
          error (printf "reset failed (%d)" (fromIntegral exitCode :: Int))