@@ -33,6 +33,7 @@ module Obelisk.Command.Thunk
34import Control.Applicative
34import Control.Applicative
35import Control.Exception (displayException, throwIO, try)
35import Control.Exception (displayException, throwIO, try)
36+import Control.Lens.Indexed hiding ((<.>))
36import Control.Monad
37import Control.Monad
37import Control.Monad.Catch (handle)
38import Control.Monad.Catch (handle)
38import Control.Monad.Except
39import Control.Monad.Except
@@ -49,6 +50,7 @@ import qualified Data.Git.Ref as Ref
49import qualified Data.List as L
50import qualified Data.List as L
50import Data.List.NonEmpty (NonEmpty (..))
51import Data.List.NonEmpty (NonEmpty (..))
51import qualified Data.List.NonEmpty as NonEmpty
52import qualified Data.List.NonEmpty as NonEmpty
52import qualified Data.Map as Map
54import qualified Data.Map as Map
53import Data.Maybe
55import Data.Maybe
54import Data.Set (Set)
56import Data.Set (Set)
@@ -555,7 +557,7 @@ updateThunk p f = withSystemTempDirectory "obelisk-thunkptr-" $ \tmpDir -> do
555 p' <- copyThunkToTmp tmpDir p
557 p' <- copyThunkToTmp tmpDir p
556 unpackThunk' True p'
558 unpackThunk' True p'
557 result <- f p'
559 result <- f p'
558− updateThunkFromTmp p' "origin" -- TODO: stop hardcoding remote
560+ updateThunkFromTmp p'
559 return result
561 return result
561 copyThunkToTmp tmpDir thunkDir = readThunk thunkDir >>= \case
563 copyThunkToTmp tmpDir thunkDir = readThunk thunkDir >>= \case
@@ -566,8 +568,8 @@ updateThunk p f = withSystemTempDirectory "obelisk-thunkptr-" $ \tmpDir -> do
566 proc "cp" ["-r", "-T", thunkDir, tmpThunk]
568 proc "cp" ["-r", "-T", thunkDir, tmpThunk]
567 return tmpThunk
569 return tmpThunk
568 Right _ -> failWith $ "Thunk is not packed"
570 Right _ -> failWith $ "Thunk is not packed"
569− updateThunkFromTmp p' remote = do
571+ updateThunkFromTmp p' = do
570− packThunk' True p' remote
572+ packThunk' True p'
571 callProcessAndLogOutput (Notice, Error) $
573 callProcessAndLogOutput (Notice, Error) $
572 proc "cp" ["-r", "-T", p', p]
574 proc "cp" ["-r", "-T", p', p]
@@ -595,7 +597,7 @@ unpackThunk' noTrail thunkDir = readThunk thunkDir >>= \case
595 Just c -> return $ T.unpack $ getUrl c
597 Just c -> return $ T.unpack $ getUrl c
597 withSpinner' ("Fetching thunk " <> T.pack thunkDir)
599 withSpinner' ("Fetching thunk " <> T.pack thunkDir)
598− ( finalMsg noTrail $ const $ "Fetched thunk " <> T.pack thunkDir) $ do
600+ (finalMsg noTrail $ const $ "Fetched thunk " <> T.pack thunkDir) $ do
599 callProcessAndLogOutput (Debug, Debug) $
601 callProcessAndLogOutput (Debug, Debug) $
600 proc "hub" ["clone", "-n", githubURI, tmpRepo]
602 proc "hub" ["clone", "-n", githubURI, tmpRepo]
601 --If this directory already exists then something is weird and we should fail
603 --If this directory already exists then something is weird and we should fail
@@ -641,86 +643,123 @@ unpackThunk' noTrail thunkDir = readThunk thunkDir >>= \case
642--TODO: add force mode to pack even if changes are present
644--TODO: add force mode to pack even if changes are present
643--TODO: add a rollback mode to pack to the original thunk
645--TODO: add a rollback mode to pack to the original thunk
644−packThunk :: MonadObelisk m => FilePath -> Text -> m ()
646+packThunk :: MonadObelisk m => FilePath -> m ()
645packThunk = packThunk' False
647packThunk = packThunk' False
647−packThunk' :: MonadObelisk m => Bool -> FilePath -> Text -> m ()
649+packThunk' :: MonadObelisk m => Bool -> FilePath -> m ()
648−packThunk' noTrail thunkDir upstream = readThunk thunkDir >>= \case
650+packThunk' noTrail thunkDir = readThunk thunkDir >>= \case
649 Left err -> failWith $ T.pack $ "thunk pack: " <> show err
651 Left err -> failWith $ T.pack $ "thunk pack: " <> show err
650 Right (ThunkData_Packed _) -> failWith "pack: thunk is already packed"
652 Right (ThunkData_Packed _) -> failWith "pack: thunk is already packed"
651 Right (ThunkData_Checkout _) -> do
653 Right (ThunkData_Checkout _) -> do
652 withSpinner' ("Packing thunk " <> T.pack thunkDir)
654 withSpinner' ("Packing thunk " <> T.pack thunkDir)
653 (finalMsg noTrail $ const $ "Packed thunk " <> T.pack thunkDir) $ do
655 (finalMsg noTrail $ const $ "Packed thunk " <> T.pack thunkDir) $ do
654− thunkPtr <- getThunkPtr thunkDir upstream
656+ thunkPtr <- getThunkPtr thunkDir
655 callProcessAndLogOutput (Debug, Error) $ proc "rm" ["-rf", thunkDir]
657 callProcessAndLogOutput (Debug, Error) $ proc "rm" ["-rf", thunkDir]
656 liftIO $ createThunk thunkDir thunkPtr
658 liftIO $ createThunk thunkDir thunkPtr
658−getThunkPtr :: MonadObelisk m => FilePath -> Text -> m ThunkPtr
660+getThunkPtr :: MonadObelisk m => FilePath -> m ThunkPtr
659getThunkPtr = getThunkPtr' True
661getThunkPtr = getThunkPtr' True
661−getThunkPtr' :: MonadObelisk m => Bool -> FilePath -> Text -> m ThunkPtr
663+getThunkPtr' :: forall m. MonadObelisk m => Bool -> FilePath -> m ThunkPtr
662−getThunkPtr' checkClean thunkDir upstream = do
664+getThunkPtr' checkClean thunkDir = do
663− when checkClean $ ensureCleanGitRepo thunkDir True $
665+ when checkClean $ ensureCleanGitRepo thunkDir True $
664− "thunk pack: thunk checkout contains unsaved modifications"
666+ "thunk pack: thunk checkout contains unsaved modifications"
666− -- Check whether there are any stashes
668+ -- Check whether there are any stashes
667− stashOutput <- readGitProcess thunkDir ["stash", "list"]
669+ stashOutput <- readGitProcess thunkDir ["stash", "list"]
668− when checkClean $ case T.null stashOutput of
670+ when checkClean $ case T.null stashOutput of
669− False -> do
671+ False -> do
670− failWith $ T.unlines $
672+ failWith $ T.unlines $
671− [ "thunk pack: thunk checkout has stashes"
673+ [ "thunk pack: thunk checkout has stashes"
672− , "git stash list:"
674+ , "git stash list:"
673− ] ++ T.lines stashOutput
675+ ] ++ T.lines stashOutput
674− True -> return ()
676+ True -> return ()
676− --Check whether all local heads are pushed
678+ -- Get information on all branches and their (optional) designated upstream
677− repoHeads <- T.lines <$> readGitProcess thunkDir ["for-each-ref", "--format=%(refname:short)", "refs/heads/"]
680+ (headDump :: [Text]) <- T.lines <$> readGitProcess thunkDir
678− remotes <- T.lines <$> readGitProcess thunkDir ["remote"]
681+ [ "for-each-ref"
679− --Check that the upstream specified actually exists
682+ , "--format=%(refname:short) %(upstream:short)"
680− case L.find (== upstream) remotes of
683+ , "refs/heads/"
681− Nothing -> failWith $ "thunk pack: upstream " <> upstream <> " does not exist. Available upstreams are " <> T.intercalate ", " remotes
684+ ]
682− Just _ -> return ()
685+ (headInfo :: Map Text (Maybe Text)) <- fmap Map.fromList $ forM headDump $ \line -> do
683− -- iterate over cartesian product
686+ (branch : restOfLine) <- pure $ T.words line
687+ mUpstream <- case restOfLine of
689+ [u] -> pure $ Just u
690+ (_:_) -> failWith "git for-each-ref invalid output"
691+ pure (branch, mUpstream)
693+ let errorMap :: Map Text ()
694+ headUpstream :: Map Text Text
695+ (errorMap, headUpstream) = flip Map.mapEither headInfo $ \case
699+ -- Check that every branch has a remote equivalent
684− when checkClean $ forM_ repoHeads $ \hd -> do
700+ when checkClean $ do
685− [localRev] <- T.lines <$> readGitProcess thunkDir ["rev-parse", T.unpack hd]
701+ let untrackedBranches = Map.keys errorMap
686− forM_ remotes $ \rm -> do
702+ when (not $ L.null untrackedBranches) $ failWith $ T.unlines $
687− --TODO: The error you get if a branch isn't pushed anywhere could be made more user friendly
703+ [ "thunk pack: Certain branches in the thunk have no upstream branch \
688− [remoteMergeRev] <- fmap T.lines $ readGitProcess thunkDir
704+ \ set. This means don't know to check whether all your work is \
689− ["merge-base", T.unpack localRev, "remotes/" <> T.unpack rm <> "/" <> T.unpack hd]
705+ \ saved. The offending branches are:"
690− case remoteMergeRev == localRev of
706+ , ""
691− False -> failWith $ mconcat [ "thunk unpack: branch ", hd, " has not been pushed to ", rm ]
707+ , T.unwords untrackedBranches
692− True -> return ()
708+ , ""
693−
709+ , "To fix this, you probably want to do:"
712+ ((\branch -> "git push -u origin " <> branch) <$> untrackedBranches) ++
714+ , "These will push the branches to the default remote under the same \
715+ \ name, and (thanks to the `-u`) remember that choice so you don't \
716+ \ get this error again."
719+ -- loosely by https://stackoverflow.com/questions/7773939/show-git-ahead-and-behind-info-for-all-branches-including-remotes
720+ stats <- iforM headUpstream $ \branch upstream -> do
721+ (stat :: [Text]) <- T.lines <$> readGitProcess thunkDir
722+ [ "rev-list", "--left-right"
723+ , T.unpack branch <> "..." <> T.unpack upstream
725+ let ahead = length $ [ () | Just ('<', _) <- T.uncons <$> stat ]
726+ behind = length $ [ () | Just ('>', _) <- T.uncons <$> stat ]
727+ pure (upstream, (ahead, behind))
729+ -- Those branches which have commits ahead of, i.e. not on, the upstream
730+ -- branch. Purely being behind is fine.
731+ let nonGood = Map.filter ((/= 0) . fst . snd) stats
733+ when (not $ Map.null nonGood) $ failWith $ T.unlines $
734+ [ "thunk pack: Certain branches in the thunk have commits not yet pushed upstream:"
737+ (flip map (Map.toList nonGood) $ \(branch, (upstream, (ahead, behind))) -> mconcat
738+ [" ", branch, " ahead: ", T.pack (show ahead), " behind: ", T.pack (show behind), " remote branch ", upstream]) ++
740+ , "Please push these upstream and try again."
694− --We assume it's safe to pack the thunk at this point
743+ -- We assume it's safe to pack the thunk at this point
745+ -- Get current branch `git rev-parse --abbrev-ref HEAD`
746+ mCurrentBranch <- listToMaybe
748+ <$> readGitProcess thunkDir ["rev-parse", "--abbrev-ref", "HEAD"]
750+ let remote = maybe "origin" id $
751+ flip Map.lookup headUpstream =<< mCurrentBranch
695− [remoteUri'] <- fmap T.lines $ readGitProcess thunkDir ["config", "--get", "remote." <> T.unpack upstream <> ".url"]
753+ [remoteUri'] <- fmap T.lines $ readGitProcess thunkDir
756+ , "remote." <> T.unpack remote <> ".url"
759+ remoteUri <- case parseGitUri remoteUri' of
697− let uriParseFailure = "Could not identify git remote: " <> remoteUri'
760+ Nothing -> failWith $ "Could not identify git remote: " <> remoteUri'
698− mRemoteUri = parseGitUri remoteUri'
761+ Just uri -> pure uri
699− case mRemoteUri of
762+ uriThunkPtr remoteUri mCurrentBranch
700− Nothing -> failWith uriParseFailure
701− Just remoteUri -> do
702− refs <- fmap (map (refsToTuples . T.words) . T.lines) $ readGitProcess thunkDir ["show-ref", "--head"]
703− -- safe because exactly one hash is associated with HEAD
704− let Just currentHead = fst <$> L.find ((== "HEAD") . snd) refs
705− remoteHeads = Map.fromList $ catMaybes $ fmap
706− (\(x,y) -> (,) x <$> T.stripPrefix ("refs/remotes/" <> upstream <> "/") y)
708− localHeads = Map.fromList $ catMaybes $ fmap
709− (\(x,y) -> (,) x <$> T.stripPrefix "refs/heads/" y)
711− mCurrentUpstreamBranch = Map.lookup currentHead remoteHeads
712− <|> Map.lookup currentHead localHeads
713− let src = uriToThunkSource remoteUri mCurrentUpstreamBranch
715− ThunkSource_GitHub s -> githubThunkRev s currentHead
716− ThunkSource_Git s -> gitThunkRev s currentHead
718− { _thunkPtr_rev = rev
719− , _thunkPtr_source = src
722− refsToTuples [x, y] = (x, y)
723− refsToTuples x = error $ "thunk pack: cannot parse ref " <> show x
725-- | Get the latest revision available from the given source
764-- | Get the latest revision available from the given source
726getLatestRev :: MonadObelisk m => ThunkSource -> m ThunkRev
765getLatestRev :: MonadObelisk m => ThunkSource -> m ThunkRev