module Darcs.UI.Commands.Diff ( diffCommand ) where
import Darcs.Prelude hiding ( all )
import Control.Monad ( unless, when )
import Data.Maybe ( fromMaybe )
import Data.Maybe ( isJust )
import System.Directory ( copyFile, createDirectory, findExecutable, listDirectory )
import System.FilePath.Posix ( takeFileName, (</>) )
import Darcs.Patch ( listTouchedFiles )
import Darcs.Patch.Apply ( Apply(..) )
import Darcs.Patch.Depends ( findCommonWithThem )
import Darcs.Patch.Info ( displayPatchInfo )
import Darcs.Patch.Match ( matchFirstPatchset, matchSecondPatchset, secondMatch )
import Darcs.Patch.Named ( anonymous )
import Darcs.Patch.PatchInfoAnd ( info, n2pia )
import Darcs.Patch.Set ( patchSetSnoc )
import Darcs.Patch.Witnesses.Ordered ( (:>)(..), mapFL )
import Darcs.Patch.Witnesses.Sealed ( Sealed(..), seal )
import Darcs.Repository ( RepoJob(..), readRepo, withRepository )
import Darcs.Repository.Flags ( DiffAlgorithm(MyersDiff), WantGuiPause(..) )
import Darcs.Repository.Paths ( pristineDirPath )
import Darcs.Repository.State
( ScanKnown(..)
, applyTreeFilter
, readRecorded
, restrictSubpaths
, unrecordedChanges
)
import Darcs.UI.Commands
( DarcsCommand(..)
, amInHashedRepository
, nodefaults
, withStdOpts
)
import Darcs.UI.Completion ( knownFileArgs )
import Darcs.UI.External ( diffProgram )
import Darcs.UI.Flags ( DarcsFlag, pathSetFromArgs, useCache, wantGuiPause )
import Darcs.UI.Options ( defaultFlags, ocheck, odesc, parseFlags, (?), (^) )
import qualified Darcs.UI.Options.All as O
import Darcs.Util.CommandLine ( parseCmd )
import Darcs.Util.Exec ( execInteractive )
import Darcs.Util.File ( withCurrentDirectory )
import Darcs.Util.Global ( debugMessage )
import Darcs.Util.Lock ( withTempDir )
import Darcs.Util.Path ( AbsolutePath, AnchoredPath, isPrefix, toFilePath )
import Darcs.Util.Printer ( Doc, putDoc, text, vcat )
import Darcs.Util.Prompt ( askEnter )
import Darcs.Util.Tree.Hashed ( hashedTreeIO )
import Darcs.Util.Tree.Plain ( writePlainTree )
import Darcs.Util.Workaround ( getCurrentDirectory )
diffDescription :: String
diffDescription = "Create a diff between two versions of the repository."
diffHelp :: Doc
diffHelp = text $
"The `darcs diff` command compares two versions of the working tree of\n" ++
"the current repository. Without options, the pristine (recorded) and\n" ++
"unrecorded working trees are compared. This is lower-level than\n" ++
"the `darcs whatsnew` command, since it outputs a line-by-line diff,\n" ++
"and it is also slower. As with `darcs whatsnew`, if you specify\n" ++
"files or directories, changes to other files are not listed.\n" ++
"The command always uses an external diff utility.\n" ++
"\n" ++
"With the `--patch` option, the comparison will be made between working\n" ++
"trees with and without that patch. Patches *after* the selected patch\n" ++
"are not present in either of the compared working trees. The\n" ++
"`--from-patch` and `--to-patch` options allow the set of patches in the\n" ++
"`old' and `new' working trees to be specified separately.\n" ++
"\n" ++
"The associated tag and match options are also understood, e.g. `darcs\n" ++
"diff --from-tag 1.0 --to-tag 1.1`. All these options assume an\n" ++
"ordering of the patch set, so results may be affected by operations\n" ++
"such as `darcs optimize reorder`.\n" ++
"\n" ++
"diff(1) is always called with the arguments `-rN` and by default also\n" ++
"with `-u` to show the differences in unified format. This can be turned\n" ++
"off by passing `--no-unified`. An additional argument can be passed\n" ++
"using `--diff-opts`, such as `--diff-opts=-ud` or `--diff-opts=-wU9`.\n" ++
"\n" ++
"The `--diff-command` option can be used to specify an alternative\n" ++
"utility. Arguments may be included, separated by whitespace. The value\n" ++
"is not interpreted by a shell, so shell constructs cannot be used. The\n" ++
"arguments %1 and %2 MUST be included, these are substituted for the two\n" ++
"working trees being compared. For instance:\n" ++
"\n" ++
" darcs diff -p . --diff-command \"meld %1 %2\"\n" ++
"\n" ++
"If this option is used, `--diff-opts` is ignored.\n"
diffCommand :: DarcsCommand
diffCommand = DarcsCommand
{ commandProgramName = "darcs"
, commandName = "diff"
, commandHelp = diffHelp
, commandDescription = diffDescription
, commandExtraArgs = -1
, commandExtraArgHelp = ["[FILE or DIRECTORY]..."]
, commandCommand = diffCmd
, commandPrereq = amInHashedRepository
, commandCompleteArgs = knownFileArgs
, commandArgdefaults = nodefaults
, commandAdvancedOptions = odesc diffAdvancedOpts
, commandBasicOptions = odesc diffBasicOpts
, commandDefaults = defaultFlags diffOpts
, commandCheckOptions = ocheck diffOpts
}
where
diffBasicOpts
= O.matchOneOrRange
^ O.extDiff
^ O.repoDir
^ O.storeInMemory
diffAdvancedOpts = O.pauseForGui ^ O.useIndex
diffOpts = diffBasicOpts `withStdOpts` diffAdvancedOpts
diffCmd :: (AbsolutePath, AbsolutePath) -> [DarcsFlag] -> [String] -> IO ()
diffCmd fps opts args
| not (null (O.matchLast ? opts)) &&
not (null (O.matchFrom ? opts)) =
fail $ "using --patch and --last at the same time with the 'diff'" ++
" command doesn't make sense. Use --from-patch to create a diff" ++
" from this patch to the present, or use just '--patch' to view" ++
" this specific patch."
| otherwise = doDiff opts =<< pathSetFromArgs fps args
doDiff :: [DarcsFlag] -> Maybe [AnchoredPath] -> IO ()
doDiff opts mpaths = withRepository (useCache ? opts) $ RepoJob $ \repository -> do
patchset <- readRepo repository
unrecorded <- unrecordedChanges (O.useIndex ? opts, ScanKnown, MyersDiff)
O.NoLookForMoves O.NoLookForReplaces
repository mpaths
unrecorded' <- n2pia `fmap` anonymous unrecorded
let matchFlags = parseFlags O.matchOneOrRange opts
Sealed all <- return $
if secondMatch matchFlags
then seal patchset
else seal $ patchSetSnoc patchset unrecorded'
Sealed ctx <- return $
fromMaybe (seal patchset) $ matchFirstPatchset matchFlags patchset
Sealed match <- return $
fromMaybe (seal all) $ matchSecondPatchset matchFlags patchset
(_ :> todiff) <- return $ findCommonWithThem match ctx
(_ :> tounapply) <- return $ findCommonWithThem all match
Sealed logmatch <- return $
if secondMatch matchFlags
then seal match
else seal patchset
(_ :> tolog) <- return $ findCommonWithThem logmatch ctx
let touched = listTouchedFiles todiff
files = case mpaths of
Nothing -> touched
Just paths ->
concatMap (\path -> filter (isPrefix path) touched) paths
relevant <- restrictSubpaths repository files
formerdir <- getCurrentDirectory
let thename = takeFileName formerdir
withTempDir "darcs-diff" $ \tmpdir -> do
getCurrentDirectory >>= debugMessage . ("doDiff: I am now in "++)
let pdir = toFilePath tmpdir </> ("pristine.hashed-"++thename)
createDirectory pdir
let odir = toFilePath tmpdir </> ("old-"++thename)
createDirectory odir
let ndir = toFilePath tmpdir </> ("new-"++thename)
createDirectory ndir
withCurrentDirectory formerdir $ do
let pdirpath = toFilePath pdir
pfiles <- listDirectory pristineDirPath
let copy srcdir destdir name = copyFile (srcdir</>name) (destdir</>name)
mapM_ (copy pristineDirPath pdirpath) pfiles
pristine <- readRecorded repository
base <- if secondMatch matchFlags
then return pristine
else snd <$> hashedTreeIO (apply unrecorded') pristine pdirpath
newtree <- snd <$> hashedTreeIO (unapply tounapply) base pdirpath
oldtree <- snd <$> hashedTreeIO (unapply todiff) newtree pdirpath
writePlainTree (applyTreeFilter relevant oldtree) (toFilePath odir)
writePlainTree (applyTreeFilter relevant newtree) (toFilePath ndir)
putDoc $ vcat $ map displayPatchInfo $ reverse $ mapFL info tolog
cmd <- diffProgram
let old = takeFileName $ toFilePath odir
new = takeFileName $ toFilePath ndir
case getDiffCmdAndArgs cmd opts old new of
Left err -> fail err
Right (d_cmd, d_args) -> do
cmdExists <- findExecutable d_cmd
unless (isJust cmdExists) $
fail $ d_cmd ++ " is not an executable in --diff-command"
let pausingForGui = (wantGuiPause opts == YesWantGuiPause)
cmdline = unwords (d_cmd : d_args)
when pausingForGui $ putStrLn $ "Running command '" ++ cmdline ++ "'"
_ <- execInteractive cmdline Nothing
when pausingForGui $ askEnter "Hit return to move on..."
getDiffCmdAndArgs :: String -> [DarcsFlag] -> String -> String
-> Either String (String, [String])
getDiffCmdAndArgs cmd opts f1 f2 = helper (O.extDiff ? opts) where
helper extDiff =
case O.diffCmd extDiff of
Just c ->
case parseCmd [ ('1', f1) , ('2', f2) ] c of
Left err -> Left $ show err
Right ([],_) -> error "parseCmd should never return empty list"
Right (cmd':args,_)
| length (filter (== f1) args) == 1
, length (filter (== f2) args) == 1 -> Right (cmd',args)
| otherwise -> Left $ "Invalid argument (%1 or %2) in --diff-command"
Nothing ->
Right (cmd, "-rN":getDiffOpts extDiff++[f1,f2])
getDiffOpts :: O.ExternalDiff -> [String]
getDiffOpts O.ExternalDiff {O.diffOpts=os,O.diffUnified=u} = addUnified os where
addUnified = if u then ("-u":) else id