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