module Darcs.UI.Commands.Move ( move, mv ) where
import Darcs.Prelude
import Control.Monad ( when, unless, forM_, forM )
import Data.Maybe ( fromMaybe )
import Darcs.Util.SignalHandler ( withSignalsBlocked )
import Darcs.UI.Commands
( DarcsCommand(..), withStdOpts, nodefaults, commandAlias, amInHashedRepository
, putInfo
)
import Darcs.UI.Completion ( knownFileArgs )
import Darcs.UI.Flags
( DarcsFlag
, allowCaseDifferingFilenames, allowWindowsReservedFilenames
, useCache, dryRun, umask, pathsFromArgs
)
import Darcs.UI.Options ( (^), odesc, ocheck, defaultFlags, (?) )
import qualified Darcs.UI.Options.All as O
import Darcs.Repository.Diff ( treeDiff )
import Darcs.Repository.Flags ( UpdatePending (..), DiffAlgorithm(..) )
import Darcs.Repository.Prefs ( filetypeFunction )
import System.Directory ( renameDirectory, renameFile )
import Darcs.Repository.State ( readRecordedAndPending, readRecorded, updateIndex )
import Darcs.Repository
( Repository
, withRepoLock
, RepoJob(..)
, addPendingDiffToPending
)
import Darcs.Patch.Witnesses.Ordered ( FL(..), (+>+) )
import Darcs.Patch.Witnesses.Sealed ( emptyGap, freeGap, joinGap, FreeLeft )
import Darcs.Util.Global ( debugMessage )
import qualified Darcs.Patch
import Darcs.Patch ( RepoPatch, PrimPatch )
import Darcs.Patch.Apply( ApplyState )
import Data.List.Ordered ( nubSort )
import qualified System.FilePath.Windows as WindowsFilePath
import Darcs.UI.Commands.Util.Tree ( treeHas, treeHasDir, treeHasAnycase, treeHasFile )
import Darcs.Util.Tree( Tree, modifyTree )
import Darcs.Util.Tree.Plain( readPlainTree )
import Darcs.Util.Path
( AbsolutePath
, AnchoredPath
, displayPath
, isRoot
, parent
, realPath
, replaceParent
)
import Darcs.Util.Printer ( Doc, text, hsep )
moveDescription :: String
moveDescription = "Move or rename files."
moveHelp :: Doc
moveHelp = text $
"Darcs cannot reliably distinguish between a file being deleted and a\n" ++
"new one added, and a file being moved. Therefore Darcs always assumes\n" ++
"the former, and provides the `darcs mv` command to let Darcs know when\n" ++
"you want the latter. This command will also move the file in the\n" ++
"working tree (unlike `darcs remove`), unless it has already been moved.\n" ++
"\n" ++
"Darcs will not rename a file if another file in the same folder has\n" ++
"the same name, except for case. The `--case-ok` option overrides this\n" ++
"behaviour. Windows and OS X usually use filesystems that do not allow\n" ++
"files a folder to have the same name except for case (for example,\n" ++
"`ReadMe` and `README`). If `--case-ok` is used, the repository might be\n" ++
"unusable on those systems!\n"
move :: DarcsCommand
move = DarcsCommand
{ commandProgramName = "darcs"
, commandName = "move"
, commandHelp = moveHelp
, commandDescription = moveDescription
, commandExtraArgs = -1
, commandExtraArgHelp = ["<SOURCE> ... <DESTINATION>"]
, commandCommand = moveCmd
, commandPrereq = amInHashedRepository
, commandCompleteArgs = knownFileArgs
, commandArgdefaults = nodefaults
, commandAdvancedOptions = odesc moveAdvancedOpts
, commandBasicOptions = odesc moveBasicOpts
, commandDefaults = defaultFlags moveOpts
, commandCheckOptions = ocheck moveOpts
}
where
moveBasicOpts = O.allowProblematicFilenames ^ O.repoDir
moveAdvancedOpts = O.umask
moveOpts = moveBasicOpts `withStdOpts` moveAdvancedOpts
moveCmd :: (AbsolutePath, AbsolutePath) -> [DarcsFlag] -> [String] -> IO ()
moveCmd fps opts args
| length args < 2 =
fail "The `darcs move' command requires at least two arguments."
| otherwise = do
paths <- pathsFromArgs fps args
when (length paths < 2) $
fail "Note enough valid path arguments remaining."
case paths of
[from, to] -> do
when (from == to) $ fail "Cannot rename a file or directory onto itself."
when (isRoot from) $ fail "Cannot move the root of the repository."
moveFile opts from to
_ -> do
let froms = init paths
to = last paths
when (to `elem` froms) $
fail "Cannot rename a file or directory onto itself."
when (any isRoot froms) $
fail "Cannot move the root of the repository."
moveFilesToDir opts (nubSort froms) to
data FileKind = Dir | File
deriving (Show, Eq)
data FileStatus =
Nonexistant
| Unadded FileKind
| Shadow FileKind
| Known FileKind
deriving Show
fileStatus :: Tree IO
-> Tree IO
-> Tree IO
-> AnchoredPath
-> IO FileStatus
fileStatus work cur recorded fp = do
existsInCur <- treeHas cur fp
existsInRec <- treeHas recorded fp
existsInWork <- treeHas work fp
case (existsInRec, existsInCur, existsInWork) of
(_, True, True) -> do
isDirCur <- treeHasDir cur fp
isDirWork <- treeHasDir work fp
unless (isDirCur == isDirWork) . fail $ "don't know what to do with " ++ displayPath fp
return . Known $ if isDirCur then Dir else File
(_, False, True) -> do
isDir <- treeHasDir work fp
if isDir
then return $ Unadded Dir
else return $ Unadded File
(False, False, False) -> return Nonexistant
(_, _, False) -> do
isDir <- treeHasDir cur fp
if isDir
then return $ Shadow Dir
else return $ Shadow File
moveFile :: [DarcsFlag] -> AnchoredPath -> AnchoredPath -> IO ()
moveFile opts old new = withRepoAndState opts $ \(repo, work, cur, recorded) -> do
new_fs <- fileStatus work cur recorded new
old_fs <- fileStatus work cur recorded old
let doSimpleMove = simpleMove repo opts cur work old new
case (old_fs, new_fs) of
(Nonexistant, _) -> fail $ displayPath old ++ " does not exist."
(Unadded k, _) -> fail $ show k ++ " " ++ displayPath old ++ " is unadded."
(Known _, Nonexistant) -> doSimpleMove
(Known _, Shadow _) -> doSimpleMove
(_, Nonexistant) -> fail $ displayPath old ++ " is not in the repository."
(Known _, Known Dir) -> moveToDir repo opts cur work [old] new
(Known _, Unadded Dir) -> fail $
displayPath new ++ " is not known to darcs; please add it to the repository."
(Known _, _) -> fail $ displayPath new ++ " already exists."
(Shadow k, Unadded k') | k == k' -> doSimpleMove
(Shadow File, Known Dir) -> moveToDir repo opts cur work [old] new
(Shadow Dir, Known Dir) -> doSimpleMove
(Shadow File, Known File) -> doSimpleMove
(Shadow k, _) -> fail $
"cannot move " ++ show k ++ " " ++ displayPath old ++ " into " ++ displayPath new
++ " : " ++ "did you already move it elsewhere?"
moveFilesToDir :: [DarcsFlag] -> [AnchoredPath] -> AnchoredPath -> IO ()
moveFilesToDir opts froms to =
withRepoAndState opts $ \(repo, work, cur, _) ->
moveToDir repo opts cur work froms to
withRepoAndState :: [DarcsFlag]
-> (forall rt p wR wU .
(ApplyState p ~ Tree, RepoPatch p) =>
(Repository rt p wR wU wR, Tree IO, Tree IO, Tree IO)
-> IO ())
-> IO ()
withRepoAndState opts f =
withRepoLock (dryRun ? opts) (useCache ? opts) YesUpdatePending (umask ? opts) $
RepoJob $ \repo -> do
work <- readPlainTree "."
cur <- readRecordedAndPending repo
recorded <- readRecorded repo
f (repo, work, cur, recorded)
simpleMove :: (RepoPatch p, ApplyState p ~ Tree)
=> Repository rt p wR wU wR
-> [DarcsFlag] -> Tree IO -> Tree IO -> AnchoredPath -> AnchoredPath
-> IO ()
simpleMove repository opts cur work old new = do
doMoves repository opts cur work [(old, new)]
putInfo opts $ hsep $ map text ["Finished moving:", displayPath old, "to:", displayPath new]
moveToDir :: (RepoPatch p, ApplyState p ~ Tree)
=> Repository rt p wR wU wR
-> [DarcsFlag] -> Tree IO -> Tree IO -> [AnchoredPath] -> AnchoredPath
-> IO ()
moveToDir repository opts cur work moved finaldir = do
let replaceParentPath a1 a2 =
fromMaybe (error "cannot replace parent of root path") $ replaceParent a1 a2
let moves = zip moved $ map (replaceParentPath finaldir) moved
doMoves repository opts cur work moves
putInfo opts $ hsep $ map text $ ["Finished moving:"] ++ map displayPath moved ++ ["to:", displayPath finaldir]
doMoves :: (RepoPatch p, ApplyState p ~ Tree)
=> Repository rt p wR wU wR
-> [DarcsFlag] -> Tree IO -> Tree IO
-> [(AnchoredPath, AnchoredPath)] -> IO ()
doMoves repository opts cur work moves = do
patches <- forM moves $ \(old, new) -> do
prePatch <- generatePreMovePatches opts cur work (old,new)
return (prePatch, old, new)
withSignalsBlocked $ do
forM_ patches $ \(prePatch, old, new) -> do
let
pendingDiff = joinGap (+>+)
(fromMaybe (emptyGap NilFL) prePatch)
(freeGap $ Darcs.Patch.move old new :>: NilFL)
addPendingDiffToPending repository pendingDiff
moveFileOrDir work old new
updateIndex repository
generatePreMovePatches :: PrimPatch prim => [DarcsFlag] -> Tree IO -> Tree IO
-> (AnchoredPath, AnchoredPath)
-> IO (Maybe (FreeLeft (FL prim)))
generatePreMovePatches opts cur work (old,new) = do
unless newIsOkWindowsPath $ fail newNotOkWindowsPathMsg
let dirPath =
fromMaybe (error "unexpected root path in generatePreMovePatches") $ parent new
haveNewParent <- treeHasDir cur dirPath
unless haveNewParent $
fail $ "The target directory " ++ displayPath dirPath
++ " isn't known in the repository, did you forget to add it?"
newInRecorded <- hasNew cur
newInWorking <- hasNew work
oldInWorking <- treeHas work old
if oldInWorking
then do
when newInWorking $ fail $ alreadyExists "working directory"
if newInRecorded
then Just <$> deleteNewFromRepoPatches
else return Nothing
else do
putInfo opts $ text "Detected post-hoc move."
unless newInWorking $
fail $ "Cannot determine post-hoc move target, "
++ "no file/dir named:\n" ++ displayPath new
Just <$> if newInRecorded
then deleteNewFromRepoPatches
else return $ emptyGap NilFL
where
newIsOkWindowsPath =
allowWindowsReservedFilenames ? opts || WindowsFilePath.isValid (realPath new)
newNotOkWindowsPathMsg =
"The filename " ++ displayPath new ++ " is not valid under Windows.\n"
++ "Use --reserved-ok to allow such filenames."
deleteNewFromRepoPatches = do
putInfo opts $ text $
"Existing recorded contents of " ++ displayPath new ++ " will be overwritten."
ftf <- filetypeFunction
let curNoNew = modifyTree cur new Nothing
treeDiff MyersDiff ftf cur curNoNew
hasNew s = treeHas_case (modifyTree s old Nothing) new
treeHas_case = if allowCaseDifferingFilenames ? opts then treeHas else treeHasAnycase
alreadyExists inWhat =
if allowCaseDifferingFilenames ? opts
then "A file or dir named "++displayPath new++" already exists in "
++ inWhat ++ "."
else "A file or dir named "++displayPath new++" (or perhaps differing "
++ "only in case)\nalready exists in "++ inWhat ++ ".\n"
++ "Use --case-ok to allow files differing only in case."
moveFileOrDir :: Tree IO -> AnchoredPath -> AnchoredPath -> IO ()
moveFileOrDir work old new = do
has_file <- treeHasFile work old
has_dir <- treeHasDir work old
when has_file $ do
debugMessage $ unwords ["renameFile", displayPath old, displayPath new]
renameFile (realPath old) (realPath new)
when has_dir $ do
debugMessage $ unwords ["renameDirectory", displayPath old, displayPath new]
renameDirectory (realPath old) (realPath new)
mv :: DarcsCommand
mv = commandAlias "mv" Nothing move