1{-# LANGUAGE DataKinds #-}
2{-# LANGUAGE FlexibleContexts #-}
3{-# LANGUAGE LambdaCase #-}
4{-# LANGUAGE MultiParamTypeClasses #-}
5{-# LANGUAGE MultiWayIf #-}
6{-# LANGUAGE OverloadedStrings #-}
7{-# LANGUAGE PatternGuards #-}
8{-# LANGUAGE QuasiQuotes #-}
9{-# LANGUAGE ScopedTypeVariables #-}
10{-# LANGUAGE TypeApplications #-}
11module Obelisk.Command.Thunk where
13import Control.Applicative
14import Control.Exception (displayException, try)
15import Control.Lens (ifor, ifor_, (.~))
17import Control.Monad.Extra (findM)
18import Control.Monad.Catch (MonadCatch, handle)
19import Control.Monad.Except
20import Data.Aeson ((.=))
21import qualified Data.Aeson as Aeson
22import Data.Aeson.Encode.Pretty
23import qualified Data.Aeson.Types as Aeson
24import Data.Bifunctor (first)
25import qualified Data.ByteString.Lazy as LBS
26import Data.Containers.ListUtils (nubOrd)
28import Data.Either.Combinators (fromRight', rightToMaybe)
29import Data.Foldable (toList)
30import Data.Function ((&))
31import Data.Functor ((<&>))
32import Data.Git.Ref (Ref)
33import qualified Data.Git.Ref as Ref
34import qualified Data.List as L
35import Data.List.NonEmpty (NonEmpty (..), nonEmpty)
36import qualified Data.List.NonEmpty as NonEmpty
38import qualified Data.Map as Map
41import qualified Data.Set as Set
42import Data.String.Here.Interpolated (i)
43import Data.String.Here.Uninterpolated (here)
44import Data.Text (Text)
45import qualified Data.Text as T
46import Data.Text.Encoding
47import qualified Data.Text.IO as T
48import Data.Time.Clock.POSIX (posixSecondsToUTCTime)
49import Data.Traversable (for)
50import Data.Yaml (parseMaybe)
52import GitHub.Data.Name
53import Obelisk.Command.Nix
54import System.Directory
59import System.PosixCompat.Files (getSymbolicLinkStatus, modificationTime)
60import qualified Text.URI as URI
62import Obelisk.App (MonadObelisk)
64import Obelisk.Command.Utils
66--TODO: Support symlinked thunk data
68 = ThunkData_Packed ThunkSpec ThunkPtr
71 -- ^ Checked out thunk that was unpacked from this pointer
73-- | A reference to the exact data that a thunk should translate into
74data ThunkPtr = ThunkPtr
75 { _thunkPtr_rev :: ThunkRev
76 , _thunkPtr_source :: ThunkSource
78 deriving (Show, Eq, Ord)
80type NixSha256 = Text --TODO: Use a smart constructor and make this actually verify itself
82-- | A specific revision of data; it may be available from multiple sources
83data ThunkRev = ThunkRev
84 { _thunkRev_commit :: Ref Ref.SHA1
85 , _thunkRev_nixSha256 :: NixSha256
87 deriving (Show, Eq, Ord)
89-- | A location from which a thunk's data can be retrieved
91 -- | A source specialized for GitHub
92 = ThunkSource_GitHub GitHubSource
93 -- | A plain repo source
94 | ThunkSource_Git GitSource
95 deriving (Show, Eq, Ord)
97thunkSourceToGitSource :: ThunkSource -> GitSource
98thunkSourceToGitSource = \case
99 ThunkSource_GitHub s -> forgetGithub False s
100 ThunkSource_Git s -> s
102data GitHubSource = GitHubSource
103 { _gitHubSource_owner :: Name Owner
104 , _gitHubSource_repo :: Name Repo
105 , _gitHubSource_branch :: Maybe (Name Branch)
106 , _gitHubSource_private :: Bool
108 deriving (Show, Eq, Ord)
110newtype GitUri = GitUri { unGitUri :: URI.URI } deriving (Eq, Ord, Show)
112gitUriToText :: GitUri -> Text
113gitUriToText (GitUri uri)
114 | (T.toLower . URI.unRText <$> URI.uriScheme uri) == Just "file"
115 , Just (_, path) <- URI.uriPath uri
116 = "/" <> T.intercalate "/" (map URI.unRText $ NonEmpty.toList path)
117 | otherwise = URI.render uri
119data GitSource = GitSource
120 { _gitSource_url :: GitUri
121 , _gitSource_branch :: Maybe (Name Branch)
122 , _gitSource_fetchSubmodules :: Bool
123 , _gitSource_private :: Bool
125 deriving (Show, Eq, Ord)
127newtype ThunkConfig = ThunkConfig
128 { _thunkConfig_private :: Maybe Bool
131data ThunkUpdateConfig = ThunkUpdateConfig
132 { _thunkUpdateConfig_branch :: Maybe String
133 , _thunkUpdateConfig_config :: ThunkConfig
136data ThunkPackConfig = ThunkPackConfig
137 { _thunkPackConfig_force :: Bool
138 , _thunkPackConfig_config :: ThunkConfig
141-- | Convert a GitHub source to a regular Git source. Assumes no submodules.
142forgetGithub :: Bool -> GitHubSource -> GitSource
143forgetGithub useSsh s = GitSource
144 { _gitSource_url = GitUri $ URI.URI
145 { URI.uriScheme = Just $ fromRight' $ URI.mkScheme $ if useSsh then "ssh" else "https"
146 , URI.uriAuthority = Right $ URI.Authority
147 { URI.authUserInfo = URI.UserInfo (fromRight' $ URI.mkUsername "git") Nothing
149 , URI.authHost = fromRight' $ URI.mkHost "github.com"
150 , URI.authPort = Nothing
152 , URI.uriPath = Just ( False
153 , fromRight' . URI.mkPathPiece <$>
154 untagName (_gitHubSource_owner s)
155 :| [ untagName (_gitHubSource_repo s) <> ".git" ]
158 , URI.uriFragment = Nothing
160 , _gitSource_branch = _gitHubSource_branch s
161 , _gitSource_fetchSubmodules = False
162 , _gitSource_private = _gitHubSource_private s
165getThunkGitBranch :: ThunkPtr -> Maybe Text
166getThunkGitBranch (ThunkPtr _ src) = fmap untagName $ case src of
167 ThunkSource_GitHub s -> _gitHubSource_branch s
168 ThunkSource_Git s -> _gitSource_branch s
170commitNameToRef :: Name Commit -> Ref Ref.SHA1
171commitNameToRef (N c) = Ref.fromHex $ encodeUtf8 c
173-- TODO: Use spinner here.
174getNixSha256ForUriUnpacked
178getNixSha256ForUriUnpacked uri =
179 withExitFailMessage ("nix-prefetch-url: Failed to determine sha256 hash of URL " <> gitUriToText uri) $ do
180 [hash] <- fmap T.lines $ readProcessAndLogOutput (Debug, Debug) $
181 proc nixPrefetchUrlPath ["--unpack", "--type", "sha256", T.unpack $ gitUriToText uri]
184nixPrefetchGit :: MonadObelisk m => GitUri -> Text -> Bool -> m NixSha256
185nixPrefetchGit uri rev fetchSubmodules =
186 withExitFailMessage ("nix-prefetch-git: Failed to determine sha256 hash of Git repo " <> gitUriToText uri <> " at " <> rev) $ do
187 out <- readProcessAndLogStderr Debug $
188 proc nixPrefetchGitPath $ filter (/="")
189 [ "--url", T.unpack $ gitUriToText uri
190 , "--rev", T.unpack rev
191 , if fetchSubmodules then "--fetch-submodules" else ""
195 case parseMaybe (Aeson..: "sha256") =<< Aeson.decodeStrict (encodeUtf8 out) of
196 Nothing -> failWith $ "nix-prefetch-git: unrecognized output " <> out
199--TODO: Pretty print these
201 = ReadThunkError_UnrecognizedThunk
202 | ReadThunkError_UnrecognizedPaths (NonEmpty FilePath)
203 | ReadThunkError_MissingPaths (NonEmpty FilePath)
204 | ReadThunkError_UnparseablePtr FilePath String
205 | ReadThunkError_FileError IOError
206 | ReadThunkError_FileDoesNotMatch FilePath Text
207 | ReadThunkError_UnrecognizedState String
208 | ReadThunkError_AmbiguousPackedState ThunkPtr ThunkPtr
211unpackedDirName :: FilePath
214attrCacheFileName :: FilePath
215attrCacheFileName = ".attr-cache"
217-- | Specification for how a file in a thunk version works.
219 = ThunkFileSpec_Ptr (LBS.ByteString -> Either String ThunkPtr) -- ^ This file specifies 'ThunkPtr' data
220 | ThunkFileSpec_FileMatches Text -- ^ This file must match the given content exactly
221 | ThunkFileSpec_CheckoutIndicator -- ^ Existence of this directory indicates that the thunk is unpacked
222 | ThunkFileSpec_AttrCache -- ^ This directory is an attribute cache
224-- | Specification for how a set of files in a thunk version work.
225data ThunkSpec = ThunkSpec
226 { _thunkSpec_name :: !Text
227 , _thunkSpec_files :: !(Map FilePath ThunkFileSpec)
230thunkSpecTypes :: NonEmpty (NonEmpty ThunkSpec)
231thunkSpecTypes = gitThunkSpecs :| [gitHubThunkSpecs]
233-- | Attempts to match a 'ThunkSpec' to a given directory.
235 :: (MonadError ReadThunkError m, MonadIO m, MonadCatch m)
236 => ThunkSpec -- ^ 'ThunkSpec' to match against the given files/directory
237 -> FilePath -- ^ Path to directory
238 -> Set FilePath -- ^ Set of file paths relative to the given directory
240matchThunkSpecToDir thunkSpec dir dirFiles = do
241 case nonEmpty (toList $ dirFiles `Set.difference` expectedPaths) of
242 Just fs -> throwError $ ReadThunkError_UnrecognizedPaths $ (dir </>) <$> fs
244 case nonEmpty (toList $ requiredPaths `Set.difference` dirFiles) of
245 Just fs -> throwError $ ReadThunkError_MissingPaths $ (dir </>) <$> fs
247 datas <- fmap toList $ flip Map.traverseMaybeWithKey (_thunkSpec_files thunkSpec) $ \expectedPath -> \case
248 ThunkFileSpec_AttrCache -> Nothing <$ dirMayExist expectedPath
249 ThunkFileSpec_CheckoutIndicator -> liftIO (doesDirectoryExist (dir </> expectedPath)) <&> \case
251 True -> Just ThunkData_Checkout
252 ThunkFileSpec_FileMatches expectedContents -> handle (\(e :: IOError) -> throwError $ ReadThunkError_FileError e) $ do
253 actualContents <- liftIO (T.readFile $ dir </> expectedPath)
254 case T.strip expectedContents == T.strip actualContents of
256 False -> throwError $ ReadThunkError_FileDoesNotMatch (dir </> expectedPath) expectedContents
257 ThunkFileSpec_Ptr parser -> handle (\(e :: IOError) -> throwError $ ReadThunkError_FileError e) $ do
258 let path = dir </> expectedPath
259 liftIO (doesFileExist path) >>= \case
260 False -> pure Nothing
262 actualContents <- liftIO $ LBS.readFile path
263 case parser actualContents of
264 Right v -> pure $ Just (ThunkData_Packed thunkSpec v)
265 Left e -> throwError $ ReadThunkError_UnparseablePtr (dir </> expectedPath) e
267 case nonEmpty datas of
268 Nothing -> throwError ReadThunkError_UnrecognizedThunk
269 Just xs -> fold1WithM xs $ \a b -> either throwError pure (mergeThunkData a b)
271 rootPathsOnly = Set.fromList . mapMaybe takeRootDir . Map.keys
272 takeRootDir = fmap NonEmpty.head . nonEmpty . splitPath
274 expectedPaths = rootPathsOnly $ _thunkSpec_files thunkSpec
276 requiredPaths = rootPathsOnly $ Map.filter isRequiredFileSpec $ _thunkSpec_files thunkSpec
277 isRequiredFileSpec = \case
278 ThunkFileSpec_FileMatches _ -> True
281 dirMayExist expectedPath = liftIO (doesFileExist (dir </> expectedPath)) >>= \case
282 True -> throwError $ ReadThunkError_UnrecognizedPaths $ expectedPath :| []
285 -- Combine 'ThunkData' from different files, preferring "Checkout" over "Packed"
286 mergeThunkData ThunkData_Checkout ThunkData_Checkout = Right ThunkData_Checkout
287 mergeThunkData ThunkData_Checkout ThunkData_Packed{} = Left bothPackedAndUnpacked
288 mergeThunkData ThunkData_Packed{} ThunkData_Checkout = Left bothPackedAndUnpacked
289 mergeThunkData a@(ThunkData_Packed _ ptrA) (ThunkData_Packed _ ptrB) =
290 if ptrA == ptrB then Right a else Left $ ReadThunkError_AmbiguousPackedState ptrA ptrB
292 bothPackedAndUnpacked = ReadThunkError_UnrecognizedState "Both packed data and checkout present"
294 fold1WithM (x :| xs) f = foldM f x xs
298 => NonEmpty (NonEmpty ThunkSpec) -> FilePath -> m (Either ReadThunkError ThunkData)
299readThunkWith specTypes dir = do
300 dirFiles <- Set.fromList <$> liftIO (listDirectory dir)
301 let specs = concatMap toList $ toList $ NonEmpty.transpose specTypes -- Interleave spec types so we try each one in a "fair" ordering
302 flip fix specs $ \loop -> \case
303 [] -> pure $ Left ReadThunkError_UnrecognizedThunk
304 spec:rest -> runExceptT (matchThunkSpecToDir spec dir dirFiles) >>= \case
305 Left e -> putLog Debug [i|Thunk specification ${_thunkSpec_name spec} did not match ${dir}: ${e}|] *> loop rest
306 x@(Right _) -> x <$ putLog Debug [i|Thunk specification ${_thunkSpec_name spec} matched ${dir}|]
308-- | Read a thunk and validate that it is exactly a packed thunk.
309-- If additional data is present, fail.
310readThunk :: (MonadObelisk m) => FilePath -> m (Either ReadThunkError ThunkData)
311readThunk = readThunkWith thunkSpecTypes
313parseThunkPtr :: (Aeson.Object -> Aeson.Parser ThunkSource) -> Aeson.Object -> Aeson.Parser ThunkPtr
314parseThunkPtr parseSrc v = do
315 rev <- v Aeson..: "rev"
316 sha256 <- v Aeson..: "sha256"
319 { _thunkPtr_rev = ThunkRev
320 { _thunkRev_commit = Ref.fromHexString rev
321 , _thunkRev_nixSha256 = sha256
323 , _thunkPtr_source = src
326parseGitHubSource :: Aeson.Object -> Aeson.Parser GitHubSource
327parseGitHubSource v = do
328 owner <- v Aeson..: "owner"
329 repo <- v Aeson..: "repo"
330 branch <- v Aeson..:! "branch"
331 private <- v Aeson..:? "private"
333 { _gitHubSource_owner = owner
334 , _gitHubSource_repo = repo
335 , _gitHubSource_branch = branch
336 , _gitHubSource_private = fromMaybe False private
339parseGitSource :: Aeson.Object -> Aeson.Parser GitSource
341 Just url <- parseGitUri <$> v Aeson..: "url"
342 branch <- v Aeson..:! "branch"
343 fetchSubmodules <- v Aeson..:! "fetchSubmodules"
344 private <- v Aeson..:? "private"
346 { _gitSource_url = url
347 , _gitSource_branch = branch
348 , _gitSource_fetchSubmodules = fromMaybe False fetchSubmodules
349 , _gitSource_private = fromMaybe False private
352overwriteThunk :: MonadObelisk m => FilePath -> ThunkPtr -> m ()
353overwriteThunk target thunk = do
354 -- Ensure that this directory is a valid thunk (i.e. so we aren't losing any data)
355 readThunk target >>= \case
356 Left e -> failWith [i|Invalid thunk at ${target}: ${e}|]
359 --TODO: Is there a safer way to do this overwriting?
360 liftIO $ removePathForcibly target
361 createThunk target $ Right thunk
363thunkPtrToSpec :: ThunkPtr -> ThunkSpec
364thunkPtrToSpec thunk = case _thunkPtr_source thunk of
365 ThunkSource_GitHub _ -> NonEmpty.head gitHubThunkSpecs
366 ThunkSource_Git _ -> NonEmpty.head gitThunkSpecs
368-- It's important that formatting be very consistent here, because
369-- otherwise when people update thunks, their patches will be messy
370encodeThunkPtrData :: ThunkPtr -> LBS.ByteString
371encodeThunkPtrData (ThunkPtr rev src) = case src of
372 ThunkSource_GitHub s -> encodePretty' githubCfg $ Aeson.object $ catMaybes
373 [ Just $ "owner" .= _gitHubSource_owner s
374 , Just $ "repo" .= _gitHubSource_repo s
375 , ("branch" .=) <$> _gitHubSource_branch s
376 , Just $ "rev" .= Ref.toHexString (_thunkRev_commit rev)
377 , Just $ "sha256" .= _thunkRev_nixSha256 rev
378 , Just $ "private" .= _gitHubSource_private s
380 ThunkSource_Git s -> encodePretty' plainGitCfg $ Aeson.object $ catMaybes
381 [ Just $ "url" .= gitUriToText (_gitSource_url s)
382 , Just $ "rev" .= Ref.toHexString (_thunkRev_commit rev)
383 , ("branch" .=) <$> _gitSource_branch s
384 , Just $ "sha256" .= _thunkRev_nixSha256 rev
385 , Just $ "fetchSubmodules" .= _gitSource_fetchSubmodules s
386 , Just $ "private" .= _gitSource_private s
389 githubCfg = defConfig
390 { confIndent = Spaces 2
391 , confCompare = keyOrder
399 , confTrailingNewline = True
401 plainGitCfg = defConfig
402 { confIndent = Spaces 2
403 , confCompare = keyOrder
410 , confTrailingNewline = True
413createThunk :: MonadObelisk m => FilePath -> Either ThunkSpec ThunkPtr -> m ()
414createThunk target ptrInfo =
415 ifor_ (_thunkSpec_files spec) $ \path -> \case
416 ThunkFileSpec_FileMatches content -> withReadyPath path $ \p -> liftIO $ T.writeFile p content
417 ThunkFileSpec_Ptr _ -> case ptrInfo of
418 Left _ -> pure () -- We can't write the ptr without it
419 Right ptr -> withReadyPath path $ \p -> liftIO $ LBS.writeFile p (encodeThunkPtrData ptr)
422 spec = either id thunkPtrToSpec ptrInfo
423 withReadyPath path f = do
424 let fullPath = target </> path
425 putLog Debug $ "Writing thunk file " <> T.pack fullPath
426 liftIO $ createDirectoryIfMissing True $ takeDirectory fullPath
429createThunkWithLatest :: MonadObelisk m => FilePath -> ThunkSource -> m ()
430createThunkWithLatest target s = do
431 rev <- getLatestRev s
432 createThunk target $ Right $ ThunkPtr
433 { _thunkPtr_source = s
434 , _thunkPtr_rev = rev
437updateThunkToLatest :: MonadObelisk m => ThunkUpdateConfig -> FilePath -> m ()
438updateThunkToLatest (ThunkUpdateConfig mBranch thunkConfig) target = spinner $ do
439 checkThunkDirectory target
440 -- check to see if thunk should be updated to a specific branch or just update it's current branch
443 (overwrite, ptr) <- readThunk target >>= \case
444 Left err -> failWith [i|Thunk update: ${err}|]
446 ThunkData_Packed _ t -> return (target, t)
447 ThunkData_Checkout -> failWith "cannot update an unpacked thunk"
448 let src = _thunkPtr_source ptr
449 rev <- getLatestRev src
450 overwriteThunk overwrite $ modifyThunkPtrByConfig thunkConfig $ ThunkPtr
451 { _thunkPtr_source = src
452 , _thunkPtr_rev = rev
454 Just branch -> readThunk target >>= \case
455 Left err -> failWith [i|Thunk update: ${err}|]
457 ThunkData_Packed _ t -> setThunk thunkConfig target (thunkSourceToGitSource $ _thunkPtr_source t) branch
458 ThunkData_Checkout -> failWith [i|Thunk located at ${target} is unpacked. Use 'ob thunk pack' on the desired directory and then try 'ob thunk update' again.|]
460 spinner = withSpinner' ("Updating thunk " <> T.pack target <> " to latest") (pure $ const $ "Thunk " <> T.pack target <> " updated to latest")
462setThunk :: MonadObelisk m => ThunkConfig -> FilePath -> GitSource -> String -> m ()
463setThunk thunkConfig target gs branch = do
464 newThunkPtr <- uriThunkPtr (_gitSource_url gs) (_thunkConfig_private thunkConfig) (Just $ T.pack branch) Nothing
465 overwriteThunk target newThunkPtr
466 updateThunkToLatest (ThunkUpdateConfig Nothing thunkConfig) target
468-- | All recognized github standalone loaders, ordered from newest to oldest.
469-- This tool will only ever produce the newest one when it writes a thunk.
470gitHubThunkSpecs :: NonEmpty ThunkSpec
479gitHubThunkSpecV1 :: ThunkSpec
480gitHubThunkSpecV1 = legacyGitHubThunkSpec "github-v1"
481 "import ((import <nixpkgs> {}).fetchFromGitHub (builtins.fromJSON (builtins.readFile ./github.json)))"
483gitHubThunkSpecV2 :: ThunkSpec
484gitHubThunkSpecV2 = legacyGitHubThunkSpec "github-v2" $ T.unlines
485 [ "# DO NOT HAND-EDIT THIS FILE" --TODO: Add something about how to get more info on Obelisk, etc.
486 , "import ((import <nixpkgs> {}).fetchFromGitHub ("
487 , " let json = builtins.fromJSON (builtins.readFile ./github.json);"
488 , " in { inherit (json) owner repo rev sha256;"
489 , " private = json.private or false;"
494gitHubThunkSpecV3 :: ThunkSpec
495gitHubThunkSpecV3 = legacyGitHubThunkSpec "github-v3" $ T.unlines
496 [ "# DO NOT HAND-EDIT THIS FILE"
498 , " fetch = { private ? false, ... }@args: if private && builtins.hasAttr \"fetchGit\" builtins"
499 , " then fetchFromGitHubPrivate args"
500 , " else (import <nixpkgs> {}).fetchFromGitHub (builtins.removeAttrs args [\"branch\"]);"
501 , " fetchFromGitHubPrivate ="
502 , " { owner, repo, rev, branch ? null, name ? null, sha256 ? null, private ? false"
503 , " , fetchSubmodules ? false, githubBase ? \"github.com\", ..."
504 , " }: assert !fetchSubmodules;"
505 , " builtins.fetchGit ({"
506 , " url = \"ssh://git@${githubBase}/${owner}/${repo}.git\";"
509 , " // (if branch == null then {} else { ref = branch; })"
510 , " // (if name == null then {} else { inherit name; }));"
511 , "in import (fetch (builtins.fromJSON (builtins.readFile ./github.json)))"
514gitHubThunkSpecV4 :: ThunkSpec
515gitHubThunkSpecV4 = legacyGitHubThunkSpec "github-v4" $ T.unlines
516 [ "# DO NOT HAND-EDIT THIS FILE"
517 , "let fetch = { private ? false, fetchSubmodules ? false, owner, repo, rev, sha256, ... }:"
518 , " if !fetchSubmodules && !private then builtins.fetchTarball {"
519 , " url = \"https://github.com/${owner}/${repo}/archive/${rev}.tar.gz\"; inherit sha256;"
520 , " } else (import <nixpkgs> {}).fetchFromGitHub {"
521 , " inherit owner repo rev sha256 fetchSubmodules private;"
523 , "in import (fetch (builtins.fromJSON (builtins.readFile ./github.json)))"
526legacyGitHubThunkSpec :: Text -> Text -> ThunkSpec
527legacyGitHubThunkSpec name loader = ThunkSpec name $ Map.fromList
528 [ ("default.nix", ThunkFileSpec_FileMatches $ T.strip loader)
529 , ("github.json" , ThunkFileSpec_Ptr parseGitHubJsonBytes)
530 , (attrCacheFileName, ThunkFileSpec_AttrCache)
531 , (".git", ThunkFileSpec_CheckoutIndicator)
534gitHubThunkSpecV5 :: ThunkSpec
535gitHubThunkSpecV5 = mkThunkSpec "github-v5" "github.json" parseGitHubJsonBytes [here|
536# DO NOT HAND-EDIT THIS FILE
537let fetch = { private ? false, fetchSubmodules ? false, owner, repo, rev, sha256, ... }:
538 if !fetchSubmodules && !private then builtins.fetchTarball {
539 url = "https://github.com/${owner}/${repo}/archive/${rev}.tar.gz"; inherit sha256;
540 } else (import <nixpkgs> {}).fetchFromGitHub {
541 inherit owner repo rev sha256 fetchSubmodules private;
543 json = builtins.fromJSON (builtins.readFile ./github.json);
547parseGitHubJsonBytes :: LBS.ByteString -> Either String ThunkPtr
548parseGitHubJsonBytes = parseJsonObject $ parseThunkPtr $ \v ->
549 ThunkSource_GitHub <$> parseGitHubSource v <|> ThunkSource_Git <$> parseGitSource v
551gitThunkSpecs :: NonEmpty ThunkSpec
560gitThunkSpecV1 :: ThunkSpec
561gitThunkSpecV1 = legacyGitThunkSpec "git-v1" $ T.unlines
562 [ "# DO NOT HAND-EDIT THIS FILE"
563 , "let fetchGit = {url, rev, ref ? null, branch ? null, sha256 ? null, fetchSubmodules ? null}:"
564 , " assert !fetchSubmodules; (import <nixpkgs> {}).fetchgit { inherit url rev sha256; };"
565 , "in import (fetchGit (builtins.fromJSON (builtins.readFile ./git.json)))"
568gitThunkSpecV2 :: ThunkSpec
569gitThunkSpecV2 = legacyGitThunkSpec "git-v2" $ T.unlines
570 [ "# DO NOT HAND-EDIT THIS FILE"
571 , "let fetchGit = {url, rev, ref ? null, branch ? null, sha256 ? null, fetchSubmodules ? null}:"
572 , " if builtins.hasAttr \"fetchGit\" builtins"
573 , " then builtins.fetchGit ({ inherit url rev; } // (if branch == null then {} else { ref = branch; }))"
574 , " else abort \"Plain Git repositories are only supported on nix 2.0 or higher.\";"
575 , "in import (fetchGit (builtins.fromJSON (builtins.readFile ./git.json)))"
578-- This loader has a bug because @builtins.fetchGit@ is not given a @ref@
579-- and will fail to find commits without this because it does shallow clones.
580gitThunkSpecV3 :: ThunkSpec
581gitThunkSpecV3 = legacyGitThunkSpec "git-v3" $ T.unlines
582 [ "# DO NOT HAND-EDIT THIS FILE"
583 , "let fetch = {url, rev, ref ? null, sha256 ? null, fetchSubmodules ? false, private ? false, ...}:"
584 , " let realUrl = let firstChar = builtins.substring 0 1 url; in"
585 , " if firstChar == \"/\" then /. + url"
586 , " else if firstChar == \".\" then ./. + url"
588 , " in if !fetchSubmodules && private then builtins.fetchGit {"
589 , " url = realUrl; inherit rev;"
590 , " } else (import <nixpkgs> {}).fetchgit {"
591 , " url = realUrl; inherit rev sha256;"
593 , "in import (fetch (builtins.fromJSON (builtins.readFile ./git.json)))"
596gitThunkSpecV4 :: ThunkSpec
597gitThunkSpecV4 = legacyGitThunkSpec "git-v4" $ T.unlines
598 [ "# DO NOT HAND-EDIT THIS FILE"
599 , "let fetch = {url, rev, branch ? null, sha256 ? null, fetchSubmodules ? false, private ? false, ...}:"
600 , " let realUrl = let firstChar = builtins.substring 0 1 url; in"
601 , " if firstChar == \"/\" then /. + url"
602 , " else if firstChar == \".\" then ./. + url"
604 , " in if !fetchSubmodules && private then builtins.fetchGit {"
605 , " url = realUrl; inherit rev;"
606 , " ${if branch == null then null else \"ref\"} = branch;"
607 , " } else (import <nixpkgs> {}).fetchgit {"
608 , " url = realUrl; inherit rev sha256;"
610 , "in import (fetch (builtins.fromJSON (builtins.readFile ./git.json)))"
613legacyGitThunkSpec :: Text -> Text -> ThunkSpec
614legacyGitThunkSpec name loader = ThunkSpec name $ Map.fromList
615 [ ("default.nix", ThunkFileSpec_FileMatches $ T.strip loader)
616 , ("git.json" , ThunkFileSpec_Ptr parseGitJsonBytes)
617 , (attrCacheFileName, ThunkFileSpec_AttrCache)
618 , (".git", ThunkFileSpec_CheckoutIndicator)
621gitThunkSpecV5 :: ThunkSpec
622gitThunkSpecV5 = mkThunkSpec "git-v5" "git.json" parseGitJsonBytes [here|
623# DO NOT HAND-EDIT THIS FILE
624let fetch = {url, rev, branch ? null, sha256 ? null, fetchSubmodules ? false, private ? false, ...}:
625 let realUrl = let firstChar = builtins.substring 0 1 url; in
626 if firstChar == "/" then /. + url
627 else if firstChar == "." then ./. + url
629 in if !fetchSubmodules && private then builtins.fetchGit {
630 url = realUrl; inherit rev;
631 ${if branch == null then null else "ref"} = branch;
632 } else (import <nixpkgs> {}).fetchgit {
633 url = realUrl; inherit rev sha256;
635 json = builtins.fromJSON (builtins.readFile ./git.json);
639parseGitJsonBytes :: LBS.ByteString -> Either String ThunkPtr
640parseGitJsonBytes = parseJsonObject $ parseThunkPtr $ fmap ThunkSource_Git . parseGitSource
642mkThunkSpec :: Text -> FilePath -> (LBS.ByteString -> Either String ThunkPtr) -> Text -> ThunkSpec
643mkThunkSpec name jsonFileName parser srcNix = ThunkSpec name $ Map.fromList
644 [ ("default.nix", ThunkFileSpec_FileMatches defaultNixViaSrc)
645 , ("thunk.nix", ThunkFileSpec_FileMatches srcNix)
646 , (jsonFileName, ThunkFileSpec_Ptr parser)
647 , (attrCacheFileName, ThunkFileSpec_AttrCache)
648 , (normalise $ unpackedDirName </> ".git", ThunkFileSpec_CheckoutIndicator)
651 defaultNixViaSrc = [here|
652# DO NOT HAND-EDIT THIS FILE
653import (import ./thunk.nix)
657parseJsonObject :: (Aeson.Object -> Aeson.Parser a) -> LBS.ByteString -> Either String a
658parseJsonObject p bytes = Aeson.parseEither p =<< Aeson.eitherDecode bytes
660-- | Checks a cache directory to see if there is a fresh symlink
661-- to the result of building an attribute of a thunk.
662-- If no cache hit is found, nix-build is called to build the attribute
663-- and the result is symlinked into the cache.
664nixBuildThunkAttrWithCache
668 -- ^ Path to directory containing Thunk
670 -- ^ Attribute to build
671 -> m (Maybe FilePath)
672 -- ^ Symlink to cached or built nix output
673-- WARNING: If the thunk uses an impure reference such as '<nixpkgs>'
674-- the caching mechanism will fail as it merely measures the modification
675-- time of the cache link and the expression to build.
676nixBuildThunkAttrWithCache thunkSpec thunkDir attr = do
677 latestChange <- liftIO $ do
679 getModificationTimeMaybe = fmap rightToMaybe . try @IOError . getModificationTime
680 thunkFileNames = Map.keys $ _thunkSpec_files thunkSpec
681 maximum . catMaybes <$> traverse (getModificationTimeMaybe . (thunkDir </>)) thunkFileNames
683 let cachePaths' = nonEmpty $ Map.keys $ Map.filter (\case ThunkFileSpec_AttrCache -> True; _ -> False) $
684 _thunkSpec_files thunkSpec
685 for cachePaths' $ \cachePaths ->
686 fmap NonEmpty.head $ for cachePaths $ \cacheDir -> do
688 cachePath = thunkDir </> cacheDir </> attr <.> "out"
690 | isDoesNotExistError e = pure Nothing -- expected from a cache miss
691 | otherwise = Nothing <$ putLog Error (T.pack $ displayException e)
692 cacheHit <- handle cacheErrHandler $ do
693 cacheTime <- liftIO $ posixSecondsToUTCTime . realToFrac . modificationTime <$> getSymbolicLinkStatus cachePath
694 pure $ if latestChange <= cacheTime
700 putLog Warning $ T.pack $ mconcat [thunkDir, ": ", attr, " not cached, building ..."]
701 liftIO $ createDirectoryIfMissing True (takeDirectory cachePath)
702 (cachePath <$) $ nixCmd $ NixCmd_Build $ def
703 & nixBuildConfig_outLink .~ OutLink_IndirectRoot cachePath
704 & nixCmdConfig_target .~ Target
705 { _target_path = Just thunkDir
706 , _target_attr = Just attr
707 , _target_expr = Nothing
710-- | Build a nix attribute, and cache the result if possible
714 -- ^ Path to directory containing Thunk
716 -- ^ Attribute to build
718 -- ^ Symlink to cached or built nix output
719nixBuildAttrWithCache exprPath attr = readThunk exprPath >>= \case
720 -- Only packed thunks are cached. In particular, checkouts are not.
721 Right (ThunkData_Packed spec _) ->
722 maybe build pure =<< nixBuildThunkAttrWithCache spec exprPath attr
725 build = nixCmd $ NixCmd_Build $ def
726 & nixBuildConfig_outLink .~ OutLink_None
727 & nixCmdConfig_target .~ Target
728 { _target_path = Just exprPath
729 , _target_attr = Just attr
730 , _target_expr = Nothing
733-- | Safely update thunk using a custom action
735-- A temporary working space is used to do any update. When the custom
736-- action successfully completes, the resulting (packed) thunk is copied
737-- back to the original location.
738updateThunk :: MonadObelisk m => FilePath -> (FilePath -> m a) -> m a
739updateThunk p f = withSystemTempDirectory "obelisk-thunkptr-" $ \tmpDir -> do
740 p' <- copyThunkToTmp tmpDir p
743 updateThunkFromTmp p'
746 copyThunkToTmp tmpDir thunkDir = readThunk thunkDir >>= \case
747 Left err -> failWith $ "withThunkUnpacked: " <> T.pack (show err)
748 Right ThunkData_Packed{} -> do
749 let tmpThunk = tmpDir </> "thunk"
750 callProcessAndLogOutput (Notice, Error) $
751 proc cp ["-r", "-T", thunkDir, tmpThunk]
753 Right _ -> failWith "Thunk is not packed"
754 updateThunkFromTmp p' = do
755 _ <- packThunk' True (ThunkPackConfig False (ThunkConfig Nothing)) p'
756 callProcessAndLogOutput (Notice, Error) $
757 proc cp ["-r", "-T", p', p]
759finalMsg :: Bool -> (a -> Text) -> Maybe (a -> Text)
760finalMsg noTrail s = if noTrail then Nothing else Just s
762-- | Check that we are not somewhere inside the thunk directory
763checkThunkDirectory :: MonadObelisk m => FilePath -> m ()
764checkThunkDirectory thunkDir = do
765 currentDir <- liftIO getCurrentDirectory
766 thunkDir' <- liftIO $ canonicalizePath thunkDir
767 when (thunkDir' `L.isInfixOf` currentDir) $
768 failWith [i|Can't perform thunk operations from within the thunk directory: ${thunkDir}|]
770 -- Don't let thunk commands work when directly given an unpacked repo
771 when (takeFileName thunkDir == unpackedDirName) $
772 readThunk (takeDirectory thunkDir) >>= \case
773 Right _ -> failWith [i|Refusing to perform thunk operation on ${thunkDir} because it is a thunk's unpacked source|]
776unpackThunk :: MonadObelisk m => FilePath -> m ()
777unpackThunk = unpackThunk' False
779unpackThunk' :: MonadObelisk m => Bool -> FilePath -> m ()
780unpackThunk' noTrail thunkDir = checkThunkDirectory thunkDir *> readThunk thunkDir >>= \case
781 Left err -> failWith [i|Invalid thunk at ${thunkDir}: ${err}|]
782 --TODO: Overwrite option that rechecks out thunk; force option to do so even if working directory is dirty
783 Right ThunkData_Checkout -> failWith [i|Thunk at ${thunkDir} is already unpacked|]
784 Right (ThunkData_Packed _ tptr) -> do
785 let (thunkParent, thunkName) = splitFileName thunkDir
786 withTempDirectory thunkParent thunkName $ \tmpThunk -> do
788 gitSrc = thunkSourceToGitSource $ _thunkPtr_source tptr
789 newSpec = case _thunkPtr_source tptr of
790 ThunkSource_GitHub _ -> NonEmpty.head gitHubThunkSpecs
791 ThunkSource_Git _ -> NonEmpty.head gitThunkSpecs
792 withSpinner' ("Fetching thunk " <> T.pack thunkName)
793 (finalMsg noTrail $ const $ "Fetched thunk " <> T.pack thunkName) $ do
794 let unpackedPath = tmpThunk </> unpackedDirName
795 gitCloneForThunkUnpack gitSrc (_thunkRev_commit $ _thunkPtr_rev tptr) unpackedPath
797 let normalizeMore = dropTrailingPathSeparator . normalise
798 when (normalizeMore unpackedPath /= normalizeMore tmpThunk) $ -- Only write meta data if the checkout is not inplace
799 createThunk tmpThunk $ Left newSpec
802 removePathForcibly thunkDir
803 renameDirectory tmpThunk thunkDir
805gitCloneForThunkUnpack
807 => GitSource -- ^ Git source to use
808 -> Ref hash -- ^ Commit hash to reset to
809 -> FilePath -- ^ Directory to clone into
811gitCloneForThunkUnpack gitSrc commit dir = do
812 let git = callProcessAndLogOutput (Notice, Notice) . gitProc dir
814 ++ ["--recursive" | _gitSource_fetchSubmodules gitSrc]
815 ++ [ T.unpack $ gitUriToText $ _gitSource_url gitSrc ]
816 ++ do branch <- maybeToList $ _gitSource_branch gitSrc
817 [ "--branch", T.unpack $ untagName branch ]
818 git ["reset", "--hard", Ref.toHexString commit]
819 when (_gitSource_fetchSubmodules gitSrc) $
820 git ["submodule", "update", "--recursive", "--init"]
822--TODO: add a rollback mode to pack to the original thunk
823packThunk :: MonadObelisk m => ThunkPackConfig -> FilePath -> m ThunkPtr
824packThunk = packThunk' False
826packThunk' :: MonadObelisk m => Bool -> ThunkPackConfig -> FilePath -> m ThunkPtr
827packThunk' noTrail (ThunkPackConfig force thunkConfig) thunkDir = checkThunkDirectory thunkDir *> readThunk thunkDir >>= \case
828 Right ThunkData_Packed{} -> failWith [i|Thunk at ${thunkDir} is is already packed|]
830 ("Packing thunk " <> T.pack thunkDir)
831 (finalMsg noTrail $ const $ "Packed thunk " <> T.pack thunkDir) $
833 let checkClean = if force then CheckClean_NoCheck else CheckClean_FullCheck
834 thunkPtr <- modifyThunkPtrByConfig thunkConfig <$> getThunkPtr checkClean thunkDir (_thunkConfig_private thunkConfig)
835 liftIO $ removePathForcibly thunkDir
836 createThunk thunkDir $ Right thunkPtr
839modifyThunkPtrByConfig :: ThunkConfig -> ThunkPtr -> ThunkPtr
840modifyThunkPtrByConfig (ThunkConfig markPrivate') ptr = case markPrivate' of
842 Just markPrivate -> ptr { _thunkPtr_source = case _thunkPtr_source ptr of
843 ThunkSource_Git s -> ThunkSource_Git $ s { _gitSource_private = markPrivate }
844 ThunkSource_GitHub s -> ThunkSource_GitHub $ s { _gitHubSource_private = markPrivate }
848 = CheckClean_FullCheck
849 -- ^ Check that the repo is clean, including .gitignored files
850 | CheckClean_NotIgnored
851 -- ^ Check that the repo is clean, not including .gitignored files
853 -- ^ Don't check that the repo is clean
855getThunkPtr :: forall m. MonadObelisk m => CheckClean -> FilePath -> Maybe Bool -> m ThunkPtr
856getThunkPtr gitCheckClean dir mPrivate = do
857 let repoLocations = nubOrd $ map (first normalise)
858 [(".git", "."), (unpackedDirName </> ".git", unpackedDirName)]
859 repoLocation' <- liftIO $ flip findM repoLocations $ doesDirectoryExist . (dir </>) . fst
860 thunkDir <- case repoLocation' of
861 Nothing -> failWith [i|Can't find an unpacked thunk in ${dir}|]
862 Just (_, path) -> pure $ normalise $ dir </> path
864 let (checkClean, checkIgnored) = case gitCheckClean of
865 CheckClean_FullCheck -> (True, True)
866 CheckClean_NotIgnored -> (True, False)
867 CheckClean_NoCheck -> (False, False)
868 when checkClean $ ensureCleanGitRepo thunkDir checkIgnored
869 "thunk pack: thunk checkout contains unsaved modifications"
871 -- Check whether there are any stashes
873 stashOutput <- readGitProcess thunkDir ["stash", "list"]
874 unless (T.null stashOutput) $
875 failWith $ T.unlines $
876 [ "thunk pack: thunk checkout has stashes"
878 ] ++ T.lines stashOutput
880 -- Get current branch
881 (mCurrentBranch, mCurrentCommit) <- do
882 b <- listToMaybe . T.lines <$> readGitProcess thunkDir ["rev-parse", "--abbrev-ref", "HEAD"]
883 c <- listToMaybe . T.lines <$> readGitProcess thunkDir ["rev-parse", "HEAD"]
885 (Just "HEAD") -> failWith $ T.unlines
886 [ "thunk pack: You are in 'detached HEAD' state."
887 , "If you want to pack at the current ref \
888 \then please create a new branch with 'git checkout -b <new-branch-name>' and push this upstream."
892 -- Get information on all branches and their (optional) designated upstream
894 headDump :: [Text] <- T.lines <$> readGitProcess thunkDir
896 , "--format=%(refname:short) %(upstream:short) %(upstream:remotename)"
900 (headInfo :: Map Text (Maybe (Text, Text)))
901 <- fmap Map.fromList $ forM headDump $ \line -> do
902 (branch : restOfLine) <- pure $ T.words line
903 mUpstream <- case restOfLine of
905 [u, r] -> pure $ Just (u, r)
906 (_:_) -> failWith "git for-each-ref invalid output"
907 pure (branch, mUpstream)
909 putLog Debug $ "branches: " <> T.pack (show headInfo)
911 let errorMap :: Map Text ()
912 headUpstream :: Map Text (Text, Text)
913 (errorMap, headUpstream) = flip Map.mapEither headInfo $ \case
917 putLog Debug $ "branches with upstream branch set: " <> T.pack (show headUpstream)
919 -- Check that every branch has a remote equivalent
921 let untrackedBranches = Map.keys errorMap
922 when (not $ L.null untrackedBranches) $ failWith $ T.unlines $
923 [ "thunk pack: Certain branches in the thunk have no upstream branch \
924 \set. This means we don't know to check whether all your work is \
925 \saved. The offending branches are:"
927 , T.unwords untrackedBranches
929 , "To fix this, you probably want to do:"
932 ((\branch -> "git push -u origin " <> branch) <$> untrackedBranches) ++
934 , "These will push the branches to the default remote under the same \
935 \name, and (thanks to the `-u`) remember that choice so you don't \
936 \get this error again."
939 -- loosely by https://stackoverflow.com/questions/7773939/show-git-ahead-and-behind-info-for-all-branches-including-remotes
940 stats <- ifor headUpstream $ \branch (upstream, _remote) -> do
941 (stat :: [Text]) <- T.lines <$> readGitProcess thunkDir
942 [ "rev-list", "--left-right"
943 , T.unpack branch <> "..." <> T.unpack upstream
945 let ahead = length $ [ () | Just ('<', _) <- T.uncons <$> stat ]
946 behind = length $ [ () | Just ('>', _) <- T.uncons <$> stat ]
947 pure (upstream, (ahead, behind))
949 -- Those branches which have commits ahead of, i.e. not on, the upstream
950 -- branch. Purely being behind is fine.
951 let nonGood = Map.filter ((/= 0) . fst . snd) stats
953 when (not $ Map.null nonGood) $ failWith $ T.unlines $
954 [ "thunk pack: Certain branches in the thunk have commits not yet pushed upstream:"
957 flip map (Map.toList nonGood) (\(branch, (upstream, (ahead, behind))) -> mconcat
958 [" ", branch, " ahead: ", T.pack (show ahead), " behind: ", T.pack (show behind), " remote branch ", upstream]) ++
960 , "Please push these upstream and try again. (Or just fetch, if they are somehow \
961 \pushed but this repo's remote tracking branches don't know it.)"
965 -- We assume it's safe to pack the thunk at this point
966 putLog Informational "All changes safe in git remotes. OK to pack thunk."
968 let remote = maybe "origin" snd $ flip Map.lookup headUpstream =<< mCurrentBranch
970 [remoteUri'] <- fmap T.lines $ readGitProcess thunkDir
973 , "remote." <> T.unpack remote <> ".url"
976 remoteUri <- case parseGitUri remoteUri' of
977 Nothing -> failWith $ "Could not identify git remote: " <> remoteUri'
979 uriThunkPtr remoteUri mPrivate mCurrentBranch mCurrentCommit
981-- | Get the latest revision available from the given source
982getLatestRev :: MonadObelisk m => ThunkSource -> m ThunkRev
984 let gitS = thunkSourceToGitSource os
985 (_, commit) <- gitGetCommitBranch (_gitSource_url gitS) (untagName <$> _gitSource_branch gitS)
987 ThunkSource_GitHub s -> githubThunkRev s commit
988 ThunkSource_Git s -> gitThunkRev s commit
990-- | Convert a URI to a thunk
992-- If the URL is a github URL, we try to just download an archive for
993-- performance. If that doesn't work (e.g. authentication issue), we fall back
994-- on just doing things the normal way for git repos in general, and save it as
995-- a regular git thunk.
996uriThunkPtr :: MonadObelisk m => GitUri -> Maybe Bool -> Maybe Text -> Maybe Text -> m ThunkPtr
997uriThunkPtr uri mPrivate mbranch mcommit = do
998 commit <- case mcommit of
999 Nothing -> gitGetCommitBranch uri mbranch >>= return . snd
1000 (Just c) -> return c
1001 (src, rev) <- uriToThunkSource uri mPrivate mbranch >>= \case
1002 ThunkSource_GitHub s -> do
1003 rev <- runExceptT $ githubThunkRev s commit
1005 Right r -> pure (ThunkSource_GitHub s, r)
1008\Failed to fetch archive from GitHub. This is probably a private repo. \
1009\Falling back on normal fetchgit. Original failure:"
1011 let s' = forgetGithub True s
1012 (,) (ThunkSource_Git s') <$> gitThunkRev s' commit
1013 ThunkSource_Git s -> (,) (ThunkSource_Git s) <$> gitThunkRev s commit
1015 { _thunkPtr_rev = rev
1016 , _thunkPtr_source = src
1019-- | N.B. Cannot infer all fields.
1021-- If the thunk is a GitHub thunk and fails, we do *not* fall back like with
1022-- `uriThunkPtr`. Unlike a plain URL, a thunk src explicitly states which method
1023-- should be employed, and so we respect that.
1024uriToThunkSource :: MonadObelisk m => GitUri -> Maybe Bool -> Maybe Text -> m ThunkSource
1025uriToThunkSource (GitUri u) mPrivate
1026 | Right uriAuth <- URI.uriAuthority u
1027 , Just scheme <- URI.unRText <$> URI.uriScheme u
1029 "ssh" -> uriAuth == URI.Authority
1030 { URI.authUserInfo = Just $ URI.UserInfo (fromRight' $ URI.mkUsername "git") Nothing
1031 , URI.authHost = fromRight' $ URI.mkHost "github.com"
1032 , URI.authPort = Nothing
1034 s -> s `L.elem` [ "git", "https", "http" ] -- "http:" just redirects to "https:"
1035 && URI.unRText (URI.authHost uriAuth) == "github.com"
1036 , Just (_, owner :| [repoish]) <- URI.uriPath u
1038 isPrivate <- getIsPrivate
1039 pure $ ThunkSource_GitHub $ GitHubSource
1040 { _gitHubSource_owner = N $ URI.unRText owner
1041 , _gitHubSource_repo = N $ let
1042 repoish' = URI.unRText repoish
1043 in fromMaybe repoish' $ T.stripSuffix ".git" repoish'
1044 , _gitHubSource_branch = N <$> mbranch
1045 , _gitHubSource_private = isPrivate
1048 | otherwise = \mbranch -> do
1049 isPrivate <- getIsPrivate
1050 pure $ ThunkSource_Git $ GitSource
1051 { _gitSource_url = GitUri u
1052 , _gitSource_branch = N <$> mbranch
1053 , _gitSource_fetchSubmodules = False -- TODO: How do we determine if this should be true?
1054 , _gitSource_private = isPrivate
1057 getIsPrivate = maybe (guessGitRepoIsPrivate $ GitUri u) pure mPrivate
1059guessGitRepoIsPrivate :: MonadObelisk m => GitUri -> m Bool
1060guessGitRepoIsPrivate uri = flip fix urisToTry $ \loop -> \case
1063 result <- readCreateProcessWithExitCode $
1070 , T.unpack $ gitUriToText uriAttempt
1073 (ExitSuccess, _, _) -> pure False -- Must be a public repo
1076 urisToTry = nubOrd $
1077 -- Include the original URI if it isn't using SSH because SSH will certainly fail.
1078 [uri | fmap URI.unRText (URI.uriScheme (unGitUri uri)) /= Just "ssh"] <>
1079 [changeScheme "https" uri, changeScheme "http" uri]
1080 changeScheme scheme (GitUri u) = GitUri $ u
1081 { URI.uriScheme = URI.mkScheme scheme
1082 , URI.uriAuthority = (\x -> x { URI.authUserInfo = Nothing }) <$> URI.uriAuthority u
1085-- Funny signature indicates no effects depend on the optional branch name.
1092githubThunkRev s commit = do
1093 owner <- forcePP $ _gitHubSource_owner s
1094 repo <- forcePP $ _gitHubSource_repo s
1095 revTarball <- URI.mkPathPiece $ commit <> ".tar.gz"
1096 let archiveUri = GitUri $ URI.URI
1097 { URI.uriScheme = Just $ fromRight' $ URI.mkScheme "https"
1098 , URI.uriAuthority = Right $ URI.Authority
1099 { URI.authUserInfo = Nothing
1100 , URI.authHost = fromRight' $ URI.mkHost "github.com"
1101 , URI.authPort = Nothing
1103 , URI.uriPath = Just ( False
1104 , owner :| [ repo, fromRight' $ URI.mkPathPiece "archive", revTarball ]
1107 , URI.uriFragment = Nothing
1109 hash <- getNixSha256ForUriUnpacked archiveUri
1110 putLog Debug $ "Nix sha256 is " <> hash
1112 { _thunkRev_commit = commitNameToRef $ N commit
1113 , _thunkRev_nixSha256 = hash
1116 forcePP :: Name entity -> m (URI.RText 'URI.PathPiece)
1117 forcePP = URI.mkPathPiece . untagName
1124gitThunkRev s commit = do
1125 let u = _gitSource_url s
1126 protocols = ["file", "https", "ssh", "git"]
1127 scheme = maybe "file" URI.unRText $ URI.uriScheme $ (\(GitUri x) -> x) u
1128 unless (T.toLower scheme `elem` protocols) $
1129 failWith $ "obelisk currently only supports "
1130 <> T.intercalate ", " protocols <> " protocols for plain Git remotes"
1131 hash <- nixPrefetchGit u commit $ _gitSource_fetchSubmodules s
1132 putLog Informational $ "Nix sha256 is " <> hash
1134 { _thunkRev_commit = commitNameToRef (N commit)
1135 , _thunkRev_nixSha256 = hash
1138-- | Given the URI to a git remote, and an optional branch name, return the name
1139-- of the branch along with the hash of the commit at tip of that branch.
1141-- If the branch name is passed in, it is returned exactly as-is. If it is not
1142-- passed it, the default branch of the repo is used instead.
1145 :: MonadObelisk m => GitUri -> Maybe Text -> m (Text, CommitId)
1146gitGetCommitBranch uri mbranch = withExitFailMessage ("Failure for git remote " <> uriMsg) $ do
1147 (_, bothMaps) <- gitLsRemote
1148 (T.unpack $ gitUriToText uri)
1149 (GitRef_Branch <$> mbranch)
1151 branch <- case mbranch of
1152 Nothing -> withExitFailMessage "Failed to find default branch" $ do
1153 b <- rethrowE $ gitLookupDefaultBranch bothMaps
1154 putLog Debug $ "Default branch for remote repo " <> uriMsg <> " is " <> b
1157 commit <- rethrowE $ gitLookupCommitForRef bothMaps (GitRef_Branch branch)
1158 putLog Informational $ "Latest commit in branch " <> branch
1159 <> " from remote repo " <> uriMsg
1161 pure (branch, commit)
1163 rethrowE = either failWith pure
1164 uriMsg = gitUriToText uri
1166parseGitUri :: Text -> Maybe GitUri
1167parseGitUri x = GitUri <$> (parseFileURI x <|> parseAbsoluteURI x <|> parseSshShorthand x)
1169parseFileURI :: Text -> Maybe URI.URI
1170parseFileURI uri = if "/" `T.isPrefixOf` uri then parseAbsoluteURI ("file://" <> uri) else Nothing
1172parseAbsoluteURI :: Text -> Maybe URI.URI
1173parseAbsoluteURI uri = do
1174 parsedUri <- URI.mkURI uri
1175 guard $ URI.isPathAbsolute parsedUri
1178parseSshShorthand :: Text -> Maybe URI.URI
1179parseSshShorthand uri = do
1180 -- This is what git does to check that the remote
1181 -- is not a local file path when parsing shorthand.
1182 -- Last referenced from here:
1183 -- https://github.com/git/git/blob/95ec6b1b3393eb6e26da40c565520a8db9796e9f/connect.c#L712
1185 (authAndHostname, colonAndPath) = T.break (== ':') uri
1186 properUri = "ssh://" <> authAndHostname <> "/" <> T.drop 1 colonAndPath
1187 -- Shorthand is valid iff a colon is present and it occurs before the first slash
1188 -- This check is used to disambiguate a filepath containing a colon from shorthand
1189 guard $ isNothing (T.findIndex (=='/') authAndHostname)
1190 && not (T.null colonAndPath)
keysesc to close
- j k
- move down, up
- enter
- open
- esc
- back, or up a level
- 1 2 3 4
- readme, files, commits, refs
- /
- filter
- y
- copy the clone command
- c
- compare the chosen commit with another (or shift-click both)
- ?
- show or hide this list