@@ -27,10 +27,13 @@ module Obelisk.Command.Thunk
27 , updateThunk
27 , updateThunk
28 , getThunkPtr
28 , getThunkPtr
29 , getThunkPtr'
29 , getThunkPtr'
32import Control.Applicative
34import Control.Applicative
33−import Control.Exception (displayException, throwIO, try)
35+import Control.Exception (displayException, try)
36+import Control.Lens.Indexed hiding ((<.>))
34import Control.Monad
37import Control.Monad
35import Control.Monad.Catch (handle)
38import Control.Monad.Catch (handle)
36import Control.Monad.Except
39import Control.Monad.Except
@@ -40,13 +43,13 @@ import Data.Aeson.Encode.Pretty
40import qualified Data.Aeson.Types as Aeson
43import qualified Data.Aeson.Types as Aeson
41import qualified Data.ByteString.Lazy as LBS
44import qualified Data.ByteString.Lazy as LBS
42import Data.Default
45import Data.Default
43−import Data.Either.Combinators (rightToMaybe)
46+import Data.Either.Combinators (fromRight', rightToMaybe)
45import Data.Git.Ref (Ref)
47import Data.Git.Ref (Ref)
46import qualified Data.Git.Ref as Ref
48import qualified Data.Git.Ref as Ref
47import qualified Data.List as L
49import qualified Data.List as L
48import Data.List.NonEmpty (NonEmpty (..))
50import Data.List.NonEmpty (NonEmpty (..))
49import qualified Data.List.NonEmpty as NonEmpty
51import qualified Data.List.NonEmpty as NonEmpty
50import qualified Data.Map as Map
53import qualified Data.Map as Map
51import Data.Maybe
54import Data.Maybe
52import Data.Set (Set)
55import Data.Set (Set)
@@ -56,12 +59,9 @@ import qualified Data.Text as T
56import Data.Text.Encoding
59import Data.Text.Encoding
57import qualified Data.Text.IO as T
60import qualified Data.Text.IO as T
58import Data.Time.Clock.POSIX (posixSecondsToUTCTime)
61import Data.Time.Clock.POSIX (posixSecondsToUTCTime)
59−import Data.Yaml (parseMaybe, (.:))
62+import Data.Yaml (parseMaybe)
60−import qualified Data.Yaml as Yaml
61import GitHub
63import GitHub
62import GitHub.Data.Name
64import GitHub.Data.Name
63−import GitHub.Endpoints.Repos.Contents (archiveForR)
65import Obelisk.Command.Nix
65import Obelisk.Command.Nix
66import System.Directory
66import System.Directory
67import System.FilePath
67import System.FilePath
@@ -69,6 +69,7 @@ import System.IO.Error
69import System.IO.Temp
69import System.IO.Temp
70import System.Posix (getSymbolicLinkStatus, modificationTime)
70import System.Posix (getSymbolicLinkStatus, modificationTime)
71import System.Process (proc)
71import System.Process (proc)
73import Obelisk.App (MonadObelisk)
74import Obelisk.App (MonadObelisk)
74import Obelisk.CliApp
75import Obelisk.CliApp
@@ -111,21 +112,43 @@ data GitHubSource = GitHubSource
111 { _gitHubSource_owner :: Name Owner
112 { _gitHubSource_owner :: Name Owner
112 , _gitHubSource_repo :: Name Repo
113 , _gitHubSource_repo :: Name Repo
113 , _gitHubSource_branch :: Maybe (Name Branch)
114 , _gitHubSource_branch :: Maybe (Name Branch)
114− , _gitHubSource_private :: Bool
116 deriving (Show, Eq, Ord)
116 deriving (Show, Eq, Ord)
118data GitSource = GitSource
118data GitSource = GitSource
119 { _gitSource_url :: URI
119 { _gitSource_url :: URI
120− , _gitSource_branch :: Name Branch
120+ , _gitSource_branch :: Maybe (Name Branch)
121 , _gitSource_fetchSubmodules :: Bool
121 , _gitSource_fetchSubmodules :: Bool
123 deriving (Show, Eq, Ord)
123 deriving (Show, Eq, Ord)
125+-- | Convert a GitHub source to a regular Git source. Assumes no submodules.
126+forgetGithub :: Bool -> GitHubSource -> GitSource
127+forgetGithub useSsh s = GitSource
128+ { _gitSource_url = URI
129+ { uriScheme = Just $ fromRight' $ mkScheme $ if useSsh then "ssh" else "https"
130+ , uriAuthority = Right $ Authority
131+ { authUserInfo = UserInfo (fromRight' $ mkUsername "git") Nothing
133+ , authHost = fromRight' $ mkHost "github.com"
134+ , authPort = Nothing
136+ , uriPath = Just ( False
137+ , fromRight' . mkPathPiece <$>
138+ untagName (_gitHubSource_owner s)
139+ :| [ untagName (_gitHubSource_repo s) <> ".git" ]
142+ , uriFragment = Nothing
144+ , _gitSource_branch = _gitHubSource_branch s
145+ , _gitSource_fetchSubmodules = False
125getThunkGitBranch :: ThunkPtr -> Maybe Text
148getThunkGitBranch :: ThunkPtr -> Maybe Text
126getThunkGitBranch (ThunkPtr _ src) = fmap untagName $ case src of
149getThunkGitBranch (ThunkPtr _ src) = fmap untagName $ case src of
127 ThunkSource_GitHub s -> _gitHubSource_branch s
150 ThunkSource_GitHub s -> _gitHubSource_branch s
128− ThunkSource_Git s -> Just $ _gitSource_branch s
151+ ThunkSource_Git s -> _gitSource_branch s
130commitNameToRef :: Name Commit -> Ref
153commitNameToRef :: Name Commit -> Ref
131commitNameToRef (N c) = Ref.fromHex $ encodeUtf8 c
154commitNameToRef (N c) = Ref.fromHex $ encodeUtf8 c
@@ -136,17 +159,17 @@ getNixSha256ForUriUnpacked
137 -> m NixSha256
160 -> m NixSha256
138getNixSha256ForUriUnpacked uri =
161getNixSha256ForUriUnpacked uri =
139− withExitFailMessage ("nix-prefetch-url: Failed to determine sha256 hash of URL " <> T.pack (show uri)) $ do
162+ withExitFailMessage ("nix-prefetch-url: Failed to determine sha256 hash of URL " <> render uri) $ do
140− [hash] <- fmap T.lines $ withNixRemoteCheck $ readProcessAndLogStderr Debug $
163+ [hash] <- fmap T.lines $ withNixRemoteCheck $ readProcessAndLogOutput (Debug, Debug) $
141− proc "nix-prefetch-url" ["--unpack" , "--type" , "sha256" , show uri]
164+ proc "nix-prefetch-url" ["--unpack" , "--type" , "sha256" , T.unpack $ render uri]
142 pure hash
165 pure hash
144nixPrefetchGit :: MonadObelisk m => URI -> Text -> Bool -> m NixSha256
167nixPrefetchGit :: MonadObelisk m => URI -> Text -> Bool -> m NixSha256
145nixPrefetchGit uri rev fetchSubmodules =
168nixPrefetchGit uri rev fetchSubmodules =
146− withExitFailMessage ("nix-prefetch-git: Failed to determine sha256 hash of Git repo " <> T.pack (show uri) <> " at " <> rev) $ do
169+ withExitFailMessage ("nix-prefetch-git: Failed to determine sha256 hash of Git repo " <> render uri <> " at " <> rev) $ do
147 out <- withNixRemoteCheck $ readProcessAndLogStderr Debug $
170 out <- withNixRemoteCheck $ readProcessAndLogStderr Debug $
148 proc "nix-prefetch-git" $ filter (/="")
171 proc "nix-prefetch-git" $ filter (/="")
149− [ "--url", show uri
172+ [ "--url", T.unpack $ render uri
150 , "--rev", T.unpack rev
173 , "--rev", T.unpack rev
151 , if fetchSubmodules then "--fetch-submodules" else ""
174 , if fetchSubmodules then "--fetch-submodules" else ""
152 , "--quiet"
175 , "--quiet"
@@ -156,56 +179,6 @@ nixPrefetchGit uri rev fetchSubmodules =
156 Nothing -> failWith $ "nix-prefetch-git: unrecognized output " <> out
179 Nothing -> failWith $ "nix-prefetch-git: unrecognized output " <> out
157 Just x -> pure x
180 Just x -> pure x
159−getLatestGitRev :: MonadObelisk m => GitSource -> m ThunkRev
160−getLatestGitRev s = withSystemTempDirectory "git-clone" $ \tmpDir -> do
161− withExitFailMessage ("Unable to determine latest git revision for branch " <> branch) $ do
162− let git = readProcessAndLogStderr Debug . gitProc tmpDir
164− (git ["clone", "--no-checkout", show $ _gitSource_url s]) -- Might be able to use --bare here instead to go faster.
165− (git ["log", T.unpack branch, "--max-count=1", "--format=%H"])
167− case T.strip <$> safeLast (T.lines out) of
168− Nothing -> failWith $ "Unable to parse git log: " <> out
170− putLog Debug $ "Latest commit in " <> branch <> " is " <> rev
171− sha <- nixPrefetchGit (_gitSource_url s) rev (_gitSource_fetchSubmodules s)
172− putLog Debug $ "Nix sha256 is " <> sha
174− { _thunkRev_commit = Ref.fromHexString $ T.unpack rev
175− , _thunkRev_nixSha256 = sha
178− remote :: Text = "origin"
179− branch :: Text = remote <> "/" <> untagName (_gitSource_branch s)
181− safeLast [] = Nothing
182− safeLast xs = Just $ last xs
184−-- | Get the latest revision available from the given source
185−getLatestRev :: MonadObelisk m => ThunkSource -> m ThunkRev
187− ThunkSource_GitHub s -> do
188− auth <- liftIO $ getHubAuth "github.com"
189− commitsResult <- liftIO $ executeRequestMaybe auth $ commitsWithOptionsForR
190− (_gitHubSource_owner s)
191− (_gitHubSource_repo s)
193− $ case _gitHubSource_branch s of
195− Just b -> [CommitQuerySha $ untagName b]
196− commitInfos <- either (liftIO . throwIO) return commitsResult
197− commitInfo : _ <- return $ toList commitInfos
198− let commit = commitSha commitInfo
199− putLog Debug $ "Latest commit is " <> untagName commit
200− Right archiveUri <- liftIO $ executeRequestMaybe auth $ archiveForR (_gitHubSource_owner s) (_gitHubSource_repo s) ArchiveFormatTarball $ Just $ untagName commit
201− nixSha256 <- getNixSha256ForUriUnpacked archiveUri
202− putLog Debug $ "Nix sha256 is " <> nixSha256
204− { _thunkRev_commit = commitNameToRef commit
205− , _thunkRev_nixSha256 = nixSha256
207− ThunkSource_Git s -> getLatestGitRev s
209--TODO: Pretty print these
182--TODO: Pretty print these
210data ReadThunkError
183data ReadThunkError
211 = ReadThunkError_UnrecognizedFiles
184 = ReadThunkError_UnrecognizedFiles
@@ -314,18 +287,16 @@ parseGitHubSource v = do
314 owner <- v Aeson..: "owner"
287 owner <- v Aeson..: "owner"
315 repo <- v Aeson..: "repo"
288 repo <- v Aeson..: "repo"
316 branch <- v Aeson..:! "branch"
289 branch <- v Aeson..:! "branch"
317− mPrivate <- v Aeson..:! "private"
318 pure $ GitHubSource
290 pure $ GitHubSource
319 { _gitHubSource_owner = owner
291 { _gitHubSource_owner = owner
320 , _gitHubSource_repo = repo
292 , _gitHubSource_repo = repo
321 , _gitHubSource_branch = branch
293 , _gitHubSource_branch = branch
322− , _gitHubSource_private = fromMaybe False mPrivate
325parseGitSource :: Aeson.Object -> Aeson.Parser GitSource
296parseGitSource :: Aeson.Object -> Aeson.Parser GitSource
326parseGitSource v = do
297parseGitSource v = do
327 Just url <- parseGitUri <$> v Aeson..: "url"
298 Just url <- parseGitUri <$> v Aeson..: "url"
328− branch <- v Aeson..: "branch"
299+ branch <- v Aeson..:! "branch"
329 fetchSubmodules <- v Aeson..:! "fetchSubmodules"
300 fetchSubmodules <- v Aeson..:! "fetchSubmodules"
330 pure $ GitSource
301 pure $ GitSource
331 { _gitSource_url = url
302 { _gitSource_url = url
@@ -354,20 +325,14 @@ encodeThunkPtrData (ThunkPtr rev src) = case src of
354 ThunkSource_GitHub s -> encodePretty' githubCfg $ Aeson.object $ catMaybes
325 ThunkSource_GitHub s -> encodePretty' githubCfg $ Aeson.object $ catMaybes
355 [ Just $ "owner" .= _gitHubSource_owner s
326 [ Just $ "owner" .= _gitHubSource_owner s
356 , Just $ "repo" .= _gitHubSource_repo s
327 , Just $ "repo" .= _gitHubSource_repo s
357− , case _gitHubSource_branch s of
328+ , ("branch" .=) <$> _gitHubSource_branch s
358− Just b -> Just $ "branch" .= b
360− , if _gitHubSource_private s
361− then Just $ "private" .= True
363 , Just $ "rev" .= Ref.toHexString (_thunkRev_commit rev)
329 , Just $ "rev" .= Ref.toHexString (_thunkRev_commit rev)
364 , Just $ "sha256" .= _thunkRev_nixSha256 rev
330 , Just $ "sha256" .= _thunkRev_nixSha256 rev
366 ThunkSource_Git s -> encodePretty' plainGitCfg $ Aeson.object $ catMaybes
332 ThunkSource_Git s -> encodePretty' plainGitCfg $ Aeson.object $ catMaybes
367− --TODO: Is this safe? read/show is jank for `URI`
368− [ Just $ "url" .= show (_gitSource_url s)
333+ [ Just $ "url" .= render (_gitSource_url s)
369 , Just $ "rev" .= Ref.toHexString (_thunkRev_commit rev)
334 , Just $ "rev" .= Ref.toHexString (_thunkRev_commit rev)
370− , Just $ "branch" .= _gitSource_branch s
335+ , ("branch" .=) <$> _gitSource_branch s
371 , Just $ "sha256" .= _thunkRev_nixSha256 rev
336 , Just $ "sha256" .= _thunkRev_nixSha256 rev
372 , Just $ "fetchSubmodules" .= _gitSource_fetchSubmodules s
337 , Just $ "fetchSubmodules" .= _gitSource_fetchSubmodules s
@@ -499,20 +464,6 @@ plainGitStandaloneLoaderV2 = T.unlines
499thunkFileNames :: [FilePath]
464thunkFileNames :: [FilePath]
500thunkFileNames = ["default.nix", "github.json", "git.json"]
465thunkFileNames = ["default.nix", "github.json", "git.json"]
502−-- | Look for authentication info from the 'hub' command
504− :: Text -- ^ Domain name
506−getHubAuth domain = do
507− hubConfig <- getXdgDirectory XdgConfig "hub"
508− Yaml.decodeFileEither hubConfig >>= \case
509− Left _ -> return Nothing
510− Right v -> return $ flip parseMaybe v $ \v' -> do
511− Yaml.Array domainConfigs <- v' .: domain --TODO: Determine what multiple domainConfigs means
512− [Yaml.Object domainConfig] <- return $ toList domainConfigs
513− token <- domainConfig .: "oauth_token"
514− return $ OAuth $ encodeUtf8 token
516--TODO: when checking something out, make a shallow clone
467--TODO: when checking something out, make a shallow clone
518-- | Checks a cache directory to see if there is a fresh symlink
469-- | Checks a cache directory to see if there is a fresh symlink
@@ -591,7 +542,7 @@ updateThunk p f = withSystemTempDirectory "obelisk-thunkptr-" $ \tmpDir -> do
591 p' <- copyThunkToTmp tmpDir p
542 p' <- copyThunkToTmp tmpDir p
592 unpackThunk' True p'
543 unpackThunk' True p'
593 result <- f p'
544 result <- f p'
594− updateThunkFromTmp p' "origin" -- TODO: stop hardcoding remote
545+ updateThunkFromTmp p'
595 return result
546 return result
597 copyThunkToTmp tmpDir thunkDir = readThunk thunkDir >>= \case
548 copyThunkToTmp tmpDir thunkDir = readThunk thunkDir >>= \case
@@ -602,8 +553,8 @@ updateThunk p f = withSystemTempDirectory "obelisk-thunkptr-" $ \tmpDir -> do
602 proc "cp" ["-r", "-T", thunkDir, tmpThunk]
553 proc "cp" ["-r", "-T", thunkDir, tmpThunk]
603 return tmpThunk
554 return tmpThunk
604 Right _ -> failWith $ "Thunk is not packed"
555 Right _ -> failWith $ "Thunk is not packed"
605− updateThunkFromTmp p' remote = do
556+ updateThunkFromTmp p' = do
606− packThunk' True p' remote
557+ packThunk' True p'
607 callProcessAndLogOutput (Notice, Error) $
558 callProcessAndLogOutput (Notice, Error) $
608 proc "cp" ["-r", "-T", p', p]
559 proc "cp" ["-r", "-T", p', p]
@@ -622,190 +573,316 @@ unpackThunk' noTrail thunkDir = readThunk thunkDir >>= \case
622 let (thunkParent, thunkName) = splitFileName thunkDir
573 let (thunkParent, thunkName) = splitFileName thunkDir
623 withTempDirectory thunkParent thunkName $ \tmpRepo -> do
574 withTempDirectory thunkParent thunkName $ \tmpRepo -> do
624 let obGitDir = tmpRepo </> ".git" </> "obelisk"
575 let obGitDir = tmpRepo </> ".git" </> "obelisk"
625− case _thunkPtr_source tptr of
576+ s = case _thunkPtr_source tptr of
626− ThunkSource_GitHub s -> do
577+ ThunkSource_GitHub s' -> forgetGithub False s'
627− mauth <- liftIO $ getHubAuth "github.com"
628− repoResult <- liftIO $ executeRequestMaybe mauth $ repositoryR (_gitHubSource_owner s) (_gitHubSource_repo s)
629− githubURI <- either (liftIO . throwIO) (return . repoSshUrl) repoResult >>= \case
630− Nothing -> failWith "Cannot determine clone URI for thunk source"
631− Just c -> return $ T.unpack $ getUrl c
633− withSpinner' ("Fetching thunk " <> T.pack thunkDir)
634− ( finalMsg noTrail $ const $ "Fetched thunk " <> T.pack thunkDir) $ do
635− callProcessAndLogOutput (Debug, Debug) $
636− proc "hub" ["clone", "-n", githubURI, tmpRepo]
637− --If this directory already exists then something is weird and we should fail
638− liftIO $ createDirectory obGitDir
639− callProcessAndLogOutput (Debug, Error) $
640− proc "cp" ["-r", "-T", thunkDir </> ".", obGitDir </> "orig-thunk"]
643− putLog Debug $ "Checking out " <> T.pack (show $ maybe "" untagName $ _gitHubSource_branch s)
644− callProcessAndLogOutput (Debug, Debug) $
648− , maybe [] (\n -> ["-B", T.unpack (untagName n)]) (_gitHubSource_branch s)
649− , pure $ Ref.toHexString $ _thunkRev_commit $ _thunkPtr_rev tptr
651− -- Set upstream branch
652− forM_ (_gitHubSource_branch s) $ \branch ->
653− callProcessAndLogOutput (Debug, Error) $
654− proc "hub" ["-C", tmpRepo, "branch", "-u", "origin/" <> T.unpack (untagName branch)]
655− callProcessAndLogOutput (Notice, Error) $ proc "rm" ["-r", thunkDir]
656− callProcessAndLogOutput (Notice, Error) $ proc "mv" ["-T", tmpRepo, thunkDir]
658− ThunkSource_Git s -> do
578+ ThunkSource_Git s' -> s'
659− withSpinner' ("Fetching thunk " <> T.pack thunkName)
579+ withSpinner' ("Fetching thunk " <> T.pack thunkName)
660− (finalMsg noTrail $ const $ "Fetched thunk " <> T.pack thunkName) $ do
580+ (finalMsg noTrail $ const $ "Fetched thunk " <> T.pack thunkName) $ do
661− let git = callProcessAndLogOutput (Notice, Notice) . gitProc tmpRepo
581+ let git = callProcessAndLogOutput (Notice, Notice) . gitProc tmpRepo
662− git ["clone", if _gitSource_fetchSubmodules s then "--recursive" else "", show (_gitSource_url s)]
583+ ++ ("--recursive" <$ guard (_gitSource_fetchSubmodules s))
584+ ++ [ T.unpack $ render $ _gitSource_url s ]
663− git ["reset", "--hard", Ref.toHexString $ _thunkRev_commit $ _thunkPtr_rev tptr]
585+ git ["reset", "--hard", Ref.toHexString $ _thunkRev_commit $ _thunkPtr_rev tptr]
664− when (_gitSource_fetchSubmodules s) $
586+ when (_gitSource_fetchSubmodules s) $
665− git ["submodule", "update", "--recursive", "--init"]
587+ git ["submodule", "update", "--recursive", "--init"]
666− git ["branch", "-u", "origin/" <> T.unpack (untagName $ _gitSource_branch s)]
588+ case _gitSource_branch s of
667−
589+ Just b -> git ["branch", "-u", "origin/" <> T.unpack (untagName $ b)]
668− liftIO $ createDirectory obGitDir
592+ liftIO $ createDirectory obGitDir
669− callProcessAndLogOutput (Notice, Error) $
593+ callProcessAndLogOutput (Notice, Error) $
670− proc "cp" ["-r", "-T", thunkDir </> ".", obGitDir </> "orig-thunk"]
594+ proc "cp" ["-r", "-T", thunkDir </> ".", obGitDir </> "orig-thunk"]
671− callProcessAndLogOutput (Notice, Error) $
595+ callProcessAndLogOutput (Notice, Error) $
672− proc "rm" ["-r", thunkDir]
596+ proc "rm" ["-r", thunkDir]
673− callProcessAndLogOutput (Notice, Error) $
597+ callProcessAndLogOutput (Notice, Error) $
674− proc "mv" ["-T", tmpRepo, thunkDir]
598+ proc "mv" ["-T", tmpRepo, thunkDir]
676--TODO: add force mode to pack even if changes are present
600--TODO: add force mode to pack even if changes are present
677--TODO: add a rollback mode to pack to the original thunk
601--TODO: add a rollback mode to pack to the original thunk
678−packThunk :: MonadObelisk m => FilePath -> Text -> m ()
602+packThunk :: MonadObelisk m => FilePath -> m ()
679packThunk = packThunk' False
603packThunk = packThunk' False
681−packThunk' :: MonadObelisk m => Bool -> FilePath -> Text -> m ()
605+packThunk' :: MonadObelisk m => Bool -> FilePath -> m ()
682−packThunk' noTrail thunkDir upstream = readThunk thunkDir >>= \case
606+packThunk' noTrail thunkDir = readThunk thunkDir >>= \case
683 Left err -> failWith $ T.pack $ "thunk pack: " <> show err
607 Left err -> failWith $ T.pack $ "thunk pack: " <> show err
684 Right (ThunkData_Packed _) -> failWith "pack: thunk is already packed"
608 Right (ThunkData_Packed _) -> failWith "pack: thunk is already packed"
685 Right (ThunkData_Checkout _) -> do
609 Right (ThunkData_Checkout _) -> do
686 withSpinner' ("Packing thunk " <> T.pack thunkDir)
610 withSpinner' ("Packing thunk " <> T.pack thunkDir)
687 (finalMsg noTrail $ const $ "Packed thunk " <> T.pack thunkDir) $ do
611 (finalMsg noTrail $ const $ "Packed thunk " <> T.pack thunkDir) $ do
688− thunkPtr <- getThunkPtr thunkDir upstream
612+ thunkPtr <- getThunkPtr thunkDir
689 callProcessAndLogOutput (Debug, Error) $ proc "rm" ["-rf", thunkDir]
613 callProcessAndLogOutput (Debug, Error) $ proc "rm" ["-rf", thunkDir]
690 liftIO $ createThunk thunkDir thunkPtr
614 liftIO $ createThunk thunkDir thunkPtr
692−getThunkPtr :: MonadObelisk m => FilePath -> Text -> m ThunkPtr
616+getThunkPtr :: MonadObelisk m => FilePath -> m ThunkPtr
693getThunkPtr = getThunkPtr' True
617getThunkPtr = getThunkPtr' True
695−getThunkPtr' :: MonadObelisk m => Bool -> FilePath -> Text -> m ThunkPtr
619+getThunkPtr' :: forall m. MonadObelisk m => Bool -> FilePath -> m ThunkPtr
696−getThunkPtr' checkClean thunkDir upstream = do
620+getThunkPtr' checkClean thunkDir = do
697− when checkClean $ ensureCleanGitRepo thunkDir True $
621+ when checkClean $ ensureCleanGitRepo thunkDir True $
698− "thunk pack: thunk checkout contains unsaved modifications"
622+ "thunk pack: thunk checkout contains unsaved modifications"
700− -- Check whether there are any stashes
624+ -- Check whether there are any stashes
701− stashOutput <- readGitProcess thunkDir ["stash", "list"]
625+ stashOutput <- readGitProcess thunkDir ["stash", "list"]
702− when checkClean $ case T.null stashOutput of
626+ when checkClean $ case T.null stashOutput of
703− False -> do
627+ False -> do
704− failWith $ T.unlines $
628+ failWith $ T.unlines $
705− [ "thunk pack: thunk checkout has stashes"
629+ [ "thunk pack: thunk checkout has stashes"
706− , "git stash list:"
630+ , "git stash list:"
707− ] ++ T.lines stashOutput
631+ ] ++ T.lines stashOutput
708− True -> return ()
632+ True -> return ()
710− --Check whether all local heads are pushed
634+ -- Get information on all branches and their (optional) designated upstream
711− repoHeads <- T.lines <$> readGitProcess thunkDir ["for-each-ref", "--format=%(refname:short)", "refs/heads/"]
636+ (headDump :: [Text]) <- T.lines <$> readGitProcess thunkDir
712− remotes <- T.lines <$> readGitProcess thunkDir ["remote"]
637+ [ "for-each-ref"
713− --Check that the upstream specified actually exists
638+ , "--format=%(refname:short) %(upstream:short) %(upstream:remotename)"
714− case L.find (== upstream) remotes of
639+ , "refs/heads/"
715− Nothing -> failWith $ "thunk pack: upstream " <> upstream <> " does not exist. Available upstreams are " <> T.intercalate ", " remotes
640+ ]
716− Just _ -> return ()
641+
717− -- iterate over cartesian product
642+ (headInfo :: Map Text (Maybe (Text, Text)))
643+ <- fmap Map.fromList $ forM headDump $ \line -> do
644+ (branch : restOfLine) <- pure $ T.words line
645+ mUpstream <- case restOfLine of
647+ [u, r] -> pure $ Just (u, r)
648+ (_:_) -> failWith "git for-each-ref invalid output"
649+ pure (branch, mUpstream)
651+ putLog Debug $ "branches: " <> T.pack (show headInfo)
653+ let errorMap :: Map Text ()
654+ headUpstream :: Map Text (Text, Text)
655+ (errorMap, headUpstream) = flip Map.mapEither headInfo $ \case
659+ putLog Debug $ "branches with upstream branch set: " <> T.pack (show headUpstream)
661+ -- Check that every branch has a remote equivalent
718− when checkClean $ forM_ repoHeads $ \hd -> do
662+ when checkClean $ do
719− [localRev] <- T.lines <$> readGitProcess thunkDir ["rev-parse", T.unpack hd]
663+ let untrackedBranches = Map.keys errorMap
720− forM_ remotes $ \rm -> do
664+ when (not $ L.null untrackedBranches) $ failWith $ T.unlines $
721− --TODO: The error you get if a branch isn't pushed anywhere could be made more user friendly
665+ [ "thunk pack: Certain branches in the thunk have no upstream branch \
722− [remoteMergeRev] <- fmap T.lines $ readGitProcess thunkDir
666+ \set. This means don't know to check whether all your work is \
723− ["merge-base", T.unpack localRev, "remotes/" <> T.unpack rm <> "/" <> T.unpack hd]
667+ \saved. The offending branches are:"
724− case remoteMergeRev == localRev of
668+ , ""
725− False -> failWith $ mconcat [ "thunk unpack: branch ", hd, " has not been pushed to ", rm ]
669+ , T.unwords untrackedBranches
726− True -> return ()
670+ , ""
727−
671+ , "To fix this, you probably want to do:"
674+ ((\branch -> "git push -u origin " <> branch) <$> untrackedBranches) ++
676+ , "These will push the branches to the default remote under the same \
677+ \name, and (thanks to the `-u`) remember that choice so you don't \
678+ \get this error again."
681+ -- loosely by https://stackoverflow.com/questions/7773939/show-git-ahead-and-behind-info-for-all-branches-including-remotes
682+ stats <- iforM headUpstream $ \branch (upstream, _remote) -> do
683+ (stat :: [Text]) <- T.lines <$> readGitProcess thunkDir
684+ [ "rev-list", "--left-right"
685+ , T.unpack branch <> "..." <> T.unpack upstream
687+ let ahead = length $ [ () | Just ('<', _) <- T.uncons <$> stat ]
688+ behind = length $ [ () | Just ('>', _) <- T.uncons <$> stat ]
689+ pure (upstream, (ahead, behind))
691+ -- Those branches which have commits ahead of, i.e. not on, the upstream
692+ -- branch. Purely being behind is fine.
693+ let nonGood = Map.filter ((/= 0) . fst . snd) stats
695+ when (not $ Map.null nonGood) $ failWith $ T.unlines $
696+ [ "thunk pack: Certain branches in the thunk have commits not yet pushed upstream:"
699+ (flip map (Map.toList nonGood) $ \(branch, (upstream, (ahead, behind))) -> mconcat
700+ [" ", branch, " ahead: ", T.pack (show ahead), " behind: ", T.pack (show behind), " remote branch ", upstream]) ++
702+ , "Please push these upstream and try again. (Or just fetch, if they are somehow \
703+ \pushed but this repo's remote tracking branches don't know it.)"
728− --We assume it's safe to pack the thunk at this point
706+ -- We assume it's safe to pack the thunk at this point
707+ putLog Informational $ "All changes safe in git remotes. OK to pack thunk."
709+ -- Get current branch ``
710+ mCurrentBranch <- listToMaybe
712+ <$> readGitProcess thunkDir ["rev-parse", "--abbrev-ref", "HEAD"]
714+ let remote = maybe "origin" snd $ flip Map.lookup headUpstream =<< mCurrentBranch
729− [remoteUri'] <- fmap T.lines $ readGitProcess thunkDir ["config", "--get", "remote." <> T.unpack upstream <> ".url"]
716+ [remoteUri'] <- fmap T.lines $ readGitProcess thunkDir
719+ , "remote." <> T.unpack remote <> ".url"
722+ remoteUri <- case parseGitUri remoteUri' of
731− let uriParseFailure = "Could not identify git remote: " <> remoteUri'
723+ Nothing -> failWith $ "Could not identify git remote: " <> remoteUri'
732− mRemoteUri = parseGitUri remoteUri'
724+ Just uri -> pure uri
733− case mRemoteUri of
725+ uriThunkPtr remoteUri mCurrentBranch
734− Nothing -> failWith uriParseFailure
726+
735− Just remoteUri -> do
727+-- | Get the latest revision available from the given source
736− refs <- fmap (map (refsToTuples . T.words) . T.lines) $ readGitProcess thunkDir ["show-ref", "--head"]
728+getLatestRev :: MonadObelisk m => ThunkSource -> m ThunkRev
737− -- safe because exactly one hash is associated with HEAD
729+getLatestRev os = do
738− let Just currentHead = fst <$> L.find ((== "HEAD") . snd) refs
730+ let gitS = case os of
739− remoteHeads = Map.fromList $ catMaybes $ fmap
731+ ThunkSource_GitHub s -> forgetGithub False s
740− (\(x,y) -> (,) x <$> T.stripPrefix ("refs/remotes/" <> upstream <> "/") y)
732+ ThunkSource_Git s -> s
741− refs
733+ (_, commit) <- gitGetCommitBranch (_gitSource_url gitS) (untagName <$> _gitSource_branch gitS)
742− localHeads = Map.fromList $ catMaybes $ fmap
734+ case os of
743− (\(x,y) -> (,) x <$> T.stripPrefix "refs/heads/" y)
735+ ThunkSource_GitHub s -> githubThunkRev s commit
744− refs
736+ ThunkSource_Git s -> gitThunkRev s commit
745− currentUpstreamBranch = Map.lookup currentHead remoteHeads
737+
746− <|> Map.lookup currentHead localHeads
738+-- | Convert a URI to a thunk
747− if isGithubThunk remoteUri
739+--
748− then githubThunkPtr remoteUri currentHead currentUpstreamBranch
740+-- If the URL is a github URL, we try to just download an archive for
749− else gitThunkPtr remoteUri currentHead currentUpstreamBranch
741+-- performance. If that doesn't work (e.g. authentication issue), we fall back
750− where
742+-- on just doing things the normal way for git repos in general, and save it as
751− refsToTuples [x, y] = (x, y)
743+-- a regular git thunk.
752− refsToTuples x = error $ "thunk pack: cannot parse ref " <> show x
744+uriThunkPtr :: MonadObelisk m => URI -> Maybe Text -> m ThunkPtr
753− isGithubThunk u
745+uriThunkPtr uri mbranch = do
754− | Just uriAuth <- uriAuthority u
746+ (_, commit) <- gitGetCommitBranch uri mbranch
755− = case uriScheme u of
747+ (src, rev) <- case uriToThunkSource uri mbranch of
756− "ssh:" -> uriAuth == URIAuth "git@" "github.com" ""
748+ ThunkSource_GitHub s -> do
757− s -> s `L.elem` [ "git:", "https:", "http:" ] -- "http:" just redirects to "https:"
749+ rev <- runExceptT $ githubThunkRev s commit
758− && uriRegName uriAuth == "github.com"
750+ case rev of
759− | otherwise = False
751+ Right r -> pure (ThunkSource_GitHub s, r)
760− githubThunkPtr u commit' branch' = do
752+ Left e -> do
761− ["/", owner', repo'] <- return $ splitDirectories (uriPath u)
753+ putLog Warning $ "\
762− let owner = N (T.pack owner')
754+\Failed to fetch archive from GitHub. This is probably a private repo. \
763− dropGitExtension r = fromMaybe r $ T.stripSuffix ".git" r
755+\Falling back on normal fetchgit. Original failure:\n\n"
764− repo = N $ dropGitExtension $ T.pack repo'
756+ errorToWarning e
765− commit = N commit'
757+ let s' = forgetGithub True s
766− branch = N <$> branch'
758+ (,) (ThunkSource_Git s') <$> gitThunkRev s' commit
767− mauth <- liftIO $ getHubAuth "github.com"
759+ ThunkSource_Git s -> (,) (ThunkSource_Git s) <$> gitThunkRev s commit
768− repoResult <- liftIO $ executeRequestMaybe mauth $ repositoryR owner repo
760+ pure $ ThunkPtr
769− repoIsPrivate <- either (liftIO . throwIO) (return . repoPrivate) repoResult
761+ { _thunkPtr_rev = rev
770− archiveResult <- liftIO $ executeRequestMaybe mauth $
762+ , _thunkPtr_source = src
771− archiveForR owner repo ArchiveFormatTarball (Just (untagName commit))
763+ }
772− archiveUri <- either (liftIO . throwIO) return archiveResult
764+
773− hash <- getNixSha256ForUriUnpacked archiveUri
765+-- | N.B. Cannot infer all fields.
774− return $ ThunkPtr
766+--
775− { _thunkPtr_rev = ThunkRev
767+-- If the thunk is a GitHub thunk and fails, we do *not* fall back like with
776− { _thunkRev_commit = commitNameToRef commit
768+-- `uriThunkPtr`. Unlike a plain URL, a thunk src explicitly states which method
777− , _thunkRev_nixSha256 = hash
769+-- should be employed, and so we respect that.
778− }
770+uriToThunkSource :: URI -> Maybe Text -> ThunkSource
779− , _thunkPtr_source = ThunkSource_GitHub $ GitHubSource
771+uriToThunkSource u
780− { _gitHubSource_owner = owner
772+ | Right uriAuth <- uriAuthority u
781− , _gitHubSource_repo = repo
773+ , Just scheme <- unRText <$> uriScheme u
782− , _gitHubSource_branch = branch
774+ , case scheme of
783− , _gitHubSource_private = repoIsPrivate
775+ "ssh" -> uriAuth == Authority
784− }
776+ { authUserInfo = Just $ UserInfo (fromRight' $ mkUsername "git") Nothing
785− }
777+ , authHost = fromRight' $ mkHost "github.com"
786− gitThunkPtr u commit branch' = do
778+ , authPort = Nothing
787− let protocols = ["https:", "ssh:", "git:"]
788− unless (T.toLower (T.pack $ uriScheme u) `elem` protocols) $
789− failWith $ "thunk pack: obelisk currently only supports "
790− <> T.intercalate ", " protocols <> " protocols for non-GitHub remotes"
791− hash <- nixPrefetchGit u commit False
792− branch <- case branch' of
793− Nothing -> failWith "thunk pack: git thunks must track a branch"
794− Just b -> pure $ N b
796− { _thunkPtr_rev = ThunkRev
797− { _thunkRev_commit = commitNameToRef (N commit)
798− , _thunkRev_nixSha256 = hash
800− , _thunkPtr_source = ThunkSource_Git $ GitSource
801− { _gitSource_url = u
802− , _gitSource_branch = branch
803− , _gitSource_fetchSubmodules = False -- TODO: How do we determine if this should be true?
780+ s -> s `L.elem` [ "git", "https", "http" ] -- "http:" just redirects to "https:"
781+ && unRText (authHost uriAuth) == "github.com"
782+ , Just (_, owner :| [repoish]) <- uriPath u
783+ = \mbranch -> ThunkSource_GitHub $ GitHubSource
784+ { _gitHubSource_owner = N $ unRText owner
785+ , _gitHubSource_repo = N $ let
786+ repoish' = unRText repoish
787+ in case T.stripSuffix ".git" repoish' of
790+ , _gitHubSource_branch = N <$> mbranch
793+ | otherwise = \mbranch -> ThunkSource_Git $ GitSource
794+ { _gitSource_url = u
795+ , _gitSource_branch = N <$> mbranch
796+ , _gitSource_fetchSubmodules = False -- TODO: How do we determine if this should be true?
799+-- Funny signature indicates no effects depend on the optional branch name.
806+githubThunkRev s commit = do
807+ owner <- forcePP $ _gitHubSource_owner s
808+ repo <- forcePP $ _gitHubSource_repo s
809+ revTarball <- mkPathPiece $ commit <> ".tar.gz"
810+ let archiveUri = URI
811+ { uriScheme = Just $ fromRight' $ mkScheme "https"
812+ , uriAuthority = Right $ Authority
813+ { authUserInfo = Nothing
814+ , authHost = fromRight' $ mkHost "github.com"
815+ , authPort = Nothing
817+ , uriPath = Just ( False
818+ , owner :| [ repo, fromRight' $ mkPathPiece "archive", revTarball ]
821+ , uriFragment = Nothing
823+ hash <- getNixSha256ForUriUnpacked archiveUri
824+ putLog Debug $ "Nix sha256 is " <> hash
826+ { _thunkRev_commit = commitNameToRef $ N commit
827+ , _thunkRev_nixSha256 = hash
830+ forcePP :: Name entity -> m (RText 'PathPiece)
831+ forcePP = mkPathPiece . untagName
838+gitThunkRev s commit = do
839+ let u = _gitSource_url s
840+ protocols = ["https", "ssh", "git"]
841+ Just scheme <- pure $ unRText <$> uriScheme u
842+ unless (T.toLower scheme `elem` protocols) $
843+ failWith $ "obelisk currently only supports "
844+ <> T.intercalate ", " protocols <> " protocols for non-GitHub remotes"
845+ hash <- nixPrefetchGit u commit $ _gitSource_fetchSubmodules s
846+ putLog Informational $ "Nix sha256 is " <> hash
848+ { _thunkRev_commit = commitNameToRef (N commit)
849+ , _thunkRev_nixSha256 = hash
852+-- | Given the URI to a git remote, and an optional branch name, return the name
853+-- of the branch along with the hash of the commit at tip of that branch.
855+-- If the branch name is passed in, it is returned exactly as-is. If it is not
856+-- passed it, the default branch of the repo is used instead.
858+ :: MonadObelisk m => URI -> Maybe Text -> m (Text, CommitId)
859+gitGetCommitBranch uri mbranch = withExitFailMessage ("Failure for git remote " <> uriMsg) $ do
860+ bothMaps <- gitLsRemote
861+ (T.unpack $ render uri)
862+ (GitRef_Branch <$> mbranch)
863+ branch <- case mbranch of
864+ Nothing -> withExitFailMessage "Failed to find default branch" $ do
865+ b <- rethrowE $ gitLookupDefaultBranch bothMaps
866+ putLog Debug $ "Default branch for remote repo " <> uriMsg <> " is " <> b
869+ commit <- rethrowE $ gitLookupCommitForRef bothMaps (GitRef_Branch branch)
870+ putLog Informational $ "Latest commit in branch " <> branch
871+ <> " from remote repo " <> uriMsg
873+ pure (branch, commit)
875+ rethrowE = either failWith pure
807parseGitUri :: Text -> Maybe URI
878parseGitUri :: Text -> Maybe URI
808−parseGitUri x = parseURIReference (T.unpack x) <|> parseSshShorthand x
879+parseGitUri x = parseAbsoluteURI x <|> parseSshShorthand x
881+parseAbsoluteURI :: Text -> Maybe URI
882+parseAbsoluteURI uri = do
883+ parsedUri <- mkURI uri
884+ guard $ isPathAbsolute parsedUri
810parseSshShorthand :: Text -> Maybe URI
887parseSshShorthand :: Text -> Maybe URI
811parseSshShorthand uri = do
888parseSshShorthand uri = do
@@ -820,4 +897,4 @@ parseSshShorthand uri = do
820 -- This check is used to disambiguate a filepath containing a colon from shorthand
897 -- This check is used to disambiguate a filepath containing a colon from shorthand
821 guard $ isNothing (T.findIndex (=='/') authAndHostname)
898 guard $ isNothing (T.findIndex (=='/') authAndHostname)
822 && not (T.null colonAndPath)
899 && not (T.null colonAndPath)
823− parseURI $ T.unpack properUri
900+ mkURI properUri