git-annex/Command.hs

273 lines
9.5 KiB
Haskell
Raw Normal View History

2010-11-11 22:54:52 +00:00
{- git-annex commands
-
- Copyright 2010 Joey Hess <joey@kitenet.net>
-
- Licensed under the GNU GPL version 3 or higher.
-}
module Command where
2010-11-11 22:54:52 +00:00
import Control.Monad.State (liftIO)
import System.Directory
import System.Posix.Files
import Control.Monad (filterM, liftM, when)
import System.Path.WildMatch
import Text.Regex.PCRE.Light.Char8
2011-01-27 20:31:29 +00:00
import Data.List
2011-05-15 06:49:43 +00:00
import Data.Maybe
2011-05-28 20:09:11 +00:00
import Data.String.Utils
2010-11-11 22:54:52 +00:00
import Types
2010-11-04 17:28:49 +00:00
import qualified Backend
2010-11-08 19:15:21 +00:00
import Messages
2010-11-04 17:28:49 +00:00
import qualified Annex
2010-11-11 22:54:52 +00:00
import qualified GitRepo as Git
import Locations
2011-02-01 00:14:08 +00:00
import Utility
import Types.Key
{- A command runs in four stages.
-
- 0. The seek stage takes the parameters passed to the command,
- looks through the repo to find the ones that are relevant
- to that command (ie, new files to add), and generates
- a list of start stage actions. -}
type CommandSeek = [String] -> Annex [CommandStart]
{- 1. The start stage is run before anything is printed about the
- command, is passed some input, and can early abort it
- if the input does not make sense. It should run quickly and
- should not modify Annex state. -}
type CommandStart = Annex (Maybe CommandPerform)
{- 2. The perform stage is run after a message is printed about the command
- being run, and it should be where the bulk of the work happens. -}
type CommandPerform = Annex (Maybe CommandCleanup)
{- 3. The cleanup stage is run only if the perform stage succeeds, and it
- returns the overall success/fail of the command. -}
type CommandCleanup = Annex Bool
{- Some helper functions are used to build up CommandSeek and CommandStart
- functions. -}
type CommandSeekStrings = CommandStartString -> CommandSeek
type CommandStartString = String -> CommandStart
type CommandSeekWords = CommandStartWords -> CommandSeek
type CommandStartWords = [String] -> CommandStart
type CommandSeekKeys = CommandStartKey -> CommandSeek
type CommandStartKey = Key -> CommandStart
type BackendFile = (FilePath, Maybe (Backend Annex))
type CommandSeekBackendFiles = CommandStartBackendFile -> CommandSeek
type CommandStartBackendFile = BackendFile -> CommandStart
type AttrFile = (FilePath, String)
type CommandSeekAttrFiles = CommandStartAttrFile -> CommandSeek
type CommandStartAttrFile = AttrFile -> CommandStart
type CommandSeekNothing = CommandStart -> CommandSeek
type CommandStartNothing = CommandStart
data Command = Command {
2011-05-15 06:12:17 +00:00
cmdusesrepo :: Bool,
cmdname :: String,
cmdparams :: String,
cmdseek :: [CommandSeek],
2011-05-15 06:12:17 +00:00
cmddesc :: String
2010-11-04 17:28:49 +00:00
}
2011-05-15 06:02:46 +00:00
{- Most commands operate on files in a git repo. -}
repoCommand :: String -> String -> [CommandSeek] -> String -> Command
2011-05-15 06:12:17 +00:00
repoCommand = Command True
2011-05-15 06:02:46 +00:00
{- Others can run anywhere. -}
standaloneCommand :: String -> String -> [CommandSeek] -> String -> Command
2011-05-15 06:12:17 +00:00
standaloneCommand = Command False
2011-05-15 06:02:46 +00:00
{- For start and perform stages to indicate what step to run next. -}
next :: a -> Annex (Maybe a)
next a = return $ Just a
{- Or to indicate nothing needs to be done. -}
stop :: Annex (Maybe a)
stop = return Nothing
{- Prepares a list of actions to run to perform a command, based on
2010-11-04 17:28:49 +00:00
- the parameters passed to it. -}
prepCommand :: Command -> [String] -> Annex [Annex Bool]
prepCommand Command { cmdseek = seek } params = do
lists <- mapM (\s -> s params) seek
2011-01-09 22:23:58 +00:00
return $ map doCommand $ concat lists
2010-11-04 17:28:49 +00:00
{- Runs a command through the start, perform and cleanup stages -}
doCommand :: CommandStart -> CommandCleanup
2011-05-15 16:25:58 +00:00
doCommand = start
where
start = stage $ maybe (return True) perform
perform = stage $ maybe (showEndFail >> return False) cleanup
cleanup = stage $ \r -> showEndResult r >> return r
stage a b = b >>= a
2010-11-04 17:28:49 +00:00
notAnnexed :: FilePath -> Annex (Maybe a) -> Annex (Maybe a)
2011-05-15 06:49:43 +00:00
notAnnexed file a = maybe a (const $ return Nothing) =<< Backend.lookupFile file
isAnnexed :: FilePath -> ((Key, Backend Annex) -> Annex (Maybe a)) -> Annex (Maybe a)
2011-05-15 06:49:43 +00:00
isAnnexed file a = maybe (return Nothing) a =<< Backend.lookupFile file
2010-11-11 22:54:52 +00:00
notBareRepo :: Annex a -> Annex a
notBareRepo a = do
g <- Annex.gitRepo
when (Git.repoIsLocalBare g) $ do
error "You cannot run this subcommand in a bare repository."
a
2010-11-11 22:54:52 +00:00
{- These functions find appropriate files or other things based on a
user's parameters, and run a specified action on them. -}
withFilesInGit :: CommandSeekStrings
2010-11-11 22:54:52 +00:00
withFilesInGit a params = do
repo <- Annex.gitRepo
2011-02-01 00:14:08 +00:00
files <- liftIO $ runPreserveOrder (Git.inRepo repo) params
2011-02-19 21:00:40 +00:00
liftM (map a) $ filterFiles files
withAttrFilesInGit :: String -> CommandSeekAttrFiles
withAttrFilesInGit attr a params = do
repo <- Annex.gitRepo
2011-02-01 00:14:08 +00:00
files <- liftIO $ runPreserveOrder (Git.inRepo repo) params
liftM (map a) $ liftIO $ Git.checkAttr repo attr files
withBackendFilesInGit :: CommandSeekBackendFiles
withBackendFilesInGit a params = do
repo <- Annex.gitRepo
2011-02-01 00:14:08 +00:00
files <- liftIO $ runPreserveOrder (Git.inRepo repo) params
files' <- filterFiles files
backendPairs a files'
withFilesMissing :: CommandSeekStrings
2010-11-11 22:54:52 +00:00
withFilesMissing a params = do
files <- liftIO $ filterM missing params
2011-02-19 21:00:40 +00:00
liftM (map a) $ filterFiles files
2010-11-11 22:54:52 +00:00
where
missing f = do
e <- doesFileExist f
return $ not e
withFilesNotInGit :: CommandSeekBackendFiles
2010-11-11 22:54:52 +00:00
withFilesNotInGit a params = do
repo <- Annex.gitRepo
2011-02-01 00:14:08 +00:00
newfiles <- liftIO $ runPreserveOrder (Git.notInRepo repo) params
newfiles' <- filterFiles newfiles
backendPairs a newfiles'
withWords :: CommandSeekWords
withWords a params = return [a params]
withStrings :: CommandSeekStrings
2010-11-15 22:04:19 +00:00
withStrings a params = return $ map a params
withFilesToBeCommitted :: CommandSeekStrings
2010-11-11 22:54:52 +00:00
withFilesToBeCommitted a params = do
repo <- Annex.gitRepo
2011-02-02 01:58:47 +00:00
tocommit <- liftIO $ runPreserveOrder (Git.stagedFilesNotDeleted repo) params
2011-02-19 21:00:40 +00:00
liftM (map a) $ filterFiles tocommit
withFilesUnlocked :: CommandSeekBackendFiles
withFilesUnlocked = withFilesUnlocked' Git.typeChangedFiles
withFilesUnlockedToBeCommitted :: CommandSeekBackendFiles
withFilesUnlockedToBeCommitted = withFilesUnlocked' Git.typeChangedStagedFiles
withFilesUnlocked' :: (Git.Repo -> [FilePath] -> IO [FilePath]) -> CommandSeekBackendFiles
withFilesUnlocked' typechanged a params = do
-- unlocked files have changed type from a symlink to a regular file
2010-11-11 22:54:52 +00:00
repo <- Annex.gitRepo
2011-02-01 00:14:08 +00:00
typechangedfiles <- liftIO $ runPreserveOrder (typechanged repo) params
unlockedfiles <- liftIO $ filterM notSymlink $
map (\f -> Git.workTree repo ++ "/" ++ f) typechangedfiles
unlockedfiles' <- filterFiles unlockedfiles
backendPairs a unlockedfiles'
withKeys :: CommandSeekKeys
withKeys a params = return $ map a $ map parse params
where
2011-05-15 06:49:43 +00:00
parse p = maybe (error "bad key") id $ readKey p
withTempFile :: CommandSeekStrings
2010-11-11 22:54:52 +00:00
withTempFile a params = return $ map a params
withNothing :: CommandSeekNothing
2010-11-13 20:15:45 +00:00
withNothing a [] = return [a]
withNothing _ _ = error "This command takes no parameters."
2010-11-11 22:54:52 +00:00
backendPairs :: CommandSeekBackendFiles
2011-02-19 21:00:40 +00:00
backendPairs a files = liftM (map a) $ Backend.chooseBackends files
{- Filter out files from the state directory, and those matching the
- exclude glob pattern, if it was specified. -}
filterFiles :: [FilePath] -> Annex [FilePath]
filterFiles l = do
let l' = filter notState l
exclude <- Annex.getState Annex.exclude
if null exclude
then return l'
2011-01-30 04:08:17 +00:00
else return $ filter (notExcluded $ wildsRegex exclude) l'
where
2011-01-27 21:58:30 +00:00
notState f = not $ stateDir `isPrefixOf` f
notExcluded r f = isNothing $ match r f []
2011-01-30 03:47:10 +00:00
2011-01-30 04:08:17 +00:00
wildsRegex :: [String] -> Regex
wildsRegex ws = compile regex []
2011-05-28 20:09:11 +00:00
where
regex = "^(" ++ alternatives ++ ")"
alternatives = join "|" $ map wildToRegex ws
2010-11-11 22:54:52 +00:00
{- filter out symlinks -}
notSymlink :: FilePath -> IO Bool
2011-02-19 21:00:40 +00:00
notSymlink f = liftM (not . isSymbolicLink) $ liftIO $ getSymbolicLinkStatus f
{- Descriptions of params used in usage messages. -}
paramRepeating :: String -> String
paramRepeating s = s ++ " ..."
paramOptional :: String -> String
paramOptional s = "[" ++ s ++ "]"
2011-03-03 21:21:00 +00:00
paramPair :: String -> String -> String
paramPair a b = a ++ " " ++ b
paramPath :: String
paramPath = "PATH"
paramKey :: String
paramKey = "KEY"
paramDesc :: String
paramDesc = "DESC"
paramNumber :: String
paramNumber = "NUMBER"
paramRemote :: String
paramRemote = "REMOTE"
paramGlob :: String
paramGlob = "GLOB"
paramName :: String
paramName = "NAME"
2011-03-29 03:22:31 +00:00
paramType :: String
paramType = "TYPE"
paramKeyValue :: String
paramKeyValue = "K=V"
paramNothing :: String
paramNothing = ""
{- The Key specified by the --key parameter. -}
cmdlineKey :: Annex Key
cmdlineKey = do
k <- Annex.getState Annex.defaultkey
case k of
Nothing -> nokey
Just "" -> nokey
2011-05-15 06:49:43 +00:00
Just kstring -> maybe badkey return $ readKey kstring
where
nokey = error "please specify the key with --key"
2011-05-15 06:49:43 +00:00
badkey = error "bad key"
{- Given an original list of files, and an expanded list derived from it,
- ensures that the original list's ordering is preserved.
-
- The input list may contain a directory, like "dir" or "dir/". Any
- items in the expanded list that are contained in that directory will
- appear at the same position as it did in the input list.
-}
preserveOrder :: [FilePath] -> [FilePath] -> [FilePath]
-- optimisation, only one item in original list, so no reordering needed
preserveOrder [_] new = new
preserveOrder orig new = collect orig new
where
collect [] n = n
collect [_] n = n -- optimisation
collect (l:ls) n = found ++ collect ls rest
where (found, rest)=partition (l `dirContains`) n
2011-02-01 00:14:08 +00:00
{- Runs an action that takes a list of FilePaths, and ensures that
- its return list preserves order.
-
- This assumes that it's cheaper to call preserveOrder on the result,
2011-02-19 21:00:40 +00:00
- than it would be to run the action separately with each param. In the case
2011-02-01 00:14:08 +00:00
- of git file list commands, that assumption tends to hold.
-}
runPreserveOrder :: ([FilePath] -> IO [FilePath]) -> [FilePath] -> IO [FilePath]
2011-02-19 21:00:40 +00:00
runPreserveOrder a files = liftM (preserveOrder files) (a files)