on master help sign in
Move hashing code to its own module
7ac8c0fSridhar Ratnakumar
lib/command/obelisk-command.cabal+1
@@ -50,6 +50,7 @@ library
50 Obelisk.Command.Run
50 Obelisk.Command.Run
51 Obelisk.Command.Thunk
51 Obelisk.Command.Thunk
52 Obelisk.Command.Upgrade
52 Obelisk.Command.Upgrade
53+ Obelisk.Command.Upgrade.Hash
53 Obelisk.Command.Utils
54 Obelisk.Command.Utils
54 Obelisk.Command.VmBuilder
55 Obelisk.Command.VmBuilder
55 -- -fobject-code is so that the StaticPointers extension can work in ghci
56 -- -fobject-code is so that the StaticPointers extension can work in ghci
lib/command/src/Obelisk/Command.hs+1
@@ -36,6 +36,7 @@ import Obelisk.Command.Project
36import Obelisk.Command.Run
36import Obelisk.Command.Run
37import Obelisk.Command.Thunk
37import Obelisk.Command.Thunk
38import Obelisk.Command.Upgrade
38import Obelisk.Command.Upgrade
39+import Obelisk.Command.Upgrade.Hash (getDirectoryHash, getHashAtGitRevision)
39import Obelisk.Command.Utils (getObeliskExe)
40import Obelisk.Command.Utils (getObeliskExe)
40import qualified Obelisk.Command.VmBuilder as VmBuilder
41import qualified Obelisk.Command.VmBuilder as VmBuilder
41import Obelisk.Migration (Hash)
42import Obelisk.Migration (Hash)
lib/command/src/Obelisk/Command/Upgrade.hs+26−132
@@ -6,10 +6,9 @@
6{-# LANGUAGE ScopedTypeVariables #-}
6{-# LANGUAGE ScopedTypeVariables #-}
7module Obelisk.Command.Upgrade where
7module Obelisk.Command.Upgrade where
8
8
9−import Control.Monad (forM, forM_, unless, void)
9+import Control.Monad (forM, unless, void)
10import Control.Monad.Catch (onException)
10import Control.Monad.Catch (onException)
11import Control.Monad.IO.Class (liftIO)
11import Control.Monad.IO.Class (liftIO)
12−import Data.Bool (bool)
13import Data.Maybe (catMaybes)
12import Data.Maybe (catMaybes)
14import Data.Monoid (Any (..), getAny)
13import Data.Monoid (Any (..), getAny)
15import Data.Semigroup ((<>))
14import Data.Semigroup ((<>))
@@ -18,9 +17,7 @@ import Data.Text (Text)
18import qualified Data.Text as T
17import qualified Data.Text as T
19import System.Directory
18import System.Directory
20import System.FilePath
19import System.FilePath
21−import System.IO.Temp
22import System.Posix.Process (executeFile)
20import System.Posix.Process (executeFile)
23−import System.Process (cwd, proc)
24
21
25import Obelisk.App (MonadObelisk)
22import Obelisk.App (MonadObelisk)
26import Obelisk.CliApp
23import Obelisk.CliApp
@@ -30,6 +27,7 @@ import Obelisk.Command.Project (toImplDir)
30import Obelisk.Command.Project (findProjectObeliskCommand)
27import Obelisk.Command.Project (findProjectObeliskCommand)
31import Obelisk.Command.Thunk (ThunkData (..), getThunkGitBranch, readThunk, updateThunk)
28import Obelisk.Command.Thunk (ThunkData (..), getThunkGitBranch, readThunk, updateThunk)
32
29
30+import Obelisk.Command.Upgrade.Hash
33import Obelisk.Migration
31import Obelisk.Migration
34
32
35newtype HandOffAction = HandoffAction Any
33newtype HandOffAction = HandoffAction Any
@@ -41,7 +39,7 @@ instance Action HandOffAction where
41data MigrationGraph
39data MigrationGraph
42 = MigrationGraph_ObeliskUpgrade
40 = MigrationGraph_ObeliskUpgrade
43 | MigrationGraph_ObeliskHandoff
41 | MigrationGraph_ObeliskHandoff
44− deriving (Eq, Show)
42+ deriving (Eq, Show, Bounded, Enum)
45
43
46graphName :: MigrationGraph -> Text
44graphName :: MigrationGraph -> Text
47graphName = \case
45graphName = \case
@@ -54,6 +52,9 @@ fromGraphName = \case
54 "obelisk-upgrade" -> MigrationGraph_ObeliskUpgrade
52 "obelisk-upgrade" -> MigrationGraph_ObeliskUpgrade
55 _ -> error "Invalid graph name specified"
53 _ -> error "Invalid graph name specified"
56
54
55+graphNames :: [Text]
56+graphNames = fmap graphName $ [minBound .. maxBound]
57+
57ensureCleanProject :: MonadObelisk m => FilePath -> m ()
58ensureCleanProject :: MonadObelisk m => FilePath -> m ()
58ensureCleanProject project =
59ensureCleanProject project =
59 ensureCleanGitRepo project False "Cannot upgrade with uncommited changes"
60 ensureCleanGitRepo project False "Cannot upgrade with uncommited changes"
@@ -177,51 +178,6 @@ getMigrationGraph' obDir graph = do
177computeVertexHash :: MonadObelisk m => FilePath -> m Hash
178computeVertexHash :: MonadObelisk m => FilePath -> m Hash
178computeVertexHash = getDirectoryHash [migrationDirName]
179computeVertexHash = getDirectoryHash [migrationDirName]
179
180
180−migrationDir :: FilePath -> FilePath
181−migrationDir project = project </> migrationDirName
182−
183−migrationIgnore :: [FilePath]
184−migrationIgnore = [migrationDirName]
185−
186−migrationDirName :: FilePath
187−migrationDirName = "migration"
188−
189−-- TODO: Move this to migration library? (but we rely on wrapProgram exes)
190−
191−-- | Get the unique hash of the given directory
192−--
193−-- Excludes the following before computing the hash:
194−-- * the specified top-level files/ directories.
195−-- * .git directory
196−-- * untracked Git files
197−-- * ignored Git files
198−--
199−-- Uses the same predictive algorithm that Nix (`nix hash-path`).
200−--
201−-- This function will do a full copy of the directory to a temporary location before
202−-- computing the hash. Because it will be deleting the files in exclude list, and
203−-- other files if the directory is a git repo. This needs to be done as `nix hash-path`
204−-- doesn't support taking an excludes list.
205−getDirectoryHash :: MonadObelisk m => [FilePath] -> FilePath -> m Hash
206−getDirectoryHash excludes dir = withSystemTempDirectory "obelisk-hash-" $ \tmpDir -> do
207− withSpinnerNoTrail (T.pack $ "Copying " <> dir <> " to " <> tmpDir) $ do
208− runProc $ copyDir dir tmpDir
209− getDirectoryHashDestructive excludes tmpDir
210−
211−-- Do /not/ call this directly! Call `getDirectoryHash` instead.
212−getDirectoryHashDestructive :: MonadObelisk m => [FilePath] -> FilePath -> m Hash
213−getDirectoryHashDestructive excludes dir = do
214− liftIO (doesDirectoryExist $ dir </> ".git") >>= \case
215− True -> do
216− tidyUpGitWorkingCopy dir
217− withSpinnerNoTrail "Removing .git directory" $
218− liftIO $ removePathForcibly $ dir </> ".git"
219− False -> pure ()
220− withSpinnerNoTrail "Removing excluded paths" $ do
221− forM_ (fmap (dir </>) excludes) $
222− liftIO . removePathForcibly
223− nixHash dir
224−
225createMigrationEdgeFromHEAD :: MonadObelisk m => FilePath -> m ()
181createMigrationEdgeFromHEAD :: MonadObelisk m => FilePath -> m ()
226createMigrationEdgeFromHEAD obDir = do
182createMigrationEdgeFromHEAD obDir = do
227 headHash <- getHeadVertex obDir
183 headHash <- getHeadVertex obDir
@@ -232,9 +188,11 @@ createMigrationEdgeFromHEAD obDir = do
232 else do
188 else do
233 -- TODO: Add `ob internal create-migration --backfill=n` do it for all revisions.
189 -- TODO: Add `ob internal create-migration --backfill=n` do it for all revisions.
234 -- See the failure condition in getHeadVertex
190 -- See the failure condition in getHeadVertex
235− written <- writeEdge (migrationDir obDir) headHash wcHash
191+ writeEdge (migrationDir obDir) graphNames headHash wcHash >>= \case
236− unless written $
192+ Nothing ->
237− putLog Warning "No migration was created"
193+ putLog Warning "No migration was created"
194+ Just edgeDir ->
195+ putLog Notice $ "Created edge: " <> T.pack edgeDir
238
196
239-- | Return the hash corresponding to HEAD
197-- | Return the hash corresponding to HEAD
240--
198--
@@ -254,32 +212,28 @@ getHeadVertex obDir = do
254 failWith $ "No vertex found for HEAD (" <> headHash <> ")"
212 failWith $ "No vertex found for HEAD (" <> headHash <> ")"
255 return headHash
213 return headHash
256
214
257−-- | TODO: Verify the integrity of the migration graph in relation to the Git repo.
258−verifyGraph :: MonadObelisk m => FilePath -> m ()
259−verifyGraph = undefined
260−
261-- | Create, or update, the migration graph with new edges and new vertices
215-- | Create, or update, the migration graph with new edges and new vertices
262-- corresponding to every commit in the Git history from HEAD.
216-- corresponding to every commit in the Git history from HEAD.
263--
217--
264-- NOTE: This creates a linear graph, and doesn't follow the Git graph
218-- NOTE: This creates a linear graph, and doesn't follow the Git graph
265-- structure.
219-- structure.
266backfillGraph :: MonadObelisk m => Maybe Int -> FilePath -> m ()
220backfillGraph :: MonadObelisk m => Maybe Int -> FilePath -> m ()
267−backfillGraph lastN project = do
221+backfillGraph lastN obDir = do
268 revs <- fmap (takeM lastN . fmap (fst . T.breakOn " ") . T.lines) $
222 revs <- fmap (takeM lastN . fmap (fst . T.breakOn " ") . T.lines) $
269− readProc $ gitProc project ["log", "--pretty=oneline"]
223+ readProc $ gitProc obDir ["log", "--pretty=oneline"]
270 -- Note: we need to take unique hashes only; this is fine for backfilling.
224 -- Note: we need to take unique hashes only; this is fine for backfilling.
271 -- But future migrations should ensure that there are no duplicate hashes
225 -- But future migrations should ensure that there are no duplicate hashes
272 -- (which would cause cycles) such as those introduced by revert commits.
226 -- (which would cause cycles) such as those introduced by revert commits.
273 vertices :: [Hash] <- withSpinnerNoTrail "Computing hash for git history" $
227 vertices :: [Hash] <- withSpinnerNoTrail "Computing hash for git history" $
274− fmap (unique . reverse) $ getHashAtGitRevision revs [migrationDirName] project
228+ fmap (unique . reverse) $ getHashAtGitRevision revs [migrationDirName] obDir
275 void $ withSpinner'
229 void $ withSpinner'
276 ("Backfilling with " <> tshow (length vertices) <> " vertices")
230 ("Backfilling with " <> tshow (length vertices) <> " vertices")
277 (Just $ \n -> "Backfilled " <> tshow n <> " edges.") $ do
231 (Just $ \n -> "Backfilled " <> tshow n <> " edges.") $ do
278 let vertexPairs = zip vertices $ drop 1 vertices
232 let vertexPairs = zip vertices $ drop 1 vertices
279− let edgesDir = migrationDir project
233+ let edgesDir = migrationDir obDir
280 liftIO $ createDirectoryIfMissing False edgesDir
234 liftIO $ createDirectoryIfMissing False edgesDir
281− fmap (length . filter (== True)) $ forM vertexPairs $ \(v1, v2) -> do
235+ fmap (length . catMaybes) $ forM vertexPairs $ \(v1, v2) -> do
282− writeEdge edgesDir v1 v2
236+ writeEdge edgesDir graphNames v1 v2
283 where
237 where
284 -- Return unique items in the list /while/ preserving order
238 -- Return unique items in the list /while/ preserving order
285 unique = loop mempty
239 unique = loop mempty
@@ -292,75 +246,15 @@ backfillGraph lastN project = do
292 Just n -> take n xs
246 Just n -> take n xs
293 Nothing -> xs
247 Nothing -> xs
294
248
295−-- | Write the edge to filesystem.
249+-- | TODO: Verify the integrity of the migration graph in relation to the Git repo.
296−--
297−-- Return True if the edge was created, False if already exists.
298−writeEdge :: MonadObelisk m => FilePath -> Hash -> Hash -> m Bool
299−writeEdge dir v1 v2 = do
300− unless (v1 /= v2) $
301− failWith $ "Cannot create self loop with: " <> v1
302− let edgeDir = dir </> (T.unpack $ v1 <> "-" <> v2)
303− liftIO (doesDirectoryExist edgeDir) >>= \case
304− True -> pure False
305− False -> do
306− putLog Notice $ T.pack $ "Creating edge " <> edgeDir
307− liftIO $ createDirectory edgeDir
308− forM_ actionFiles $ \fp -> liftIO $
309− writeFile (edgeDir </> fp) ""
310− pure True
311− where
312− actionFiles = ["obelisk-handoff", "obelisk-upgrade"]
313−
314−getHashAtGitRevision :: MonadObelisk m => [Text] -> [FilePath] -> FilePath -> m [Hash]
250+verifyGraph :: MonadObelisk m => FilePath -> m ()
315−getHashAtGitRevision revs excludes dir = withSystemTempDirectory "obelisk-hashrev-" $ \tmpDir -> do
251+verifyGraph = undefined
316− withSpinner (T.pack $ "Copying " <> dir <> " to " <> tmpDir) $ do
317− runProc $ copyDir dir tmpDir
318− tidyUpGitWorkingCopy tmpDir
319− -- Discard changes to tracked files
320− runProcSilently $ gitProc tmpDir ["reset", "--hard"]
321− forM revs $ \rev -> do
322− runProcSilently $ gitProc tmpDir ["checkout", T.unpack rev]
323− -- Checking out an arbitrary revision can leave untracked files (from
324− -- previous revison) around, so tidy them up.
325− tidyUpGitWorkingCopy tmpDir
326− withFilesStashed tmpDir (excludes <> [".git"]) $
327− nixHash tmpDir
328− where
329− withFilesStashed base fs m = withSystemTempDirectory "obelisk-hashrev-stash-" $ \stashDir -> do
330− existingPaths <- fmap catMaybes $ forM fs $ \p -> do
331− liftIO (doesPathExist $ base </> p) >>= pure . bool Nothing (Just p)
332− forM_ existingPaths $ \p ->
333− liftIO $ renamePath (base </> p) (stashDir </> p)
334− result <- m
335− forM_ existingPaths $ \p ->
336− liftIO $ renamePath (stashDir </> p) (base </> p)
337− return result
338
252
339−nixHash :: MonadObelisk m => FilePath -> m Hash
253+migrationDir :: FilePath -> FilePath
340−nixHash dir = withSpinnerNoTrail "Running `nix hash-path`" $
254+migrationDir project = project </> migrationDirName
341− readProc $ proc "nix" ["hash-path", "--type", "md5", dir]
342
255
343−-- | Clean up the following files in the git working copy
256+migrationIgnore :: [FilePath]
344−--
257+migrationIgnore = [migrationDirName]
345−-- * Paths ignored by .gitignored, but still present in the filesystem
346−-- * Untracked files (not added to git index)
347−-- * Any empty directories (these are not tracked by git)
348−--
349−-- Note that this leaves modified (staged or unstaged) files as they are.
350−tidyUpGitWorkingCopy :: MonadObelisk m => FilePath -> m ()
351−tidyUpGitWorkingCopy dir = withSpinnerNoTrail "Tidying up git working copy" $ do
352− ignored <- gitLsFiles dir ["--ignored", "--exclude-standard", "--others"]
353− untracked <- gitLsFiles dir ["--exclude-standard", "--others"]
354− putLog Debug $ T.pack $ "Found " <> show (length ignored) <> " ignored files."
355− putLog Debug $ T.pack $ "Untracked:\n" <> unlines untracked
356− putLog Debug $ T.pack $ "Ignored:\n" <> unlines ignored
357− withSpinnerNoTrail "Removing untracked and ignored files" $ do
358− forM_ (fmap (dir </>) $ ignored <> untracked) $
359− liftIO . removePathForcibly
360− -- Empty directories won't be included in these lists. Git doesn't track them
361− -- So we must delete these separately.
362− runProc $ proc "find" [dir, "-depth", "-empty", "-type", "d", "-delete"]
363− where
364− gitLsFiles pwd opts = fmap lines $ readProcessAndLogStderr Error $
365− (proc "git" $ ["ls-files", "."] <> opts) { cwd = Just pwd }
366
258
259+migrationDirName :: FilePath
260+migrationDirName = "migration"
lib/command/src/Obelisk/Command/Upgrade/Hash.hsnew file+115
@@ -0,0 +1,115 @@
1+{-# LANGUAGE FlexibleContexts #-}
2+{-# LANGUAGE LambdaCase #-}
3+{-# LANGUAGE OverloadedStrings #-}
4+{-# LANGUAGE ScopedTypeVariables #-}
5+{-# LANGUAGE TupleSections #-}
6+module Obelisk.Command.Upgrade.Hash
7+ ( getDirectoryHash
8+ , getHashAtGitRevision
9+ ) where
10+
11+import Control.Monad (forM, forM_)
12+import Control.Monad.IO.Class (liftIO)
13+import Data.Bool (bool)
14+import Data.Maybe (catMaybes)
15+import Data.Semigroup ((<>))
16+import Data.Text (Text)
17+import qualified Data.Text as T
18+import System.Directory
19+import System.FilePath
20+import System.IO.Temp
21+import System.Process (cwd, proc)
22+
23+import Obelisk.App (MonadObelisk)
24+import Obelisk.CliApp
25+import Obelisk.Command.Utils
26+
27+import Obelisk.Migration
28+
29+-- | Get the unique hash of the given directory
30+--
31+-- Excludes the following before computing the hash:
32+-- * the specified top-level files/ directories.
33+-- * .git directory
34+-- * untracked Git files
35+-- * ignored Git files
36+--
37+-- Uses the same predictive algorithm that Nix (`nix hash-path`).
38+--
39+-- This function will do a full copy of the directory to a temporary location before
40+-- computing the hash. Because it will be deleting the files in exclude list, and
41+-- other files if the directory is a git repo. This needs to be done as `nix hash-path`
42+-- doesn't support taking an excludes list.
43+getDirectoryHash :: MonadObelisk m => [FilePath] -> FilePath -> m Hash
44+getDirectoryHash excludes dir = withSystemTempDirectory "obelisk-hash-" $ \tmpDir -> do
45+ withSpinnerNoTrail (T.pack $ "Copying " <> dir <> " to " <> tmpDir) $ do
46+ runProc $ copyDir dir tmpDir
47+ getDirectoryHashDestructive excludes tmpDir
48+
49+-- Do /not/ call this directly! Call `getDirectoryHash` instead.
50+getDirectoryHashDestructive :: MonadObelisk m => [FilePath] -> FilePath -> m Hash
51+getDirectoryHashDestructive excludes dir = do
52+ liftIO (doesDirectoryExist $ dir </> ".git") >>= \case
53+ True -> do
54+ tidyUpGitWorkingCopy dir
55+ withSpinnerNoTrail "Removing .git directory" $
56+ liftIO $ removePathForcibly $ dir </> ".git"
57+ False -> pure ()
58+ withSpinnerNoTrail "Removing excluded paths" $ do
59+ forM_ (fmap (dir </>) excludes) $
60+ liftIO . removePathForcibly
61+ nixHash dir
62+
63+getHashAtGitRevision :: MonadObelisk m => [Text] -> [FilePath] -> FilePath -> m [Hash]
64+getHashAtGitRevision revs excludes dir = withSystemTempDirectory "obelisk-hashrev-" $ \tmpDir -> do
65+ withSpinner (T.pack $ "Copying " <> dir <> " to " <> tmpDir) $ do
66+ runProc $ copyDir dir tmpDir
67+ tidyUpGitWorkingCopy tmpDir
68+ -- Discard changes to tracked files
69+ runProcSilently $ gitProc tmpDir ["reset", "--hard"]
70+ forM revs $ \rev -> do
71+ runProcSilently $ gitProc tmpDir ["checkout", T.unpack rev]
72+ -- Checking out an arbitrary revision can leave untracked files (from
73+ -- previous revison) around, so tidy them up.
74+ tidyUpGitWorkingCopy tmpDir
75+ withFilesStashed tmpDir (excludes <> [".git"]) $
76+ nixHash tmpDir
77+ where
78+ withFilesStashed base fs m = withSystemTempDirectory "obelisk-hashrev-stash-" $ \stashDir -> do
79+ existingPaths <- fmap catMaybes $ forM fs $ \p -> do
80+ liftIO (doesPathExist $ base </> p) >>= pure . bool Nothing (Just p)
81+ forM_ existingPaths $ \p ->
82+ liftIO $ renamePath (base </> p) (stashDir </> p)
83+ result <- m
84+ forM_ existingPaths $ \p ->
85+ liftIO $ renamePath (stashDir </> p) (base </> p)
86+ return result
87+
88+nixHash :: MonadObelisk m => FilePath -> m Hash
89+nixHash dir = withSpinnerNoTrail "Running `nix hash-path`" $
90+ readProc $ proc "nix" ["hash-path", "--type", "md5", dir]
91+
92+-- | Clean up the following files in the git working copy
93+--
94+-- * Paths ignored by .gitignored, but still present in the filesystem
95+-- * Untracked files (not added to git index)
96+-- * Any empty directories (these are not tracked by git)
97+--
98+-- Note that this leaves modified (staged or unstaged) files as they are.
99+tidyUpGitWorkingCopy :: MonadObelisk m => FilePath -> m ()
100+tidyUpGitWorkingCopy dir = withSpinnerNoTrail "Tidying up git working copy" $ do
101+ ignored <- gitLsFiles dir ["--ignored", "--exclude-standard", "--others"]
102+ untracked <- gitLsFiles dir ["--exclude-standard", "--others"]
103+ putLog Debug $ T.pack $ "Found " <> show (length ignored) <> " ignored files."
104+ putLog Debug $ T.pack $ "Untracked:\n" <> unlines untracked
105+ putLog Debug $ T.pack $ "Ignored:\n" <> unlines ignored
106+ withSpinnerNoTrail "Removing untracked and ignored files" $ do
107+ forM_ (fmap (dir </>) $ ignored <> untracked) $
108+ liftIO . removePathForcibly
109+ -- Empty directories won't be included in these lists. Git doesn't track them
110+ -- So we must delete these separately.
111+ runProc $ proc "find" [dir, "-depth", "-empty", "-type", "d", "-delete"]
112+ where
113+ gitLsFiles pwd opts = fmap lines $ readProcessAndLogStderr Error $
114+ (proc "git" $ ["ls-files", "."] <> opts) { cwd = Just pwd }
115+
lib/migration/src/Obelisk/Migration.hs+21−2
@@ -5,8 +5,9 @@
5{-# LANGUAGE TupleSections #-}
5{-# LANGUAGE TupleSections #-}
6module Obelisk.Migration where
6module Obelisk.Migration where
7
7
8−import Control.Monad (forM)
8+import Control.Monad (forM, forM_, unless)
9import Control.Monad.Catch (Exception, MonadThrow, throwM)
9import Control.Monad.Catch (Exception, MonadThrow, throwM)
10+import Control.Monad.IO.Class (MonadIO, liftIO)
10import Control.Monad.State.Strict (evalStateT, get, put)
11import Control.Monad.State.Strict (evalStateT, get, put)
11import Data.Map (Map)
12import Data.Map (Map)
12import qualified Data.Map as Map
13import qualified Data.Map as Map
@@ -15,7 +16,7 @@ import Data.Semigroup ((<>))
15import qualified Data.Set as Set
16import qualified Data.Set as Set
16import Data.Text (Text)
17import Data.Text (Text)
17import qualified Data.Text as T
18import qualified Data.Text as T
18−import System.Directory (doesDirectoryExist, doesFileExist, listDirectory)
19+import System.Directory
19import System.FilePath
20import System.FilePath
20
21
21import Algebra.Graph.AdjacencyMap
22import Algebra.Graph.AdjacencyMap
@@ -150,3 +151,21 @@ getLast g = lastVertices >>= \case
150-- | Get the first vertex of the given DAG. Nothing if empty graph.
151-- | Get the first vertex of the given DAG. Nothing if empty graph.
151getFirst :: (MonadThrow m, Ord a, Eq a) => AdjacencyMap a -> m (Maybe a)
152getFirst :: (MonadThrow m, Ord a, Eq a) => AdjacencyMap a -> m (Maybe a)
152getFirst = getLast . transpose
153getFirst = getLast . transpose
154+
155+-- | Write an edge directly to the filesystem.
156+--
157+-- Actions are written with empty string.
158+--
159+-- Return the edge directory created.
160+writeEdge :: MonadIO m => FilePath -> [Text] -> Hash -> Hash -> m (Maybe FilePath)
161+writeEdge dir actionFiles v1 v2 = do
162+ unless (v1 /= v2) $
163+ fail $ "Cannot create self loop with: " <> T.unpack v1
164+ let edgeDir = dir </> (T.unpack $ v1 <> "-" <> v2)
165+ liftIO (doesDirectoryExist edgeDir) >>= \case
166+ True -> pure Nothing
167+ False -> do
168+ liftIO $ createDirectory edgeDir
169+ forM_ actionFiles $ \fp -> liftIO $
170+ writeFile (edgeDir </> T.unpack fp) ""
171+ pure $ Just edgeDir
migration/3da702ef7f531ad6db874ac3f85e9c06-73bcf3ed6593d074bd097cd43f5145d1/obelisk-handoffnew file

No changes to the content.

migration/3da702ef7f531ad6db874ac3f85e9c06-73bcf3ed6593d074bd097cd43f5145d1/obelisk-upgradenew file

No changes to the content.