move/copy: option parsing for --from with --to
authorJoey Hess <joeyh@joeyh.name>
Wed, 18 Jan 2023 18:42:39 +0000 (14:42 -0400)
committerJoey Hess <joeyh@joeyh.name>
Wed, 18 Jan 2023 18:42:39 +0000 (14:42 -0400)
Allowing --from and --to as an alternative to --from or --to
is hard to do with optparse-applicative!

The obvious approach of (pfrom <|> pto <|> pfromandto) does not work
when pfromandto uses the same option names as pfrom and pto do.
It compiles but the generated parser does not work for all desired
combinations.

Instead, have to parse optionally from and optionally to. When neither
is provided, the parser succeeds, but it's a result that can't be
handled. So, have to giveup after option parsing. There does not seem to
be a way to make an optparse-applicative Parser give up internally
either.

Also, need seek' because I first tried making fto be a where binding,
but that resulted in a hang when git-annex move was run without --from
or --to. I think because startConcurrency was not expecting the stages
value to contain an exception and so ended up blocking.

Sponsored-by: Dartmouth College's DANDI project
CHANGELOG
CmdLine/GitAnnex/Options.hs
Command/Copy.hs
Command/Move.hs
doc/git-annex-copy.mdwn
doc/git-annex-move.mdwn

index c11d40cce754e72f312b5c1d78bb64299d972eff..d9293a49ffa14a7962f61fc3519880469bad99f0 100644 (file)
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -23,6 +23,8 @@ git-annex (10.20221213) UNRELEASED; urgency=medium
   * Added an optional cost= configuration to all special remotes.
   * adb: Support the remote.name.cost and remote.name.cost-command configs.
   * findkeys: New command, very similar to git-annex find but operating on keys.
+  * move, copy: Support combining --from and --to in order to move/copy
+    the content of files that are in one remote to another remote.
 
  -- Joey Hess <id@joeyh.name>  Mon, 12 Dec 2022 13:04:54 -0400
 
index 37203c207657c8537f6a8ae89efab38172683645..36434aad143f062059c817640d62bd389b93c9e0 100644 (file)
@@ -1,6 +1,6 @@
 {- git-annex command-line option parsing
  -
- - Copyright 2010-2021 Joey Hess <id@joeyh.name>
+ - Copyright 2010-2023 Joey Hess <id@joeyh.name>
  -
  - Licensed under the GNU AGPL version 3 or higher.
  -}
@@ -134,7 +134,7 @@ parseDryRunOption = DryRun <$> switch
        <> help "don't make changes, but show what would be done"
        )
 
--- | From or To a remote.
+-- | From or To a remote but not both.
 data FromToOptions
        = FromRemote (DeferredParse Remote)
        | ToRemote (DeferredParse Remote)
@@ -162,23 +162,34 @@ parseToOption = strOption
        <> completeRemotes
        )
 
--- | Like FromToOptions, but with a special --to=here
-type FromToHereOptions = Either ToHere FromToOptions
+-- | From or to a remote, or both, or a special --to=here
+data FromToHereOptions 
+       = FromOrToRemote FromToOptions
+       | ToHere
+       | FromRemoteToRemote (DeferredParse Remote) (DeferredParse Remote)
 
-data ToHere = ToHere
-
-parseFromToHereOptions :: Parser FromToHereOptions
-parseFromToHereOptions = parsefrom <|> parseto
+parseFromToHereOptions :: Parser (Maybe FromToHereOptions)
+parseFromToHereOptions = go
+       <$> optional parseFromOption
+       <*> optional parseToOption
   where
-       parsefrom = Right . FromRemote . parseRemoteOption <$> parseFromOption
-       parseto = herespecialcase <$> parseToOption
-         where
-               herespecialcase "here" = Left ToHere
-               herespecialcase "." = Left ToHere
-               herespecialcase n = Right $ ToRemote $ parseRemoteOption n
+       go (Just from) (Just to) = Just $ FromRemoteToRemote
+               (parseRemoteOption from)
+               (parseRemoteOption to)
+       go (Just from) Nothing = Just $ FromOrToRemote
+               (FromRemote $ parseRemoteOption from)
+       go Nothing (Just to) = Just $ case to of
+               "here" -> ToHere
+               "." -> ToHere
+               _ -> FromOrToRemote $ ToRemote $ parseRemoteOption to
+       go Nothing Nothing = Nothing
 
 instance DeferredParseClass FromToHereOptions where
-       finishParse = either (pure . Left) (Right <$$> finishParse)
+       finishParse (FromOrToRemote v) = FromOrToRemote <$> finishParse v
+       finishParse ToHere = pure ToHere
+       finishParse (FromRemoteToRemote v1 v2) = FromRemoteToRemote
+               <$> finishParse v1
+               <*> finishParse v2
 
 -- Options for acting on keys, rather than work tree files.
 data KeyOptions
index 267fa71d3d175d21daaf6be4f268a878b2533837..11ef1ddd08778ab2e2fc8d4ca1b245247a475f1f 100644 (file)
@@ -1,6 +1,6 @@
 {- git-annex command
  -
- - Copyright 2010 Joey Hess <id@joeyh.name>
+ - Copyright 2010-2023 Joey Hess <id@joeyh.name>
  -
  - Licensed under the GNU AGPL version 3 or higher.
  -}
@@ -21,7 +21,7 @@ cmd = withAnnexOptions [jobsOption, jsonOptions, jsonProgressOption, annexedMatc
 
 data CopyOptions = CopyOptions
        { copyFiles :: CmdParams
-       , fromToOptions :: FromToHereOptions
+       , fromToOptions :: Maybe FromToHereOptions
        , keyOptions :: Maybe KeyOptions
        , autoMode :: Bool
        , batchOption :: BatchMode
@@ -38,13 +38,19 @@ optParser desc = CopyOptions
 instance DeferredParseClass CopyOptions where
        finishParse v = CopyOptions
                <$> pure (copyFiles v)
-               <*> finishParse (fromToOptions v)
+               <*> maybe (pure Nothing) (Just <$$> finishParse)
+                       (fromToOptions v)
                <*> pure (keyOptions v)
                <*> pure (autoMode v)
                <*> pure (batchOption v)
 
 seek :: CopyOptions -> CommandSeek
-seek o = startConcurrency commandStages $ do
+seek o = case fromToOptions o of
+       Just fto -> seek' o fto
+       Nothing -> giveup "Specify --from or --to"
+
+seek' :: CopyOptions -> FromToHereOptions -> CommandSeek
+seek' o fto = startConcurrency commandStages $ do
        case batchOption o of
                NoBatch -> withKeyOptions
                        (keyOptions o) (autoMode o) seeker
@@ -57,30 +63,33 @@ seek o = startConcurrency commandStages $ do
        ww = WarnUnmatchLsFiles
        
        seeker = AnnexedFileSeeker
-               { startAction = start o
-               , checkContentPresent = case fromToOptions o of
-                       Right (FromRemote _) -> Just False
-                       Right (ToRemote _) -> Just True
-                       Left ToHere -> Just False
+               { startAction = start o fto
+               , checkContentPresent = case fto of
+                       FromOrToRemote (FromRemote _) -> Just False
+                       FromOrToRemote (ToRemote _) -> Just True
+                       ToHere -> Just False
+                       FromRemoteToRemote _ _ -> Just False
                , usesLocationLog = True
                }
-       keyaction = Command.Move.startKey (fromToOptions o) Command.Move.RemoveNever
+       keyaction = Command.Move.startKey fto Command.Move.RemoveNever
 
 {- A copy is just a move that does not delete the source file.
  - However, auto mode avoids unnecessary copies, and avoids getting or
  - sending non-preferred content. -}
-start :: CopyOptions -> SeekInput -> RawFilePath -> Key -> CommandStart
-start o si file key = stopUnless shouldCopy $ 
-       Command.Move.start (fromToOptions o) Command.Move.RemoveNever si file key
+start :: CopyOptions -> FromToHereOptions -> SeekInput -> RawFilePath -> Key -> CommandStart
+start o fto si file key = stopUnless shouldCopy $ 
+       Command.Move.start fto Command.Move.RemoveNever si file key
   where
        shouldCopy
                | autoMode o = want <||> numCopiesCheck file key (<)
                | otherwise = return True
-       want = case fromToOptions o of
-               Right (ToRemote dest) ->
+       want = case fto of
+               FromOrToRemote (ToRemote dest) ->
+                       (Remote.uuid <$> getParsed dest) >>= checkwantsend
+               FromOrToRemote (FromRemote _) -> checkwantget
+               ToHere -> checkwantget
+               FromRemoteToRemote _ dest ->
                        (Remote.uuid <$> getParsed dest) >>= checkwantsend
-               Right (FromRemote _) -> checkwantget
-               Left ToHere -> checkwantget
                        
        checkwantsend = wantGetBy False (Just key) (AssociatedFile (Just file))
        checkwantget = wantGet False (Just key) (AssociatedFile (Just file))
index abe207c2f2c40e6083a8e95d659022dea9302386..b14707eb275ebece3a53015e55e680d9c6fe2d2c 100644 (file)
@@ -1,6 +1,6 @@
 {- git-annex command
  -
- - Copyright 2010-2022 Joey Hess <id@joeyh.name>
+ - Copyright 2010-2023 Joey Hess <id@joeyh.name>
  -
  - Licensed under the GNU AGPL version 3 or higher.
  -}
@@ -32,7 +32,7 @@ cmd = withAnnexOptions [jobsOption, jsonOptions, jsonProgressOption, annexedMatc
 
 data MoveOptions = MoveOptions
        { moveFiles :: CmdParams
-       , fromToOptions :: FromToHereOptions
+       , fromToOptions :: Maybe FromToHereOptions
        , removeWhen :: RemoveWhen
        , keyOptions :: Maybe KeyOptions
        , batchOption :: BatchMode
@@ -49,7 +49,8 @@ optParser desc = MoveOptions
 instance DeferredParseClass MoveOptions where
        finishParse v = MoveOptions
                <$> pure (moveFiles v)
-               <*> finishParse (fromToOptions v)
+               <*> maybe (pure Nothing) (Just <$$> finishParse)
+                       (fromToOptions v)
                <*> pure (removeWhen v)
                <*> pure (keyOptions v)
                <*> pure (batchOption v)
@@ -58,7 +59,12 @@ data RemoveWhen = RemoveSafe | RemoveNever
        deriving (Show, Eq)
 
 seek :: MoveOptions -> CommandSeek
-seek o = startConcurrency stages $ do
+seek o = case fromToOptions o of
+       Just fto -> seek' o fto
+       Nothing -> giveup "Specify --from or --to"
+
+seek' :: MoveOptions -> FromToHereOptions -> CommandSeek
+seek' o fto = startConcurrency stages $ do
        case batchOption o of
                NoBatch -> withKeyOptions (keyOptions o) False seeker
                        (commandAction . keyaction)
@@ -68,18 +74,20 @@ seek o = startConcurrency stages $ do
                        batchAnnexed fmt seeker keyaction
   where
        seeker = AnnexedFileSeeker
-               { startAction = start (fromToOptions o) (removeWhen o)
-               , checkContentPresent = case fromToOptions o of
-                       Right (FromRemote _) -> Nothing
-                       Right (ToRemote _) -> Just True
-                       Left ToHere -> Nothing
+               { startAction = start fto (removeWhen o)
+               , checkContentPresent = case fto of
+                       FromOrToRemote (FromRemote _) -> Nothing
+                       FromOrToRemote (ToRemote _) -> Just True
+                       ToHere -> Nothing
+                       FromRemoteToRemote _ _ -> Nothing
                , usesLocationLog = True
                }
-       stages = case fromToOptions o of
-               Right (FromRemote _) -> downloadStages
-               Right (ToRemote _) -> commandStages
-               Left ToHere -> downloadStages
-       keyaction = startKey (fromToOptions o) (removeWhen o)
+       stages = case fto of
+               FromOrToRemote (FromRemote _) -> downloadStages
+               FromOrToRemote (ToRemote _) -> commandStages
+               ToHere -> downloadStages
+               FromRemoteToRemote _ _ -> commandStages
+       keyaction = startKey fto (removeWhen o)
        ww = WarnUnmatchLsFiles
 
 start :: FromToHereOptions -> RemoveWhen -> SeekInput -> RawFilePath -> Key -> CommandStart
@@ -95,13 +103,13 @@ startKey fromto removewhen (si, k, ai) =
 start' :: FromToHereOptions -> RemoveWhen -> AssociatedFile -> SeekInput -> Key -> ActionItem -> CommandStart
 start' fromto removewhen afile si key ai =
        case fromto of
-               Right (FromRemote src) ->
+               FromOrToRemote (FromRemote src) ->
                        checkFailedTransferDirection ai Download $
                                fromStart removewhen afile key ai si =<< getParsed src
-               Right (ToRemote dest) ->
+               FromOrToRemote (ToRemote dest) ->
                        checkFailedTransferDirection ai Upload $
                                toStart removewhen afile key ai si =<< getParsed dest
-               Left ToHere ->
+               ToHere ->
                        checkFailedTransferDirection ai Download $
                                toHereStart removewhen afile key ai si
 
index 5a4e805cdf2ecc755cc1175c2db48b844bdc14d1..03563bda11415249a9247a952386d4a8c3e98721 100644 (file)
@@ -32,6 +32,13 @@ Paths of files or directories to operate on can be specified.
   Copy the content of files from all reachable remotes to the local
   repository.
 
+* `--from=remote1 --to=remote2`
+
+  Copy the content of files that are in remote1 to remote2. Does not change
+  what is stored in the local repository.
+
+  Note: This may need to store an intermediate copy of the content on disk.
+
 * `--jobs=N` `-JN`
 
   Enables parallel transfers with up to the specified number of jobs
index 0fbea84d5887bedae75b369047ce9ad344581e3e..c2d5ec1e0c59cdaab890f9a3da1bec6b6fb69d12 100644 (file)
@@ -28,6 +28,13 @@ Paths of files or directories to operate on can be specified.
   Move the content of files from all reachable remotes to the local
   repository.
 
+* `--from=remote1 --to=remote2`
+
+  Move the content of files that are in remote1 to remote2. Does not change
+  what is stored in the local repository. 
+
+  Note: This may need to store an intermediate copy of the content on disk.
+
 * `--force`
 
   Override numcopies and required content checking, and always remove