diff options
Diffstat (limited to 'CmdLine')
-rw-r--r-- | CmdLine/Action.hs | 70 | ||||
-rw-r--r-- | CmdLine/GitAnnex.hs | 194 | ||||
-rw-r--r-- | CmdLine/GitAnnex/Options.hs | 101 | ||||
-rw-r--r-- | CmdLine/GitAnnexShell.hs | 199 | ||||
-rw-r--r-- | CmdLine/GitAnnexShell/Fields.hs | 36 | ||||
-rw-r--r-- | CmdLine/Option.hs | 83 | ||||
-rw-r--r-- | CmdLine/Seek.hs | 183 | ||||
-rw-r--r-- | CmdLine/Usage.hs | 113 |
8 files changed, 979 insertions, 0 deletions
diff --git a/CmdLine/Action.hs b/CmdLine/Action.hs new file mode 100644 index 000000000..247c658bc --- /dev/null +++ b/CmdLine/Action.hs @@ -0,0 +1,70 @@ +{- git-annex command-line actions + - + - Copyright 2010-2014 Joey Hess <joey@kitenet.net> + - + - Licensed under the GNU GPL version 3 or higher. + -} + +{-# LANGUAGE BangPatterns #-} + +module CmdLine.Action where + +import Common.Annex +import qualified Annex +import Types.Command +import qualified Annex.Queue +import Annex.Exception + +type CommandActionRunner = CommandStart -> CommandCleanup + +{- Runs a command, starting with the check stage, and then + - the seek stage. Finishes by printing the number of commandActions that + - failed. -} +performCommandAction :: Command -> CmdParams -> Annex () +performCommandAction Command { cmdseek = seek, cmdcheck = c, cmdname = name } params = do + mapM_ runCheck c + Annex.changeState $ \s -> s { Annex.errcounter = 0 } + seek params + showerrcount =<< Annex.getState Annex.errcounter + where + showerrcount 0 = noop + showerrcount cnt = error $ name ++ ": " ++ show cnt ++ " failed" + +{- Runs one of the actions needed to perform a command. + - Individual actions can fail without stopping the whole command, + - including by throwing IO errors (but other errors terminate the whole + - command). + - + - This should only be run in the seek stage. -} +commandAction :: CommandActionRunner +commandAction a = handle =<< tryAnnexIO go + where + go = do + Annex.Queue.flushWhenFull + callCommandAction a + handle (Right True) = return True + handle (Right False) = incerr + handle (Left err) = do + showErr err + showEndFail + incerr + incerr = do + Annex.changeState $ \s -> + let ! c = Annex.errcounter s + 1 + ! s' = s { Annex.errcounter = c } + in s' + return False + +{- Runs a single command action through the start, perform and cleanup + - stages, without catching errors. Useful if one command wants to run + - part of another command. -} +callCommandAction :: CommandActionRunner +callCommandAction = start + where + start = stage $ maybe skip perform + perform = stage $ maybe failure cleanup + cleanup = stage $ status + stage = (=<<) + skip = return True + failure = showEndFail >> return False + status r = showEndResult r >> return r diff --git a/CmdLine/GitAnnex.hs b/CmdLine/GitAnnex.hs new file mode 100644 index 000000000..3604681f9 --- /dev/null +++ b/CmdLine/GitAnnex.hs @@ -0,0 +1,194 @@ +{- git-annex main program + - + - Copyright 2010-2013 Joey Hess <joey@kitenet.net> + - + - Licensed under the GNU GPL version 3 or higher. + -} + +{-# LANGUAGE CPP, OverloadedStrings #-} + +module CmdLine.GitAnnex where + +import qualified Git.CurrentRepo +import CmdLine +import Command + +import qualified Command.Add +import qualified Command.Unannex +import qualified Command.Drop +import qualified Command.Move +import qualified Command.Copy +import qualified Command.Get +import qualified Command.LookupKey +import qualified Command.ExamineKey +import qualified Command.FromKey +import qualified Command.DropKey +import qualified Command.TransferKey +import qualified Command.TransferKeys +import qualified Command.ReKey +import qualified Command.MetaData +import qualified Command.View +import qualified Command.VAdd +import qualified Command.VFilter +import qualified Command.VPop +import qualified Command.VCycle +import qualified Command.Reinject +import qualified Command.Fix +import qualified Command.Init +import qualified Command.Describe +import qualified Command.InitRemote +import qualified Command.EnableRemote +import qualified Command.Fsck +import qualified Command.Repair +import qualified Command.Unused +import qualified Command.DropUnused +import qualified Command.AddUnused +import qualified Command.Unlock +import qualified Command.Lock +import qualified Command.PreCommit +import qualified Command.Find +import qualified Command.Whereis +import qualified Command.List +import qualified Command.Log +import qualified Command.Merge +import qualified Command.Info +import qualified Command.Status +import qualified Command.Migrate +import qualified Command.Uninit +import qualified Command.NumCopies +import qualified Command.Trust +import qualified Command.Untrust +import qualified Command.Semitrust +import qualified Command.Dead +import qualified Command.Group +import qualified Command.Wanted +import qualified Command.Schedule +import qualified Command.Ungroup +import qualified Command.Vicfg +import qualified Command.Sync +import qualified Command.Mirror +import qualified Command.AddUrl +#ifdef WITH_FEED +import qualified Command.ImportFeed +#endif +import qualified Command.RmUrl +import qualified Command.Import +import qualified Command.Map +import qualified Command.Direct +import qualified Command.Indirect +import qualified Command.Upgrade +import qualified Command.Forget +import qualified Command.Version +import qualified Command.Help +#ifdef WITH_ASSISTANT +import qualified Command.Watch +import qualified Command.Assistant +#ifdef WITH_WEBAPP +import qualified Command.WebApp +#endif +#ifdef WITH_XMPP +import qualified Command.XMPPGit +#endif +#endif +import qualified Command.Test +#ifdef WITH_TESTSUITE +import qualified Command.FuzzTest +#endif +#ifdef WITH_EKG +import System.Remote.Monitoring +#endif + +cmds :: [Command] +cmds = concat + [ Command.Add.def + , Command.Get.def + , Command.Drop.def + , Command.Move.def + , Command.Copy.def + , Command.Unlock.def + , Command.Lock.def + , Command.Sync.def + , Command.Mirror.def + , Command.AddUrl.def +#ifdef WITH_FEED + , Command.ImportFeed.def +#endif + , Command.RmUrl.def + , Command.Import.def + , Command.Init.def + , Command.Describe.def + , Command.InitRemote.def + , Command.EnableRemote.def + , Command.Reinject.def + , Command.Unannex.def + , Command.Uninit.def + , Command.PreCommit.def + , Command.NumCopies.def + , Command.Trust.def + , Command.Untrust.def + , Command.Semitrust.def + , Command.Dead.def + , Command.Group.def + , Command.Wanted.def + , Command.Schedule.def + , Command.Ungroup.def + , Command.Vicfg.def + , Command.LookupKey.def + , Command.ExamineKey.def + , Command.FromKey.def + , Command.DropKey.def + , Command.TransferKey.def + , Command.TransferKeys.def + , Command.ReKey.def + , Command.MetaData.def + , Command.View.def + , Command.VAdd.def + , Command.VFilter.def + , Command.VPop.def + , Command.VCycle.def + , Command.Fix.def + , Command.Fsck.def + , Command.Repair.def + , Command.Unused.def + , Command.DropUnused.def + , Command.AddUnused.def + , Command.Find.def + , Command.Whereis.def + , Command.List.def + , Command.Log.def + , Command.Merge.def + , Command.Info.def + , Command.Status.def + , Command.Migrate.def + , Command.Map.def + , Command.Direct.def + , Command.Indirect.def + , Command.Upgrade.def + , Command.Forget.def + , Command.Version.def + , Command.Help.def +#ifdef WITH_ASSISTANT + , Command.Watch.def + , Command.Assistant.def +#ifdef WITH_WEBAPP + , Command.WebApp.def +#endif +#ifdef WITH_XMPP + , Command.XMPPGit.def +#endif +#endif + , Command.Test.def +#ifdef WITH_TESTSUITE + , Command.FuzzTest.def +#endif + ] + +header :: String +header = "git-annex command [option ...]" + +run :: [String] -> IO () +run args = do +#ifdef WITH_EKG + _ <- forkServer "localhost" 4242 +#endif + dispatch True args cmds gitAnnexOptions [] header Git.CurrentRepo.get diff --git a/CmdLine/GitAnnex/Options.hs b/CmdLine/GitAnnex/Options.hs new file mode 100644 index 000000000..f9f5989ee --- /dev/null +++ b/CmdLine/GitAnnex/Options.hs @@ -0,0 +1,101 @@ +{- git-annex options + - + - Copyright 2010, 2013 Joey Hess <joey@kitenet.net> + - + - Licensed under the GNU GPL version 3 or higher. + -} + +module CmdLine.GitAnnex.Options where + +import System.Console.GetOpt + +import Common.Annex +import qualified Git.Config +import Git.Types +import Types.TrustLevel +import Types.NumCopies +import Types.Messages +import qualified Annex +import qualified Remote +import qualified Limit +import qualified Limit.Wanted +import CmdLine.Option +import CmdLine.Usage + +gitAnnexOptions :: [Option] +gitAnnexOptions = commonOptions ++ + [ Option ['N'] ["numcopies"] (ReqArg setnumcopies paramNumber) + "override default number of copies" + , Option [] ["trust"] (trustArg Trusted) + "override trust setting" + , Option [] ["semitrust"] (trustArg SemiTrusted) + "override trust setting back to default" + , Option [] ["untrust"] (trustArg UnTrusted) + "override trust setting to untrusted" + , Option ['c'] ["config"] (ReqArg setgitconfig "NAME=VALUE") + "override git configuration setting" + , Option ['x'] ["exclude"] (ReqArg Limit.addExclude paramGlob) + "skip files matching the glob pattern" + , Option ['I'] ["include"] (ReqArg Limit.addInclude paramGlob) + "limit to files matching the glob pattern" + , Option ['i'] ["in"] (ReqArg Limit.addIn paramRemote) + "match files present in a remote" + , Option ['C'] ["copies"] (ReqArg Limit.addCopies paramNumber) + "skip files with fewer copies" + , Option [] ["lackingcopies"] (ReqArg (Limit.addLackingCopies False) paramNumber) + "match files that need more copies" + , Option [] ["approxlackingcopies"] (ReqArg (Limit.addLackingCopies True) paramNumber) + "match files that need more copies (faster)" + , Option ['B'] ["inbackend"] (ReqArg Limit.addInBackend paramName) + "match files using a key-value backend" + , Option [] ["inallgroup"] (ReqArg Limit.addInAllGroup paramGroup) + "match files present in all remotes in a group" + , Option [] ["largerthan"] (ReqArg Limit.addLargerThan paramSize) + "match files larger than a size" + , Option [] ["smallerthan"] (ReqArg Limit.addSmallerThan paramSize) + "match files smaller than a size" + , Option [] ["metadata"] (ReqArg Limit.addMetaData "FIELD=VALUE") + "match files with attached metadata" + , Option [] ["want-get"] (NoArg Limit.Wanted.addWantGet) + "match files the repository wants to get" + , Option [] ["want-drop"] (NoArg Limit.Wanted.addWantDrop) + "match files the repository wants to drop" + , Option ['T'] ["time-limit"] (ReqArg Limit.addTimeLimit paramTime) + "stop after the specified amount of time" + , Option [] ["user-agent"] (ReqArg setuseragent paramName) + "override default User-Agent" + , Option [] ["trust-glacier"] (NoArg (Annex.setFlag "trustglacier")) + "Trust Amazon Glacier inventory" + ] ++ matcherOptions + where + trustArg t = ReqArg (Remote.forceTrust t) paramRemote + setnumcopies v = maybe noop + (\n -> Annex.changeState $ \s -> s { Annex.forcenumcopies = Just $ NumCopies n }) + (readish v) + setuseragent v = Annex.changeState $ \s -> s { Annex.useragent = Just v } + setgitconfig v = inRepo (Git.Config.store v) + >>= pure . (\r -> r { gitGlobalOpts = gitGlobalOpts r ++ [Param "-c", Param v] }) + >>= Annex.changeGitRepo + +keyOptions :: [Option] +keyOptions = + [ Option ['A'] ["all"] (NoArg (Annex.setFlag "all")) + "operate on all versions of all files" + , Option ['U'] ["unused"] (NoArg (Annex.setFlag "unused")) + "operate on files found by last run of git-annex unused" + , Option [] ["key"] (ReqArg (Annex.setField "key") paramKey) + "operate on specified key" + ] + +fromOption :: Option +fromOption = fieldOption ['f'] "from" paramRemote "source remote" + +toOption :: Option +toOption = fieldOption ['t'] "to" paramRemote "destination remote" + +fromToOptions :: [Option] +fromToOptions = [fromOption, toOption] + +jsonOption :: Option +jsonOption = Option ['j'] ["json"] (NoArg (Annex.setOutput JSONOutput)) + "enable JSON output" diff --git a/CmdLine/GitAnnexShell.hs b/CmdLine/GitAnnexShell.hs new file mode 100644 index 000000000..f490792b0 --- /dev/null +++ b/CmdLine/GitAnnexShell.hs @@ -0,0 +1,199 @@ +{- git-annex-shell main program + - + - Copyright 2010-2012 Joey Hess <joey@kitenet.net> + - + - Licensed under the GNU GPL version 3 or higher. + -} + +module CmdLine.GitAnnexShell where + +import System.Environment +import System.Console.GetOpt + +import Common.Annex +import qualified Git.Construct +import CmdLine +import Command +import Annex.UUID +import Annex (setField) +import CmdLine.GitAnnexShell.Fields +import Utility.UserInfo +import Remote.GCrypt (getGCryptUUID) +import qualified Annex +import Annex.Init + +import qualified Command.ConfigList +import qualified Command.InAnnex +import qualified Command.DropKey +import qualified Command.RecvKey +import qualified Command.SendKey +import qualified Command.TransferInfo +import qualified Command.Commit +import qualified Command.GCryptSetup + +cmds_readonly :: [Command] +cmds_readonly = concat + [ gitAnnexShellCheck Command.ConfigList.def + , gitAnnexShellCheck Command.InAnnex.def + , gitAnnexShellCheck Command.SendKey.def + , gitAnnexShellCheck Command.TransferInfo.def + ] + +cmds_notreadonly :: [Command] +cmds_notreadonly = concat + [ gitAnnexShellCheck Command.RecvKey.def + , gitAnnexShellCheck Command.DropKey.def + , gitAnnexShellCheck Command.Commit.def + , Command.GCryptSetup.def + ] + +cmds :: [Command] +cmds = map adddirparam $ cmds_readonly ++ cmds_notreadonly + where + adddirparam c = c { cmdparamdesc = "DIRECTORY " ++ cmdparamdesc c } + +options :: [OptDescr (Annex ())] +options = commonOptions ++ + [ Option [] ["uuid"] (ReqArg checkUUID paramUUID) "local repository uuid" + ] + where + checkUUID expected = getUUID >>= check + where + check u | u == toUUID expected = noop + check NoUUID = checkGCryptUUID expected + check u = unexpectedUUID expected u + checkGCryptUUID expected = check =<< getGCryptUUID True =<< gitRepo + where + check (Just u) | u == toUUID expected = noop + check Nothing = unexpected expected "uninitialized repository" + check (Just u) = unexpectedUUID expected u + unexpectedUUID expected u = unexpected expected $ "UUID " ++ fromUUID u + unexpected expected s = error $ + "expected repository UUID " ++ expected ++ " but found " ++ s + +header :: String +header = "git-annex-shell [-c] command [parameters ...] [option ...]" + +run :: [String] -> IO () +run [] = failure +-- skip leading -c options, passed by eg, ssh +run ("-c":p) = run p +-- a command can be either a builtin or something to pass to git-shell +run c@(cmd:dir:params) + | cmd `elem` builtins = builtin cmd dir params + | otherwise = external c +run c@(cmd:_) + -- Handle the case of being the user's login shell. It will be passed + -- a single string containing all the real parameters. + | "git-annex-shell " `isPrefixOf` cmd = run $ drop 1 $ shellUnEscape cmd + | cmd `elem` builtins = failure + | otherwise = external c + +builtins :: [String] +builtins = map cmdname cmds + +builtin :: String -> String -> [String] -> IO () +builtin cmd dir params = do + checkNotReadOnly cmd + checkDirectory $ Just dir + let (params', fieldparams, opts) = partitionParams params + fields = filter checkField $ parseFields fieldparams + cmds' = map (newcmd $ unwords opts) cmds + dispatch False (cmd : params') cmds' options fields header $ + Git.Construct.repoAbsPath dir >>= Git.Construct.fromAbsPath + where + addrsyncopts opts seek k = setField "RsyncOptions" opts >> seek k + newcmd opts c = c { cmdseek = addrsyncopts opts (cmdseek c) } + +external :: [String] -> IO () +external params = do + {- Normal git-shell commands all have the directory as their last + - parameter. -} + let lastparam = lastMaybe =<< shellUnEscape <$> lastMaybe params + (params', _, _) = partitionParams params + checkDirectory lastparam + checkNotLimited + unlessM (boolSystem "git-shell" $ map Param $ "-c":params') $ + error "git-shell failed" + +{- Split the input list into 3 groups separated with a double dash --. + - Parameters between two -- markers are field settings, in the form: + - field=value field=value + - + - Parameters after the last -- are the command itself and its arguments e.g., + - rsync --bandwidth=100. + -} +partitionParams :: [String] -> ([String], [String], [String]) +partitionParams ps = case segment (== "--") ps of + params:fieldparams:rest -> ( params, fieldparams, intercalate ["--"] rest ) + [params] -> (params, [], []) + _ -> ([], [], []) + +parseFields :: [String] -> [(String, String)] +parseFields = map (separate (== '=')) + +{- Only allow known fields to be set, ignore others. + - Make sure that field values make sense. -} +checkField :: (String, String) -> Bool +checkField (field, value) + | field == fieldName remoteUUID = fieldCheck remoteUUID value + | field == fieldName associatedFile = fieldCheck associatedFile value + | field == fieldName direct = fieldCheck direct value + | otherwise = False + +failure :: IO () +failure = error $ "bad parameters\n\n" ++ usage header cmds + +checkNotLimited :: IO () +checkNotLimited = checkEnv "GIT_ANNEX_SHELL_LIMITED" + +checkNotReadOnly :: String -> IO () +checkNotReadOnly cmd + | cmd `elem` map cmdname cmds_readonly = noop + | otherwise = checkEnv "GIT_ANNEX_SHELL_READONLY" + +checkDirectory :: Maybe FilePath -> IO () +checkDirectory mdir = do + v <- catchMaybeIO $ getEnv "GIT_ANNEX_SHELL_DIRECTORY" + case (v, mdir) of + (Nothing, _) -> noop + (Just d, Nothing) -> req d Nothing + (Just d, Just dir) + | d `equalFilePath` dir -> noop + | otherwise -> do + home <- myHomeDir + d' <- canondir home d + dir' <- canondir home dir + if d' `equalFilePath` dir' + then noop + else req d' (Just dir') + where + req d mdir' = error $ unwords + [ "Only allowed to access" + , d + , maybe "and could not determine directory from command line" ("not " ++) mdir' + ] + + {- A directory may start with ~/ or in some cases, even /~/, + - or could just be relative to home, or of course could + - be absolute. -} + canondir home d + | "~/" `isPrefixOf` d = return d + | "/~/" `isPrefixOf` d = return $ drop 1 d + | otherwise = relHome $ absPathFrom home d + +checkEnv :: String -> IO () +checkEnv var = do + v <- catchMaybeIO $ getEnv var + case v of + Nothing -> noop + Just "" -> noop + Just _ -> error $ "Action blocked by " ++ var + +{- Modifies a Command to check that it is run in either a git-annex + - repository, or a repository with a gcrypt-id set. -} +gitAnnexShellCheck :: [Command] -> [Command] +gitAnnexShellCheck = map $ addCheck okforshell . dontCheck repoExists + where + okforshell = unlessM (isInitialized <||> isJust . gcryptId <$> Annex.getGitConfig) $ + error "Not a git-annex or gcrypt repository." diff --git a/CmdLine/GitAnnexShell/Fields.hs b/CmdLine/GitAnnexShell/Fields.hs new file mode 100644 index 000000000..4f208773b --- /dev/null +++ b/CmdLine/GitAnnexShell/Fields.hs @@ -0,0 +1,36 @@ +{- git-annex-shell fields + - + - Copyright 2012 Joey Hess <joey@kitenet.net> + - + - Licensed under the GNU GPL version 3 or higher. + -} + +module CmdLine.GitAnnexShell.Fields where + +import Common.Annex +import qualified Annex +import Git.FilePath + +import Data.Char + +{- A field, stored in Annex state, with a value sanity checker. -} +data Field = Field + { fieldName :: String + , fieldCheck :: String -> Bool + } + +getField :: Field -> Annex (Maybe String) +getField = Annex.getField . fieldName + +remoteUUID :: Field +remoteUUID = Field "remoteuuid" $ + -- does it look like a UUID? + all (\c -> isAlphaNum c || c == '-') + +associatedFile :: Field +associatedFile = Field "associatedfile" $ \f -> + -- is the file a safe relative filename? + not (absoluteGitPath f) && not ("../" `isPrefixOf` f) + +direct :: Field +direct = Field "direct" $ \f -> f == "1" diff --git a/CmdLine/Option.hs b/CmdLine/Option.hs new file mode 100644 index 000000000..ce44d2ace --- /dev/null +++ b/CmdLine/Option.hs @@ -0,0 +1,83 @@ +{- common command-line options + - + - Copyright 2010-2011 Joey Hess <joey@kitenet.net> + - + - Licensed under the GNU GPL version 3 or higher. + -} + +module CmdLine.Option ( + commonOptions, + matcherOptions, + flagOption, + fieldOption, + optionName, + ArgDescr(..), + OptDescr(..), +) where + +import System.Console.GetOpt + +import Common.Annex +import qualified Annex +import Types.Messages +import Types.DesktopNotify +import Limit +import CmdLine.Usage + +commonOptions :: [Option] +commonOptions = + [ Option [] ["force"] (NoArg (setforce True)) + "allow actions that may lose annexed data" + , Option ['F'] ["fast"] (NoArg (setfast True)) + "avoid slow operations" + , Option ['a'] ["auto"] (NoArg (setauto True)) + "automatic mode" + , Option ['q'] ["quiet"] (NoArg (Annex.setOutput QuietOutput)) + "avoid verbose output" + , Option ['v'] ["verbose"] (NoArg (Annex.setOutput NormalOutput)) + "allow verbose output (default)" + , Option ['d'] ["debug"] (NoArg setdebug) + "show debug messages" + , Option [] ["no-debug"] (NoArg unsetdebug) + "don't show debug messages" + , Option ['b'] ["backend"] (ReqArg setforcebackend paramName) + "specify key-value backend to use" + , Option [] ["notify-finish"] (NoArg (setdesktopnotify mkNotifyFinish)) + "show desktop notification after transfer finishes" + , Option [] ["notify-start"] (NoArg (setdesktopnotify mkNotifyStart)) + "show desktop notification after transfer completes" + ] + where + setforce v = Annex.changeState $ \s -> s { Annex.force = v } + setfast v = Annex.changeState $ \s -> s { Annex.fast = v } + setauto v = Annex.changeState $ \s -> s { Annex.auto = v } + setforcebackend v = Annex.changeState $ \s -> s { Annex.forcebackend = Just v } + setdebug = Annex.changeGitConfig $ \c -> c { annexDebug = True } + unsetdebug = Annex.changeGitConfig $ \c -> c { annexDebug = False } + setdesktopnotify v = Annex.changeState $ \s -> s { Annex.desktopnotify = Annex.desktopnotify s <> v } + +matcherOptions :: [Option] +matcherOptions = + [ longopt "not" "negate next option" + , longopt "and" "both previous and next option must match" + , longopt "or" "either previous or next option must match" + , shortopt "(" "open group of options" + , shortopt ")" "close group of options" + ] + where + longopt o = Option [] [o] $ NoArg $ addToken o + shortopt o = Option o [] $ NoArg $ addToken o + +{- An option that sets a flag. -} +flagOption :: String -> String -> String -> Option +flagOption short opt description = + Option short [opt] (NoArg (Annex.setFlag opt)) description + +{- An option that sets a field. -} +fieldOption :: String -> String -> String -> String -> Option +fieldOption short opt paramdesc description = + Option short [opt] (ReqArg (Annex.setField opt) paramdesc) description + +{- The flag or field name used for an option. -} +optionName :: Option -> String +optionName (Option _ o _ _) = Prelude.head o diff --git a/CmdLine/Seek.hs b/CmdLine/Seek.hs new file mode 100644 index 000000000..abbe52af8 --- /dev/null +++ b/CmdLine/Seek.hs @@ -0,0 +1,183 @@ +{- git-annex command seeking + - + - These functions find appropriate files or other things based on + - the values a user passes to a command, and prepare actions operating + - on them. + - + - Copyright 2010-2014 Joey Hess <joey@kitenet.net> + - + - Licensed under the GNU GPL version 3 or higher. + -} + +module CmdLine.Seek where + +import Common.Annex +import Types.Command +import Types.Key +import Types.FileMatcher +import qualified Annex +import qualified Git +import qualified Git.Command +import qualified Git.LsFiles as LsFiles +import qualified Limit +import CmdLine.Option +import CmdLine.Action +import Logs.Location +import Logs.Unused +import Annex.CatFile + +withFilesInGit :: (FilePath -> CommandStart) -> CommandSeek +withFilesInGit a params = seekActions $ prepFiltered a $ + seekHelper LsFiles.inRepo params + +withFilesNotInGit :: Bool -> (FilePath -> CommandStart) -> CommandSeek +withFilesNotInGit skipdotfiles a params + | skipdotfiles = do + {- dotfiles are not acted on unless explicitly listed -} + files <- filter (not . dotfile) <$> + seekunless (null ps && not (null params)) ps + dotfiles <- seekunless (null dotps) dotps + go (files++dotfiles) + | otherwise = go =<< seekunless False params + where + (dotps, ps) = partition dotfile params + seekunless True _ = return [] + seekunless _ l = do + force <- Annex.getState Annex.force + g <- gitRepo + liftIO $ Git.Command.leaveZombie <$> LsFiles.notInRepo force l g + go l = seekActions $ prepFiltered a $ + return $ concat $ segmentPaths params l + +withPathContents :: ((FilePath, FilePath) -> CommandStart) -> CommandSeek +withPathContents a params = seekActions $ + map a . concat <$> liftIO (mapM get params) + where + get p = ifM (isDirectory <$> getFileStatus p) + ( map (\f -> (f, makeRelative (parentDir p) f)) + <$> dirContentsRecursiveSkipping (".git" `isSuffixOf`) True p + , return [(p, takeFileName p)] + ) + +withWords :: ([String] -> CommandStart) -> CommandSeek +withWords a params = seekActions $ return [a params] + +withStrings :: (String -> CommandStart) -> CommandSeek +withStrings a params = seekActions $ return $ map a params + +withPairs :: ((String, String) -> CommandStart) -> CommandSeek +withPairs a params = seekActions $ return $ map a $ pairs [] params + where + pairs c [] = reverse c + pairs c (x:y:xs) = pairs ((x,y):c) xs + pairs _ _ = error "expected pairs" + +withFilesToBeCommitted :: (String -> CommandStart) -> CommandSeek +withFilesToBeCommitted a params = seekActions $ prepFiltered a $ + seekHelper LsFiles.stagedNotDeleted params + +withFilesUnlocked :: (FilePath -> CommandStart) -> CommandSeek +withFilesUnlocked = withFilesUnlocked' LsFiles.typeChanged + +withFilesUnlockedToBeCommitted :: (FilePath -> CommandStart) -> CommandSeek +withFilesUnlockedToBeCommitted = withFilesUnlocked' LsFiles.typeChangedStaged + +{- Unlocked files have changed type from a symlink to a regular file. + - + - Furthermore, unlocked files used to be a git-annex symlink, + - not some other sort of symlink. + -} +withFilesUnlocked' :: ([FilePath] -> Git.Repo -> IO ([FilePath], IO Bool)) -> (FilePath -> CommandStart) -> CommandSeek +withFilesUnlocked' typechanged a params = seekActions $ + prepFiltered a unlockedfiles + where + check f = liftIO (notSymlink f) <&&> + (isJust <$> catKeyFile f <||> isJust <$> catKeyFileHEAD f) + unlockedfiles = filterM check =<< seekHelper typechanged params + +{- Finds files that may be modified. -} +withFilesMaybeModified :: (FilePath -> CommandStart) -> CommandSeek +withFilesMaybeModified a params = seekActions $ + prepFiltered a $ seekHelper LsFiles.modified params + +withKeys :: (Key -> CommandStart) -> CommandSeek +withKeys a params = seekActions $ return $ map (a . parse) params + where + parse p = fromMaybe (error "bad key") $ file2key p + +{- Gets the value of a field options, which is fed into + - a conversion function. + -} +getOptionField :: Option -> (Maybe String -> Annex a) -> Annex a +getOptionField option converter = converter <=< Annex.getField $ optionName option + +getOptionFlag :: Option -> Annex Bool +getOptionFlag option = Annex.getFlag (optionName option) + +withNothing :: CommandStart -> CommandSeek +withNothing a [] = seekActions $ return [a] +withNothing _ _ = error "This command takes no parameters." + +{- If --all is specified, or in a bare repo, runs an action on all + - known keys. + - + - If --unused is specified, runs an action on all keys found by + - the last git annex unused scan. + - + - If --key is specified, operates only on that key. + - + - Otherwise, fall back to a regular CommandSeek action on + - whatever params were passed. -} +withKeyOptions :: (Key -> CommandStart) -> CommandSeek -> CommandSeek +withKeyOptions keyop fallbackop params = do + bare <- fromRepo Git.repoIsLocalBare + allkeys <- Annex.getFlag "all" + unused <- Annex.getFlag "unused" + specifickey <- Annex.getField "key" + auto <- Annex.getState Annex.auto + when (auto && bare) $ + error "Cannot use --auto in a bare repository" + case (allkeys, unused, null params, specifickey) of + (False , False , True , Nothing) + | bare -> go auto loggedKeys + | otherwise -> fallbackop params + (False , False , _ , Nothing) -> fallbackop params + (True , False , True , Nothing) -> go auto loggedKeys + (False , True , True , Nothing) -> go auto unusedKeys' + (False , False , True , Just ks) -> case file2key ks of + Nothing -> error "Invalid key" + Just k -> go auto $ return [k] + _ -> error "Can only specify one of file names, --all, --unused, or --key" + where + go True _ = error "Cannot use --auto with --all or --unused or --key" + go False a = do + matcher <- Limit.getMatcher + seekActions $ map (process matcher) <$> a + process matcher k = ifM (matcher $ MatchingKey k) + ( keyop k , return Nothing) + +prepFiltered :: (FilePath -> CommandStart) -> Annex [FilePath] -> Annex [CommandStart] +prepFiltered a fs = do + matcher <- Limit.getMatcher + map (process matcher) <$> fs + where + process matcher f = ifM (matcher $ MatchingFile $ FileInfo f f) + ( a f , return Nothing ) + +seekActions :: Annex [CommandStart] -> Annex () +seekActions gen = do + as <- gen + mapM_ commandAction as + +seekHelper :: ([FilePath] -> Git.Repo -> IO ([FilePath], IO Bool)) -> [FilePath] -> Annex [FilePath] +seekHelper a params = do + ll <- inRepo $ \g -> + runSegmentPaths (\fs -> Git.Command.leaveZombie <$> a fs g) params + {- Show warnings only for files/directories that do not exist. -} + forM_ (map fst $ filter (null . snd) $ zip params ll) $ \p -> + unlessM (isJust <$> liftIO (catchMaybeIO $ getSymbolicLinkStatus p)) $ + fileNotFound p + return $ concat ll + +notSymlink :: FilePath -> IO Bool +notSymlink f = liftIO $ not . isSymbolicLink <$> getSymbolicLinkStatus f diff --git a/CmdLine/Usage.hs b/CmdLine/Usage.hs new file mode 100644 index 000000000..1d0bba954 --- /dev/null +++ b/CmdLine/Usage.hs @@ -0,0 +1,113 @@ +{- git-annex usage messages + - + - Copyright 2010-2011 Joey Hess <joey@kitenet.net> + - + - Licensed under the GNU GPL version 3 or higher. + -} + +module CmdLine.Usage where + +import Common.Annex + +import Types.Command + +import System.Console.GetOpt + +usageMessage :: String -> String +usageMessage s = "Usage: " ++ s + +{- Usage message with lists of commands by section. -} +usage :: String -> [Command] -> String +usage header cmds = unlines $ usageMessage header : concatMap go [minBound..] + where + go section + | null cs = [] + | otherwise = + [ "" + , descSection section ++ ":" + , "" + ] ++ map cmdline cs + where + cs = filter (\c -> cmdsection c == section) scmds + cmdline c = concat + [ cmdname c + , namepad (cmdname c) + , cmdparamdesc c + , descpad (cmdparamdesc c) + , cmddesc c + ] + pad n s = replicate (n - length s) ' ' + namepad = pad $ longest cmdname + 1 + descpad = pad $ longest cmdparamdesc + 2 + longest f = foldl max 0 $ map (length . f) cmds + scmds = sort cmds + +{- Usage message for a single command. -} +commandUsage :: Command -> String +commandUsage cmd = unlines + [ usageInfo header (cmdoptions cmd) + , "To see additional options common to all commands, run: git annex help options" + ] + where + header = usageMessage $ unwords + [ "git-annex" + , cmdname cmd + , cmdparamdesc cmd + , "[option ...]" + ] + +{- Descriptions of params used in usage messages. -} +paramPaths :: String +paramPaths = paramOptional $ paramRepeating paramPath -- most often used +paramPath :: String +paramPath = "PATH" +paramKey :: String +paramKey = "KEY" +paramDesc :: String +paramDesc = "DESC" +paramUrl :: String +paramUrl = "URL" +paramNumber :: String +paramNumber = "NUMBER" +paramNumRange :: String +paramNumRange = "NUM|RANGE" +paramRemote :: String +paramRemote = "REMOTE" +paramField :: String +paramField = "FIELD" +paramGlob :: String +paramGlob = "GLOB" +paramName :: String +paramName = "NAME" +paramValue :: String +paramValue = "VALUE" +paramUUID :: String +paramUUID = "UUID" +paramType :: String +paramType = "TYPE" +paramDate :: String +paramDate = "DATE" +paramTime :: String +paramTime = "TIME" +paramFormat :: String +paramFormat = "FORMAT" +paramFile :: String +paramFile = "FILE" +paramGroup :: String +paramGroup = "GROUP" +paramExpression :: String +paramExpression = "EXPR" +paramSize :: String +paramSize = "SIZE" +paramAddress :: String +paramAddress = "ADDRESS" +paramKeyValue :: String +paramKeyValue = "K=V" +paramNothing :: String +paramNothing = "" +paramRepeating :: String -> String +paramRepeating s = s ++ " ..." +paramOptional :: String -> String +paramOptional s = "[" ++ s ++ "]" +paramPair :: String -> String -> String +paramPair a b = a ++ " " ++ b |