@@ -6,10 +6,9 @@
6{-# LANGUAGE ScopedTypeVariables #-}
6{-# LANGUAGE ScopedTypeVariables #-}
7module Obelisk.Command.Upgrade where
7module Obelisk.Command.Upgrade where
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
22import System.Posix.Process (executeFile)
20import System.Posix.Process (executeFile)
23−import System.Process (cwd, proc)
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)
30+import Obelisk.Command.Upgrade.Hash
33import Obelisk.Migration
31import Obelisk.Migration
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)
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+graphNames = fmap graphName $ [minBound .. maxBound]
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]
180−migrationDir :: FilePath -> FilePath
181−migrationDir project = project </> migrationDirName
183−migrationIgnore :: [FilePath]
184−migrationIgnore = [migrationDirName]
186−migrationDirName :: FilePath
187−migrationDirName = "migration"
189−-- TODO: Move this to migration library? (but we rely on wrapProgram exes)
191−-- | Get the unique hash of the given directory
193−-- Excludes the following before computing the hash:
194−-- * the specified top-level files/ directories.
196−-- * untracked Git files
197−-- * ignored Git files
199−-- Uses the same predictive algorithm that Nix (`nix hash-path`).
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
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
216− tidyUpGitWorkingCopy dir
217− withSpinnerNoTrail "Removing .git directory" $
218− liftIO $ removePathForcibly $ dir </> ".git"
220− withSpinnerNoTrail "Removing excluded paths" $ do
221− forM_ (fmap (dir </>) excludes) $
222− liftIO . removePathForcibly
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
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"
195+ putLog Notice $ "Created edge: " <> T.pack edgeDir
239-- | Return the hash corresponding to HEAD
197-- | Return the hash corresponding to HEAD
@@ -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
257−-- | TODO: Verify the integrity of the migration graph in relation to the Git repo.
258−verifyGraph :: MonadObelisk m => FilePath -> m ()
259−verifyGraph = undefined
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.
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
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
295−-- | Write the edge to filesystem.
249+-- | TODO: Verify the integrity of the migration graph in relation to the Git repo.
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
301− failWith $ "Cannot create self loop with: " <> v1
302− let edgeDir = dir </> (T.unpack $ v1 <> "-" <> v2)
303− liftIO (doesDirectoryExist edgeDir) >>= \case
306− putLog Notice $ T.pack $ "Creating edge " <> edgeDir
307− liftIO $ createDirectory edgeDir
308− forM_ actionFiles $ \fp -> liftIO $
309− writeFile (edgeDir </> fp) ""
312− actionFiles = ["obelisk-handoff", "obelisk-upgrade"]
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"]) $
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)
335− forM_ existingPaths $ \p ->
336− liftIO $ renamePath (stashDir </> p) (base </> p)
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]
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)
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"]
364− gitLsFiles pwd opts = fmap lines $ readProcessAndLogStderr Error $
365− (proc "git" $ ["ls-files", "."] <> opts) { cwd = Just pwd }
259+migrationDirName :: FilePath
260+migrationDirName = "migration"