% Copyright (C) 2002-2004 David Roundy
%
% This program is free software; you can redistribute it and/or modify
% it under the terms of the GNU General Public License as published by
% the Free Software Foundation; either version 2, or (at your option)
% any later version.
%
% This program is distributed in the hope that it will be useful,
% but WITHOUT ANY WARRANTY; without even the implied warranty of
% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
% GNU General Public License for more details.
%
% You should have received a copy of the GNU General Public License
% along with this program; see the file COPYING. If not, write to
% the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
% Boston, MA 02110-1301, USA.
\darcsCommand{add}
\begin{code}
module Darcs.Commands.Add ( add, expandDirs ) where
import Data.List ( (\\), nub)
import Data.Maybe( isNothing )
import Control.Monad ( when, unless, liftM )
import Storage.Hashed.Tree( Tree, findTree, expand )
import Storage.Hashed.AnchoredPath( floatPath, anchorPath, parents )
import Darcs.Commands(DarcsCommand(..), putVerbose, putWarning, nodefaults)
import Darcs.Arguments (noskipBoring, allowProblematicFilenames,
fancyMoveAdd,
recursive, workingRepoDir, dryRunNoxml, umaskOption,
listFiles, listUnregisteredFiles,
DarcsFlag ( Recursive, FancyMoveAdd, DryRun, Verbose),
fixSubPaths,
)
import Darcs.Flags( includeBoring, doAllowCaseOnly, doAllowWindowsReserved,)
import Darcs.Utils ( withCurrentDirectory, nubsort )
import IsoDate ( getIsoDateTime )
import Darcs.Repository.State( readRecordedAndPending )
import Darcs.Repository ( amInRepository, withRepoLock, ($-), addToPending )
import Darcs.Patch ( Prim, applyToTree, addfile, adddir, move )
import Darcs.Witnesses.Ordered ( FL(..), (+>+), nullFL )
import Darcs.Witnesses.Sealed ( Sealed(..), Gap(..), FreeLeft, unFreeLeft )
import Darcs.Utils ( isFileReallySymlink, doesDirectoryReallyExist
, doesFileReallyExist, treeHas, treeHasDir, treeHasAnycase )
import Darcs.RepoPath ( SubPath, toFilePath, simpleSubPath, toPath )
import Darcs.Repository.Prefs ( darcsdirFilter, boringFileFilter )
import Data.Maybe ( maybeToList, fromJust )
import System.FilePath.Posix ( takeDirectory, (</>) )
import qualified System.FilePath.Windows as WindowsFilePath
import Printer( text )
#include "gadts.h"
addDescription :: String
addDescription = "Add one or more new files or directories."
addHelp :: String
addHelp =
"Generally a repository contains both files that should be version\n" ++
"controlled (such as source code) and files that Darcs should ignore\n" ++
"(such as executables compiled from the source code). The `darcs add'\n" ++
"command is used to tell Darcs which files to version control.\n" ++
"\n" ++
"When an existing project is first imported into a Darcs repository, it\n" ++
"is common to run `darcs add -r *' or `darcs record -l' to add all\n" ++
"initial source files into darcs.\n"++
"\n" ++
"Adding symbolic links (symlinks) is not supported.\n\n"
add :: DarcsCommand
add = DarcsCommand {commandName = "add",
commandHelp = addHelp ++ addHelp' ++ addHelp'',
commandDescription = addDescription,
commandExtraArgs = 1,
commandExtraArgHelp = ["<FILE or DIRECTORY> ..."],
commandCommand = addCmd,
commandPrereq = amInRepository,
commandGetArgPossibilities = listUnregisteredFiles,
commandArgdefaults = nodefaults,
commandAdvancedOptions = [umaskOption],
commandBasicOptions =
[noskipBoring, allowProblematicFilenames,
recursive "add contents of subdirectories",
fancyMoveAdd,
workingRepoDir, dryRunNoxml]}
addHelp' :: String
addHelp' =
"Darcs will ignore all files and folders that look `boring'. The\n" ++
"--boring option overrides this behaviour.\n" ++
"\n" ++
"Darcs will not add file if another file in the same folder has the\n" ++
"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\n"
addCmd :: [DarcsFlag] -> [String] -> IO ()
addCmd opts args = withRepoLock opts $- \repository ->
do
cur <- expand =<< readRecordedAndPending repository
when (null args) $
putStrLn "Nothing specified, nothing added." >>
putStrLn "Maybe you wanted to say `darcs add --recursive .'?"
origfiles <- fixSubPaths opts args
let parlist = getParents cur (map toFilePath origfiles)
flist' <- if Recursive `elem` opts
then expandDirs origfiles
else return origfiles
let flist = nubsort (parlist ++ toFilePath `map` flist')
nboring <- if includeBoring opts
then return darcsdirFilter
else boringFileFilter
let fixedOpts = if DryRun `elem` opts then Verbose:opts else opts
mapM_ (putWarning fixedOpts . text . ((msgSkipping msgs ++ " boring file ")++)) $
flist \\ nboring flist
date <- getIsoDateTime
Sealed ps <- fmap unFreeLeft $ addp msgs fixedOpts date cur $ nboring flist
when (nullFL ps && not (null args)) $
fail "No files were added"
unless gotDryRun $ addToPending repository ps
where
gotDryRun = DryRun `elem` opts
msgs | gotDryRun = dryRunMessages
| otherwise = normalMessages
addp :: AddMessages -> [DarcsFlag] -> String -> Tree IO -> [FilePath] -> IO (FreeLeft (FL Prim))
addp msgs opts date cur0 files = do
(ps, dups) <-
foldr
(\f rest cur accPS accDups -> do
(cur', mp, mdup) <- addp' cur f
rest cur' (maybeToList mp ++ accPS) (maybeToList mdup ++ accDups))
(\_ ps dups -> return (reverse ps, dups))
files
cur0 [] []
let uniq_dups = nub dups
caseMsg =
if gotAllowCaseOnly then ":"
else ";\nnote that to ensure portability we don't allow\n" ++
"files that differ only in case. Use --case-ok to override this:"
unless (null dups) $ do
dupMsg <-
case uniq_dups of
[f] ->
do
isDir <- doesDirectoryReallyExist f
if isDir
then return $
"The following directory "++msgIs msgs++" already in the repository"
else return $
"The following file "++msgIs msgs++" already in the repository"
fs ->
do
areDirs <- mapM doesDirectoryReallyExist fs
if and areDirs
then return $
"The following directories "++msgAre msgs++" already in the repository"
else
(if or areDirs
then return $
"The following files and directories " ++
msgAre msgs ++ " already in the repository"
else return $
"The following files " ++ msgAre msgs ++ " already in the repository")
putWarning opts . text $ dupMsg ++ caseMsg
mapM_ (putWarning opts . text) uniq_dups
return $ foldr (joinGap (+>+)) (emptyGap NilFL) ps
where
addp' :: Tree IO -> FilePath -> IO (Tree IO, Maybe (FreeLeft (FL Prim)), Maybe FilePath)
addp' cur f = do
already_has <- (if gotAllowCaseOnly then treeHas else treeHasAnycase) cur f
isdir <- doesDirectoryReallyExist f
isfile <- doesFileReallyExist f
islink <- isFileReallySymlink f
case (already_has, is_badfilename, isdir, isfile, islink) of
(True, _, _, _, _) -> return (cur, Nothing, Just f)
(_, True, _, _, _) ->
do putWarning opts . text $
"The filename " ++ f ++ " is invalid under Windows.\nUse --reserved-ok to allow it."
return add_failure
(_, _, True, _, _) -> trypatch $ myadddir f
(_, _, _, True, _) -> trypatch $ myaddfile f
(_, _, _, _, True) -> do putWarning opts . text $ "Sorry, file " ++ f ++
" is a symbolic link, which is unsupported by darcs."
return add_failure
_ -> do putWarning opts . text $ "File "++ f ++" does not exist!"
return add_failure
where is_badfilename = not (gotAllowWindowsReserved || WindowsFilePath.isValid f)
add_failure = (cur, Nothing, Nothing)
trypatch :: FreeLeft (FL Prim) -> IO (Tree IO, Maybe (FreeLeft (FL Prim)), Maybe FilePath)
trypatch p = do Sealed p' <- return $ unFreeLeft p
tree <- applyToTree p' cur
putVerbose opts . text $ msgAdding msgs++" '"++f++"'"
return (tree, Just p, Nothing)
`catch` \_ -> do
err <- parent_error
putWarning opts . text $ msgSkipping msgs ++ " '" ++ f ++ "' ... " ++ err
return (cur, Nothing, Nothing)
parentdir = takeDirectory f
have_parentdir = treeHasDir cur parentdir
parent_error = have_parentdir >>= \x -> return $
if x then ""
else "couldn't add parent directory '"++parentdir++"' to repository."
myadddir d = if gotFancyMoveAdd
then freeGap (adddir (d++"-"++date) :>:
move (d++"-"++date) d :>: NilFL)
else freeGap (adddir d :>: NilFL)
myaddfile d = if gotFancyMoveAdd
then freeGap (addfile (d++"-"++date) :>:
move (d++"-"++date) d :>: NilFL)
else freeGap (addfile d :>: NilFL)
gotFancyMoveAdd = FancyMoveAdd `elem` opts
gotAllowCaseOnly = doAllowCaseOnly opts
gotAllowWindowsReserved = doAllowWindowsReserved opts
data AddMessages =
AddMessages
{ msgSkipping :: String
, msgAdding :: String
, msgIs :: String
, msgAre :: String
}
normalMessages, dryRunMessages :: AddMessages
normalMessages =
AddMessages
{ msgSkipping = "Skipping"
, msgAdding = "Adding"
, msgIs = "is"
, msgAre = "are"
}
dryRunMessages =
AddMessages
{ msgSkipping = "Would skip"
, msgAdding = "Would add"
, msgIs = "would be"
, msgAre = "would be"
}
addHelp'' :: String
addHelp'' =
"The --date-trick option allows you to enable an experimental trick to\n" ++
"make add conflicts, in which two users each add a file or directory\n" ++
"with the same name, less problematic. While this trick is completely\n" ++
"safe, it is not clear to what extent it is beneficial.\n"
expandDirs :: [SubPath] -> IO [SubPath]
expandDirs fs = liftM (map (fromJust . simpleSubPath)) (concat `fmap` mapM (expandOne . toPath) fs)
expandOne :: FilePath -> IO [FilePath]
expandOne "" = listFiles
expandOne f = do
isdir <- doesDirectoryReallyExist f
if not isdir then return [f]
else do fs <- withCurrentDirectory f listFiles
return $ f: map (f </>) fs
getParents :: Tree IO -> [FilePath] -> [FilePath]
getParents cur = map (anchorPath "") . go . map floatPath
where go fs = filter (isNothing . findTree cur) $ concatMap parents fs
\end{code}