on master help sign in
Remove migration code
commit07d8e450d0d3e71e2fcdfb38fb23e91c5c5c9e64 authorJohn Ericson <John.Ericson@Obsidian.Systems> dateThu 18 Oct 2018, 15:17 (8 years ago) parent49f11dc
6 files changed+24−644
default.nix−14 lib/command/obelisk-command.cabal−3 lib/command/src/Obelisk/Command.hs+24−87 lib/command/src/Obelisk/Command/Upgrade.hs−293 lib/migration/obelisk-migration.cabal−24 lib/migration/src/Obelisk/Migration.hs−223
default.nix−14
@@ -54,14 +54,6 @@ let
5454 };
5555
5656 fixUpstreamPkgs = self: super: {
57− algebraic-graphs = pkgs.haskell.lib.doJailbreak
58− (self.callCabal2nix "algebraic-graphs" (pkgs.fetchFromGitHub {
59− owner = "snowleopard";
60− repo = "alga";
61− rev = "480a73137e9b38ad3f1bc2c628847953d2fb3e25";
62− sha256 = "0dpwi5ffs88brl3lz51bwb004c6zm8ds8pkw1vzsg2a6aaiyhlzl";
63− }) {});
64−
6557 # Need deriveSomeUniverse
6658 # PR: https://github.com/dmwit/universe/pull/32
6759 universe-template = self.callCabal2nix "universe-template" (pkgs.fetchFromGitHub {
@@ -87,11 +79,6 @@ let
8779 obelisk-command = (self.callCabal2nix "obelisk-command" (cleanSource ./lib/command) {}).overrideAttrs
8880 (drv: {
8981 buildInputs = drv.buildInputs ++ [ pkgs.makeWrapper ];
90− postInstall = ''
91− ${drv.postInstall or ""}
92− # Install migrations
93− cp -r ${./migration} $out/migration;
94− '';
9582 postFixup = ''
9683 ${drv.postFixup or ""}
9784 # Make `ob` reference its runtime dependencies.
@@ -101,7 +88,6 @@ let
10188 obelisk-executable-config = executableConfig.haskellPackage self;
10289 obelisk-executable-config-inject = executableConfig.platforms.web.inject self;
10390 obelisk-frontend = self.callCabal2nix "obelisk-frontend" (cleanSource ./lib/frontend) {};
104− obelisk-migration = self.callCabal2nix "obelisk-migration" (cleanSource ./lib/migration) {};
10591 obelisk-run = self.callCabal2nix "obelisk-run" (cleanSource ./lib/run) {};
10692 obelisk-route = self.callCabal2nix "obelisk-route" (cleanSource ./lib/route) {};
10793 obelisk-selftest = self.callCabal2nix "obelisk-selftest" (cleanSource ./lib/selftest) {};
lib/command/obelisk-command.cabal−3
@@ -15,7 +15,6 @@ library
1515 , binary
1616 , bytestring
1717 , obelisk-cliapp
18− , obelisk-migration
1918 , containers
2019 , data-default
2120 , directory
@@ -52,8 +51,6 @@ library
5251 Obelisk.Command.Project
5352 Obelisk.Command.Run
5453 Obelisk.Command.Thunk
55− Obelisk.Command.Upgrade
56− Obelisk.Command.Upgrade.Hash
5754 Obelisk.Command.Utils
5855 Obelisk.Command.VmBuilder
5956 -- -fobject-code is so that the StaticPointers extension can work in ghci
lib/command/src/Obelisk/Command.hs+24−87
@@ -14,8 +14,7 @@ import Data.Bool (bool)
1414import qualified Data.ByteString.Base16 as Base16
1515import qualified Data.ByteString.Lazy as LBS
1616import Data.List
17−import Data.Maybe (catMaybes, listToMaybe)
18−import Data.Text (Text)
17+import Data.Maybe (catMaybes)
1918import qualified Data.Text as T
2019import Data.Text.Encoding
2120import GHC.StaticPtr
@@ -34,11 +33,8 @@ import Obelisk.Command.Deploy
3433import Obelisk.Command.Project
3534import Obelisk.Command.Run
3635import Obelisk.Command.Thunk
37−import Obelisk.Command.Upgrade
38−import Obelisk.Command.Upgrade.Hash (getDirectoryHash, getHashAtGitRevision)
3936import Obelisk.Command.Utils
4037import qualified Obelisk.Command.VmBuilder as VmBuilder
41−import Obelisk.Migration (Hash)
4238
4339
4440data Args = Args
@@ -95,16 +91,11 @@ data ObCommand
9591 | ObCommand_Thunk ThunkCommand
9692 | ObCommand_Repl
9793 | ObCommand_Watch
98− | ObCommand_Upgrade (Maybe Text)
9994 | ObCommand_Internal ObInternal
10095 deriving Show
10196
10297data ObInternal
10398 = ObInternal_RunStaticIO StaticKey
104− | ObInternal_Hash (Maybe Text)
105− | ObInternal_CreateMigration
106− | ObInternal_VerifyMigration
107− | ObInternal_Migrate Hash
10899 | ObInternal_CLIDemo
109100 deriving Show
110101
@@ -135,7 +126,6 @@ obCommand cfg = hsubparser
135126 , command "thunk" $ info (ObCommand_Thunk <$> thunkCommand) $ progDesc "Manipulate thunk directories"
136127 , command "repl" $ info (pure ObCommand_Repl) $ progDesc "Open an interactive interpreter"
137128 , command "watch" $ info (pure ObCommand_Watch) $ progDesc "Watch current project for errors and warnings"
138− , command "upgrade" $ info (ObCommand_Upgrade <$> argument (maybeReader $ Just . Just . T.pack) (action "branch" <> metavar "GITBRANCH" <> value Nothing <> help "Git branch of obelisk to update to (defaults to the thunk's branch)")) $ progDesc "Upgrade Obelisk in the project"
139129 ])
140130 <|> subparser
141131 (mconcat
@@ -211,10 +201,6 @@ data DeployInitOpts = DeployInitOpts
211201internalCommand :: Parser ObInternal
212202internalCommand = subparser $ mconcat
213203 [ command "run-static-io" $ info (ObInternal_RunStaticIO <$> argument (eitherReader decodeStaticKey) (action "static-key")) mempty
214− , command "hash" $ info (ObInternal_Hash <$> argument (maybeReader $ Just . Just . T.pack) (action "rev" <> metavar "GITREVISION" <> value Nothing <> help "Calculate for the specified git revision (defaults to working copy")) $ progDesc "Computes hash of working directory (or git revision)"
215− , command "create-migration" $ info (pure ObInternal_CreateMigration) mempty
216− , command "verify-migration" $ info (pure ObInternal_VerifyMigration) mempty
217− , command "migrate" $ info (ObInternal_Migrate <$> strArgument (action "fromHash" <> metavar "FROMHASH" <> help "Migrate from this hash")) $ progDesc "Perform a migrate from the given hash to HEAD of obelisk thunk"
218204 , command "clidemo" $ info (pure ObInternal_CLIDemo) mempty
219205 ]
220206
@@ -322,65 +308,29 @@ main' argsCfg = do
322308 , "logging-level=" <> show logLevel
323309 ]
324310
325− (mainWithHandOff argsCfg <=< parseHandoff) =<< liftIO getArgs
326−
327−-- Type representing the result of a handoff calculation.
328−data HandOff m
329− = HandOff_Yes FilePath [String] -- Handoff immediately to the given impl with the given args
330− | HandOff_Decide (m Bool) FilePath [String] -- Handoff only if the action returns True
331− | HandOff_No [String] -- Do not handoff, and continue with the given args
332−
333−mainWithHandOff :: MonadObelisk m => ArgsConfig -> HandOff m -> m ()
334−mainWithHandOff argsCfg = \case
335− HandOff_Yes impl as -> do
336− putLog Debug $ "Handing off to project obelisk " <> T.pack impl
337− liftIO $ executeFile impl False ("--no-handoff" : as) Nothing
338− HandOff_Decide f impl as -> do
339− withSpinner' "Deciding whether to handoff"
340− (Just $ bool
341− "Continuing in ambient obelisk without handing off"
342− "Handed off to project obelisk") f >>= \case
343− True -> mainWithHandOff argsCfg $ HandOff_Yes impl as
344− False -> mainWithHandOff argsCfg $ HandOff_No as
345− HandOff_No as -> do
346− putLog Debug $ "Not handing off"
347− args' <- liftIO $ parseCLIArgs argsCfg as
348− warnIfExtraneousFlag args'
349− ob $ _args_command args'
350− where
351− warnIfExtraneousFlag a = case _args_noHandOffPassed a of
352− False -> return ()
353− True -> putLog Warning $
354− "Ignoring unexpected --no-handoff (should only be passed once and as the first argument)"
355−
356−-- Handoff logic
357−parseHandoff :: MonadObelisk m => [String] -> m (HandOff m)
358−parseHandoff as' = case hasNoHandoff as' of
359− (True, as) ->
360− pure $ HandOff_No as
361− (False, as) -> findProjectObeliskCommand "." >>= \case
362− Nothing -> do
363− putLog Debug "Not in a project; no need to hand off"
364− pure $ HandOff_No as
365− Just impl -> case getSubCommand as of
366− Just "upgrade" ->
367− pure $ HandOff_Decide (decideHandOffToProjectOb ".") impl as
368− _ ->
369− pure $ HandOff_Yes impl as
370− where
371− getSubCommand = listToMaybe . filter (not . isPrefixOf "-")
372− hasNoHandoff = \case
373− "--no-handoff" : xs ->
374− -- If we've been told not to hand off, don't hand off
375− (True, xs)
376− x:xs
377− -- Otherwise bash completion would always hand-off even if the user isn't trying to
378− | "--bash-completion" `isPrefixOf` x && "--no-handoff" `elem` xs ->
379− (True, x:xs)
380− | otherwise ->
381− (False, x:xs)
382− xs ->
383− (False, xs)
311+ --TODO: We'd like to actually use the parser to determine whether to hand off,
312+ --but in the case where this implementation of 'ob' doesn't support all
313+ --arguments being passed along, this could fail. For now, we don't bother
314+ --with optparse-applicative until we've done the handoff.
315+ let go as = do
316+ args' <- liftIO $ handleParseResult (execParserPure parserPrefs (argsInfo argsCfg) as)
317+ case _args_noHandOffPassed args' of
318+ False -> return ()
319+ True -> putLog Warning "--no-handoff should only be passed once and as the first argument; ignoring"
320+ ob $ _args_command args'
321+ handoffAndGo as = findProjectObeliskCommand "." >>= \case
322+ Nothing -> go as -- If not in a project, just run ourselves
323+ Just impl -> do
324+ -- Invoke the real implementation, using --no-handoff to prevent infinite recursion
325+ putLog Debug $ "Handing off to " <> T.pack impl
326+ liftIO $ executeFile impl False ("--no-handoff" : myArgs) Nothing
327+ case myArgs of
328+ "--no-handoff" : as -> go as -- If we've been told not to hand off, don't hand off
329+ a:as -- Otherwise bash completion would always hand-off even if the user isn't trying to
330+ | "--bash-completion" `isPrefixOf` a
331+ && "--no-handoff" `elem` as -> go (a:as)
332+ | otherwise -> handoffAndGo (a:as)
333+ as -> handoffAndGo as
384334
385335ob :: MonadObelisk m => ObCommand -> m ()
386336ob = \case
@@ -426,25 +376,12 @@ ob = \case
426376 ThunkCommand_Pack thunks -> forM_ thunks $ \(ThunkPackOpts dir upstream) -> packThunk dir (T.pack upstream)
427377 ObCommand_Repl -> runRepl
428378 ObCommand_Watch -> inNixShell' $ static runWatch
429− ObCommand_Upgrade branch -> do
430− upgradeObelisk "." branch
431379 ObCommand_Internal icmd -> case icmd of
432380 ObInternal_RunStaticIO k -> liftIO (unsafeLookupStaticPtr @(ObeliskT IO ()) k) >>= \case
433381 Nothing -> failWith $ "ObInternal_RunStaticIO: no such StaticKey: " <> T.pack (show k)
434382 Just p -> do
435383 c <- getObelisk
436384 liftIO $ runObelisk c $ deRefStaticPtr p
437− ObInternal_Hash revM -> do
438− hash <- case revM of
439− Nothing -> getDirectoryHash [migrationDirName] "."
440− Just rev -> fmap head $ getHashAtGitRevision [rev] [migrationDirName] "."
441− putLog Notice hash
442− ObInternal_CreateMigration ->
443− createMigrationEdgeFromHEAD "."
444− ObInternal_VerifyMigration ->
445− verifyMigration "."
446− ObInternal_Migrate fromHash ->
447− migrateObelisk "." fromHash
448385 ObInternal_CLIDemo -> cliDemo
449386
450387--TODO: Clean up all the magic strings throughout this codebase
lib/command/src/Obelisk/Command/Upgrade.hsdeleted−293
@@ -1,293 +0,0 @@
1−{-# LANGUAGE FlexibleContexts #-}
2−{-# LANGUAGE GeneralizedNewtypeDeriving #-}
3−{-# LANGUAGE LambdaCase #-}
4−{-# LANGUAGE MultiParamTypeClasses #-}
5−{-# LANGUAGE OverloadedStrings #-}
6−{-# LANGUAGE ScopedTypeVariables #-}
7−{-# LANGUAGE TypeApplications #-}
8−module Obelisk.Command.Upgrade where
9−
10−import Control.Monad (forM, unless, void)
11−import Control.Monad.Catch (onException)
12−import Control.Monad.Except
13−import Control.Monad.IO.Class (liftIO)
14−import Data.Maybe (catMaybes)
15−import Data.Monoid (Any (..), getAny)
16−import Data.Semigroup ((<>))
17−import qualified Data.Set as S
18−import Data.Text (Text)
19−import qualified Data.Text as T
20−import System.Directory
21−import System.FilePath
22−import System.Posix.Process (executeFile)
23−
24−import Obelisk.App (MonadObelisk)
25−import Obelisk.CliApp
26−import Obelisk.Command.Utils
27−
28−import Obelisk.Command.Project (toImplDir)
29−import Obelisk.Command.Project (findProjectObeliskCommand)
30−import Obelisk.Command.Thunk (ThunkData (..), getThunkGitBranch, readThunk, updateThunk)
31−
32−import Obelisk.Command.Upgrade.Hash
33−import Obelisk.Migration
34−
35−newtype HandOffAction = HandoffAction Any
36− deriving (Monoid, Semigroup, Ord, Eq, Show)
37−
38−instance Action HandOffAction where
39− parseEdgeMeta = HandoffAction . Any . (== "True")
40−
41−data MigrationGraph
42− = MigrationGraph_ObeliskUpgrade
43− | MigrationGraph_ObeliskHandoff
44− deriving (Eq, Show, Bounded, Enum)
45−
46−graphName :: MigrationGraph -> Text
47−graphName = \case
48− MigrationGraph_ObeliskHandoff -> "obelisk-handoff"
49− MigrationGraph_ObeliskUpgrade -> "obelisk-upgrade"
50−
51−fromGraphName :: Text -> MigrationGraph
52−fromGraphName = \case
53− "obelisk-handoff" -> MigrationGraph_ObeliskHandoff
54− "obelisk-upgrade" -> MigrationGraph_ObeliskUpgrade
55− _ -> error "Invalid graph name specified"
56−
57−graphNames :: [Text]
58−graphNames = fmap graphName $ [minBound .. maxBound]
59−
60−ensureCleanProject :: MonadObelisk m => FilePath -> m ()
61−ensureCleanProject project =
62− ensureCleanGitRepo project False "Cannot upgrade with uncommited changes"
63−
64−-- | Decide whether we (ambient ob) should handoff to project obelisk before performing upgrade
65−decideHandOffToProjectOb :: MonadObelisk m => FilePath -> m Bool
66−decideHandOffToProjectOb project = do
67− ensureCleanProject project
68− updateThunk (toImplDir project) $ \projectOb -> do
69− (ambientGraph, ambientHash) <- getAmbientObInfo
70− projectHash <- computeVertexHash projectOb
71− case hasVertex projectHash ambientGraph of
72− False -> do
73− putLog Warning "Project ob not found in ambient ob's migration graph; handing off anyway"
74− return True
75− True -> handleMigrationError (findPathAction ambientGraph projectHash ambientHash) >>= \case
76− Nothing -> do
77− putLog Warning "No migration path between project and ambient ob; handing off anyway"
78− return True
79− Just (HandoffAction dontHandoff) -> do
80− return $ not $ getAny dontHandoff
81− where
82− getAmbientObInfo = do
83− ambientOb <- getAmbientOb
84− getMigrationGraph' ambientOb MigrationGraph_ObeliskHandoff >>= \case
85− Nothing -> do
86− failWith "Ambient ob has no migration (this can't be possible)"
87− Just m -> do
88− -- We don't have ambient's ob source code, so locate its hash from the
89− -- graph. The last vertex should be it.
90− ambientHash <- handleMigrationError $ getLast (_migration_graph m)
91− return (m, ambientHash)
92−
93−-- | Return the path to the current ('ambient') obelisk process Nix directory
94−getAmbientOb :: MonadObelisk m => m FilePath
95−getAmbientOb = takeDirectory . takeDirectory <$> liftIO getObeliskExe
96−
97−upgradeObelisk :: MonadObelisk m => FilePath -> Maybe Text -> m ()
98−upgradeObelisk project gitBranchM = do
99− ensureCleanProject project
100− gitBranch <- maybe (getObeliskBranch project) pure gitBranchM
101− updateObelisk project gitBranch >>= handOffToNewOb project
102−
103−getObeliskBranch :: MonadObelisk m => FilePath -> m Text
104−getObeliskBranch project = readThunk (toImplDir project) >>= \case
105− Left e -> failWith $ T.pack $ show e
106− Right (ThunkData_Checkout _) -> failWith "obelisk thunk must be packed"
107− Right (ThunkData_Packed tptr) -> case getThunkGitBranch tptr of
108− Just v -> pure v
109− Nothing ->
110− failWith "You must run `ob upgrade` with the Git branch specified explicitly, as the project's obelisk thunk does not specify any."
111−
112−updateObelisk :: MonadObelisk m => FilePath -> Text -> m Hash
113−updateObelisk project gitBranch =
114− withSpinner ("Fetching new obelisk [" <> gitBranch <> "]") $
115− updateThunk (toImplDir project) $ \obImpl -> do
116− fromHash <- computeVertexHash obImpl
117− callProcessAndLogOutput (Debug, Debug) $
118− gitProc obImpl ["checkout", T.unpack gitBranch]
119− callProcessAndLogOutput (Debug, Debug) $
120− gitProc obImpl ["pull"]
121− return fromHash
122−
123−handOffToNewOb :: MonadObelisk m => FilePath -> Hash -> m ()
124−handOffToNewOb project fromHash = do
125− impl <- withSpinner' "Preparing for handoff" (Just $ ("Handed off to new obelisk " <>) . T.pack) $
126− findProjectObeliskCommand project >>= \case
127− Nothing -> failWith "Not an obelisk project"
128− Just impl -> pure impl
129− let opts = ["internal", "migrate", T.unpack fromHash]
130− liftIO $ executeFile impl False ("--no-handoff" : opts) Nothing
131−
132−migrateObelisk :: MonadObelisk m => FilePath -> Hash -> m ()
133−migrateObelisk project fromHash = void $ withSpinner' "Migrating to new obelisk" (Just id) $ do
134− updateThunk (toImplDir project) $ \obImpl -> revertObImplOnFail obImpl $ do
135− toHash <- computeVertexHash obImpl
136− g <- getMigrationGraph' obImpl MigrationGraph_ObeliskUpgrade >>= \case
137− Nothing -> failWith "New obelisk has no migration metadata"
138− Just m -> pure m
139−
140− unless (hasVertex fromHash g) $ do
141− failWith $ "Current obelisk hash " <> fromHash <> " missing in migration graph of new obelisk"
142− unless (hasVertex toHash g) $ do
143− -- This usually means that the target obelisk branch does not have
144− -- migration vertex for its latest commit; typically due to developer
145− -- negligence.
146− failWith $ "New obelisk hash " <> toHash <> " missing in its migration graph"
147−
148− if fromHash == toHash
149− then do
150− pure $ "No upgrade available (new Obelisk is the same)"
151− else do
152− putLog Debug $ "Migrating from " <> fromHash <> " to " <> toHash
153− handleMigrationError (findPathAction g fromHash toHash) >>= \case
154− Nothing -> do
155− failWith "Unable to find migration path"
156− Just action -> do
157− unless (action == mempty) $ do
158− putLog Notice "To upgrade your project to the new version of obelisk, please follow these instructions:\n"
159− putLog Notice action
160− pure $ "Migrated from " <> fromHash <> " to " <> toHash
161− where
162− revertObImplOnFail impl f = f `onException` do
163− putLog Notice $ T.pack $ "Reverting changes to " <> impl
164− callProcessAndLogOutput (Notice, Notice) $ gitProc project ["checkout", impl]
165−
166−-- | Get the migration graph for project
167−getMigrationGraph
168− :: (Action action, MonadObelisk m)
169− => FilePath -> MigrationGraph -> m (Migration action)
170−getMigrationGraph obDir graph = getMigrationGraph' obDir graph >>= \case
171− Nothing -> failWith "Migration graph missing"
172− Just m -> pure m
173−
174−getMigrationGraph'
175− :: (Action action, MonadObelisk m)
176− => FilePath -> MigrationGraph -> m (Maybe (Migration action))
177−getMigrationGraph' obDir graph = do
178− let name = graphName graph
179− dir = migrationDir obDir
180− putLog Debug $ "Reading migration graph " <> name <> " from " <> T.pack dir
181− liftIO $ readGraph dir name
182−
183−computeVertexHash :: MonadObelisk m => FilePath -> m Hash
184−computeVertexHash = getDirectoryHash [migrationDirName]
185−
186−-- | Verify the integrity of the migration graph in relation to the Git repo.
187−verifyMigration :: MonadObelisk m => FilePath -> m ()
188−verifyMigration obDir = do
189− upgradeGraph <- withSpinner "Reading migration graph" $
190− getMigrationGraph @Text obDir MigrationGraph_ObeliskUpgrade
191− withSpinner "Checking graph integrity" $
192− handleMigrationError $ ensureGraphIntegrity upgradeGraph
193− headVertex <- liftIO (doesDirectoryExist $ obDir </> ".git") >>= \case
194− True ->
195− withSpinner "Checking existence of HEAD vertex" $
196− getHeadVertex obDir
197− False -> do
198− hash <- computeVertexHash obDir
199− unless (hasVertex hash upgradeGraph) $
200− failWith $ "No vertex found for obelisk hash " <> hash
201− pure hash
202− withSpinner "Checking graph linearity" $
203− -- NOTE: We don't support merge commits (i.e., diamonds in a graph) yet.
204− -- Until we do, ensure that the graph is a fully connected linear list.
205− runExceptT (ensureGraphLinearity upgradeGraph) >>= \case
206− Left (NonLinearGraph_MultipleAdjacents vertex) ->
207− failWith $ "Graph is not linear; branches at vertex: " <> tshow vertex
208− Left (NonLinearGraph_NotConnected isolatedVertices) ->
209− failWith $ "Graph is partly linear, with isolated vertices: " <> tshow isolatedVertices
210− Left e ->
211− failWith $ "Error in migration graph: " <> tshow e
212− Right lastVertex -> unless (lastVertex == headVertex) $
213− failWith $ "Graph is linear, but its last vertex (" <> lastVertex <> ") is different from head vertex (" <> headVertex <> ")"
214−
215−-- | Create an edge from HEAD vertex to the hash corresponding to the Git
216−-- working copy. The HEAD vertex must already exist.
217−createMigrationEdgeFromHEAD :: MonadObelisk m => FilePath -> m ()
218−createMigrationEdgeFromHEAD obDir = do
219− headHash <- getHeadVertex obDir
220− wcHash <- getDirectoryHash migrationIgnore obDir
221− if (headHash == wcHash)
222− then
223− putLog Warning "No migration necessary (working copy has the same hash as that of HEAD)"
224− else do
225− writeEdge (migrationDir obDir) graphNames headHash wcHash >>= \case
226− Nothing ->
227− putLog Warning "No migration was created"
228− Just edgeDir ->
229− putLog Notice $ "Created edge: " <> T.pack edgeDir
230−
231−-- | Return the hash corresponding to HEAD
232−--
233−-- Fail if the hash does not exist in project's migration graph.
234−getHeadVertex :: MonadObelisk m => FilePath -> m Hash
235−getHeadVertex obDir = do
236− projectGraph <- getMigrationGraph @Text obDir MigrationGraph_ObeliskUpgrade
237− [headHash] <- getHashAtGitRevision ["HEAD"] migrationIgnore obDir
238− unless (hasVertex headHash projectGraph) $ do
239− -- This means that the HEAD commit has no vertex in the graph,
240− -- possibly due to developer negligence when commiting it.
241− -- Perhaps we should use a post-commit hook or some such thing
242− -- to reject such commits in the first place? For now, just
243− -- error out.
244− failWith $ "No vertex found for HEAD (" <> headHash <> ")"
245− return headHash
246−
247−-- | Create, or update, the migration graph with new edges and new vertices
248−-- corresponding to every commit in the Git history from HEAD.
249−--
250−-- NOTE: This creates a linear graph, and doesn't follow the Git graph
251−-- structure.
252−backfillGraph :: MonadObelisk m => Maybe Int -> FilePath -> m ()
253−backfillGraph lastN obDir = do
254− revs <- fmap (takeM lastN . fmap (fst . T.breakOn " ") . T.lines) $
255− readProc $ gitProc obDir ["log", "--pretty=oneline"]
256− -- Note: we need to take unique hashes only; this is fine for backfilling.
257− -- But future migrations should ensure that there are no duplicate hashes
258− -- (which would cause cycles) such as those introduced by revert commits.
259− vertices :: [Hash] <- withSpinnerNoTrail "Computing hash for git history" $
260− fmap (unique . reverse) $ getHashAtGitRevision revs [migrationDirName] obDir
261− void $ withSpinner'
262− ("Backfilling with " <> tshow (length vertices) <> " vertices")
263− (Just $ \n -> "Backfilled " <> tshow n <> " edges.") $ do
264− let vertexPairs = zip vertices $ drop 1 vertices
265− let edgesDir = migrationDir obDir
266− liftIO $ createDirectoryIfMissing False edgesDir
267− fmap (length . catMaybes) $ forM vertexPairs $ \(v1, v2) -> do
268− writeEdge edgesDir graphNames v1 v2
269− where
270− -- Return unique items in the list /while/ preserving order
271− unique = loop mempty
272− where
273− loop _ [] = []
274− loop s (x : xs)
275− | S.member x s = loop s xs
276− | otherwise = x : loop (S.insert x s) xs
277− takeM n' xs = case n' of
278− Just n -> take n xs
279− Nothing -> xs
280−
281−handleMigrationError :: MonadObelisk m => Except MigrationError a -> m a
282−handleMigrationError m = case runExcept m of
283− Left e -> failWith $ "Error in migration graph: " <> tshow e
284− Right a -> return a
285−
286−migrationDir :: FilePath -> FilePath
287−migrationDir project = project </> migrationDirName
288−
289−migrationIgnore :: [FilePath]
290−migrationIgnore = [migrationDirName]
291−
292−migrationDirName :: FilePath
293−migrationDirName = "migration"
lib/migration/obelisk-migration.cabaldeleted−24
@@ -1,24 +0,0 @@
1−name: obelisk-migration
2−version: 0.1
3−license: BSD3
4−build-type: Simple
5−cabal-version: >=1.2
6−
7−library
8− hs-source-dirs: src
9− build-depends:
10− algebraic-graphs >= 0.2
11− , base
12− , containers
13− , directory
14− , exceptions
15− , filepath
16− , mtl
17− , obelisk-cliapp
18− , process
19− , text
20− , transformers
21− exposed-modules:
22− Obelisk.Migration
23− other-modules:
24− ghc-options: -Wall -fobject-code
lib/migration/src/Obelisk/Migration.hsdeleted−223
@@ -1,223 +0,0 @@
1−{-# LANGUAGE FlexibleContexts #-}
2−{-# LANGUAGE LambdaCase #-}
3−{-# LANGUAGE OverloadedStrings #-}
4−{-# LANGUAGE ScopedTypeVariables #-}
5−{-# LANGUAGE TupleSections #-}
6−module Obelisk.Migration where
7−
8−import Control.Monad (forM, forM_, unless, void)
9−import Control.Monad.Except
10−import Control.Monad.IO.Class (MonadIO, liftIO)
11−import Control.Monad.State.Strict (evalStateT, get, put)
12−import Data.Map (Map)
13−import qualified Data.Map as Map
14−import Data.Maybe (catMaybes)
15−import Data.Semigroup ((<>))
16−import Data.Set (Set)
17−import qualified Data.Set as Set
18−import Data.Text (Text)
19−import qualified Data.Text as T
20−import qualified Data.Text.IO as T
21−import System.Directory
22−import System.FilePath
23−
24−import Algebra.Graph.AdjacencyMap
25−
26−type Hash = Text
27−
28−-- Migration graph with edge metadata
29−data Migration action = Migration
30− { _migration_graph :: AdjacencyMap Hash
31− , _migration_actions :: Map (Hash, Hash) action -- Edge metadata
32− }
33− deriving (Eq, Show)
34−
35−-- A path is a list of edges
36−type Path = [(Hash, Hash)]
37−
38−data MigrationError
39− = GraphInternalError Text
40− | CyclicGraphError
41− | MultipleTerminalVertices
42− | NoTerminalVertex
43− | NonEquivalentPaths Hash Hash
44− | MissingEdgeInActionMap (Hash, Hash)
45− | NonLinearGraph_MultipleAdjacents Hash
46− | NonLinearGraph_NotConnected (Set Hash)
47− deriving (Eq, Ord, Show)
48−
49−class Action action where
50− parseEdgeMeta :: Text -> action
51−
52−instance Action Text where
53− parseEdgeMeta = id
54−
55−-- | Get the directed-acylic graph of migration
56−-- TODO: Consider calling getDag only when reading the graph for efficiency.
57−getDag :: (MonadError MigrationError m, Ord a) => AdjacencyMap a -> m (AdjacencyMap a)
58−getDag g = case topSort g of
59− Just _ -> pure g
60− Nothing -> throwError CyclicGraphError
61−
62−-- | Read the graph from a directory of files.
63−readGraph :: Action action => FilePath -> Text -> IO (Maybe (Migration action))
64−readGraph root name = doesDirectoryExist root >>= \case
65− False -> return Nothing
66− True -> do
67− allVertexPairs <- listDirectory root >>= return . catMaybes . fmap getEdgeVertices
68− edgesInfo <- fmap catMaybes $ forM allVertexPairs $ \vs -> do
69− getEdgeFor name vs >>= return . fmap (vs, )
70− return $ pure $ Migration (edges $ fmap fst edgesInfo) (Map.fromList edgesInfo)
71− where
72− getEdgeVertices p = case T.splitOn "-" $ T.pack p of
73− [v1, v2] -> Just (v1, v2)
74− _ -> Nothing
75− getEdgeFor graph (v1, v2) = do
76− let f = root </> (T.unpack $ v1 <> "-" <> v2) </> T.unpack graph
77− doesFileExist f >>= \case
78− True -> do
79− c <- parseEdgeMeta <$> T.readFile f
80− return $ Just c
81− False -> do
82− return Nothing
83−
84−-- | Ensure that the migration graph is correct
85−--
86−-- In a correct migration graph: for all pairs of nodes, all paths between
87−-- those nodes have the same migration value
88−--
89−-- Throw the following if integrity check fails
90−-- * `CyclicGraphError`
91−-- * `NonEquivalentPaths`
92−--
93−-- Complexity: O(V+E)
94−ensureGraphIntegrity
95− :: (MonadError MigrationError m, Monoid action, Ord action)
96− => Migration action -> m ()
97−ensureGraphIntegrity m = do
98− graph <- fmap adjacencyMap $ getDag $ _migration_graph m
99− firstVertex <- getFirst $ _migration_graph m
100− let
101− visitFrom visited acc start = case Map.lookup start graph of
102− Nothing -> pure ()
103− Just adjs -> void $ flip traverse (Set.toList adjs) $ \adj -> do
104− acc' <- fmap (mappend acc) $ getAction m (start, adj)
105− visited' <- case Map.lookup adj visited of
106− Nothing -> pure $ Map.insert adj acc' visited
107− Just adjAcc -> if acc' == adjAcc
108− then pure visited
109− else throwError $ NonEquivalentPaths start adj
110− visitFrom visited' acc' adj
111− -- We only check all paths from the _first_ vertex; it is not necessary to
112− -- check all paths between _all_ pairs of vertices as that can be proved by
113− -- induction, as long as there is exactly one first vertex (which we check on
114− -- further above).
115− visitFrom mempty mempty firstVertex
116−
117−-- | Ensure that the graph is a fully connected linear list
118−--
119−-- Returns the last vertex in the linear list.
120−ensureGraphLinearity :: MonadError MigrationError m => Migration action -> m Hash
121−ensureGraphLinearity m = do
122− graph <- fmap adjacencyMap $ getDag $ _migration_graph m
123− firstVertex <- getFirst $ _migration_graph m
124− let
125− allVertices = vertexSet $ _migration_graph m
126− visitFrom start = case Map.lookup start graph of
127− Nothing -> throwError $ GraphInternalError $ "Vertex not found: " <> start
128− Just adjs -> case (Set.toList adjs) of
129− [] -> pure (start, [start])
130− [adj] -> fmap (start :) <$> visitFrom adj
131− _ -> throwError $ NonLinearGraph_MultipleAdjacents start
132− (lastVertex, visitedVertices) <- visitFrom firstVertex
133− let unvisitedVertices = Set.difference allVertices $ Set.fromList visitedVertices
134− if Set.null unvisitedVertices
135− then pure lastVertex
136− else throwError $ NonLinearGraph_NotConnected unvisitedVertices
137−
138−-- | Find the concatenated actions between two vertices
139−--
140−-- All paths between the vertices must have the same mappend'ed action;
141−-- otherwise this will throw.
142−--
143−-- Complexity: O(V+E)
144−findPathAction
145− :: (MonadError MigrationError m, Action action, Monoid action, Ord action)
146− => Migration action -> Hash -> Hash -> m (Maybe action)
147−findPathAction m start end = flip evalStateT Map.empty $ do
148− graph <- fmap adjacencyMap $ getDag $ _migration_graph m
149− let
150− findPathFromCached start' = do
151− -- TODO: refactor the caching part into a separate `memoize` function
152− cache <- get
153− case Map.lookup start' cache of
154− Just result ->
155− pure result
156− Nothing -> do
157− result <- findPathFrom start'
158− put $ Map.insert start' result cache
159− pure result
160− findPathFrom start' = if start' == end
161− then
162− pure $ Just mempty
163− else case Map.lookup start' graph of
164− Just adjs -> do
165− actions <- fmap catMaybes $ flip traverse (Set.toList adjs) $ \adj -> do
166− action0 <- getAction m (start', adj)
167− actionM <- findPathFromCached adj
168− pure $ fmap (mappend action0) $ actionM
169− case uniqs actions of
170− [] -> pure Nothing
171− [v] -> pure $ Just v -- Exactly one monoidal value; accept.
172− _ -> throwError $ NonEquivalentPaths start' end
173− Nothing ->
174− pure Nothing
175− uniqs = Set.toList . Set.fromList
176− findPathFromCached start
177−
178−hasVertex :: Hash -> Migration action -> Bool
179−hasVertex h (Migration g _) = Set.member h $ vertexSet g
180−
181−-- | Get the action for the given edge (which must exist)
182−getAction :: MonadError MigrationError m => Migration action -> (Hash, Hash) -> m action
183−getAction (Migration _ h) e = case Map.lookup e h of
184− Just v -> pure v
185− Nothing -> throwError $ MissingEdgeInActionMap e
186−
187−-- | Get the last vertex of the given DAG. Nothing if empty graph.
188−--
189−-- Assumes that the graph is fully connected.
190−getLast :: (MonadError MigrationError m, Ord a, Eq a) => AdjacencyMap a -> m a
191−getLast g = lastVertices >>= \case
192− [] -> throwError NoTerminalVertex
193− [x] -> pure x
194− _ -> throwError MultipleTerminalVertices
195− where
196− lastVertices
197− = pure
198− . fmap fst
199− . filter (\(_, nexts) -> nexts == [])
200− . adjacencyList
201− =<< getDag g
202−
203−-- | Get the first vertex of the given DAG. Nothing if empty graph.
204−getFirst :: (MonadError MigrationError m, Ord a, Eq a) => AdjacencyMap a -> m a
205−getFirst = getLast . transpose
206−
207−-- | Write an edge directly to the filesystem.
208−--
209−-- Actions are written with empty string.
210−--
211−-- Return the edge directory created.
212−writeEdge :: MonadIO m => FilePath -> [Text] -> Hash -> Hash -> m (Maybe FilePath)
213−writeEdge dir actionFiles v1 v2 = do
214− unless (v1 /= v2) $
215− fail $ "Cannot create self loop with: " <> T.unpack v1
216− let edgeDir = dir </> (T.unpack $ v1 <> "-" <> v2)
217− liftIO (doesDirectoryExist edgeDir) >>= \case
218− True -> pure Nothing
219− False -> do
220− liftIO $ createDirectory edgeDir
221− forM_ actionFiles $ \fp -> liftIO $
222− writeFile (edgeDir </> T.unpack fp) ""
223− pure $ Just edgeDir