{- git-annex command line -} module Commands (parseCmd) where import System.Console.GetOpt import Control.Monad.State (liftIO) import System.Posix.Files import System.Directory import System.Path import Data.String.Utils import List import IO import qualified GitRepo as Git import qualified Annex import Utility import Locations import qualified Backend import UUID import LocationLog import Types import Core import qualified Remotes import qualified TypeInternals data CmdWants = FilesInGit | FilesNotInGit | RepoName | SingleString data Command = Command { cmdname :: String, cmdaction :: (String -> Annex ()), cmdwants :: CmdWants, cmddesc :: String } cmds :: [Command] cmds = [ (Command "add" addCmd FilesNotInGit "add files to annex") , (Command "get" getCmd FilesInGit "make content of annexed files available") , (Command "drop" dropCmd FilesInGit "indicate content of files not currently wanted") , (Command "unannex" unannexCmd FilesInGit "undo accidential add command") , (Command "init" initCmd SingleString "initialize git-annex with repository description") , (Command "fix" fixCmd FilesInGit "fix up files' symlinks to point to annexed content") ] options = [ Option ['f'] ["force"] (NoArg Force) "allow actions that may lose annexed data" ] header = "Usage: git-annex " ++ (join "|" $ map cmdname cmds) ++ " [path ...]" usage = usageInfo header options ++ "\nSubcommands:\n" ++ cmddescs where cmddescs = unlines $ map (\c -> indent $ showcmd c) cmds showcmd c = (cmdname c) ++ (take (10 - (length (cmdname c))) $ repeat ' ') ++ (cmddesc c) indent l = " " ++ l {- Finds the type of parameters a command wants, from among the passed - parameter list. -} findWanted :: CmdWants -> [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 SingleString params _ = do return $ [unwords params] findWanted RepoName params _ = do return $ params {- Parses command line and returns a list of flags and a list of - actions to be run in the Annex monad. -} parseCmd :: [String] -> AnnexState -> IO ([Flag], [Annex ()]) parseCmd argv state = do (flags, params) <- getopt case (length params) of 0 -> error usage _ -> case (lookupCmd (params !! 0)) of [] -> error usage [Command _ action want _] -> do f <- findWanted want (drop 1 params) (TypeInternals.repo state) return (flags, map action $ filter notstate f) 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) (_, _, errs) -> ioError (userError (concat errs ++ usage)) lookupCmd cmd = filter (\c -> cmd == cmdname c) cmds {- 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. -} addCmd :: FilePath -> Annex () addCmd file = inBackend file $ do s <- liftIO $ getSymbolicLinkStatus file if ((isSymbolicLink s) || (not $ isRegularFile s)) then return () else do showStart "add" file g <- Annex.gitRepo stored <- Backend.storeFileKey file case (stored) of Nothing -> showEndFail Just (key, backend) -> do logStatus key ValuePresent setup g key where setup g key = do let dest = annexLocation g key liftIO $ createDirectoryIfMissing True (parentDir dest) liftIO $ renameFile file dest link <- calcGitLink file key liftIO $ createSymbolicLink link file liftIO $ Git.run g ["add", file] showEndOk {- Undo addCmd. -} unannexCmd :: FilePath -> Annex () unannexCmd file = notinBackend file $ \(key, backend) -> do showStart "unannex" file Annex.flagChange Force True -- force backend to always remove Backend.removeKey backend key logStatus key ValueMissing g <- Annex.gitRepo let src = annexLocation g key moveout g src where moveout g src = do 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 showEndOk {- Gets an annexed file from one of the backends. -} getCmd :: FilePath -> Annex () getCmd file = notinBackend file $ \(key, backend) -> do inannex <- inAnnex key if (inannex) then return () else do showStart "get" file g <- Annex.gitRepo let dest = annexLocation g key let tmp = (annexTmpLocation g) ++ (keyFile key) liftIO $ createDirectoryIfMissing True (parentDir tmp) success <- Backend.retrieveKeyFile backend key tmp if (success) then do liftIO $ renameFile tmp dest logStatus key ValuePresent showEndOk else do showEndFail {- Indicates a file's content is not wanted anymore, and should be removed - if it's safe to do so. -} dropCmd :: FilePath -> Annex () dropCmd file = notinBackend file $ \(key, backend) -> do inbackend <- Backend.hasKey key if (not inbackend) then return () -- no-op else do showStart "drop" file success <- Backend.removeKey backend key if (success) then do cleanup key showEndOk else showEndFail where cleanup key = do logStatus key ValueMissing inannex <- inAnnex key if (inannex) then do g <- Annex.gitRepo let loc = annexLocation g key liftIO $ removeFile loc return () else return () {- Fixes the symlink to an annexed file. -} fixCmd :: String -> Annex () fixCmd file = notinBackend file $ \(key, backend) -> do link <- calcGitLink file key l <- liftIO $ readSymbolicLink file if (link == l) then return () else do showStart "fix" file liftIO $ createDirectoryIfMissing True (parentDir file) liftIO $ removeFile file liftIO $ createSymbolicLink link file g <- Annex.gitRepo liftIO $ Git.run g ["add", file] showEndOk {- Stores description for the repository. -} initCmd :: String -> Annex () initCmd description = do if (0 == length description) then error $ "please specify a description of this repository\n" ++ usage else do g <- Annex.gitRepo u <- getUUID g describeUUID u description log <- uuidLog liftIO $ Git.run g ["add", log] liftIO $ Git.run g ["commit", "-m", "git annex init", log] liftIO $ putStrLn "description set" -- helpers inBackend file a = do r <- Backend.lookupFile file case (r) of Just v -> return () Nothing -> a notinBackend file a = do r <- Backend.lookupFile file case (r) of Just v -> a v Nothing -> return ()