@@ -5,62 +5,48 @@
55{-# LANGUAGE MultiWayIf #-}
66{-# LANGUAGE OverloadedStrings #-}
77{-# LANGUAGE PatternGuards #-}
8+{-# LANGUAGE QuasiQuotes #-}
89{-# LANGUAGE ScopedTypeVariables #-}
910{-# LANGUAGE TypeApplications #-}
10−module Obelisk.Command.Thunk
11− ( ThunkPtr (..)
12− , ThunkRev (..)
13− , ThunkSource (..)
14− , ThunkData (..)
15− , ReadThunkError (..)
16− , GitHubSource (..)
17− , getThunkGitBranch
18− , getLatestRev
19− , updateThunkToLatest
20− , createThunk
21− , createThunkWithLatest
22− , nixBuildAttrWithCache
23− , nixBuildThunkAttrWithCache
24− , unpackThunk
25− , packThunk
26− , readThunk
27− , updateThunk
28− , getThunkPtr
29− , getThunkPtr'
30− , parseGitUri
31− , uriThunkPtr
32− , setThunk
33− ) where
11+module Obelisk.Command.Thunk where
3412
3513import Control.Applicative
3614import Control.Exception (displayException, try)
37−import qualified Control.Lens as Lens
38−import Control.Lens.Indexed hiding ((<.>))
15+import Control.Lens (ifor, ifor_, (.~))
3916import Control.Monad
40−import Control.Monad.Catch (handle)
17+import Control.Monad.Extra (findM)
18+import Control.Monad.Catch (MonadCatch, handle)
4119import Control.Monad.Except
4220import Data.Aeson ((.=))
4321import qualified Data.Aeson as Aeson
4422import Data.Aeson.Encode.Pretty
4523import qualified Data.Aeson.Types as Aeson
24+import Data.Bifunctor (first)
4625import qualified Data.ByteString.Lazy as LBS
26+import Data.Containers.ListUtils (nubOrd)
4727import Data.Default
4828import Data.Either.Combinators (fromRight', rightToMaybe)
29+import Data.Foldable (toList)
30+import Data.Function ((&))
31+import Data.Functor ((<&>))
4932import Data.Git.Ref (Ref)
5033import qualified Data.Git.Ref as Ref
5134import qualified Data.List as L
52−import Data.List.NonEmpty (NonEmpty (..))
35+import Data.List.NonEmpty (NonEmpty (..), nonEmpty)
5336import qualified Data.List.NonEmpty as NonEmpty
5437import Data.Map (Map)
5538import qualified Data.Map as Map
5639import Data.Maybe
5740import Data.Set (Set)
5841import qualified Data.Set as Set
42+import Data.String.Here.Interpolated (i)
43+import Data.String.Here.Uninterpolated (here)
5944import Data.Text (Text)
6045import qualified Data.Text as T
6146import Data.Text.Encoding
6247import qualified Data.Text.IO as T
6348import Data.Time.Clock.POSIX (posixSecondsToUTCTime)
49+import Data.Traversable (for)
6450import Data.Yaml (parseMaybe)
6551import GitHub
6652import GitHub.Data.Name
@@ -70,9 +56,8 @@ import System.Exit
7056import System.FilePath
7157import System.IO.Error
7258import System.IO.Temp
73−import System.Posix (getSymbolicLinkStatus, modificationTime)
74−import System.Process (proc)
75−import Text.URI
59+import System.PosixCompat.Files (getSymbolicLinkStatus, modificationTime)
60+import qualified Text.URI as URI
7661
7762import Obelisk.App (MonadObelisk)
7863import Obelisk.CliApp
@@ -80,11 +65,10 @@ import Obelisk.Command.Utils
8065
8166--TODO: Support symlinked thunk data
8267data ThunkData
83− = ThunkData_Packed ThunkPtr
68+ = ThunkData_Packed ThunkSpec ThunkPtr
8469 -- ^ Packed thunk
85− | ThunkData_Checkout (Maybe ThunkPtr)
70+ | ThunkData_Checkout
8671 -- ^ Checked out thunk that was unpacked from this pointer
87− deriving (Show, Eq, Ord)
8872
8973-- | A reference to the exact data that a thunk should translate into
9074data ThunkPtr = ThunkPtr
@@ -97,7 +81,7 @@ type NixSha256 = Text --TODO: Use a smart constructor and make this actually ver
9781
9882-- | A specific revision of data; it may be available from multiple sources
9983data ThunkRev = ThunkRev
100− { _thunkRev_commit :: Ref
84+ { _thunkRev_commit :: Ref Ref.SHA1
10185 , _thunkRev_nixSha256 :: NixSha256
10286 }
10387 deriving (Show, Eq, Ord)
@@ -110,41 +94,72 @@ data ThunkSource
11094 | ThunkSource_Git GitSource
11195 deriving (Show, Eq, Ord)
11296
97+thunkSourceToGitSource :: ThunkSource -> GitSource
98+thunkSourceToGitSource = \case
99+ ThunkSource_GitHub s -> forgetGithub False s
100+ ThunkSource_Git s -> s
101+
113102data GitHubSource = GitHubSource
114103 { _gitHubSource_owner :: Name Owner
115104 , _gitHubSource_repo :: Name Repo
116105 , _gitHubSource_branch :: Maybe (Name Branch)
106+ , _gitHubSource_private :: Bool
117107 }
118108 deriving (Show, Eq, Ord)
119109
110+newtype GitUri = GitUri { unGitUri :: URI.URI } deriving (Eq, Ord, Show)
111+
112+gitUriToText :: GitUri -> Text
113+gitUriToText (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
118+
120119data GitSource = GitSource
121− { _gitSource_url :: URI
120+ { _gitSource_url :: GitUri
122121 , _gitSource_branch :: Maybe (Name Branch)
123122 , _gitSource_fetchSubmodules :: Bool
123+ , _gitSource_private :: Bool
124124 }
125125 deriving (Show, Eq, Ord)
126126
127+newtype ThunkConfig = ThunkConfig
128+ { _thunkConfig_private :: Maybe Bool
129+ } deriving Show
130+
131+data ThunkUpdateConfig = ThunkUpdateConfig
132+ { _thunkUpdateConfig_branch :: Maybe String
133+ , _thunkUpdateConfig_config :: ThunkConfig
134+ } deriving Show
135+
136+data ThunkPackConfig = ThunkPackConfig
137+ { _thunkPackConfig_force :: Bool
138+ , _thunkPackConfig_config :: ThunkConfig
139+ } deriving Show
140+
127141-- | Convert a GitHub source to a regular Git source. Assumes no submodules.
128142forgetGithub :: Bool -> GitHubSource -> GitSource
129143forgetGithub useSsh s = GitSource
130− { _gitSource_url = URI
131− { uriScheme = Just $ fromRight' $ mkScheme $ if useSsh then "ssh" else "https"
132− , uriAuthority = Right $ Authority
133− { authUserInfo = UserInfo (fromRight' $ mkUsername "git") Nothing
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
134148 <$ guard useSsh
135− , authHost = fromRight' $ mkHost "github.com"
136− , authPort = Nothing
149+ , URI.authHost = fromRight' $ URI.mkHost "github.com"
150+ , URI.authPort = Nothing
137151 }
138− , uriPath = Just ( False
139− , fromRight' . mkPathPiece <$>
152+ , URI.uriPath = Just ( False
153+ , fromRight' . URI.mkPathPiece <$>
140154 untagName (_gitHubSource_owner s)
141155 :| [ untagName (_gitHubSource_repo s) <> ".git" ]
142156 )
143− , uriQuery = []
144− , uriFragment = Nothing
157+ , URI.uriQuery = []
158+ , URI.uriFragment = Nothing
145159 }
146160 , _gitSource_branch = _gitHubSource_branch s
147161 , _gitSource_fetchSubmodules = False
162+ , _gitSource_private = _gitHubSource_private s
148163 }
149164
150165getThunkGitBranch :: ThunkPtr -> Maybe Text
@@ -152,26 +167,26 @@ getThunkGitBranch (ThunkPtr _ src) = fmap untagName $ case src of
152167 ThunkSource_GitHub s -> _gitHubSource_branch s
153168 ThunkSource_Git s -> _gitSource_branch s
154169
155−commitNameToRef :: Name Commit -> Ref
170+commitNameToRef :: Name Commit -> Ref Ref.SHA1
156171commitNameToRef (N c) = Ref.fromHex $ encodeUtf8 c
157172
158173-- TODO: Use spinner here.
159174getNixSha256ForUriUnpacked
160175 :: MonadObelisk m
161− => URI
176+ => GitUri
162177 -> m NixSha256
163178getNixSha256ForUriUnpacked uri =
164− withExitFailMessage ("nix-prefetch-url: Failed to determine sha256 hash of URL " <> render uri) $ do
179+ withExitFailMessage ("nix-prefetch-url: Failed to determine sha256 hash of URL " <> gitUriToText uri) $ do
165180 [hash] <- fmap T.lines $ readProcessAndLogOutput (Debug, Debug) $
166− proc "nix-prefetch-url" ["--unpack" , "--type" , "sha256" , T.unpack $ render uri]
181+ proc nixPrefetchUrlPath ["--unpack", "--type", "sha256", T.unpack $ gitUriToText uri]
167182 pure hash
168183
169−nixPrefetchGit :: MonadObelisk m => URI -> Text -> Bool -> m NixSha256
184+nixPrefetchGit :: MonadObelisk m => GitUri -> Text -> Bool -> m NixSha256
170185nixPrefetchGit uri rev fetchSubmodules =
171− withExitFailMessage ("nix-prefetch-git: Failed to determine sha256 hash of Git repo " <> render uri <> " at " <> rev) $ do
186+ withExitFailMessage ("nix-prefetch-git: Failed to determine sha256 hash of Git repo " <> gitUriToText uri <> " at " <> rev) $ do
172187 out <- readProcessAndLogStderr Debug $
173− proc "nix-prefetch-git" $ filter (/="")
174− [ "--url", T.unpack $ render uri
188+ proc nixPrefetchGitPath $ filter (/="")
189+ [ "--url", T.unpack $ gitUriToText uri
175190 , "--rev", T.unpack rev
176191 , if fetchSubmodules then "--fetch-submodules" else ""
177192 , "--quiet"
@@ -183,93 +198,117 @@ nixPrefetchGit uri rev fetchSubmodules =
183198
184199--TODO: Pretty print these
185200data ReadThunkError
186− = ReadThunkError_UnrecognizedFiles
187− | ReadThunkError_AmbiguousFiles
188− | ReadThunkError_UnrecognizedLoader Text
189− | ReadThunkError_UnparseablePtr LBS.ByteString
190− deriving (Show)
191−
192−-- | Read a thunk and validate that it is only a thunk, either packed or unpacked.
193−-- If the thunk is packed and additional data is present, fail.
194−readThunk :: MonadObelisk m => FilePath -> m (Either ReadThunkError ThunkData)
195−readThunk thunkDir = do
196− files <- liftIO $ listDirectory thunkDir
197− case ".git" `elem` files of
198− True -> fmap ThunkData_Checkout <$> do
199− let origThunkPath = thunkDir </> ".git" </> "obelisk" </> "orig-thunk"
200− liftIO $ doesDirectoryExist origThunkPath >>= \case
201− True -> fmap Just <$> readPackedThunk origThunkPath
202− False -> return $ return Nothing
203− False -> liftIO $ fmap ThunkData_Packed <$> readPackedThunk thunkDir
204−
205−data ThunkType = ThunkType
206− { _thunkType_loader :: FilePath
207− , _thunkType_json :: FilePath
208− , _thunkType_optional :: Set FilePath
209− , _thunkType_loaderVersions :: NonEmpty Text
210− , _thunkType_parser :: Aeson.Object -> Aeson.Parser ThunkPtr
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
209+ deriving (Show)
210+
211+unpackedDirName :: FilePath
212+unpackedDirName = "."
213+
214+attrCacheFileName :: FilePath
215+attrCacheFileName = ".attr-cache"
216+
217+-- | Specification for how a file in a thunk version works.
218+data ThunkFileSpec
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
223+
224+-- | Specification for how a set of files in a thunk version work.
225+data ThunkSpec = ThunkSpec
226+ { _thunkSpec_name :: !Text
227+ , _thunkSpec_files :: !(Map FilePath ThunkFileSpec)
211228 }
212229
213−gitHubThunkType :: ThunkType
214−gitHubThunkType = ThunkType
215− { _thunkType_loader = "default.nix"
216− , _thunkType_json = "github.json"
217− , _thunkType_optional = Set.fromList [".attr-cache"]
218− , _thunkType_loaderVersions = gitHubStandaloneLoaders
219− , _thunkType_parser = parseThunkPtr $ \v ->
220− ThunkSource_GitHub <$> parseGitHubSource v <|> ThunkSource_Git <$> parseGitSource v
221− }
222−
223−gitThunkType :: ThunkType
224−gitThunkType = ThunkType
225− { _thunkType_loader = "default.nix"
226− , _thunkType_json = "git.json"
227− , _thunkType_optional = Set.fromList [".attr-cache"]
228− , _thunkType_loaderVersions = plainGitStandaloneLoaders
229− , _thunkType_parser = parseThunkPtr $ fmap ThunkSource_Git . parseGitSource
230− }
231−
232−thunkTypes :: [ThunkType]
233−thunkTypes = [gitThunkType, gitHubThunkType]
234−
235−findThunkType :: [ThunkType] -> FilePath -> IO (Either ReadThunkError ThunkType)
236−findThunkType types thunkDir = do
237− matches <- fmap catMaybes $ forM types $ \thunkType -> do
238− let
239− expectedContents = Set.fromList $ (thunkDir </>) <$>
240− [ _thunkType_loader thunkType
241− , _thunkType_json thunkType
242− ]
243− optionalContents = Set.map (thunkDir </>) (_thunkType_optional thunkType)
244−
245− -- Ensure that there aren't any other files in the thunk
246− -- NB: System.Directory.listDirectory returns the contents without the directory path
247− files <- liftIO $ Set.fromList . fmap (thunkDir </>) <$> listDirectory thunkDir
248− let unexpectedContents = files `Set.difference` (expectedContents `Set.union` optionalContents)
249− missingContents = expectedContents `Set.difference` files
250− pure $ if Set.null unexpectedContents && Set.null missingContents
251− then Just thunkType
252− else Nothing
253−
254− pure $ case matches of
255− [singleMatch] -> Right singleMatch
256− [] -> Left ReadThunkError_UnrecognizedFiles
257− _ -> Left ReadThunkError_AmbiguousFiles
230+thunkSpecTypes :: NonEmpty (NonEmpty ThunkSpec)
231+thunkSpecTypes = gitThunkSpecs :| [gitHubThunkSpecs]
232+
233+-- | Attempts to match a 'ThunkSpec' to a given directory.
234+matchThunkSpecToDir
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
239+ -> m ThunkData
240+matchThunkSpecToDir thunkSpec dir dirFiles = do
241+ case nonEmpty (toList $ dirFiles `Set.difference` expectedPaths) of
242+ Just fs -> throwError $ ReadThunkError_UnrecognizedPaths $ (dir </>) <$> fs
243+ Nothing -> pure ()
244+ case nonEmpty (toList $ requiredPaths `Set.difference` dirFiles) of
245+ Just fs -> throwError $ ReadThunkError_MissingPaths $ (dir </>) <$> fs
246+ Nothing -> pure ()
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
250+ False -> Nothing
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
255+ True -> pure Nothing
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
261+ True -> do
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
266+
267+ case nonEmpty datas of
268+ Nothing -> throwError ReadThunkError_UnrecognizedThunk
269+ Just xs -> fold1WithM xs $ \a b -> either throwError pure (mergeThunkData a b)
270+ where
271+ rootPathsOnly = Set.fromList . mapMaybe takeRootDir . Map.keys
272+ takeRootDir = fmap NonEmpty.head . nonEmpty . splitPath
273+
274+ expectedPaths = rootPathsOnly $ _thunkSpec_files thunkSpec
275+
276+ requiredPaths = rootPathsOnly $ Map.filter isRequiredFileSpec $ _thunkSpec_files thunkSpec
277+ isRequiredFileSpec = \case
278+ ThunkFileSpec_FileMatches _ -> True
279+ _ -> False
280+
281+ dirMayExist expectedPath = liftIO (doesFileExist (dir </> expectedPath)) >>= \case
282+ True -> throwError $ ReadThunkError_UnrecognizedPaths $ expectedPath :| []
283+ False -> pure ()
284+
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
291+
292+ bothPackedAndUnpacked = ReadThunkError_UnrecognizedState "Both packed data and checkout present"
293+
294+ fold1WithM (x :| xs) f = foldM f x xs
295+
296+readThunkWith
297+ :: (MonadObelisk m)
298+ => NonEmpty (NonEmpty ThunkSpec) -> FilePath -> m (Either ReadThunkError ThunkData)
299+readThunkWith 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}|]
258307
259308-- | Read a thunk and validate that it is exactly a packed thunk.
260309-- If additional data is present, fail.
261−readPackedThunk :: FilePath -> IO (Either ReadThunkError ThunkPtr)
262−readPackedThunk thunkDir = runExceptT $ do
263− thunkType <- ExceptT $ findThunkType thunkTypes thunkDir
264− -- Ensure that we recognize the thunk loader
265− loader <- liftIO $ T.readFile $ thunkDir </> _thunkType_loader thunkType
266− unless (loader `elem` _thunkType_loaderVersions thunkType) $ do
267− throwError $ ReadThunkError_UnrecognizedLoader loader
268−
269− txt <- liftIO $ LBS.readFile $ thunkDir </> _thunkType_json thunkType
270− case parseMaybe (_thunkType_parser thunkType) =<< Aeson.decode txt of
271− Nothing -> throwError $ ReadThunkError_UnparseablePtr txt
272− Just ptr -> return ptr
310+readThunk :: (MonadObelisk m) => FilePath -> m (Either ReadThunkError ThunkData)
311+readThunk = readThunkWith thunkSpecTypes
273312
274313parseThunkPtr :: (Aeson.Object -> Aeson.Parser ThunkSource) -> Aeson.Object -> Aeson.Parser ThunkPtr
275314parseThunkPtr parseSrc v = do
@@ -289,10 +328,12 @@ parseGitHubSource v = do
289328 owner <- v Aeson..: "owner"
290329 repo <- v Aeson..: "repo"
291330 branch <- v Aeson..:! "branch"
331+ private <- v Aeson..:? "private"
292332 pure $ GitHubSource
293333 { _gitHubSource_owner = owner
294334 , _gitHubSource_repo = repo
295335 , _gitHubSource_branch = branch
336+ , _gitHubSource_private = fromMaybe False private
296337 }
297338
298339parseGitSource :: Aeson.Object -> Aeson.Parser GitSource
@@ -300,25 +341,29 @@ parseGitSource v = do
300341 Just url <- parseGitUri <$> v Aeson..: "url"
301342 branch <- v Aeson..:! "branch"
302343 fetchSubmodules <- v Aeson..:! "fetchSubmodules"
344+ private <- v Aeson..:? "private"
303345 pure $ GitSource
304346 { _gitSource_url = url
305347 , _gitSource_branch = branch
306348 , _gitSource_fetchSubmodules = fromMaybe False fetchSubmodules
349+ , _gitSource_private = fromMaybe False private
307350 }
308351
309352overwriteThunk :: MonadObelisk m => FilePath -> ThunkPtr -> m ()
310353overwriteThunk target thunk = do
311354 -- Ensure that this directory is a valid thunk (i.e. so we aren't losing any data)
312− Right _ <- readThunk target
355+ readThunk target >>= \case
356+ Left e -> failWith [i|Invalid thunk at ${target}: ${e}|]
357+ Right _ -> pure ()
313358
314359 --TODO: Is there a safer way to do this overwriting?
315− liftIO $ removeDirectoryRecursive target
316− liftIO $ createThunk target thunk
360+ liftIO $ removePathForcibly target
361+ createThunk target $ Right thunk
317362
318−thunkPtrLoader :: ThunkPtr -> Text
319−thunkPtrLoader thunk = case _thunkPtr_source thunk of
320− ThunkSource_GitHub _ -> NonEmpty.head gitHubStandaloneLoaders
321− ThunkSource_Git _ -> NonEmpty.head plainGitStandaloneLoaders
363+thunkPtrToSpec :: ThunkPtr -> ThunkSpec
364+thunkPtrToSpec thunk = case _thunkPtr_source thunk of
365+ ThunkSource_GitHub _ -> NonEmpty.head gitHubThunkSpecs
366+ ThunkSource_Git _ -> NonEmpty.head gitThunkSpecs
322367
323368-- It's important that formatting be very consistent here, because
324369-- otherwise when people update thunks, their patches will be messy
@@ -330,13 +375,15 @@ encodeThunkPtrData (ThunkPtr rev src) = case src of
330375 , ("branch" .=) <$> _gitHubSource_branch s
331376 , Just $ "rev" .= Ref.toHexString (_thunkRev_commit rev)
332377 , Just $ "sha256" .= _thunkRev_nixSha256 rev
378+ , Just $ "private" .= _gitHubSource_private s
333379 ]
334380 ThunkSource_Git s -> encodePretty' plainGitCfg $ Aeson.object $ catMaybes
335− [ Just $ "url" .= render (_gitSource_url s)
381+ [ Just $ "url" .= gitUriToText (_gitSource_url s)
336382 , Just $ "rev" .= Ref.toHexString (_thunkRev_commit rev)
337383 , ("branch" .=) <$> _gitSource_branch s
338384 , Just $ "sha256" .= _thunkRev_nixSha256 rev
339385 , Just $ "fetchSubmodules" .= _gitSource_fetchSubmodules s
386+ , Just $ "private" .= _gitSource_private s
340387 ]
341388 where
342389 githubCfg = defConfig
@@ -357,62 +404,84 @@ encodeThunkPtrData (ThunkPtr rev src) = case src of
357404 [ "url"
358405 , "rev"
359406 , "sha256"
407+ , "private"
360408 , "fetchSubmodules"
361409 ] <> compare
362410 , confTrailingNewline = True
363411 }
364412
365−createThunk :: MonadIO m => FilePath -> ThunkPtr -> m ()
366−createThunk target thunk = liftIO $ do
367− createDirectoryIfMissing True (target </> ".attr-cache")
368− T.writeFile (target </> "default.nix") (thunkPtrLoader thunk)
369− let
370− jsonFileName = case _thunkPtr_source thunk of
371− ThunkSource_GitHub _ -> "github"
372− ThunkSource_Git _ -> "git"
373− LBS.writeFile (target </> jsonFileName <.> "json") (encodeThunkPtrData thunk)
413+createThunk :: MonadObelisk m => FilePath -> Either ThunkSpec ThunkPtr -> m ()
414+createThunk 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)
420+ _ -> pure ()
421+ where
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
427+ f fullPath
374428
375429createThunkWithLatest :: MonadObelisk m => FilePath -> ThunkSource -> m ()
376430createThunkWithLatest target s = do
377431 rev <- getLatestRev s
378− createThunk target $ ThunkPtr
432+ createThunk target $ Right $ ThunkPtr
379433 { _thunkPtr_source = s
380434 , _thunkPtr_rev = rev
381435 }
382436
383−updateThunkToLatest :: MonadObelisk m => FilePath -> Maybe String -> m ()
384−updateThunkToLatest target mBranch = withSpinner' ("Updating thunk " <> T.pack target <> " to latest") (pure $ const $ "Thunk " <> T.pack target <> " updated to latest") $
437+updateThunkToLatest :: MonadObelisk m => ThunkUpdateConfig -> FilePath -> m ()
438+updateThunkToLatest (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
385441 case mBranch of
386442 Nothing -> do
387443 (overwrite, ptr) <- readThunk target >>= \case
388− Left err -> failWith $ T.pack $ "thunk update: " <> show err
444+ Left err -> failWith [i|Thunk update: ${err}|]
389445 Right c -> case c of
390− ThunkData_Packed t -> return (target, t)
391− ThunkData_Checkout _ -> failWith "cannot update an unpacked thunk"
446+ ThunkData_Packed _ t -> return (target, t)
447+ ThunkData_Checkout -> failWith "cannot update an unpacked thunk"
392448 let src = _thunkPtr_source ptr
393449 rev <- getLatestRev src
394− overwriteThunk overwrite $ ThunkPtr
450+ overwriteThunk overwrite $ modifyThunkPtrByConfig thunkConfig $ ThunkPtr
395451 { _thunkPtr_source = src
396452 , _thunkPtr_rev = rev
397453 }
398− Just branch -> setThunk branch target
454+ Just branch -> readThunk target >>= \case
455+ Left err -> failWith [i|Thunk update: ${err}|]
456+ Right c -> case c of
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.|]
459+ where
460+ spinner = withSpinner' ("Updating thunk " <> T.pack target <> " to latest") (pure $ const $ "Thunk " <> T.pack target <> " updated to latest")
461+
462+setThunk :: MonadObelisk m => ThunkConfig -> FilePath -> GitSource -> String -> m ()
463+setThunk 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
399467
400468-- | All recognized github standalone loaders, ordered from newest to oldest.
401469-- This tool will only ever produce the newest one when it writes a thunk.
402−gitHubStandaloneLoaders :: NonEmpty Text
403−gitHubStandaloneLoaders =
404− gitHubStandaloneLoaderV2 :|
405− [ gitHubStandaloneLoaderV3
406− , gitHubStandaloneLoaderV1
470+gitHubThunkSpecs :: NonEmpty ThunkSpec
471+gitHubThunkSpecs =
472+ gitHubThunkSpecV5 :|
473+ [ gitHubThunkSpecV4
474+ , gitHubThunkSpecV3
475+ , gitHubThunkSpecV2
476+ , gitHubThunkSpecV1
407477 ]
408478
409−gitHubStandaloneLoaderV1 :: Text
410−gitHubStandaloneLoaderV1 = T.unlines
411− [ "import ((import <nixpkgs> {}).fetchFromGitHub (builtins.fromJSON (builtins.readFile ./github.json)))"
412− ]
479+gitHubThunkSpecV1 :: ThunkSpec
480+gitHubThunkSpecV1 = legacyGitHubThunkSpec "github-v1"
481+ "import ((import <nixpkgs> {}).fetchFromGitHub (builtins.fromJSON (builtins.readFile ./github.json)))"
413482
414−gitHubStandaloneLoaderV2 :: Text
415−gitHubStandaloneLoaderV2 = T.unlines
483+gitHubThunkSpecV2 :: ThunkSpec
484+gitHubThunkSpecV2 = legacyGitHubThunkSpec "github-v2" $ T.unlines
416485 [ "# DO NOT HAND-EDIT THIS FILE" --TODO: Add something about how to get more info on Obelisk, etc.
417486 , "import ((import <nixpkgs> {}).fetchFromGitHub ("
418487 , " let json = builtins.fromJSON (builtins.readFile ./github.json);"
@@ -422,8 +491,8 @@ gitHubStandaloneLoaderV2 = T.unlines
422491 , "))"
423492 ]
424493
425−gitHubStandaloneLoaderV3 :: Text
426−gitHubStandaloneLoaderV3 = T.unlines
494+gitHubThunkSpecV3 :: ThunkSpec
495+gitHubThunkSpecV3 = legacyGitHubThunkSpec "github-v3" $ T.unlines
427496 [ "# DO NOT HAND-EDIT THIS FILE"
428497 , "let"
429498 , " fetch = { private ? false, ... }@args: if private && builtins.hasAttr \"fetchGit\" builtins"
@@ -442,22 +511,62 @@ gitHubStandaloneLoaderV3 = T.unlines
442511 , "in import (fetch (builtins.fromJSON (builtins.readFile ./github.json)))"
443512 ]
444513
445−plainGitStandaloneLoaders :: NonEmpty Text
446−plainGitStandaloneLoaders =
447− plainGitStandaloneLoaderV1 :|
448− [ plainGitStandaloneLoaderV2
514+gitHubThunkSpecV4 :: ThunkSpec
515+gitHubThunkSpecV4 = 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;"
522+ , " };"
523+ , "in import (fetch (builtins.fromJSON (builtins.readFile ./github.json)))"
524+ ]
525+
526+legacyGitHubThunkSpec :: Text -> Text -> ThunkSpec
527+legacyGitHubThunkSpec 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)
532+ ]
533+
534+gitHubThunkSpecV5 :: ThunkSpec
535+gitHubThunkSpecV5 = mkThunkSpec "github-v5" "github.json" parseGitHubJsonBytes [here|
536+# DO NOT HAND-EDIT THIS FILE
537+let 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;
542+ };
543+ json = builtins.fromJSON (builtins.readFile ./github.json);
544+in fetch json
545+|]
546+
547+parseGitHubJsonBytes :: LBS.ByteString -> Either String ThunkPtr
548+parseGitHubJsonBytes = parseJsonObject $ parseThunkPtr $ \v ->
549+ ThunkSource_GitHub <$> parseGitHubSource v <|> ThunkSource_Git <$> parseGitSource v
550+
551+gitThunkSpecs :: NonEmpty ThunkSpec
552+gitThunkSpecs =
553+ gitThunkSpecV5 :|
554+ [ gitThunkSpecV4
555+ , gitThunkSpecV3
556+ , gitThunkSpecV2
557+ , gitThunkSpecV1
449558 ]
450559
451−plainGitStandaloneLoaderV1 :: Text
452−plainGitStandaloneLoaderV1 = T.unlines
560+gitThunkSpecV1 :: ThunkSpec
561+gitThunkSpecV1 = legacyGitThunkSpec "git-v1" $ T.unlines
453562 [ "# DO NOT HAND-EDIT THIS FILE"
454563 , "let fetchGit = {url, rev, ref ? null, branch ? null, sha256 ? null, fetchSubmodules ? null}:"
455564 , " assert !fetchSubmodules; (import <nixpkgs> {}).fetchgit { inherit url rev sha256; };"
456565 , "in import (fetchGit (builtins.fromJSON (builtins.readFile ./git.json)))"
457566 ]
458567
459−plainGitStandaloneLoaderV2 :: Text
460−plainGitStandaloneLoaderV2 = T.unlines
568+gitThunkSpecV2 :: ThunkSpec
569+gitThunkSpecV2 = legacyGitThunkSpec "git-v2" $ T.unlines
461570 [ "# DO NOT HAND-EDIT THIS FILE"
462571 , "let fetchGit = {url, rev, ref ? null, branch ? null, sha256 ? null, fetchSubmodules ? null}:"
463572 , " if builtins.hasAttr \"fetchGit\" builtins"
@@ -466,10 +575,87 @@ plainGitStandaloneLoaderV2 = T.unlines
466575 , "in import (fetchGit (builtins.fromJSON (builtins.readFile ./git.json)))"
467576 ]
468577
469−thunkFileNames :: [FilePath]
470−thunkFileNames = ["default.nix", "github.json", "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.
580+gitThunkSpecV3 :: ThunkSpec
581+gitThunkSpecV3 = 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"
587+ , " else 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;"
592+ , " };"
593+ , "in import (fetch (builtins.fromJSON (builtins.readFile ./git.json)))"
594+ ]
595+
596+gitThunkSpecV4 :: ThunkSpec
597+gitThunkSpecV4 = 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"
603+ , " else 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;"
609+ , " };"
610+ , "in import (fetch (builtins.fromJSON (builtins.readFile ./git.json)))"
611+ ]
612+
613+legacyGitThunkSpec :: Text -> Text -> ThunkSpec
614+legacyGitThunkSpec 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)
619+ ]
620+
621+gitThunkSpecV5 :: ThunkSpec
622+gitThunkSpecV5 = mkThunkSpec "git-v5" "git.json" parseGitJsonBytes [here|
623+# DO NOT HAND-EDIT THIS FILE
624+let 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
628+ else 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;
634+ };
635+ json = builtins.fromJSON (builtins.readFile ./git.json);
636+in fetch json
637+|]
638+
639+parseGitJsonBytes :: LBS.ByteString -> Either String ThunkPtr
640+parseGitJsonBytes = parseJsonObject $ parseThunkPtr $ fmap ThunkSource_Git . parseGitSource
641+
642+mkThunkSpec :: Text -> FilePath -> (LBS.ByteString -> Either String ThunkPtr) -> Text -> ThunkSpec
643+mkThunkSpec 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)
649+ ]
650+ where
651+ defaultNixViaSrc = [here|
652+# DO NOT HAND-EDIT THIS FILE
653+import (import ./thunk.nix)
654+|]
655+
471656
472−--TODO: when checking something out, make a shallow clone
657+parseJsonObject :: (Aeson.Object -> Aeson.Parser a) -> LBS.ByteString -> Either String a
658+parseJsonObject p bytes = Aeson.parseEither p =<< Aeson.eitherDecode bytes
473659
474660-- | Checks a cache directory to see if there is a fresh symlink
475661-- to the result of building an attribute of a thunk.
@@ -477,44 +663,49 @@ thunkFileNames = ["default.nix", "github.json", "git.json"]
477663-- and the result is symlinked into the cache.
478664nixBuildThunkAttrWithCache
479665 :: MonadObelisk m
480− => FilePath
666+ => ThunkSpec
667+ -> FilePath
481668 -- ^ Path to directory containing Thunk
482669 -> String
483670 -- ^ Attribute to build
484− -> m FilePath
671+ -> m (Maybe FilePath)
485672 -- ^ Symlink to cached or built nix output
486673-- WARNING: If the thunk uses an impure reference such as '<nixpkgs>'
487674-- the caching mechanism will fail as it merely measures the modification
488675-- time of the cache link and the expression to build.
489−nixBuildThunkAttrWithCache thunkDir attr = do
490− --NB: Expects thunkDir to be normalised with no trailing path separator.
491− --This should be guaranteed by the command argument parser.
492− let cacheErrHandler e
493− | isDoesNotExistError e = return Nothing -- expected from a cache miss
494− | otherwise = putLog Error (T.pack $ displayException e) >> return Nothing
495− cacheDir = thunkDir </> ".attr-cache"
496− cachePath = cacheDir </> attr <.> "out"
676+nixBuildThunkAttrWithCache thunkSpec thunkDir attr = do
497677 latestChange <- liftIO $ do
498− createDirectoryIfMissing False cacheDir
499− let getModificationTimeMaybe = fmap rightToMaybe . try @IOError . getModificationTime
500− maximum . catMaybes <$> mapM (getModificationTimeMaybe . (thunkDir </>)) thunkFileNames
501− cacheHit <- handle cacheErrHandler $ do
502− cacheTime <- liftIO $ posixSecondsToUTCTime . realToFrac . modificationTime <$> getSymbolicLinkStatus cachePath
503− return $ if latestChange <= cacheTime
504− then Just cachePath
505− else Nothing
506− case cacheHit of
507− Just c -> return c
508− Nothing -> do
509− putLog Warning $ T.pack $ mconcat [thunkDir, ": ", attr, " not cached, building ..."]
510− _ <- nixCmd $ NixCmd_Build$ def
511− Lens.& nixBuildConfig_outLink Lens..~ OutLink_IndirectRoot cachePath
512− Lens.& nixCmdConfig_target Lens..~ Target
513− { _target_path = Just thunkDir
514− , _target_attr = Just attr
515− , _target_expr = Nothing
516− }
517− return cachePath
678+ let
679+ getModificationTimeMaybe = fmap rightToMaybe . try @IOError . getModificationTime
680+ thunkFileNames = Map.keys $ _thunkSpec_files thunkSpec
681+ maximum . catMaybes <$> traverse (getModificationTimeMaybe . (thunkDir </>)) thunkFileNames
682+
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
687+ let
688+ cachePath = thunkDir </> cacheDir </> attr <.> "out"
689+ cacheErrHandler e
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
695+ then Just cachePath
696+ else Nothing
697+ case cacheHit of
698+ Just c -> pure c
699+ Nothing -> do
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
708+ }
518709
519710-- | Build a nix attribute, and cache the result if possible
520711nixBuildAttrWithCache
@@ -525,13 +716,15 @@ nixBuildAttrWithCache
525716 -- ^ Attribute to build
526717 -> m FilePath
527718 -- ^ Symlink to cached or built nix output
528−nixBuildAttrWithCache exprPath attr = do
529− readThunk exprPath >>= \case
530− -- Only packed thunks are cached. in particular, checkouts are not
531− Right (ThunkData_Packed _) -> nixBuildThunkAttrWithCache exprPath attr
532− _ -> nixCmd $ NixCmd_Build $ def
533− Lens.& nixBuildConfig_outLink Lens..~ OutLink_None
534− Lens.& nixCmdConfig_target Lens..~ Target
719+nixBuildAttrWithCache 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
723+ _ -> build
724+ where
725+ build = nixCmd $ NixCmd_Build $ def
726+ & nixBuildConfig_outLink .~ OutLink_None
727+ & nixCmdConfig_target .~ Target
535728 { _target_path = Just exprPath
536729 , _target_attr = Just attr
537730 , _target_expr = Nothing
@@ -552,142 +745,153 @@ updateThunk p f = withSystemTempDirectory "obelisk-thunkptr-" $ \tmpDir -> do
552745 where
553746 copyThunkToTmp tmpDir thunkDir = readThunk thunkDir >>= \case
554747 Left err -> failWith $ "withThunkUnpacked: " <> T.pack (show err)
555− Right (ThunkData_Packed _) -> do
748+ Right ThunkData_Packed{} -> do
556749 let tmpThunk = tmpDir </> "thunk"
557750 callProcessAndLogOutput (Notice, Error) $
558− proc "cp" ["-r", "-T", thunkDir, tmpThunk]
751+ proc cp ["-r", "-T", thunkDir, tmpThunk]
559752 return tmpThunk
560− Right _ -> failWith $ "Thunk is not packed"
753+ Right _ -> failWith "Thunk is not packed"
561754 updateThunkFromTmp p' = do
562− _ <- packThunk' True p'
755+ _ <- packThunk' True (ThunkPackConfig False (ThunkConfig Nothing)) p'
563756 callProcessAndLogOutput (Notice, Error) $
564− proc "cp" ["-r", "-T", p', p]
757+ proc cp ["-r", "-T", p', p]
565758
566759finalMsg :: Bool -> (a -> Text) -> Maybe (a -> Text)
567760finalMsg noTrail s = if noTrail then Nothing else Just s
568761
569−unpackThunk :: MonadObelisk m => FilePath -> m ()
570−unpackThunk = unpackThunk' False
571−
572762-- | Check that we are not somewhere inside the thunk directory
573763checkThunkDirectory :: MonadObelisk m => FilePath -> m ()
574764checkThunkDirectory thunkDir = do
575765 currentDir <- liftIO getCurrentDirectory
576766 thunkDir' <- liftIO $ canonicalizePath thunkDir
577767 when (thunkDir' `L.isInfixOf` currentDir) $
578− failWith "Can't pack/unpack from within the thunk directory"
768+ failWith [i|Can't perform thunk operations from within the thunk directory: ${thunkDir}|]
769+
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|]
774+ Left _ -> pure ()
775+
776+unpackThunk :: MonadObelisk m => FilePath -> m ()
777+unpackThunk = unpackThunk' False
579778
580779unpackThunk' :: MonadObelisk m => Bool -> FilePath -> m ()
581−unpackThunk' noTrail thunkDir = checkThunkDirectory thunkDir >> readThunk thunkDir >>= \case
582− Left err -> failWith $ "thunk unpack: " <> T.pack (show err)
780+unpackThunk' noTrail thunkDir = checkThunkDirectory thunkDir *> readThunk thunkDir >>= \case
781+ Left err -> failWith [i|Invalid thunk at ${thunkDir}: ${err}|]
583782 --TODO: Overwrite option that rechecks out thunk; force option to do so even if working directory is dirty
584− Right (ThunkData_Checkout _) -> failWith "thunk unpack: thunk is already unpacked"
585− Right (ThunkData_Packed tptr) -> do
783+ Right ThunkData_Checkout -> failWith [i|Thunk at ${thunkDir} is already unpacked|]
784+ Right (ThunkData_Packed _ tptr) -> do
586785 let (thunkParent, thunkName) = splitFileName thunkDir
587− withTempDirectory thunkParent thunkName $ \tmpRepo -> do
588− let obGitDir = tmpRepo </> ".git" </> "obelisk"
589− s = case _thunkPtr_source tptr of
590− ThunkSource_GitHub s' -> forgetGithub False s'
591− ThunkSource_Git s' -> s'
786+ withTempDirectory thunkParent thunkName $ \tmpThunk -> do
787+ let
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
592792 withSpinner' ("Fetching thunk " <> T.pack thunkName)
593793 (finalMsg noTrail $ const $ "Fetched thunk " <> T.pack thunkName) $ do
594− let git = callProcessAndLogOutput (Notice, Notice) . gitProc tmpRepo
595− git $ [ "clone" ]
596− ++ ("--recursive" <$ guard (_gitSource_fetchSubmodules s))
597− ++ [ T.unpack $ render $ _gitSource_url s ]
598− ++ do branch <- maybeToList $ _gitSource_branch s
599− [ "--branch", T.unpack $ untagName branch ]
600− git ["reset", "--hard", Ref.toHexString $ _thunkRev_commit $ _thunkPtr_rev tptr]
601− when (_gitSource_fetchSubmodules s) $
602− git ["submodule", "update", "--recursive", "--init"]
603−
604− liftIO $ createDirectory obGitDir
605− callProcessAndLogOutput (Notice, Error) $
606− proc "cp" ["-r", "-T", thunkDir </> ".", obGitDir </> "orig-thunk"]
607− callProcessAndLogOutput (Notice, Error) $
608− proc "rm" ["-r", thunkDir]
609− callProcessAndLogOutput (Notice, Error) $
610− proc "mv" ["-T", tmpRepo, thunkDir]
794+ let unpackedPath = tmpThunk </> unpackedDirName
795+ gitCloneForThunkUnpack gitSrc (_thunkRev_commit $ _thunkPtr_rev tptr) unpackedPath
796+
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
800+
801+ liftIO $ do
802+ removePathForcibly thunkDir
803+ renameDirectory tmpThunk thunkDir
804+
805+gitCloneForThunkUnpack
806+ :: MonadObelisk m
807+ => GitSource -- ^ Git source to use
808+ -> Ref hash -- ^ Commit hash to reset to
809+ -> FilePath -- ^ Directory to clone into
810+ -> m ()
811+gitCloneForThunkUnpack gitSrc commit dir = do
812+ let git = callProcessAndLogOutput (Notice, Notice) . gitProc dir
813+ git $ [ "clone" ]
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"]
611821
612−--TODO: add force mode to pack even if changes are present
613822--TODO: add a rollback mode to pack to the original thunk
614−packThunk :: MonadObelisk m => FilePath -> m ThunkPtr
823+packThunk :: MonadObelisk m => ThunkPackConfig -> FilePath -> m ThunkPtr
615824packThunk = packThunk' False
616825
617−packThunk' :: MonadObelisk m => Bool -> FilePath -> m ThunkPtr
618−packThunk' noTrail thunkDir = checkThunkDirectory thunkDir >> readThunk thunkDir >>= \case
619− Left err -> failWith $ T.pack $ "thunk pack: " <> show err
620− Right (ThunkData_Packed _) -> failWith "pack: thunk is already packed"
621− Right (ThunkData_Checkout _) -> do
622− withSpinner' ("Packing thunk " <> T.pack thunkDir)
623− (finalMsg noTrail $ const $ "Packed thunk " <> T.pack thunkDir) $ do
624− thunkPtr <- getThunkPtr thunkDir
625− callProcessAndLogOutput (Debug, Error) $ proc "rm" ["-rf", thunkDir]
626− liftIO $ createThunk thunkDir thunkPtr
826+packThunk' :: MonadObelisk m => Bool -> ThunkPackConfig -> FilePath -> m ThunkPtr
827+packThunk' noTrail (ThunkPackConfig force thunkConfig) thunkDir = checkThunkDirectory thunkDir *> readThunk thunkDir >>= \case
828+ Right ThunkData_Packed{} -> failWith [i|Thunk at ${thunkDir} is is already packed|]
829+ _ -> withSpinner'
830+ ("Packing thunk " <> T.pack thunkDir)
831+ (finalMsg noTrail $ const $ "Packed thunk " <> T.pack thunkDir) $
832+ do
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
627837 pure thunkPtr
628838
629−-- set thunk to a desired branch
630−setThunk :: MonadObelisk m => String -> FilePath -> m ()
631−setThunk branch thunkDir = checkThunkDirectory thunkDir >> readThunk thunkDir >>= \case
632− Left err -> failWith $ T.pack $ "thunk set : " <> show err
633− Right (ThunkData_Packed thunkptr) -> do
634− let thunkSrc = _thunkPtr_source thunkptr
635− repository = case thunkSrc of
636− ThunkSource_Git gitSource -> T.unpack $ render $ _gitSource_url gitSource
637− ThunkSource_GitHub gitHubSource -> T.unpack $ render $ _gitSource_url $ forgetGithub True gitHubSource
638− -- check to see if branch exists before modifying directory
639− (exitCode, _) <- gitLsRemoteExitCode repository branch
640− case exitCode of
641− ExitSuccess -> do
642− remoteResults <- readGitProcessNoRepo ["ls-remote", repository, branch]
643− uri <- mkURI $ T.pack repository
644− let rev = head $ T.words remoteResults -- TODO: Don't use head
645− sha256 <- nixPrefetchGit uri rev False
646− let newThunkPtr = ThunkPtr
647− { _thunkPtr_source = case _thunkPtr_source thunkptr of
648− ThunkSource_Git gitSource -> ThunkSource_Git GitSource
649− { _gitSource_url = _gitSource_url gitSource
650− , _gitSource_branch = Just $ N $ T.pack branch
651− , _gitSource_fetchSubmodules = _gitSource_fetchSubmodules gitSource
652− }
653− ThunkSource_GitHub gitHubSource -> ThunkSource_GitHub GitHubSource
654− { _gitHubSource_owner = _gitHubSource_owner gitHubSource
655− , _gitHubSource_repo = _gitHubSource_repo gitHubSource
656− , _gitHubSource_branch = Just $ N $ T.pack branch
657− }
658− , _thunkPtr_rev = ThunkRev
659− { _thunkRev_commit = Ref.hash $ encodeUtf8 rev
660− , _thunkRev_nixSha256 = sha256
661− }
662− }
663− overwriteThunk thunkDir newThunkPtr
664− updateThunkToLatest thunkDir Nothing
665− ExitFailure errNum -> failWith $ T.pack $ if errNum == 2
666− then "Error: branch not found"
667− else ("Error Code: " <> show errNum <> "issue checking out the desired branch")
668− Right (ThunkData_Checkout _) -> failWith $ T.pack $ "thunk located at " <> (show thunkDir) <> " is unpacked. Use ob thunk pack on the desired directory and then try ob thunk set again."
669−
670−getThunkPtr :: MonadObelisk m => FilePath -> m ThunkPtr
671−getThunkPtr = getThunkPtr' True
672−
673−getThunkPtr' :: forall m. MonadObelisk m => Bool -> FilePath -> m ThunkPtr
674−getThunkPtr' checkClean thunkDir = do
675− when checkClean $ ensureCleanGitRepo thunkDir True $
839+modifyThunkPtrByConfig :: ThunkConfig -> ThunkPtr -> ThunkPtr
840+modifyThunkPtrByConfig (ThunkConfig markPrivate') ptr = case markPrivate' of
841+ Nothing -> ptr
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 }
845+ }
846+
847+data CheckClean
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
852+ | CheckClean_NoCheck
853+ -- ^ Don't check that the repo is clean
854+
855+getThunkPtr :: forall m. MonadObelisk m => CheckClean -> FilePath -> Maybe Bool -> m ThunkPtr
856+getThunkPtr 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
863+
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
676869 "thunk pack: thunk checkout contains unsaved modifications"
677870
678871 -- Check whether there are any stashes
679− stashOutput <- readGitProcess thunkDir ["stash", "list"]
680− when checkClean $ case T.null stashOutput of
681− False -> do
872+ when checkClean $ do
873+ stashOutput <- readGitProcess thunkDir ["stash", "list"]
874+ unless (T.null stashOutput) $
682875 failWith $ T.unlines $
683876 [ "thunk pack: thunk checkout has stashes"
684877 , "git stash list:"
685878 ] ++ T.lines stashOutput
686− True -> return ()
879+
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"]
884+ case b of
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."
889+ ]
890+ _ -> return (b, c)
687891
688892 -- Get information on all branches and their (optional) designated upstream
689893 -- correspondents
690− (headDump :: [Text]) <- T.lines <$> readGitProcess thunkDir
894+ headDump :: [Text] <- T.lines <$> readGitProcess thunkDir
691895 [ "for-each-ref"
692896 , "--format=%(refname:short) %(upstream:short) %(upstream:remotename)"
693897 , "refs/heads/"
@@ -733,7 +937,7 @@ getThunkPtr' checkClean thunkDir = do
733937 ]
734938
735939 -- loosely by https://stackoverflow.com/questions/7773939/show-git-ahead-and-behind-info-for-all-branches-including-remotes
736− stats <- iforM headUpstream $ \branch (upstream, _remote) -> do
940+ stats <- ifor headUpstream $ \branch (upstream, _remote) -> do
737941 (stat :: [Text]) <- T.lines <$> readGitProcess thunkDir
738942 [ "rev-list", "--left-right"
739943 , T.unpack branch <> "..." <> T.unpack upstream
@@ -750,20 +954,16 @@ getThunkPtr' checkClean thunkDir = do
750954 [ "thunk pack: Certain branches in the thunk have commits not yet pushed upstream:"
751955 , ""
752956 ] ++
753− (flip map (Map.toList nonGood) $ \(branch, (upstream, (ahead, behind))) -> mconcat
957+ flip map (Map.toList nonGood) (\(branch, (upstream, (ahead, behind))) -> mconcat
754958 [" ", branch, " ahead: ", T.pack (show ahead), " behind: ", T.pack (show behind), " remote branch ", upstream]) ++
755959 [ ""
756960 , "Please push these upstream and try again. (Or just fetch, if they are somehow \
757961 \pushed but this repo's remote tracking branches don't know it.)"
758962 ]
759963
760− -- We assume it's safe to pack the thunk at this point
761− putLog Informational $ "All changes safe in git remotes. OK to pack thunk."
762−
763− -- Get current branch ``
764− mCurrentBranch <- listToMaybe
765− <$> T.lines
766− <$> readGitProcess thunkDir ["rev-parse", "--abbrev-ref", "HEAD"]
964+ when checkClean $ do
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."
767967
768968 let remote = maybe "origin" snd $ flip Map.lookup headUpstream =<< mCurrentBranch
769969
@@ -776,14 +976,12 @@ getThunkPtr' checkClean thunkDir = do
776976 remoteUri <- case parseGitUri remoteUri' of
777977 Nothing -> failWith $ "Could not identify git remote: " <> remoteUri'
778978 Just uri -> pure uri
779− uriThunkPtr remoteUri mCurrentBranch
979+ uriThunkPtr remoteUri mPrivate mCurrentBranch mCurrentCommit
780980
781981-- | Get the latest revision available from the given source
782982getLatestRev :: MonadObelisk m => ThunkSource -> m ThunkRev
783983getLatestRev os = do
784− let gitS = case os of
785− ThunkSource_GitHub s -> forgetGithub False s
786− ThunkSource_Git s -> s
984+ let gitS = thunkSourceToGitSource os
787985 (_, commit) <- gitGetCommitBranch (_gitSource_url gitS) (untagName <$> _gitSource_branch gitS)
788986 case os of
789987 ThunkSource_GitHub s -> githubThunkRev s commit
@@ -795,16 +993,18 @@ getLatestRev os = do
795993-- performance. If that doesn't work (e.g. authentication issue), we fall back
796994-- on just doing things the normal way for git repos in general, and save it as
797995-- a regular git thunk.
798−uriThunkPtr :: MonadObelisk m => URI -> Maybe Text -> m ThunkPtr
799−uriThunkPtr uri mbranch = do
800− (_, commit) <- gitGetCommitBranch uri mbranch
801− (src, rev) <- case uriToThunkSource uri mbranch of
996+uriThunkPtr :: MonadObelisk m => GitUri -> Maybe Bool -> Maybe Text -> Maybe Text -> m ThunkPtr
997+uriThunkPtr 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
8021002 ThunkSource_GitHub s -> do
8031003 rev <- runExceptT $ githubThunkRev s commit
8041004 case rev of
8051005 Right r -> pure (ThunkSource_GitHub s, r)
8061006 Left e -> do
807− putLog Warning $ "\
1007+ putLog Warning "\
8081008\Failed to fetch archive from GitHub. This is probably a private repo. \
8091009\Falling back on normal fetchgit. Original failure:"
8101010 errorToWarning e
@@ -821,34 +1021,66 @@ uriThunkPtr uri mbranch = do
8211021-- If the thunk is a GitHub thunk and fails, we do *not* fall back like with
8221022-- `uriThunkPtr`. Unlike a plain URL, a thunk src explicitly states which method
8231023-- should be employed, and so we respect that.
824−uriToThunkSource :: URI -> Maybe Text -> ThunkSource
825−uriToThunkSource u
826− | Right uriAuth <- uriAuthority u
827− , Just scheme <- unRText <$> uriScheme u
1024+uriToThunkSource :: MonadObelisk m => GitUri -> Maybe Bool -> Maybe Text -> m ThunkSource
1025+uriToThunkSource (GitUri u) mPrivate
1026+ | Right uriAuth <- URI.uriAuthority u
1027+ , Just scheme <- URI.unRText <$> URI.uriScheme u
8281028 , case scheme of
829− "ssh" -> uriAuth == Authority
830− { authUserInfo = Just $ UserInfo (fromRight' $ mkUsername "git") Nothing
831− , authHost = fromRight' $ mkHost "github.com"
832− , authPort = Nothing
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
8331033 }
8341034 s -> s `L.elem` [ "git", "https", "http" ] -- "http:" just redirects to "https:"
835− && unRText (authHost uriAuth) == "github.com"
836− , Just (_, owner :| [repoish]) <- uriPath u
837− = \mbranch -> ThunkSource_GitHub $ GitHubSource
838− { _gitHubSource_owner = N $ unRText owner
839− , _gitHubSource_repo = N $ let
840− repoish' = unRText repoish
841− in case T.stripSuffix ".git" repoish' of
842− Just repo -> repo
843− Nothing -> repoish'
844− , _gitHubSource_branch = N <$> mbranch
845− }
1035+ && URI.unRText (URI.authHost uriAuth) == "github.com"
1036+ , Just (_, owner :| [repoish]) <- URI.uriPath u
1037+ = \mbranch -> do
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
1046+ }
8461047
847− | otherwise = \mbranch -> ThunkSource_Git $ GitSource
848− { _gitSource_url = u
849− , _gitSource_branch = N <$> mbranch
850− , _gitSource_fetchSubmodules = False -- TODO: How do we determine if this should be true?
851− }
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
1055+ }
1056+ where
1057+ getIsPrivate = maybe (guessGitRepoIsPrivate $ GitUri u) pure mPrivate
1058+
1059+guessGitRepoIsPrivate :: MonadObelisk m => GitUri -> m Bool
1060+guessGitRepoIsPrivate uri = flip fix urisToTry $ \loop -> \case
1061+ [] -> pure True
1062+ uriAttempt:xs -> do
1063+ result <- readCreateProcessWithExitCode $
1064+ isolateGitProc $
1065+ gitProcNoRepo
1066+ [ "ls-remote"
1067+ , "--quiet"
1068+ , "--exit-code"
1069+ , "--symref"
1070+ , T.unpack $ gitUriToText uriAttempt
1071+ ]
1072+ case result of
1073+ (ExitSuccess, _, _) -> pure False -- Must be a public repo
1074+ _ -> loop xs
1075+ where
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
1083+ }
8521084
8531085-- Funny signature indicates no effects depend on the optional branch name.
8541086githubThunkRev
@@ -860,19 +1092,19 @@ githubThunkRev
8601092githubThunkRev s commit = do
8611093 owner <- forcePP $ _gitHubSource_owner s
8621094 repo <- forcePP $ _gitHubSource_repo s
863− revTarball <- mkPathPiece $ commit <> ".tar.gz"
864− let archiveUri = URI
865− { uriScheme = Just $ fromRight' $ mkScheme "https"
866− , uriAuthority = Right $ Authority
867− { authUserInfo = Nothing
868− , authHost = fromRight' $ mkHost "github.com"
869− , authPort = Nothing
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
8701102 }
871− , uriPath = Just ( False
872− , owner :| [ repo, fromRight' $ mkPathPiece "archive", revTarball ]
1103+ , URI.uriPath = Just ( False
1104+ , owner :| [ repo, fromRight' $ URI.mkPathPiece "archive", revTarball ]
8731105 )
874− , uriQuery = []
875− , uriFragment = Nothing
1106+ , URI.uriQuery = []
1107+ , URI.uriFragment = Nothing
8761108 }
8771109 hash <- getNixSha256ForUriUnpacked archiveUri
8781110 putLog Debug $ "Nix sha256 is " <> hash
@@ -881,8 +1113,8 @@ githubThunkRev s commit = do
8811113 , _thunkRev_nixSha256 = hash
8821114 }
8831115 where
884− forcePP :: Name entity -> m (RText 'PathPiece)
885− forcePP = mkPathPiece . untagName
1116+ forcePP :: Name entity -> m (URI.RText 'URI.PathPiece)
1117+ forcePP = URI.mkPathPiece . untagName
8861118
8871119gitThunkRev
8881120 :: MonadObelisk m
@@ -891,11 +1123,11 @@ gitThunkRev
8911123 -> m ThunkRev
8921124gitThunkRev s commit = do
8931125 let u = _gitSource_url s
894− protocols = ["https", "ssh", "git"]
895− Just scheme <- pure $ unRText <$> uriScheme u
1126+ protocols = ["file", "https", "ssh", "git"]
1127+ scheme = maybe "file" URI.unRText $ URI.uriScheme $ (\(GitUri x) -> x) u
8961128 unless (T.toLower scheme `elem` protocols) $
8971129 failWith $ "obelisk currently only supports "
898− <> T.intercalate ", " protocols <> " protocols for non-GitHub remotes"
1130+ <> T.intercalate ", " protocols <> " protocols for plain Git remotes"
8991131 hash <- nixPrefetchGit u commit $ _gitSource_fetchSubmodules s
9001132 putLog Informational $ "Nix sha256 is " <> hash
9011133 pure $ ThunkRev
@@ -908,12 +1140,14 @@ gitThunkRev s commit = do
9081140--
9091141-- If the branch name is passed in, it is returned exactly as-is. If it is not
9101142-- passed it, the default branch of the repo is used instead.
1143+
9111144gitGetCommitBranch
912− :: MonadObelisk m => URI -> Maybe Text -> m (Text, CommitId)
1145+ :: MonadObelisk m => GitUri -> Maybe Text -> m (Text, CommitId)
9131146gitGetCommitBranch uri mbranch = withExitFailMessage ("Failure for git remote " <> uriMsg) $ do
914− bothMaps <- gitLsRemote
915− (T.unpack $ render uri)
1147+ (_, bothMaps) <- gitLsRemote
1148+ (T.unpack $ gitUriToText uri)
9161149 (GitRef_Branch <$> mbranch)
1150+ Nothing
9171151 branch <- case mbranch of
9181152 Nothing -> withExitFailMessage "Failed to find default branch" $ do
9191153 b <- rethrowE $ gitLookupDefaultBranch bothMaps
@@ -927,18 +1161,21 @@ gitGetCommitBranch uri mbranch = withExitFailMessage ("Failure for git remote "
9271161 pure (branch, commit)
9281162 where
9291163 rethrowE = either failWith pure
930− uriMsg = render uri
1164+ uriMsg = gitUriToText uri
1165+
1166+parseGitUri :: Text -> Maybe GitUri
1167+parseGitUri x = GitUri <$> (parseFileURI x <|> parseAbsoluteURI x <|> parseSshShorthand x)
9311168
932−parseGitUri :: Text -> Maybe URI
933−parseGitUri x = parseAbsoluteURI x <|> parseSshShorthand x
1169+parseFileURI :: Text -> Maybe URI.URI
1170+parseFileURI uri = if "/" `T.isPrefixOf` uri then parseAbsoluteURI ("file://" <> uri) else Nothing
9341171
935−parseAbsoluteURI :: Text -> Maybe URI
1172+parseAbsoluteURI :: Text -> Maybe URI.URI
9361173parseAbsoluteURI uri = do
937− parsedUri <- mkURI uri
938− guard $ isPathAbsolute parsedUri
939− pure $ parsedUri
1174+ parsedUri <- URI.mkURI uri
1175+ guard $ URI.isPathAbsolute parsedUri
1176+ pure parsedUri
9401177
941−parseSshShorthand :: Text -> Maybe URI
1178+parseSshShorthand :: Text -> Maybe URI.URI
9421179parseSshShorthand uri = do
9431180 -- This is what git does to check that the remote
9441181 -- is not a local file path when parsing shorthand.
@@ -951,4 +1188,4 @@ parseSshShorthand uri = do
9511188 -- This check is used to disambiguate a filepath containing a colon from shorthand
9521189 guard $ isNothing (T.findIndex (=='/') authAndHostname)
9531190 && not (T.null colonAndPath)
954− mkURI properUri
1191+ URI.mkURI properUri