git-annex/Commands.hs

524 lines
17 KiB
Haskell
Raw Normal View History

2010-10-14 18:38:29 +00:00
{- git-annex command line -}
2010-10-14 07:18:11 +00:00
module Commands (parseCmd) where
2010-10-14 07:18:11 +00:00
2010-10-14 18:38:29 +00:00
import System.Console.GetOpt
2010-10-14 07:18:11 +00:00
import Control.Monad.State (liftIO)
import System.Posix.Files
import System.Directory
2010-10-15 20:09:30 +00:00
import System.Path
2010-10-14 07:18:11 +00:00
import Data.String.Utils
import Control.Monad (filterM)
2010-10-14 07:18:11 +00:00
import List
2010-10-14 21:37:20 +00:00
import IO
2010-10-16 20:20:49 +00:00
2010-10-14 07:18:11 +00:00
import qualified GitRepo as Git
import qualified Annex
import Utility
import Locations
import qualified Backend
import UUID
import LocationLog
import Types
2010-10-14 18:38:29 +00:00
import Core
2010-10-14 21:37:20 +00:00
import qualified Remotes
2010-10-18 06:06:27 +00:00
import qualified TypeInternals
{- A subcommand runs in three stages. Each stage can return the next stage
2010-10-25 19:46:53 +00:00
- to run.
-
- 1. The start stage is run before anything is printed about the
- subcommand, and can early abort it if the input does not make sense.
- It should run quickly and should not modify Annex state.
-
- 2. The perform stage is run after a message is printed about the subcommand
2010-10-25 19:46:53 +00:00
- being run, and it should be where the bulk of the work happens.
-
2010-10-25 19:46:53 +00:00
- 3. The cleanup stage is run only if the perform stage succeeds, and it
- returns the overall success/fail of the subcommand.
-}
type SubCmdStart = String -> Annex (Maybe SubCmdPerform)
type SubCmdPerform = Annex (Maybe SubCmdCleanup)
type SubCmdCleanup = Annex Bool
{- Runs a subcommand through its three stages. -}
2010-10-25 23:17:11 +00:00
doSubCmd :: String -> SubCmdStart -> String -> Annex Bool
doSubCmd cmdname start param = do
res <- start param :: Annex (Maybe SubCmdPerform)
case (res) of
2010-10-25 23:17:11 +00:00
Nothing -> return True
Just perform -> do
showStart cmdname param
res <- perform :: Annex (Maybe SubCmdCleanup)
case (res) of
2010-10-25 23:17:11 +00:00
Nothing -> do
showEndFail
return False
Just cleanup -> do
res <- cleanup
if (res)
2010-10-25 23:17:11 +00:00
then do
showEndOk
return True
else do
showEndFail
return False
2010-10-25 19:49:52 +00:00
{- A subcommand can broadly want one of several kinds of input parameters.
- This allows a first stage of filtering before starting a subcommand. -}
data SubCmdWants = FilesInGit | FilesNotInGit | FilesMissing
2010-10-27 18:33:44 +00:00
| Description | Keys | Tempfile | FilesToBeCommitted
2010-10-25 19:49:52 +00:00
data SubCommand = Command {
subcmdname :: String,
subcmdaction :: SubCmdStart,
subcmdwants :: SubCmdWants,
subcmddesc :: String
}
subCmds :: [SubCommand]
subCmds = [
(Command "add" addStart FilesNotInGit
2010-10-17 21:10:20 +00:00
"add files to annex")
, (Command "get" getStart FilesInGit
2010-10-17 21:10:20 +00:00
"make content of annexed files available")
, (Command "drop" dropStart FilesInGit
2010-10-17 21:10:20 +00:00
"indicate content of files not currently wanted")
, (Command "move" moveStart FilesInGit
2010-10-23 18:27:04 +00:00
"transfer content of files to/from another repository")
, (Command "init" initStart Description
2010-10-17 21:10:20 +00:00
"initialize git-annex with repository description")
, (Command "unannex" unannexStart FilesInGit
2010-10-21 21:59:32 +00:00
"undo accidential add command")
, (Command "fix" fixStart FilesInGit
2010-10-27 18:33:44 +00:00
"fix up symlinks to point to annexed content")
, (Command "pre-commit" fixStart FilesToBeCommitted
"fix up symlinks before they are committed")
, (Command "fromkey" fromKeyStart FilesMissing
"adds a file using a specific key")
2010-10-25 22:33:59 +00:00
, (Command "dropkey" dropKeyStart Keys
"drops annexed content for specified keys")
, (Command "setkey" setKeyStart Tempfile
"sets annexed content for a key using a temp file")
]
-- Each dashed command-line option results in generation of an action
-- in the Annex monad that performs the necessary setting.
options :: [OptDescr (Annex ())]
2010-10-16 23:43:32 +00:00
options = [
2010-10-21 21:59:32 +00:00
Option ['f'] ["force"] (NoArg (storebool "force" True))
"allow actions that may lose annexed data"
, Option ['q'] ["quiet"] (NoArg (storebool "quiet" True))
"avoid verbose output"
2010-10-21 21:59:32 +00:00
, Option ['b'] ["backend"] (ReqArg (storestring "backend") "NAME")
"specify default key-value backend to use"
2010-10-21 21:59:32 +00:00
, Option ['k'] ["key"] (ReqArg (storestring "key") "KEY")
"specify a key to use"
, Option ['t'] ["to"] (ReqArg (storestring "torepository") "REPOSITORY")
2010-10-23 18:27:04 +00:00
"specify to where to transfer content"
, Option ['f'] ["from"] (ReqArg (storestring "fromrepository") "REPOSITORY")
2010-10-23 18:27:04 +00:00
"specify from where to transfer content"
2010-10-16 23:43:32 +00:00
]
2010-10-21 21:59:32 +00:00
where
storebool n b = Annex.flagChange n $ FlagBool b
storestring n s = Annex.flagChange n $ FlagString s
2010-10-16 23:43:32 +00:00
header = "Usage: git-annex " ++ (join "|" $ map subcmdname subCmds)
2010-10-17 21:10:20 +00:00
2010-10-25 19:49:52 +00:00
{- Usage message with lists of options and subcommands. -}
usage :: String
2010-10-17 21:10:20 +00:00
usage = usageInfo header options ++ "\nSubcommands:\n" ++ cmddescs
where
cmddescs = unlines $ map (\c -> indent $ showcmd c) subCmds
2010-10-17 21:10:20 +00:00
showcmd c =
(subcmdname c) ++
2010-10-27 18:33:44 +00:00
(pad 11 (subcmdname c)) ++
(descWanted (subcmdwants c)) ++
(pad 13 (descWanted (subcmdwants c))) ++
(subcmddesc c)
2010-10-17 21:10:20 +00:00
indent l = " " ++ l
pad n s = take (n - (length s)) $ repeat ' '
2010-10-25 18:10:38 +00:00
{- Generate descriptions of wanted parameters for subcommands. -}
descWanted :: SubCmdWants -> String
descWanted Description = "DESCRIPTION"
descWanted Keys = "KEY ..."
descWanted _ = "PATH ..."
2010-10-17 21:10:20 +00:00
{- Finds the type of parameters a subcommand wants, from among the passed
- parameter list. -}
findWanted :: SubCmdWants -> [String] -> Git.Repo -> IO [String]
findWanted FilesNotInGit params repo = do
files <- mapM (Git.notInRepo repo) params
return $ foldl (++) [] files
findWanted FilesInGit params repo = do
files <- mapM (Git.inRepo repo) params
return $ foldl (++) [] files
findWanted FilesMissing params repo = do
files <- liftIO $ filterM missing params
return $ files
where
missing f = do
e <- doesFileExist f
if (e) then return False else return True
findWanted Description params _ = do
2010-10-16 20:15:31 +00:00
return $ [unwords params]
2010-10-27 18:33:44 +00:00
findWanted FilesToBeCommitted params repo = do
files <- mapM gitcached params
return $ foldl (++) [] files
where
gitcached p = do
-- ask git for files staged for commit that
-- are being added, moved, or changed (but not deleted)
fs0 <- Git.pipeRead repo ["diff", "--cached",
"--name-only", "--diff-filter=ACMRT",
"-z", "HEAD", p]
return $ filter (not . null) $ split "\0" fs0
findWanted _ params _ = return params
2010-10-14 18:38:29 +00:00
{- Parses command line and returns two lists of actions to be
- run in the Annex monad. The first actions configure it
- according to command line options, while the second actions
- handle subcommands. -}
2010-10-25 23:17:11 +00:00
parseCmd :: [String] -> AnnexState -> IO ([Annex Bool], [Annex Bool])
parseCmd argv state = do
(flags, params) <- getopt
2010-10-23 00:47:14 +00:00
if (null params)
then error usage
else case (lookupCmd (params !! 0)) of
2010-10-17 00:03:41 +00:00
[] -> error usage
[Command name action want _] -> do
f <- findWanted want (drop 1 params)
2010-10-18 06:06:27 +00:00
(TypeInternals.repo state)
2010-10-25 23:17:11 +00:00
let actions = map (doSubCmd name action) $
filter notstate f
let configactions = map (\f -> do
f
return True) flags
return (configactions, actions)
where
-- never include files from the state directory
notstate f = stateLoc /= take (length stateLoc) f
getopt = case getOpt Permute options argv of
(flags, params, []) -> return (flags, params)
2010-10-17 00:03:41 +00:00
(_, _, errs) -> ioError (userError (concat errs ++ usage))
lookupCmd cmd = filter (\c -> cmd == subcmdname c) subCmds
2010-10-14 18:38:29 +00:00
{- The add subcommand annexes a file, storing it in a backend, and then
- moving it into the annex directory and setting up the symlink pointing
- to its content. -}
addStart :: FilePath -> Annex (Maybe SubCmdPerform)
addStart file = notAnnexed file $ do
s <- liftIO $ getSymbolicLinkStatus file
if ((isSymbolicLink s) || (not $ isRegularFile s))
then return Nothing
else return $ Just $ addPerform file
addPerform :: FilePath -> Annex (Maybe SubCmdCleanup)
addPerform file = do
g <- Annex.gitRepo
stored <- Backend.storeFileKey file
case (stored) of
Nothing -> return Nothing
Just (key, backend) -> return $ Just $ addCleanup file key
addCleanup :: FilePath -> Key -> Annex Bool
addCleanup file key = do
logStatus key ValuePresent
g <- Annex.gitRepo
let dest = annexLocation g key
liftIO $ createDirectoryIfMissing True (parentDir dest)
liftIO $ renameFile file dest
link <- calcGitLink file key
liftIO $ createSymbolicLink link file
2010-10-26 20:15:29 +00:00
Annex.queue "add" [] file
return True
2010-10-14 07:18:11 +00:00
{- The unannex subcommand undoes an add. -}
unannexStart :: FilePath -> Annex (Maybe SubCmdPerform)
unannexStart file = isAnnexed file $ \(key, backend) -> do
return $ Just $ unannexPerform file key backend
unannexPerform :: FilePath -> Key -> Backend -> Annex (Maybe SubCmdCleanup)
unannexPerform file key backend = do
-- force backend to always remove
Annex.flagChange "force" $ FlagBool True
Backend.removeKey backend key
return $ Just $ unannexCleanup file key
unannexCleanup :: FilePath -> Key -> Annex Bool
unannexCleanup file key = do
logStatus key ValueMissing
g <- Annex.gitRepo
let src = annexLocation g key
liftIO $ removeFile file
liftIO $ Git.run g ["rm", "--quiet", file]
-- git rm deletes empty directories; put them back
liftIO $ createDirectoryIfMissing True (parentDir file)
liftIO $ renameFile src file
return True
2010-10-14 07:18:11 +00:00
{- Gets an annexed file from one of the backends. -}
getStart :: FilePath -> Annex (Maybe SubCmdPerform)
getStart file = isAnnexed file $ \(key, backend) -> do
2010-10-14 23:36:11 +00:00
inannex <- inAnnex key
2010-10-14 07:18:11 +00:00
if (inannex)
then return Nothing
else return $ Just $ getPerform file key backend
getPerform :: FilePath -> Key -> Backend -> Annex (Maybe SubCmdCleanup)
getPerform file key backend = do
ok <- getViaTmp key (Backend.retrieveKeyFile backend key)
if (ok)
then return $ Just $ return True
else return Nothing
2010-10-21 21:59:32 +00:00
2010-10-16 20:15:31 +00:00
{- Indicates a file's content is not wanted anymore, and should be removed
- if it's safe to do so. -}
dropStart :: FilePath -> Annex (Maybe SubCmdPerform)
dropStart file = isAnnexed file $ \(key, backend) -> do
inbackend <- Backend.hasKey key
if (not inbackend)
then return Nothing
else return $ Just $ dropPerform key backend
dropPerform :: Key -> Backend -> Annex (Maybe SubCmdCleanup)
dropPerform key backend = do
success <- Backend.removeKey backend key
if (success)
then return $ Just $ dropCleanup key
else return Nothing
dropCleanup :: Key -> Annex Bool
dropCleanup key = do
logStatus key ValueMissing
inannex <- inAnnex key
if (inannex)
then do
g <- Annex.gitRepo
let loc = annexLocation g key
liftIO $ removeFile loc
return True
else return True
2010-10-14 07:18:11 +00:00
{- Drops cached content for a key. -}
dropKeyStart :: String -> Annex (Maybe SubCmdPerform)
dropKeyStart keyname = do
backends <- Backend.list
let key = genKey (backends !! 0) keyname
present <- inAnnex key
force <- Annex.flagIsSet "force"
if (not present)
then return Nothing
else if (not force)
then error "dropkey is can cause data loss; use --force if you're sure you want to do this"
else return $ Just $ dropKeyPerform key
dropKeyPerform :: Key -> Annex (Maybe SubCmdCleanup)
dropKeyPerform key = do
g <- Annex.gitRepo
let loc = annexLocation g key
liftIO $ removeFile loc
return $ Just $ dropKeyCleanup key
dropKeyCleanup :: Key -> Annex Bool
dropKeyCleanup key = do
logStatus key ValueMissing
return True
{- Sets cached content for a key. -}
setKeyStart :: FilePath -> Annex (Maybe SubCmdPerform)
setKeyStart tmpfile = do
keyname <- Annex.flagGet "key"
if (null keyname)
then error "please specify the key with --key"
else return ()
backends <- Backend.list
let key = genKey (backends !! 0) keyname
return $ Just $ setKeyPerform tmpfile key
setKeyPerform :: FilePath -> Key -> Annex (Maybe SubCmdCleanup)
setKeyPerform tmpfile key = do
g <- Annex.gitRepo
let loc = annexLocation g key
ok <- liftIO $ boolSystem "mv" [tmpfile, loc]
if (not ok)
then error "mv failed!"
else return $ Just $ setKeyCleanup tmpfile key
setKeyCleanup :: FilePath -> Key -> Annex Bool
setKeyCleanup tmpfile key = do
logStatus key ValuePresent
return True
2010-10-17 01:03:25 +00:00
{- Fixes the symlink to an annexed file. -}
fixStart :: FilePath -> Annex (Maybe SubCmdPerform)
fixStart file = isAnnexed file $ \(key, backend) -> do
2010-10-17 01:03:25 +00:00
link <- calcGitLink file key
2010-10-17 21:10:20 +00:00
l <- liftIO $ readSymbolicLink file
if (link == l)
then return Nothing
else return $ Just $ fixPerform file link
fixPerform :: FilePath -> FilePath -> Annex (Maybe SubCmdCleanup)
fixPerform file link = do
liftIO $ createDirectoryIfMissing True (parentDir file)
liftIO $ removeFile file
liftIO $ createSymbolicLink link file
2010-10-26 20:15:29 +00:00
return $ Just $ fixCleanup file
fixCleanup :: FilePath -> Annex Bool
fixCleanup file = do
Annex.queue "add" [] file
return True
2010-10-17 01:03:25 +00:00
{- Stores description for the repository etc. -}
initStart :: String -> Annex (Maybe SubCmdPerform)
initStart description = do
2010-10-23 00:47:14 +00:00
if (null description)
2010-10-17 21:10:20 +00:00
then error $
"please specify a description of this repository\n" ++
usage
else return $ Just $ initPerform description
initPerform :: String -> Annex (Maybe SubCmdCleanup)
initPerform description = do
g <- Annex.gitRepo
u <- getUUID g
describeUUID u description
liftIO $ gitAttributes g
2010-10-27 18:33:44 +00:00
liftIO $ gitPreCommitHook g
return $ Just $ initCleanup
initCleanup :: Annex Bool
initCleanup = do
g <- Annex.gitRepo
log <- uuidLog
liftIO $ Git.run g ["add", log]
liftIO $ Git.run g ["commit", "-m", "git annex init", log]
return True
2010-10-16 20:15:31 +00:00
{- Adds a file pointing at a manually-specified key -}
fromKeyStart :: FilePath -> Annex (Maybe SubCmdPerform)
fromKeyStart file = do
keyname <- Annex.flagGet "key"
2010-10-23 00:47:14 +00:00
if (null keyname)
then error "please specify the key with --key"
else return ()
backends <- Backend.list
let key = genKey (backends !! 0) keyname
inbackend <- Backend.hasKey key
if (not inbackend)
then error $ "key ("++keyname++") is not present in backend"
else return $ Just $ fromKeyPerform file key
fromKeyPerform :: FilePath -> Key -> Annex (Maybe SubCmdCleanup)
fromKeyPerform file key = do
link <- calcGitLink file key
liftIO $ createDirectoryIfMissing True (parentDir file)
liftIO $ createSymbolicLink link file
return $ Just $ fromKeyCleanup file
fromKeyCleanup :: FilePath -> Annex Bool
fromKeyCleanup file = do
2010-10-26 20:15:29 +00:00
Annex.queue "add" [] file
return True
2010-10-23 18:27:04 +00:00
{- Move a file either --to or --from a repository.
-
- This only operates on the cached file content; it does not involve
- moving data in the key-value backend. -}
moveStart :: FilePath -> Annex (Maybe SubCmdPerform)
moveStart file = do
2010-10-23 18:27:04 +00:00
fromName <- Annex.flagGet "fromrepository"
toName <- Annex.flagGet "torepository"
case (fromName, toName) of
("", "") -> error "specify either --from or --to"
("", to) -> moveToStart file
(from, "") -> moveFromStart file
2010-10-23 18:27:04 +00:00
(_, _) -> error "only one of --from or --to can be specified"
{- Moves the content of an annexed file to another repository,
- removing it from the current repository, and updates locationlog
- information on both.
-
- If the destination already has the content, it is still removed
- from the current repository.
-
- Note that unlike drop, this does not honor annex.numcopies.
- A file's content can be moved even if there are insufficient copies to
- allow it to be dropped.
2010-10-23 18:27:04 +00:00
-}
moveToStart :: FilePath -> Annex (Maybe SubCmdPerform)
moveToStart file = isAnnexed file $ \(key, backend) -> do
2010-10-23 18:27:04 +00:00
ishere <- inAnnex key
if (not ishere)
then return Nothing -- not here, so nothing to do
else return $ Just $ moveToPerform file key
moveToPerform :: FilePath -> Key -> Annex (Maybe SubCmdCleanup)
moveToPerform file key = do
-- checking the remote is expensive, so not done in the start step
remote <- Remotes.commandLineRemote
isthere <- Remotes.inAnnex remote key
case isthere of
Left err -> do
showNote $ show err
return Nothing
Right False -> do
2010-10-26 00:52:03 +00:00
Core.showNote $ "moving to " ++ (Git.repoDescribe remote) ++ "..."
let tmpfile = (annexTmpLocation remote) ++ (keyFile key)
ok <- Remotes.copyToRemote remote key tmpfile
if (ok)
then return $ Just $ moveToCleanup remote key tmpfile
else return Nothing -- failed
Right True -> return $ Just $ dropCleanup key
moveToCleanup :: Git.Repo -> Key -> FilePath -> Annex Bool
moveToCleanup remote key tmpfile = do
-- Tell remote to use the transferred content.
Remotes.runCmd remote "git-annex" ["setkey", "--quiet",
"--backend=" ++ (backendName key),
"--key=" ++ keyName key,
tmpfile]
-- Record that the key is present on the remote.
g <- Annex.gitRepo
remoteuuid <- getUUID remote
2010-10-26 20:15:29 +00:00
log <- liftIO $ logChange g key remoteuuid ValuePresent
Annex.queue "add" [] log
-- Cleanup on the local side is the same as done for the
-- drop subcommand.
dropCleanup key
2010-10-23 18:27:04 +00:00
{- Moves the content of an annexed file from another repository to the current
- repository and updates locationlog information on both.
-
- If the current repository already has the content, it is still removed
- from the other repository.
-}
moveFromStart :: FilePath -> Annex (Maybe SubCmdPerform)
moveFromStart file = isAnnexed file $ \(key, backend) -> do
g <- Annex.gitRepo
remote <- Remotes.commandLineRemote
l <- Remotes.keyPossibilities key
if (elem remote l)
then return $ Just $ moveFromPerform file key
else return Nothing
moveFromPerform :: FilePath -> Key -> Annex (Maybe SubCmdCleanup)
moveFromPerform file key = do
2010-10-23 18:27:04 +00:00
remote <- Remotes.commandLineRemote
ishere <- inAnnex key
if (ishere)
then return $ Just $ moveFromCleanup remote key
else do
2010-10-26 00:52:03 +00:00
Core.showNote $ "moving from " ++ (Git.repoDescribe remote) ++ "..."
ok <- getViaTmp key (Remotes.copyFromRemote remote key)
if (ok)
then return $ Just $ moveFromCleanup remote key
else return Nothing -- fail
moveFromCleanup :: Git.Repo -> Key -> Annex Bool
moveFromCleanup remote key = do
Remotes.runCmd remote "git-annex" ["dropkey", "--quiet", "--force",
"--backend=" ++ (backendName key),
keyName key]
-- Record locally that the key is not on the remote.
remoteuuid <- getUUID remote
g <- Annex.gitRepo
2010-10-26 20:15:29 +00:00
log <- liftIO $ logChange g key remoteuuid ValueMissing
Annex.queue "add" [] log
return True
2010-10-23 18:27:04 +00:00
2010-10-17 16:08:59 +00:00
-- helpers
2010-10-23 18:27:04 +00:00
notAnnexed file a = do
r <- Backend.lookupFile file
case (r) of
Just v -> return Nothing
Nothing -> a
2010-10-23 18:27:04 +00:00
isAnnexed file a = do
r <- Backend.lookupFile file
2010-10-14 07:40:26 +00:00
case (r) of
Just v -> a v
Nothing -> return Nothing