git-annex/Remote/Git.hs

328 lines
10 KiB
Haskell
Raw Normal View History

{- Standard git remotes.
-
- Copyright 2011-2012 Joey Hess <joey@kitenet.net>
-
- Licensed under the GNU GPL version 3 or higher.
-}
module Remote.Git (remote, repoAvail) where
2011-03-29 17:49:54 +00:00
import qualified Data.Map as M
import Control.Exception.Extensible
2011-10-05 20:02:51 +00:00
import Common.Annex
import Utility.CopyFile
import Utility.RsyncFile
2012-01-10 19:29:10 +00:00
import Remote.Helper.Ssh
import Types.Remote
import qualified Git
2011-12-14 19:56:11 +00:00
import qualified Git.Command
import qualified Git.Config
import qualified Git.Construct
import qualified Annex
import Logs.Presence
import Annex.UUID
2011-10-04 04:40:47 +00:00
import qualified Annex.Content
2011-12-12 21:38:46 +00:00
import qualified Annex.BranchState
import qualified Annex.Branch
2011-08-20 20:11:42 +00:00
import qualified Utility.Url as Url
2011-10-16 04:31:25 +00:00
import Utility.TempFile
import Config
import Init
import Types.Key
2011-12-31 08:11:39 +00:00
remote :: RemoteType
2011-03-29 18:55:59 +00:00
remote = RemoteType {
typename = "git",
2011-03-29 21:57:20 +00:00
enumerate = list,
generate = gen,
2011-03-29 18:55:59 +00:00
setup = error "not supported"
}
2011-03-29 03:51:07 +00:00
2011-03-29 21:57:20 +00:00
list :: Annex [Git.Repo]
list = do
2011-12-14 19:30:14 +00:00
c <- fromRepo Git.config
mapM (tweakurl c) =<< fromRepo Git.remotes
where
annexurl n = "remote." ++ n ++ ".annexurl"
tweakurl c r = do
2011-12-14 19:30:14 +00:00
let n = fromJust $ Git.remoteName r
case M.lookup (annexurl n) c of
Nothing -> return r
2011-12-14 19:30:14 +00:00
Just url -> inRepo $ \g ->
Git.Construct.remoteNamed n $
Git.Construct.fromRemoteLocation url g
2011-12-31 08:11:39 +00:00
gen :: Git.Repo -> UUID -> Maybe RemoteConfig -> Annex Remote
2011-03-30 19:15:46 +00:00
gen r u _ = do
{- It's assumed to be cheap to read the config of non-URL remotes,
- so this is done each time git-annex is run. Conversely,
- the config of an URL remote is only read when there is no
- cached UUID value. -}
2011-03-30 18:00:54 +00:00
let cheap = not $ Git.repoIsUrl r
notignored <- repoNotIgnored r
r' <- case (cheap, notignored, u) of
(_, False, _) -> return r
(True, _, _) -> tryGitConfigRead r
(False, _, NoUUID) -> tryGitConfigRead r
2011-03-30 18:00:54 +00:00
_ -> return r
2011-10-11 18:43:45 +00:00
u' <- getRepoUUID r'
2011-04-01 16:19:26 +00:00
2011-04-09 00:55:22 +00:00
let defcst = if cheap then cheapRemoteCost else expensiveRemoteCost
2011-03-30 19:15:46 +00:00
cst <- remoteCost r' defcst
2011-07-15 16:47:14 +00:00
return Remote {
2011-04-01 16:19:26 +00:00
uuid = u',
2011-03-30 18:00:54 +00:00
cost = cst,
name = Git.repoDescribe r',
storeKey = copyToRemote r',
retrieveKeyFile = copyFromRemote r',
retrieveKeyFileCheap = copyFromRemoteCheap r',
2011-03-30 18:00:54 +00:00
removeKey = dropKey r',
hasKey = inAnnex r',
2011-04-09 00:55:22 +00:00
hasKeyCheap = cheap,
whereisKey = Nothing,
config = Nothing,
repo = r',
remotetype = remote
}
{- Tries to read the config for a specified remote, updates state, and
- returns the updated repo. -}
tryGitConfigRead :: Git.Repo -> Annex Git.Repo
tryGitConfigRead r
2011-12-14 19:30:14 +00:00
| not $ M.null $ Git.config r = return r -- already read
| Git.repoIsSsh r = store $ onRemote r (pipedconfig, r) "configlist" []
2011-09-21 03:24:48 +00:00
| Git.repoIsHttp r = store $ safely geturlconfig
| Git.repoIsUrl r = return r
| otherwise = store $ safely $ onLocal r $ do
ensureInitialized
Annex.getState Annex.repo
where
-- Reading config can fail due to IO error or
-- for other reasons; catch all possible exceptions.
safely a = either (const $ return r) return
=<< liftIO (try a :: IO (Either SomeException Git.Repo))
pipedconfig cmd params = safely $
pOpen ReadFromPipe cmd (toCommand params) $
Git.Config.hRead r
geturlconfig = do
s <- Url.get (Git.repoLocation r ++ "/config")
2011-09-21 03:24:48 +00:00
withTempFile "git-annex.tmp" $ \tmpfile h -> do
hPutStr h s
hClose h
pOpen ReadFromPipe "git" ["config", "--null", "--list", "--file", tmpfile] $
Git.Config.hRead r
2012-01-02 18:54:23 +00:00
store = observe $ \r' -> do
g <- gitRepo
let l = Git.remotes g
2011-12-14 19:30:14 +00:00
let g' = g { Git.remotes = exchange l r' }
Annex.changeState $ \s -> s { Annex.repo = g' }
exchange [] _ = []
2012-03-16 00:39:25 +00:00
exchange (old:ls) new
| Git.remoteName old == Git.remoteName new =
new : exchange ls new
| otherwise =
old : exchange ls new
{- Checks if a given remote has the content for a key inAnnex.
- If the remote cannot be accessed, or if it cannot determine
- whether it has the content, returns a Left error message.
-}
inAnnex :: Git.Repo -> Key -> Annex (Either String Bool)
inAnnex r key
| Git.repoIsHttp r = checkhttp
| Git.repoIsUrl r = checkremote
| otherwise = checklocal
where
checkhttp = liftIO $ go undefined $ keyUrls r key
where
go e [] = return $ Left e
go _ (u:us) = do
res <- catchMsgIO $
Url.check u (keySize key)
case res of
Left e -> go e us
v -> return v
checkremote = do
showAction $ "checking " ++ Git.repoDescribe r
onRemote r (check, unknown) "inannex" [Param (show key)]
where
check c p = dispatch <$> safeSystem c p
dispatch ExitSuccess = Right True
dispatch (ExitFailure 1) = Right False
dispatch _ = unknown
checklocal = dispatch <$> check
where
check = liftIO $ catchMsgIO $ onLocal r $
Annex.Content.inAnnexSafe key
dispatch (Left e) = Left e
dispatch (Right (Just b)) = Right b
dispatch (Right Nothing) = unknown
unknown = Left $ "unable to check " ++ Git.repoDescribe r
{- Checks inexpensively if a repository is available for use. -}
repoAvail :: Git.Repo -> Annex Bool
repoAvail r
| Git.repoIsHttp r = return True
| Git.repoIsUrl r = return True
| otherwise = liftIO $ catchBoolIO $ onLocal r $ return True
{- Runs an action on a local repository inexpensively, by making an annex
- monad using that repository. -}
onLocal :: Git.Repo -> Annex a -> IO a
onLocal r a = do
-- Avoid re-reading the repository's configuration if it was
-- already read.
2011-12-14 19:30:14 +00:00
state <- if M.null $ Git.config r
then Annex.new r
else return $ Annex.newState r
Annex.eval state $ do
-- No need to update the branch; its data is not used
-- for anything onLocal is used to do.
2011-12-12 21:38:46 +00:00
Annex.BranchState.disableUpdate
2012-01-03 04:29:27 +00:00
liftIO Git.Command.reap `after` a
keyUrls :: Git.Repo -> Key -> [String]
keyUrls r key = map tourl (annexLocations key)
where
tourl l = Git.repoLocation r ++ "/" ++ l
dropKey :: Git.Repo -> Key -> Annex Bool
2011-08-17 01:20:14 +00:00
dropKey r key
| not $ Git.repoIsUrl r = commitOnCleanup r $ liftIO $ onLocal r $ do
ensureInitialized
whenM (Annex.Content.inAnnex key) $ do
Annex.Content.lockContent key $
Annex.Content.removeAnnex key
Annex.Content.logStatus key InfoMissing
Annex.Content.saveState True
return True
2011-08-17 01:20:14 +00:00
| Git.repoIsHttp r = error "dropping from http repo not supported"
| otherwise = commitOnCleanup r $ onRemote r (boolSystem, False) "dropkey"
[ Params "--quiet --force"
, Param $ show key
]
{- Tries to copy a key's content from a remote's annex to a file. -}
copyFromRemote :: Git.Repo -> Key -> FilePath -> Annex Bool
copyFromRemote r key file
| not $ Git.repoIsUrl r = do
params <- rsyncParams r
loc <- liftIO $ gitAnnexLocation key r
rsyncOrCopyFile params loc file
| Git.repoIsSsh r = rsyncHelper =<< rsyncParamsRemote r True key file
| Git.repoIsHttp r = Annex.Content.downloadUrl (keyUrls r key) file
| otherwise = error "copying from non-ssh, non-http repo not supported"
copyFromRemoteCheap :: Git.Repo -> Key -> FilePath -> Annex Bool
copyFromRemoteCheap r key file
| not $ Git.repoIsUrl r = do
loc <- liftIO $ gitAnnexLocation key r
liftIO $ catchBoolIO $ createSymbolicLink loc file >> return True
2012-03-16 00:39:25 +00:00
| Git.repoIsSsh r =
ifM (Annex.Content.preseedTmp key file)
( copyFromRemote r key file
, return False
)
| otherwise = return False
{- Tries to copy a key's content to a remote's annex. -}
copyToRemote :: Git.Repo -> Key -> Annex Bool
copyToRemote r key
| not $ Git.repoIsUrl r = commitOnCleanup r $ do
keysrc <- inRepo $ gitAnnexLocation key
params <- rsyncParams r
-- run copy from perspective of remote
liftIO $ onLocal r $ do
ensureInitialized
Annex.Content.saveState True `after`
2012-01-03 04:29:27 +00:00
Annex.Content.getViaTmp key
(rsyncOrCopyFile params keysrc)
| Git.repoIsSsh r = commitOnCleanup r $ do
keysrc <- inRepo $ gitAnnexLocation key
rsyncHelper =<< rsyncParamsRemote r False key keysrc
| otherwise = error "copying to non-ssh repo not supported"
2011-07-15 16:47:14 +00:00
rsyncHelper :: [CommandParam] -> Annex Bool
rsyncHelper p = do
showOutput -- make way for progress bar
2012-03-16 00:39:25 +00:00
ifM (liftIO $ rsync p)
( return True
, do
showLongNote "rsync failed -- run git annex again to resume file transfer"
2012-03-16 00:39:25 +00:00
return False
)
{- Copys a file with rsync unless both locations are on the same
- filesystem. Then cp could be faster. -}
rsyncOrCopyFile :: [CommandParam] -> FilePath -> FilePath -> Annex Bool
2012-03-16 00:39:25 +00:00
rsyncOrCopyFile rsyncparams src dest =
ifM (sameDeviceIds src dest)
( liftIO $ copyFileExternal src dest
, rsyncHelper $ rsyncparams ++ [Param src, Param dest]
)
where
sameDeviceIds a b = (==) <$> (getDeviceId a) <*> (getDeviceId b)
getDeviceId f = deviceID <$> liftIO (getFileStatus $ parentDir f)
{- Generates rsync parameters that ssh to the remote and asks it
- to either receive or send the key's content. -}
rsyncParamsRemote :: Git.Repo -> Bool -> Key -> FilePath -> Annex [CommandParam]
rsyncParamsRemote r sending key file = do
Just (shellcmd, shellparams) <- git_annex_shell r
(if sending then "sendkey" else "recvkey")
[ Param $ show key
-- Command is terminated with "--", because
-- rsync will tack on its own options afterwards,
-- and they need to be ignored.
, Param "--"
]
-- Convert the ssh command into rsync command line.
let eparam = rsyncShell (Param shellcmd:shellparams)
o <- rsyncParams r
if sending
then return $ o ++ eparam ++ [dummy, File file]
else return $ o ++ eparam ++ [File file, dummy]
where
-- The rsync shell parameter controls where rsync
-- goes, so the source/dest parameter can be a dummy value,
-- that just enables remote rsync mode.
-- For maximum compatability with some patched rsyncs,
-- the dummy value needs to still contain a hostname,
-- even though this hostname will never be used.
dummy = Param "dummy:"
rsyncParams :: Git.Repo -> Annex [CommandParam]
rsyncParams r = do
o <- getRemoteConfig r "rsync-options" ""
return $ options ++ map Param (words o)
where
-- --inplace to resume partial files
options = [Params "-p --progress --inplace"]
commitOnCleanup :: Git.Repo -> Annex a -> Annex a
commitOnCleanup r a = go `after` a
where
go = Annex.addCleanup (Git.repoLocation r) cleanup
cleanup
| not $ Git.repoIsUrl r = liftIO $ onLocal r $
Annex.Branch.commit "update"
| otherwise = do
Just (shellcmd, shellparams) <-
git_annex_shell r "commit" []
-- Throw away stderr, since the remote may not
-- have a new enough git-annex shell to
-- support committing.
let cmd = shellcmd ++ " "
++ unwords (map shellEscape $ toCommand shellparams)
++ ">/dev/null 2>/dev/null"
_ <- liftIO $
boolSystem "sh" [Param "-c", Param cmd]
return ()