1 %  Copyright (C) 2003-2004 David Roundy
    2 %
    3 %  This program is free software; you can redistribute it and/or modify
    4 %  it under the terms of the GNU General Public License as published by
    5 %  the Free Software Foundation; either version 2, or (at your option)
    6 %  any later version.
    7 %
    8 %  This program is distributed in the hope that it will be useful,
    9 %  but WITHOUT ANY WARRANTY; without even the implied warranty of
   10 %  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
   11 %  GNU General Public License for more details.
   12 %
   13 %  You should have received a copy of the GNU General Public License
   14 %  along with this program; see the file COPYING.  If not, write to
   15 %  the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
   16 %  Boston, MA 02110-1301, USA.
   17 
   18 \darcsCommand{diff}
   19 \begin{code}
   20 {-# OPTIONS_GHC -cpp #-}
   21 {-# LANGUAGE CPP #-}
   22 
   23 module Darcs.Commands.Diff ( diff_command ) where
   24 
   25 import System.FilePath.Posix ( takeFileName )
   26 import System.Directory ( setCurrentDirectory )
   27 import Workaround ( getCurrentDirectory )
   28 import Darcs.Utils ( askUser, withCurrentDirectory )
   29 import Control.Monad ( when )
   30 import Data.List ( (\\) )
   31 
   32 import Darcs.External( diff_program )
   33 import CommandLine ( parseCmd )
   34 import Darcs.Commands ( DarcsCommand(..), nodefaults )
   35 import Darcs.Arguments ( DarcsFlag(DiffFlags, Unified, DiffCmd,
   36                                    LastN, AfterPatch),
   37                         match_range, store_in_memory, 
   38                         diff_cmd_flag, diffflags, unidiff,
   39                          working_repo_dir, fixSubPaths,
   40                       )
   41 import Darcs.Hopefully ( info )
   42 import Darcs.RepoPath ( toFilePath, sp2fn )
   43 import Darcs.Match ( get_partial_first_match, get_partial_second_match,
   44                      first_match, second_match,
   45                      match_first_patchset, match_second_patchset )
   46 import Darcs.Repository ( PatchSet, withRepository, ($-), read_repo,
   47                           amInRepository, slurp_recorded_and_unrecorded,
   48                           createPristineDirectoryTree,
   49                           createPartialsPristineDirectoryTree )
   50 import Darcs.SlurpDirectory ( get_path_list, writeSlurpy )
   51 import Darcs.Patch ( RepoPatch )
   52 import Darcs.Ordered ( mapRL, concatRL )
   53 import Darcs.Patch.Info ( PatchInfo, human_friendly )
   54 import Darcs.External ( execPipeIgnoreError, clonePaths )
   55 import Darcs.Lock ( withTempDir )
   56 import Darcs.Sealed ( unsafeUnseal )
   57 import Printer ( Doc, putDoc, vcat, empty, ($$) )
   58 #include "impossible.h"
   59 
   60 diff_description :: String
   61 diff_description = "Create a diff between two versions of the repository."
   62 
   63 diff_help :: String
   64 diff_help =
   65  "Diff can be used to create a diff between two versions which are in your\n"++
   66  "repository.  Specifying just --from-patch will get you a diff against\n"++
   67  "your working copy.  If you give diff no version arguments, it gives\n"++
   68  "you the same information as whatsnew except that the patch is\n"++
   69  "formatted as the output of a diff command\n"
   70 
   71 diff_command :: DarcsCommand
   72 diff_command = DarcsCommand {command_name = "diff",
   73                              command_help = diff_help,
   74                              command_description = diff_description,
   75                              command_extra_args = -1,
   76                              command_extra_arg_help
   77                                  = ["[FILE or DIRECTORY]..."],
   78                              command_command = diff_cmd,
   79                              command_prereq = amInRepository,
   80                              command_get_arg_possibilities = return [],
   81                              command_argdefaults = nodefaults,
   82                              command_advanced_options = [],
   83                              command_basic_options = [match_range,
   84                                                      diff_cmd_flag,
   85                                                      diffflags, unidiff,
   86                                                      working_repo_dir, store_in_memory]}
   87 \end{code}
   88 
   89 \begin{options}
   90 --diff-opts
   91 \end{options}
   92 
   93 Diff calls an external ``diff'' command to do the actual work, and passes
   94 any unrecognized flags to this diff command.  Thus you can call
   95 \begin{verbatim}
   96 % darcs diff -t 0.9.8 -t 0.9.10 -- -u
   97 \end{verbatim}
   98 to get a diff in the unified format.  Actually, thanks to the wonders of
   99 getopt you need the ``\verb!--!'' shown above before any arguments to diff.
  100 You can also specify additional arguments to diff using the
  101 \verb!--diff-opts! flag.  The above command would look like this:
  102 \begin{verbatim}
  103 % darcs diff --diff-opts -u -t 0.9.8 -t 0.9.10
  104 \end{verbatim}
  105 This may not seem like an improvement, but it really pays off when you want
  106 to always give diff the same options.  You can do this by adding
  107 \begin{verbatim}
  108 % diff diff-opts -udp
  109 \end{verbatim}
  110 to your \verb!_darcs/prefs/defaults! file.
  111 
  112 \begin{code}
  113 get_diff_opts :: [DarcsFlag] -> [String]
  114 get_diff_opts [] = []
  115 get_diff_opts (Unified:fs) = "-u" : get_diff_opts fs
  116 get_diff_opts (DiffFlags f:fs) = f : get_diff_opts fs
  117 get_diff_opts (_:fs) = get_diff_opts fs
  118 
  119 has_diff_cmd_flag :: [DarcsFlag] -> Bool
  120 has_diff_cmd_flag (DiffCmd _:_) = True
  121 has_diff_cmd_flag (_:t) = has_diff_cmd_flag t
  122 has_diff_cmd_flag []  = False
  123 
  124 -- | Returns the command we should use for diff as a tuple (command, arguments).
  125 -- This will either be whatever the user specified via --diff-command  or the
  126 -- default 'diff_program'.  Note that this potentially involves parsing the
  127 -- user's diff-command, hence the possibility for failure with an exception.
  128 get_diff_cmd_and_args :: String -> [DarcsFlag] -> String -> String
  129                       -> Either String (String, [String])
  130 get_diff_cmd_and_args cmd opts f1 f2 = helper opts where
  131   helper (DiffCmd c:_) =
  132     case parseCmd [ ('1', f1) , ('2', f2) ] c of
  133     Left err        -> Left $ show err
  134     Right ([],_)    -> bug $ "parseCmd should never return empty list"
  135     Right ((h:t),_) -> Right (h,t)
  136   helper [] = -- if no command specified, use 'diff'
  137     Right (cmd, ("-rN":get_diff_opts opts++[f1,f2]))
  138   helper (_:t) = helper t
  139 \end{code}
  140 
  141 If you want to view only the differences to one or more files, you can do
  142 so with a command such as
  143 \begin{verbatim}
  144 % darcs diff foo.c bar.c baz/
  145 \end{verbatim}
  146 
  147 \begin{options}
  148 --diff-command
  149 \end{options}
  150 
  151 You can use a different program to view differences by including
  152 the flag \verb!--diff-command!, e.g.
  153 \begin{verbatim}
  154 --diff-command 'opendiff %1 %2'.
  155 \end{verbatim}
  156 The \verb!%1! and \verb!%2!  are replaced with the two versions to be
  157 merged.  The above example works with the FileMerge.app tool that comes with
  158 Apple's developer tools.  To use xxdiff, you would use
  159 \begin{verbatim}
  160 --diff-command 'xxdiff %1 %2'
  161 \end{verbatim}
  162 To use \verb!kdiff3!, you can use
  163 \begin{verbatim}
  164 --diff-command 'kdiff3 %1 %2'
  165 \end{verbatim}
  166 
  167 Note that the command is split into space-separated words and the first one is
  168 \verb!exec!ed with the rest as arguments---it is not a shell command.  Also
  169 the substitution of the \verb!%! escapes is only done on complete words.
  170 See \ref{resolution} for how you might work around this fact, for example,
  171 with Emacs' Ediff package.
  172 
  173 Note also that the \verb!--diff-opts! flag is ignored if you use this option.
  174 
  175 \begin{code}
  176 diff_cmd :: [DarcsFlag] -> [String] -> IO ()
  177 diff_cmd opts args = withRepository opts $- \repository -> do
  178   when (not (null [i | LastN i <- opts])
  179        && not (null [p | AfterPatch p <- opts])
  180        ) $
  181     fail ("using --patch and --last at the same time with the 'diff' command"
  182          ++ " doesn't make sense. Use --from-patch to create a diff from this"
  183          ++ " patch to the present, or use just '--patch' to view this specific"
  184          ++ " patch.")
  185   formerdir <- getCurrentDirectory
  186   path_list <- if null args
  187                then return []
  188                else map sp2fn `fmap` fixSubPaths opts args
  189   thename <- return $ takeFileName formerdir
  190   withTempDir ("old-"++thename) $ \odir -> do
  191     setCurrentDirectory formerdir
  192     withTempDir ("new-"++thename) $ \ndir -> do
  193     if first_match opts
  194        then withCurrentDirectory odir $
  195             get_partial_first_match repository opts path_list
  196        else if null path_list
  197             then createPristineDirectoryTree repository (toFilePath odir)
  198             else createPartialsPristineDirectoryTree repository path_list (toFilePath odir)
  199     if second_match opts
  200        then withCurrentDirectory ndir $
  201             get_partial_second_match repository opts path_list
  202        else do (_, s) <- slurp_recorded_and_unrecorded repository
  203                let ps = concatMap (get_path_list s . toFilePath) path_list
  204                if null path_list
  205                   then withCurrentDirectory ndir $ writeSlurpy s "."
  206                   else clonePaths formerdir (toFilePath ndir) ps
  207     thediff <- withCurrentDirectory (toFilePath odir ++ "/..") $
  208                    case path_list of
  209                    [] -> rundiff (takeFileName $ toFilePath odir) (takeFileName $ toFilePath ndir)
  210                    fs -> vcat `fmap`
  211                          mapM (\f -> rundiff
  212                                (takeFileName (toFilePath odir) ++ "/" ++ toFilePath f)
  213                                (takeFileName (toFilePath ndir) ++ "/" ++ toFilePath f)) fs
  214     morepatches <- read_repo repository
  215     putDoc $ changelog (get_diff_info opts morepatches)
  216             $$ thediff
  217     where rundiff :: String -> String -> IO Doc
  218           rundiff f1 f2 = do
  219             cmd <- diff_program
  220             case get_diff_cmd_and_args cmd opts f1 f2 of
  221              Left err -> fail err
  222              Right (d_cmd, d_args) ->
  223               let other_diff = has_diff_cmd_flag opts in
  224               do when other_diff $ putStrLn $
  225                    "Running command '" ++ unwords (d_cmd:d_args) ++ "'"
  226                  output <- execPipeIgnoreError d_cmd d_args empty
  227                  when other_diff $ do
  228                     askUser "Hit return to move on..."
  229                     return ()
  230                  return output
  231 
  232 get_diff_info :: RepoPatch p => [DarcsFlag] -> PatchSet p -> [PatchInfo]
  233 get_diff_info opts ps =
  234     let pi1s = mapRL info $ concatRL $ if first_match opts
  235                                        then unsafeUnseal $ match_first_patchset opts ps
  236                                        else ps
  237         pi2s = mapRL info $ concatRL $ if second_match opts
  238                                        then unsafeUnseal $ match_second_patchset opts ps
  239                                        else ps
  240         in pi2s \\ pi1s
  241 
  242 changelog :: [PatchInfo] -> Doc
  243 changelog pis = vcat $ map human_friendly pis
  244 \end{code}
  245