summaryrefslogtreecommitdiff
path: root/lisp/diff.el
blob: f181873384b729cbce94d1d2538f914ce71e42c6 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
;;; diff.el --- Run `diff' in compilation-mode.

;; Copyright (C) 1992, 1994, 1996 Free Software Foundation, Inc.

;; Keywords: unix, tools

;; This file is part of GNU Emacs.

;; GNU Emacs 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.

;; GNU Emacs 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 GNU Emacs; see the file COPYING.  If not, write to the
;; Free Software Foundation, Inc., 59 Temple Place - Suite 330,
;; Boston, MA 02111-1307, USA.

;;; Commentary:

;; This package helps you explore differences between files, using the
;; UNIX command diff(1).  The commands are `diff' and `diff-backup'.
;; You can specify options with `diff-switches'.

;;; Code:

(require 'compile)

(defgroup diff nil
  "Comparing files with `diff'."
  :group 'tools)

;;;###autoload
(defcustom diff-switches "-c"
  "*A string or list of strings specifying switches to be be passed to diff."
  :type '(choice string (repeat string))
  :group 'diff)

;;;###autoload
(defcustom diff-command "diff"
  "*The command to use to run diff."
  :type 'string
  :group 'diff)

(defvar diff-regexp-alist
  '(
    ;; -u format: @@ -OLDSTART,OLDEND +NEWSTART,NEWEND @@
    ("^@@ -\\([0-9]+\\),[0-9]+ \\+\\([0-9]+\\),[0-9]+ @@$" 1 2)

    ;; -c format: *** OLDSTART,OLDEND ****
    ("^\\*\\*\\* \\([0-9]+\\),[0-9]+ \\*\\*\\*\\*$" 1 nil)
    ;;            --- NEWSTART,NEWEND ----
    ("^--- \\([0-9]+\\),[0-9]+ ----$" nil 1)

    ;; plain diff format: OLDSTART[,OLDEND]{a,d,c}NEWSTART[,NEWEND]
    ("^\\([0-9]+\\)\\(,[0-9]+\\)?[adc]\\([0-9]+\\)\\(,[0-9]+\\)?$" 1 3)

    ;; -e (ed) format: OLDSTART[,OLDEND]{a,d,c}
    ("^\\([0-9]+\\)\\(,[0-9]+\\)?[adc]$" 1)

    ;; -f format: {a,d,c}OLDSTART[ OLDEND]
    ;; -n format: {a,d,c}OLDSTART LINES-CHANGED
    ("^[adc]\\([0-9]+\\)\\( [0-9]+\\)?$" 1)
    )
  "Alist (REGEXP OLD-IDX NEW-IDX) of regular expressions to match difference
sections in \\[diff] output.  If REGEXP matches, the OLD-IDX'th
subexpression gives the line number in the old file, and NEW-IDX'th
subexpression gives the line number in the new file.  If OLD-IDX or NEW-IDX
is nil, REGEXP matches only half a section.")

(defvar diff-old-file nil
  "This is the old file name in the comparison in this buffer.")
(defvar diff-new-file nil
  "This is the new file name in the comparison in this buffer.")
(defvar diff-old-temp-file nil
  "This is the name of a temp file to be deleted after diff finishes.")
(defvar diff-new-temp-file nil
  "This is the name of a temp file to be deleted after diff finishes.")

;; See compilation-parse-errors-function (compile.el).
(defun diff-parse-differences (limit-search find-at-least)
  (setq compilation-error-list nil)
  (message "Parsing differences...")

  ;; Don't reparse diffs already seen at last parse.
  (if compilation-parsing-end (goto-char compilation-parsing-end))

  ;; Construct in REGEXP a regexp composed of all those in dired-regexp-alist.
  (let ((regexp (mapconcat (lambda (elt)
			     (concat "\\(" (car elt) "\\)"))
			   diff-regexp-alist
			   "\\|"))
	;; (GROUP-IDX OLD-IDX NEW-IDX)
	(groups (let ((subexpr 1))
		  (mapcar (lambda (elt)
			    (prog1
				(cons subexpr
				      (mapcar (lambda (n)
						(and n
						     (+ subexpr n)))
					      (cdr elt)))
			      (setq subexpr (+ subexpr 1
					       (count-regexp-groupings
						(car elt))))))
			  diff-regexp-alist)))

	(new-error
	 (function (lambda (file subexpr)
		     (setq compilation-error-list
			   (cons
			    (cons (save-excursion
				    ;; Report location of message
				    ;; at beginning of line.
				    (goto-char
				     (match-beginning subexpr))
				    (beginning-of-line)
				    (point-marker))
				  ;; Report location of corresponding text.
				  (let ((line (string-to-int
					       (buffer-substring
						(match-beginning subexpr)
						(match-end subexpr)))))
				    (save-excursion
				      (save-match-data
					(set-buffer (find-file-noselect file)))
				      (save-excursion
					(goto-line line)
					(point-marker)))))
			    compilation-error-list)))))

	(found-desired nil)
	(num-loci-found 0)
	g)

    (while (and (not found-desired)
		;; We don't just pass LIMIT-SEARCH to re-search-forward
		;; because we want to find matches containing LIMIT-SEARCH
		;; but which extend past it.
		(re-search-forward regexp nil t))

      ;; Find which individual regexp matched.
      (setq g groups)
      (while (and g (null (match-beginning (car (car g)))))
	(setq g (cdr g)))
      (setq g (car g))

      (if (nth 1 g)			;OLD-IDX
	  (funcall new-error diff-old-file (nth 1 g)))
      (if (nth 2 g)			;NEW-IDX
	  (funcall new-error diff-new-file (nth 2 g)))

      (setq num-loci-found (1+ num-loci-found))
      (if (or (and find-at-least
		   (>= num-loci-found find-at-least))
	      (and limit-search (>= (point) limit-search)))
	      ;; We have found as many new loci as the user wants,
	      ;; or the user wanted a specific diff, and we're past it.
	  (setq found-desired t)))
    (if found-desired
	(setq compilation-parsing-end (point))
      ;; Set to point-max, not point, so we don't perpetually
      ;; parse the last bit of text when it isn't a diff header.
      (setq compilation-parsing-end (point-max)))
    (message "Parsing differences...done"))
  (setq compilation-error-list (nreverse compilation-error-list)))

(defun diff-process-setup ()
  "Set up \`compilation-exit-message-function' for \`diff'."
  ;; Avoid frightening people with "abnormally terminated"
  ;; if diff finds differences.
  (set (make-local-variable 'compilation-exit-message-function)
       (lambda (status code msg)
	 (cond ((not (eq status 'exit))
		(cons msg code))
	       ((zerop code)
		'("finished (no differences)\n" . "no differences"))
	       ((= code 1)
		'("finished\n" . "differences found"))
	       (t
		(cons msg code))))))

;;;###autoload
(defun diff (old new &optional switches)
  "Find and display the differences between OLD and NEW files.
Interactively the current buffer's file name is the default for NEW
and a backup file for NEW is the default for OLD.
With prefix arg, prompt for diff switches."
  (interactive
   (nconc
    (let (oldf newf)
      (nreverse
       (list
	(setq newf (buffer-file-name)
	      newf (if (and newf (file-exists-p newf))
		       (read-file-name
			(concat "Diff new file: ("
				(file-name-nondirectory newf) ") ")
			nil newf t)
		     (read-file-name "Diff new file: " nil nil t)))
	(setq oldf (file-newest-backup newf)
	      oldf (if (and oldf (file-exists-p oldf))
		       (read-file-name
			(concat "Diff original file: ("
				(file-name-nondirectory oldf) ") ")
			(file-name-directory oldf) oldf t)
		     (read-file-name "Diff original file: "
				     (file-name-directory newf) nil t))))))
    (if current-prefix-arg
	(list (read-string "Diff switches: "
			   (if (stringp diff-switches)
			       diff-switches
			     (mapconcat 'identity diff-switches " "))))
      nil)))
  (setq new (expand-file-name new)
	old (expand-file-name old))
  (let ((old-alt (file-local-copy old))
	(new-alt (file-local-copy new))
	buf)
    (save-excursion
	(let ((compilation-process-setup-function 'diff-process-setup)
	      (command
	       (mapconcat 'identity
			  (append (list diff-command)
				  ;; Use explicitly specified switches
				  (if switches
				      (if (consp switches)
					  switches (list switches))
				    ;; If not specified, use default.
				    (if (consp diff-switches)
					diff-switches
				      (list diff-switches)))
				  (if (or old-alt new-alt)
				      (list "-L" old "-L" new))
				  (list
				   (shell-quote-argument (or old-alt old)))
				  (list
				   (shell-quote-argument (or new-alt new))))
			  " ")))
	  (setq buf
		(compile-internal command
				  "No more differences" "Diff"
				  'diff-parse-differences))
	  (set-buffer buf)
	  (set (make-local-variable 'diff-old-file) old)
	  (set (make-local-variable 'diff-new-file) new)
	  (set (make-local-variable 'diff-old-temp-file) old-alt)
	  (set (make-local-variable 'diff-new-temp-file) new-alt)
	  (set (make-local-variable 'compilation-finish-function)
	       (function (lambda (buff msg)
			   (if diff-old-temp-file
			       (delete-file diff-old-temp-file))
			   (if diff-new-temp-file
			       (delete-file diff-new-temp-file)))))
	  ;; When async processes aren't available, the compilation finish
	  ;; function doesn't get chance to run.  Invoke it by hand.
	  (or (fboundp 'start-process)
	      (funcall compilation-finish-function nil nil))
	  buf))))

;;;###autoload
(defun diff-backup (file &optional switches)
  "Diff this file with its backup file or vice versa.
Uses the latest backup, if there are several numerical backups.
If this file is a backup, diff it with its original.
The backup file is the first file given to `diff'."
  (interactive (list (read-file-name "Diff (file with backup): ")
		     (if current-prefix-arg
			 (read-string "Diff switches: "
				      (if (stringp diff-switches)
					  diff-switches
					(mapconcat 'identity
						   diff-switches " ")))
		       nil)))
  (let (bak ori)
    (if (backup-file-name-p file)
	(setq bak file
	      ori (file-name-sans-versions file))
      (setq bak (or (diff-latest-backup-file file)
		    (error "No backup found for %s" file))
	    ori file))
    (diff bak ori switches)))

(defun diff-latest-backup-file (fn)	; actually belongs into files.el
  "Return the latest existing backup of FILE, or nil."
  (let ((handler (find-file-name-handler fn 'diff-latest-backup-file)))
    (if handler
	(funcall handler 'diff-latest-backup-file fn)
      ;; First try simple backup, then the highest numbered of the
      ;; numbered backups.
      ;; Ignore the value of version-control because we look for existing
      ;; backups, which maybe were made earlier or by another user with
      ;; a different value of version-control.
      (setq fn (file-chase-links (expand-file-name fn)))
      (or
       (let ((bak (make-backup-file-name fn)))
	 (if (file-exists-p bak) bak))
       ;; We use BACKUPNAME to cope with backups stored in a different dir.
       (let* ((backupname (car (find-backup-file-name fn)))
	      (dir (file-name-directory backupname))
	      (base-versions (concat (file-name-sans-versions
				      (file-name-nondirectory backupname))
				     ".~"))
	      ;; This is a fluid var for backup-extract-version.
	      (backup-extract-version-start (length base-versions)))
	 (concat dir
		 (car (sort
		       (file-name-all-completions base-versions dir)
		       (function
			(lambda (fn1 fn2)
			  (> (backup-extract-version fn1)
			     (backup-extract-version fn2))))))))))))

(provide 'diff)

;;; diff.el ends here