]> dgit.raspbian.org Git - git-annex.git/commitdiff
addon commands
authorJoey Hess <joeyh@joeyh.name>
Tue, 2 Feb 2021 20:32:25 +0000 (16:32 -0400)
committerJoey Hess <joeyh@joeyh.name>
Tue, 2 Feb 2021 20:32:49 +0000 (16:32 -0400)
Seems only fair, that, like git runs git-annex, git-annex runs
git-annex-foo.

Implementation relies on O.forwardOptions, so that any options are passed
through to the addon program. Note that this includes options before the
subcommand, eg: git-annex -cx=y foo

Unfortunately, git-annex eats the --help/-h options.
This is because it uses O.hsubparser, which injects that option into each
subcommand. Seems like this should be possible to avoid somehow, to let
commands display their own --help, instead of the dummy one git-annex
displays.

The two step searching mirrors how git works, it makes finding
git-annex-foo fast when "git annex foo" is run, but will also support fuzzy
matching, once findAllAddonCommands gets implemented.

This commit was sponsored by Dr. Land Raider on Patreon.

CHANGELOG
CmdLine.hs
CmdLine/GitAnnex.hs
CmdLine/GitAnnexShell.hs
CmdLine/GlobalSetter.hs
Command.hs
Types/Command.hs
doc/git-annex.mdwn
doc/todo/allow_git-annex_addon_commands.mdwn

index dcada643bceb264e9eea7482064aecdf723ec22c..8638542cb8d79cede2e161fecbdf743796f02de1 100644 (file)
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -1,5 +1,7 @@
 git-annex (8.20210128) UNRELEASED; urgency=medium
 
+  * Commands can be added to git-annex, by installing a program in PATH
+    with a name starting with "git-annex-"
   * Fix a reversion that made import of a tree from a special remote
     result in a merge that deleted files that were not preferred content
     of that special remote.
index 4035db51efcc0d4978d4411195489c4a9c588312..986752af7a77d4140b733c54c776e5300278a72a 100644 (file)
@@ -1,6 +1,6 @@
 {- git-annex command line parsing and dispatch
  -
- - Copyright 2010-2015 Joey Hess <id@joeyh.name>
+ - Copyright 2010-2021 Joey Hess <id@joeyh.name>
  -
  - Licensed under the GNU AGPL version 3 or higher.
  -}
@@ -15,6 +15,8 @@ module CmdLine (
 import qualified Options.Applicative as O
 import qualified Options.Applicative.Help as H
 import Control.Exception (throw)
+import Control.Monad.IO.Class (MonadIO)
+import System.Exit
 
 import Annex.Common
 import qualified Annex
@@ -26,9 +28,26 @@ import Annex.Environment
 import Command
 import Types.Messages
 
-{- Runs the passed command line. -}
-dispatch :: Bool -> CmdParams -> [Command] -> [(String, String)] -> IO Git.Repo -> String -> String -> IO ()
-dispatch fuzzyok allargs allcmds fields getgitrepo progname progdesc = do
+{- Parses input arguments, finds a matching Command, and runs it. -}
+dispatch :: Bool -> Bool -> CmdParams -> [Command] -> [(String, String)] -> IO Git.Repo -> String -> String -> IO ()
+dispatch addonok fuzzyok allargs allcmds fields getgitrepo progname progdesc =
+       go addonok allcmds $
+               findAddonCommand subcommandname >>= \case
+                       Nothing -> go False allcmds noop
+                       Just c -> go addonok (c:allcmds) $
+                               findAllAddonCommands >>= \cs ->
+                                       go False (cs++allcmds) noop
+  where
+       go p allcmds' cont =
+               let (fuzzy, cmds) = selectCmd fuzzyok allcmds' subcommandname
+               in if not p || (not fuzzy && not (null cmds))
+                       then dispatch' subcommandname args fuzzy cmds allargs allcmds' fields getgitrepo progname progdesc
+                       else cont
+       
+       (subcommandname, args) = subCmdName allargs
+
+dispatch' :: (Maybe String) -> CmdParams -> Bool -> [Command] -> CmdParams -> [Command] -> [(String, String)] -> IO Git.Repo -> String -> String -> IO ()
+dispatch' subcommandname args fuzzy cmds allargs allcmds fields getgitrepo progname progdesc = do
        setupConsole
        go =<< tryNonAsync getgitrepo
   where
@@ -70,18 +89,15 @@ dispatch fuzzyok allargs allcmds fields getgitrepo progname progdesc = do
                                handleresult (parseCmd progname progdesc correctedargs allcmds getparser)
                        res -> handleresult res
          where
-               autocorrect = Git.AutoCorrect.prepare (fromJust inputcmdname) cmdname cmds
+               autocorrect = Git.AutoCorrect.prepare (fromJust subcommandname) cmdname cmds
                name
                        | fuzzy = case cmds of
                                (c:_) -> Just (cmdname c)
-                               _ -> inputcmdname
-                       | otherwise = inputcmdname
+                               _ -> subcommandname
+                       | otherwise = subcommandname
                correctedargs = case name of
                        Nothing -> allargs
                        Just n -> n:args
-       
-       (inputcmdname, args) = findCmdName allargs
-       (fuzzy, cmds) = findCmd fuzzyok allcmds inputcmdname
 
 {- Parses command line, selecting one of the commands from the list. -}
 parseCmd :: String -> String -> CmdParams -> [Command] -> (Command -> O.Parser v) -> O.ParserResult (Command, v, GlobalSetter)
@@ -93,29 +109,30 @@ parseCmd progname progdesc allargs allcmds getparser =
        mkcommand c = O.command (cmdname c) $ O.info (mkparser c) $ O.fullDesc 
                <> O.header (synopsis (progname ++ " " ++ cmdname c) (cmddesc c))
                <> O.footer ("For details, run: " ++ progname ++ " help " ++ cmdname c)
+               <> cmdinfomod c
        mkparser c = (,,) 
                <$> pure c
                <*> getparser c
-               <*> combineGlobalOptions (cmdglobaloptions c)
+               <*> parserGlobalOptions (cmdglobaloptions c)
        synopsis n d = n ++ " - " ++ d
        intro = mconcat $ concatMap (\l -> [H.text l, H.line])
                (synopsis progname progdesc : commandList allcmds)
 
-{- Finds the Command that matches the subcommand name.
+{- Selects the Command that matches the subcommand name.
  - Does fuzzy matching if necessary, which may result in multiple Commands. -}
-findCmd :: Bool -> [Command] -> Maybe String -> (Bool, [Command])
-findCmd fuzzyok cmds (Just n)
+selectCmd :: Bool -> [Command] -> Maybe String -> (Bool, [Command])
+selectCmd fuzzyok cmds (Just n)
        | not (null exactcmds) = (False, exactcmds)
        | fuzzyok && not (null inexactcmds) = (True, inexactcmds)
        | otherwise = (False, [])
   where
        exactcmds = filter (\c -> cmdname c == n) cmds
        inexactcmds = Git.AutoCorrect.fuzzymatches n cmdname cmds
-findCmd _ _ Nothing = (False, [])
+selectCmd _ _ Nothing = (False, [])
 
 {- Parses command line params far enough to find the subcommand name. -}
-findCmdName :: CmdParams -> (Maybe String, CmdParams)
-findCmdName argv = (name, args)
+subCmdName :: CmdParams -> (Maybe String, CmdParams)
+subCmdName argv = (name, args)
   where
        (name, args) = findname argv []
        findname [] c = (Nothing, reverse c)
@@ -130,3 +147,36 @@ prepRunCommand cmd globalconfig = do
        getParsed globalconfig
        whenM (annexDebug <$> Annex.getGitConfig) $
                liftIO enableDebugOutput
+
+findAddonCommand :: Maybe String -> IO (Maybe Command)
+findAddonCommand Nothing = return Nothing
+findAddonCommand (Just subcommandname) =
+       searchPath c >>= \case
+               Nothing -> return Nothing
+               Just p -> return (Just (mkAddonCommand p subcommandname))
+  where
+       c = "git-annex-" ++ subcommandname
+
+findAllAddonCommands :: IO [Command]
+findAllAddonCommands = return [] -- TODO
+
+mkAddonCommand :: FilePath -> String -> Command
+mkAddonCommand p subcommandname = Command
+       { cmdcheck = []
+       , cmdnocommit = True
+       , cmdnomessages = True
+       , cmdname = subcommandname
+       , cmdparamdesc = "[PARAMS]"
+       , cmdsection = SectionAddOn
+       , cmddesc = "addon command"
+       , cmdglobaloptions = []
+       , cmdinfomod = O.forwardOptions
+       , cmdparser = parse
+       , cmdnorepo = Just parse
+       }
+  where
+       parse :: (Monad m, MonadIO m) => Parser (m ())
+       parse = (liftIO . run) <$> cmdParams "PARAMS"
+
+       run ps = withCreateProcess (proc p ps) $ \_ _ _ pid ->
+               exitWith =<< waitForProcess pid
index 9c433629ad44bda17e3668be095ceb9f103712ca..ecf43678d5b9dd23c6c3e238f12dde417f5ad299 100644 (file)
@@ -243,7 +243,7 @@ addGitAnnexGlobalOptions c = c { cmdglobaloptions = gitAnnexGlobalOptions ++ cmd
 run :: Parser TestOptions -> TestRunner -> MkBenchmarkGenerator -> [String] -> IO ()
 run testoptparser testrunner mkbenchmarkgenerator args = go envmodes
   where
-       go [] = dispatch True args 
+       go [] = dispatch True True args 
                (cmds testoptparser testrunner mkbenchmarkgenerator)
                [] Git.CurrentRepo.get
                "git-annex"
index ed4f83a3d4b8bae3ecaf830303d4630e25257234..2dcfc19396a1daf17c9159773d8a53a0cf32b94a 100644 (file)
@@ -122,7 +122,7 @@ builtin cmd dir params = do
        let (params', fieldparams, opts) = partitionParams params
            rsyncopts = ("RsyncOptions", unwords opts)
            fields = rsyncopts : filter checkField (parseFields fieldparams)
-       dispatch False (cmd : params') cmdsList fields mkrepo
+       dispatch False False (cmd : params') cmdsList fields mkrepo
                "git-annex-shell"
                "Restricted login shell for git-annex only SSH access"
   where
index 11c967b7077812122482b12ab73d5015d6f24057..86b64bf519ec8358e5e6dc52503d3ead5583a04f 100644 (file)
@@ -1,6 +1,6 @@
 {- git-annex global options
  -
- - Copyright 2015 Joey Hess <id@joeyh.name>
+ - Copyright 2015-2021 Joey Hess <id@joeyh.name>
  -
  - Licensed under the GNU AGPL version 3 or higher.
   -}
@@ -19,6 +19,7 @@ globalFlag setter = flag' (DeferredParse setter)
 globalSetter :: (v -> Annex ()) -> Parser v -> GlobalOption
 globalSetter setter parser = DeferredParse . setter <$> parser
 
-combineGlobalOptions :: [GlobalOption] -> Parser GlobalSetter
-combineGlobalOptions l = DeferredParse . mapM_ getParsed
+parserGlobalOptions :: [GlobalOption] -> Parser GlobalSetter
+parserGlobalOptions [] = DeferredParse <$> pure noop
+parserGlobalOptions l = DeferredParse . mapM_ getParsed
        <$> many (foldl1 (<|>) l)
index 6b4130bc532cf56a85aa0b94e6a27a817ed530b4..fafe06892c633491d55dbaf73abb96d23cafe0f2 100644 (file)
@@ -33,7 +33,7 @@ import Types.WorkerPool as ReExported
 command :: String -> CommandSection -> String -> CmdParamsDesc -> (CmdParamsDesc -> CommandParser) -> Command
 command name section desc paramdesc mkparser =
        Command commonChecks False False name paramdesc 
-               section desc (mkparser paramdesc) [] Nothing
+               section desc (mkparser paramdesc) mempty [] Nothing
 
 {- Simple option parser that takes all non-option params as-is. -}
 withParams :: (CmdParams -> v) -> CmdParamsDesc -> Parser v
index 2da131f5340fcbb0269fe144bacd02fbd10fc838..96bd5290c31e4582a14e4cbca21001ec948a0486 100644 (file)
@@ -1,14 +1,17 @@
 {- git-annex command data types
  -
- - Copyright 2010-2019 Joey Hess <id@joeyh.name>
+ - Copyright 2010-2021 Joey Hess <id@joeyh.name>
  -
  - Licensed under the GNU AGPL version 3 or higher.
  -}
 
+{-# LANGUAGE RankNTypes #-}
+
 module Types.Command where
 
 import Data.Ord
 import Options.Applicative.Types (Parser)
+import Options.Applicative.Builder (InfoMod)
 
 import Types
 import Types.DeferredParse
@@ -82,6 +85,8 @@ data Command = Command
        -- ^ description of command for usage
        , cmdparser :: CommandParser
        -- ^ command line parser
+       , cmdinfomod :: forall a. InfoMod a
+       -- ^ command-specific modifier for ParserInfo
        , cmdglobaloptions :: [GlobalOption]
        -- ^ additional global options
        , cmdnorepo :: Maybe (Parser (IO ()))
@@ -115,6 +120,7 @@ data CommandSection
        | SectionUtility
        | SectionPlumbing
        | SectionTesting
+       | SectionAddOn
        deriving (Eq, Ord, Enum, Bounded)
 
 descSection :: CommandSection -> String
@@ -126,3 +132,4 @@ descSection SectionMetaData = "Metadata commands"
 descSection SectionUtility = "Utility commands"
 descSection SectionPlumbing = "Plumbing commands"
 descSection SectionTesting = "Testing commands"
+descSection SectionAddOn = "Addon commands"
index 513c3755e34fb4273ec5d7bf36c23f9ce72877c8..e2f2acb3d040bcd5bc16326eb437397108edb562 100644 (file)
@@ -741,6 +741,12 @@ content from the key-value store.
   
   See [[git-annex-benchmark]](1) for details.
 
+# ADDON COMMANDS
+
+In addition to all the commands listed above, more commands can be added to
+git-annex by dropping commands named like "git-annex-foo" into a directory
+in the PATH.
+
 # COMMON OPTIONS
 
 These common options are accepted by all git-annex commands, and
index c5518af420570d6a86fb8bc40b48b81a128adc75..e21591a31f514163bff9d44c6f852150ddb79523 100644 (file)
@@ -10,3 +10,5 @@ search the path for all "git-annex-" commands and then
 either dispatch the one matching the inputcmdname,
 or do autocorrect with the list of those commands 
 included along with the builtins. --[[Joey]] 
+
+> [[done]] --[[Joey]]