diff-mode.el 94.7 KB
Newer Older
1
;;; diff-mode.el --- a mode for viewing/editing context diffs -*- lexical-binding: t -*-
2

Paul Eggert's avatar
Paul Eggert committed
3
;; Copyright (C) 1998-2018 Free Software Foundation, Inc.
4

Stefan Monnier's avatar
Stefan Monnier committed
5
;; Author: Stefan Monnier <monnier@iro.umontreal.ca>
6
;; Keywords: convenience patch diff vc
7 8 9

;; This file is part of GNU Emacs.

10
;; GNU Emacs is free software: you can redistribute it and/or modify
11
;; it under the terms of the GNU General Public License as published by
12 13
;; the Free Software Foundation, either version 3 of the License, or
;; (at your option) any later version.
14 15 16 17 18 19 20

;; 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
21
;; along with GNU Emacs.  If not, see <https://www.gnu.org/licenses/>.
22 23 24

;;; Commentary:

25
;; Provides support for font-lock, outline, navigation
26 27 28
;; commands, editing and various conversions as well as jumping
;; to the corresponding source file.

29
;; Inspired by Pavel Machek's patch-mode.el (<pavel@@atrey.karlin.mff.cuni.cz>)
30
;; Some efforts were spent to have it somewhat compatible with XEmacs's
31 32 33 34
;; diff-mode as well as with compilation-minor-mode

;; Bugs:

35
;; - Reverse doesn't work with normal diffs.
36 37 38

;; Todo:

39 40
;; - Improve `diff-add-change-log-entries-other-window',
;;   it is very simplistic now.
Lute Kamstra's avatar
Lute Kamstra committed
41
;;
42 43 44 45 46 47 48
;; - Add a `delete-after-apply' so C-c C-a automatically deletes hunks.
;;   Also allow C-c C-a to delete already-applied hunks.
;;
;; - Try `diff <file> <hunk>' to try and fuzzily discover the source location
;;   of a hunk.  Show then the changes between <file> and <hunk> and make it
;;   possible to apply them to <file>, <hunk-src>, or <hunk-dst>.
;;   Or maybe just make it into a ".rej to diff3-markers converter".
49
;;   Maybe just use `wiggle' (by Neil Brown) to do it for us.
50
;;
51 52
;; - in diff-apply-hunk, strip context in replace-match to better
;;   preserve markers and spacing.
53
;; - Handle `diff -b' output in context->unified.
54 55

;;; Code:
Stefan Monnier's avatar
Stefan Monnier committed
56
(eval-when-compile (require 'cl-lib))
57

58 59
(defvar add-log-buffer-file-name-function)

60 61

(defgroup diff-mode ()
62
  "Major mode for viewing/editing diffs."
63
  :version "21.1"
64 65 66
  :group 'tools
  :group 'diff)

67
(defcustom diff-default-read-only nil
68
  "If non-nil, `diff-mode' buffers default to being read-only."
69
  :type 'boolean)
70

71
(defcustom diff-jump-to-old-file nil
72
  "Non-nil means `diff-goto-source' jumps to the old file.
73
Else, it jumps to the new file."
74
  :type 'boolean)
75

76
(defcustom diff-update-on-the-fly t
Lute Kamstra's avatar
Lute Kamstra committed
77
  "Non-nil means hunk headers are kept up-to-date on-the-fly.
78 79 80 81 82
When editing a diff file, the line numbers in the hunk headers
need to be kept consistent with the actual diff.  This can
either be done on the fly (but this sometimes interacts poorly with the
undo mechanism) or whenever the file is written (can be slow
when editing big diffs)."
83
  :type 'boolean)
84

85
(defcustom diff-advance-after-apply-hunk t
Lute Kamstra's avatar
Lute Kamstra committed
86
  "Non-nil means `diff-apply-hunk' will move to the next hunk after applying."
87
  :type 'boolean)
88

89 90 91
(defcustom diff-mode-hook nil
  "Run after setting up the `diff-mode' major mode."
  :type 'hook
92 93 94 95 96 97
  :options '(diff-delete-empty-files diff-make-unified))

(defcustom diff-font-lock-refine t
  "If non-nil, font-lock highlighting includes hunk refinement."
  :version "27.1"
  :type 'boolean)
98

99 100 101 102 103
(defcustom diff-font-lock-prettify nil
  "If non-nil, font-lock will try and make the format prettier."
  :version "27.1"
  :type 'boolean)

104 105 106
(defvar diff-vc-backend nil
  "The VC backend that created the current Diff buffer, if any.")

107 108 109
(defvar diff-outline-regexp
  "\\([*+][*+][*+] [^0-9]\\|@@ ...\\|\\*\\*\\* [0-9].\\|--- [0-9]..\\)")

110
;;;;
111
;;;; keymap, menu, ...
112
;;;;
113

114
(easy-mmode-defmap diff-mode-shared-map
115
  '(("n" . diff-hunk-next)
116 117 118
    ("N" . diff-file-next)
    ("p" . diff-hunk-prev)
    ("P" . diff-file-prev)
119 120
    ("\t" . diff-hunk-next)
    ([backtab] . diff-hunk-prev)
121 122
    ("k" . diff-hunk-kill)
    ("K" . diff-file-kill)
123
    ("}" . diff-file-next)	; From compilation-minor-mode.
124
    ("{" . diff-file-prev)
125
    ("\C-m" . diff-goto-source)
126
    ([mouse-2] . diff-goto-source)
127
    ("W" . widen)
128
    ("o" . diff-goto-source)	; other-window
129 130
    ("A" . diff-ediff-patch)
    ("r" . diff-restrict-view)
131 132
    ("R" . diff-reverse-direction)
    ([remap undo] . diff-undo))
133 134
  "Basic keymap for `diff-mode', bound to various prefix keys."
  :inherit special-mode-map)
135

136
(easy-mmode-defmap diff-mode-map
137 138 139 140 141
  `(("\e" . ,(let ((map (make-sparse-keymap)))
               ;; We want to inherit most bindings from diff-mode-shared-map,
               ;; but not all since they may hide useful M-<foo> global
               ;; bindings when editing.
               (set-keymap-parent map diff-mode-shared-map)
142
               (dolist (key '("A" "r" "R" "g" "q" "W" "z"))
143 144
                 (define-key map key nil))
               map))
145 146
    ;; From compilation-minor-mode.
    ("\C-c\C-c" . diff-goto-source)
147 148
    ;; By analogy with the global C-x 4 a binding.
    ("\C-x4A" . diff-add-change-log-entries-other-window)
149
    ;; Misc operations.
150
    ("\C-c\C-a" . diff-apply-hunk)
151 152 153
    ("\C-c\C-e" . diff-ediff-patch)
    ("\C-c\C-n" . diff-restrict-view)
    ("\C-c\C-s" . diff-split-hunk)
154
    ("\C-c\C-t" . diff-test-hunk)
Stefan Monnier's avatar
Stefan Monnier committed
155
    ("\C-c\C-r" . diff-reverse-direction)
156
    ("\C-c\C-u" . diff-context->unified)
157 158
    ;; `d' because it duplicates the context :-(  --Stef
    ("\C-c\C-d" . diff-unified->context)
Stefan Monnier's avatar
Stefan Monnier committed
159 160
    ("\C-c\C-w" . diff-ignore-whitespace-hunk)
    ("\C-c\C-b" . diff-refine-hunk)  ;No reason for `b' :-(
161
    ("\C-c\C-f" . next-error-follow-minor-mode))
162 163 164 165 166
  "Keymap for `diff-mode'.  See also `diff-mode-shared-map'.")

(easy-menu-define diff-mode-menu diff-mode-map
  "Menu for `diff-mode'."
  '("Diff"
Dan Nicolaescu's avatar
Dan Nicolaescu committed
167 168 169 170 171 172 173 174
    ["Jump to Source"		diff-goto-source
     :help "Jump to the corresponding source line"]
    ["Apply hunk"		diff-apply-hunk
     :help "Apply the current hunk to the source file and go to the next"]
    ["Test applying hunk"	diff-test-hunk
     :help "See whether it's possible to apply the current hunk"]
    ["Apply diff with Ediff"	diff-ediff-patch
     :help "Call `ediff-patch-file' on the current buffer"]
175
    ["Create Change Log entries" diff-add-change-log-entries-other-window
Dan Nicolaescu's avatar
Dan Nicolaescu committed
176
     :help "Create ChangeLog entries for the changes in the diff buffer"]
Stefan Monnier's avatar
Stefan Monnier committed
177
    "-----"
Dan Nicolaescu's avatar
Dan Nicolaescu committed
178 179 180 181 182 183
    ["Reverse direction"	diff-reverse-direction
     :help "Reverse the direction of the diffs"]
    ["Context -> Unified"	diff-context->unified
     :help "Convert context diffs to unified diffs"]
    ["Unified -> Context"	diff-unified->context
     :help "Convert unified diffs to context diffs"]
184
    ;;["Fixup Headers"		diff-fixup-modifs	(not buffer-read-only)]
185
    ["Remove trailing whitespace" diff-delete-trailing-whitespace
186
     :help "Remove trailing whitespace problems introduced by the diff"]
187
    ["Show trailing whitespace" whitespace-mode
188
     :style toggle :selected (bound-and-true-p whitespace-mode)
189
     :help "Show trailing whitespace in modified lines"]
Stefan Monnier's avatar
Stefan Monnier committed
190
    "-----"
Dan Nicolaescu's avatar
Dan Nicolaescu committed
191 192 193 194 195 196 197 198 199 200 201
    ["Split hunk"		diff-split-hunk
     :active (diff-splittable-p)
     :help "Split the current (unified diff) hunk at point into two hunks"]
    ["Ignore whitespace changes" diff-ignore-whitespace-hunk
     :help "Re-diff the current hunk, ignoring whitespace differences"]
    ["Highlight fine changes"	diff-refine-hunk
     :help "Highlight changes of hunk at point at a finer granularity"]
    ["Kill current hunk"	diff-hunk-kill
     :help "Kill current hunk"]
    ["Kill current file's hunks" diff-file-kill
     :help "Kill all current file's hunks"]
Stefan Monnier's avatar
Stefan Monnier committed
202
    "-----"
Dan Nicolaescu's avatar
Dan Nicolaescu committed
203 204 205 206 207 208 209 210
    ["Previous Hunk"		diff-hunk-prev
     :help "Go to the previous count'th hunk"]
    ["Next Hunk"		diff-hunk-next
     :help "Go to the next count'th hunk"]
    ["Previous File"		diff-file-prev
     :help "Go to the previous count'th file"]
    ["Next File"		diff-file-next
     :help "Go to the next count'th file"]
211 212
    ))

213
(defcustom diff-minor-mode-prefix "\C-c="
214
  "Prefix key for `diff-minor-mode' commands."
215
  :type '(choice (string "\e") (string "C-c=") string))
216

217 218
(easy-mmode-defmap diff-minor-mode-map
  `((,diff-minor-mode-prefix . ,diff-mode-shared-map))
219 220
  "Keymap for `diff-minor-mode'.  See also `diff-mode-shared-map'.")

221
(define-minor-mode diff-auto-refine-mode
Chong Yidong's avatar
Chong Yidong committed
222 223 224 225 226 227 228
  "Toggle automatic diff hunk highlighting (Diff Auto Refine mode).

Diff Auto Refine mode is a buffer-local minor mode used with
`diff-mode'.  When enabled, Emacs automatically highlights
changes in detail as the user visits hunks.  When transitioning
from disabled to enabled, it tries to refine the current hunk, as
well."
229
  :group 'diff-mode :init-value t :lighter nil ;; " Auto-Refine"
230
  (when diff-auto-refine-mode
231
    (condition-case-unless-debug nil (diff-refine-hunk) (error nil))))
232

233
;;;;
234
;;;; font-lock support
235
;;;;
236

237
(defface diff-header
238
  '((((class color) (min-colors 88) (background light))
239
     :background "grey80")
240
    (((class color) (min-colors 88) (background dark))
241
     :background "grey45")
242
    (((class color))
243 244
     :foreground "blue1" :weight bold)
    (t :weight bold))
245
  "`diff-mode' face inherited by hunk and index header faces.")
Stefan Monnier's avatar
Stefan Monnier committed
246

247
(defface diff-file-header
248
  '((((class color) (min-colors 88) (background light))
249
     :background "grey70" :weight bold)
250
    (((class color) (min-colors 88) (background dark))
251
     :background "grey60" :weight bold)
252
    (((class color))
253 254
     :foreground "cyan" :weight bold)
    (t :weight bold))			; :height 1.3
255
  "`diff-mode' face used to highlight file header lines.")
256

257 258
(defface diff-index
  '((t :inherit diff-file-header))
259
  "`diff-mode' face used to highlight index header lines.")
260

261 262
(defface diff-hunk-header
  '((t :inherit diff-header))
263
  "`diff-mode' face used to highlight hunk header lines.")
264

265
(defface diff-removed
266 267 268 269 270 271 272 273
  '((default
     :inherit diff-changed)
    (((class color) (min-colors 88) (background light))
     :background "#ffdddd")
    (((class color) (min-colors 88) (background dark))
     :background "#553333")
    (((class color))
     :foreground "red"))
274
  "`diff-mode' face used to highlight removed lines.")
275

276
(defface diff-added
277 278 279 280 281 282 283 284
  '((default
     :inherit diff-changed)
    (((class color) (min-colors 88) (background light))
     :background "#ddffdd")
    (((class color) (min-colors 88) (background dark))
     :background "#335533")
    (((class color))
     :foreground "green"))
285
  "`diff-mode' face used to highlight added lines.")
286

287
(defface diff-changed
288
  '((t nil))
289
  "`diff-mode' face used to highlight changed lines."
290
  :version "25.1")
291

292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309
(defface diff-indicator-removed
  '((t :inherit diff-removed))
  "`diff-mode' face used to highlight indicator of removed lines (-, <)."
  :version "22.1")
(defvar diff-indicator-removed-face 'diff-indicator-removed)

(defface diff-indicator-added
  '((t :inherit diff-added))
  "`diff-mode' face used to highlight indicator of added lines (+, >)."
  :version "22.1")
(defvar diff-indicator-added-face 'diff-indicator-added)

(defface diff-indicator-changed
  '((t :inherit diff-changed))
  "`diff-mode' face used to highlight indicator of changed lines."
  :version "22.1")
(defvar diff-indicator-changed-face 'diff-indicator-changed)

310
(defface diff-function
311
  '((t :inherit diff-header))
312
  "`diff-mode' face used to highlight function names produced by \"diff -p\".")
313

314
(defface diff-context
315 316 317 318
  '((((class color grayscale) (min-colors 88) (background light))
     :foreground "#333333")
    (((class color grayscale) (min-colors 88) (background dark))
     :foreground "#dddddd"))
319
  "`diff-mode' face used to highlight context and other side-information."
320
  :version "25.1")
Stefan Monnier's avatar
Stefan Monnier committed
321

322 323
(defface diff-nonexistent
  '((t :inherit diff-file-header))
324
  "`diff-mode' face used to highlight nonexistent files in recursive diffs.")
325

326 327
(defconst diff-yank-handler '(diff-yank-function))
(defun diff-yank-function (text)
328 329 330
  ;; FIXME: the yank-handler is now called separately on each piece of text
  ;; with a yank-handler property, so the next-single-property-change call
  ;; below will always return nil :-(   --stef
331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346
  (let ((mixed (next-single-property-change 0 'yank-handler text))
	(start (point)))
    ;; First insert the text.
    (insert text)
    ;; If the text does not include any diff markers and if we're not
    ;; yanking back into a diff-mode buffer, get rid of the prefixes.
    (unless (or mixed (derived-mode-p 'diff-mode))
      (undo-boundary)		; Just in case the user wanted the prefixes.
      (let ((re (save-excursion
		  (if (re-search-backward "^[><!][ \t]" start t)
		      (if (eq (char-after) ?!)
			  "^[!+- ][ \t]" "^[<>][ \t]")
		    "^[ <>!+-]"))))
	(save-excursion
	  (while (re-search-backward re start t)
	    (replace-match "" t t)))))))
347

348
(defconst diff-hunk-header-re-unified
349
  "^@@ -\\([0-9]+\\)\\(?:,\\([0-9]+\\)\\)? \\+\\([0-9]+\\)\\(?:,\\([0-9]+\\)\\)? @@")
350 351
(defconst diff-context-mid-hunk-header-re
  "--- \\([0-9]+\\)\\(?:,\\([0-9]+\\)\\)? ----$")
352

353 354 355
(defvar diff-use-changed-face (and (face-differs-from-default-p 'diff-changed)
				   (not (face-equal 'diff-changed 'diff-added))
				   (not (face-equal 'diff-changed 'diff-removed)))
356 357 358 359
  "If non-nil, use the face `diff-changed' for changed lines in context diffs.
Otherwise, use the face `diff-removed' for removed lines,
and the face `diff-added' for added lines.")

360
(defvar diff-font-lock-keywords
361
  `((,(concat "\\(" diff-hunk-header-re-unified "\\)\\(.*\\)$")
362
     (1 'diff-hunk-header) (6 'diff-function))
363
    ("^\\(\\*\\{15\\}\\)\\(.*\\)$"                        ;context
364 365 366 367 368
     (1 'diff-hunk-header) (2 'diff-function))
    ("^\\*\\*\\* .+ \\*\\*\\*\\*". 'diff-hunk-header) ;context
    (,diff-context-mid-hunk-header-re . 'diff-hunk-header) ;context
    ("^[0-9,]+[acd][0-9,]+$"     . 'diff-hunk-header) ;normal
    ("^---$"                     . 'diff-hunk-header) ;normal
369 370 371
    ;; For file headers, accept files with spaces, but be careful to rule
    ;; out false-positives when matching hunk headers.
    ("^\\(---\\|\\+\\+\\+\\|\\*\\*\\*\\) \\([^\t\n]+?\\)\\(?:\t.*\\| \\(\\*\\*\\*\\*\\|----\\)\\)?\n"
372 373
     (0 'diff-header)
     (2 (if (not (match-end 3)) 'diff-file-header) prepend))
374
    ("^\\([-<]\\)\\(.*\n\\)"
375
     (1 diff-indicator-removed-face) (2 'diff-removed))
376
    ("^\\([+>]\\)\\(.*\n\\)"
377
     (1 diff-indicator-added-face) (2 'diff-added))
378
    ("^\\(!\\)\\(.*\n\\)"
379 380 381
     (1 (if diff-use-changed-face
	    diff-indicator-changed-face
	  ;; Otherwise, search for `diff-context-mid-hunk-header-re' and
382 383
	  ;; if the line of context diff is above, use `diff-removed';
	  ;; if below, use `diff-added'.
384 385 386 387 388 389
	  (save-match-data
	    (let ((limit (save-excursion (diff-beginning-of-hunk))))
	      (if (save-excursion (re-search-backward diff-context-mid-hunk-header-re limit t))
		  diff-indicator-added-face
		diff-indicator-removed-face)))))
     (2 (if diff-use-changed-face
390
	    'diff-changed
391 392 393 394
	  ;; Otherwise, use the same method as above.
	  (save-match-data
	    (let ((limit (save-excursion (diff-beginning-of-hunk))))
	      (if (save-excursion (re-search-backward diff-context-mid-hunk-header-re limit t))
395 396
		  'diff-added
		'diff-removed))))))
397
    ("^\\(?:Index\\|revno\\): \\(.+\\).*\n"
398 399
     (0 'diff-header) (1 'diff-index prepend))
    ("^Only in .*\n" . 'diff-nonexistent)
400
    ("^\\(#\\)\\(.*\\)"
401 402
     (1 font-lock-comment-delimiter-face)
     (2 font-lock-comment-face))
403
    ("^[^-=+*!<>#].*\n" (0 'diff-context))
404
    (,#'diff--font-lock-prettify)
405
    (,#'diff--font-lock-refined)))
406 407

(defconst diff-font-lock-defaults
408
  '(diff-font-lock-keywords t nil nil nil (font-lock-multiline . nil)))
409

410 411
(defvar diff-imenu-generic-expression
  ;; Prefer second name as first is most likely to be a backup or
412 413 414
  ;; version-control name.  The [\t\n] at the end of the unidiff pattern
  ;; catches Debian source diff files (which lack the trailing date).
  '((nil "\\+\\+\\+\\ \\([^\t\n]+\\)[\t\n]" 1) ; unidiffs
415 416
    (nil "^--- \\([^\t\n]+\\)\t.*\n\\*" 1))) ; context diffs

417
;;;;
418
;;;; Movement
419
;;;;
420

421 422 423 424
(defvar diff-valid-unified-empty-line t
  "If non-nil, empty lines are valid in unified diffs.
Some versions of diff replace all-blank context lines in unified format with
empty lines.  This makes the format less robust, but is tolerated.
425
See https://lists.gnu.org/r/emacs-devel/2007-11/msg01990.html")
426 427 428

(defconst diff-hunk-header-re
  (concat "^\\(?:" diff-hunk-header-re-unified ".*\\|\\*\\{15\\}.*\n\\*\\*\\* .+ \\*\\*\\*\\*\\|[0-9]+\\(,[0-9]+\\)?[acd][0-9]+\\(,[0-9]+\\)?\\)$"))
429
(defconst diff-file-header-re (concat "^\\(--- .+\n\\+\\+\\+ \\|\\*\\*\\* .+\n--- \\|[^-+!<>0-9@* \n]\\).+\n" (substring diff-hunk-header-re 1)))
430 431 432

(defconst diff-separator-re "^--+ ?$")

433 434
(defvar diff-narrowed-to nil)

Stefan Monnier's avatar
Stefan Monnier committed
435
(defun diff-hunk-style (&optional style)
436
  (when (looking-at diff-hunk-header-re)
Stefan Monnier's avatar
Stefan Monnier committed
437
    (setq style (cdr (assq (char-after) '((?@ . unified) (?* . context)))))
438
    (goto-char (match-end 0)))
Stefan Monnier's avatar
Stefan Monnier committed
439 440
  style)

441
(defun diff-end-of-hunk (&optional style donttrustheader)
Chong Yidong's avatar
Chong Yidong committed
442
  "Advance to the end of the current hunk, and return its position."
443 444 445 446 447 448
  (let (end)
    (when (looking-at diff-hunk-header-re)
      ;; Especially important for unified (because headers are ambiguous).
      (setq style (diff-hunk-style style))
      (goto-char (match-end 0))
      (when (and (not donttrustheader) (match-end 2))
449 450
        (let* ((nold (string-to-number (or (match-string 2) "1")))
               (nnew (string-to-number (or (match-string 4) "1")))
451
               (endold
452 453 454
                (save-excursion
                  (re-search-forward (if diff-valid-unified-empty-line
                                         "^[- \n]" "^[- ]")
455
                                     nil t nold)
456 457 458
                  (line-beginning-position
                   ;; Skip potential "\ No newline at end of file".
                   (if (looking-at ".*\n\\\\") 3 2))))
459 460 461 462 463 464 465
               (endnew
                ;; The hunk may end with a bunch of "+" lines, so the `end' is
                ;; then further than computed above.
                (save-excursion
                  (re-search-forward (if diff-valid-unified-empty-line
                                         "^[+ \n]" "^[+ ]")
                                     nil t nnew)
466 467 468
                  (line-beginning-position
                   ;; Skip potential "\ No newline at end of file".
                   (if (looking-at ".*\n\\\\") 3 2)))))
469
          (setq end (max endold endnew)))))
470 471 472
    ;; We may have a first evaluation of `end' thanks to the hunk header.
    (unless end
      (setq end (and (re-search-forward
Stefan Monnier's avatar
Stefan Monnier committed
473 474 475 476 477 478 479 480 481
                      (pcase style
                        (`unified
                         (concat (if diff-valid-unified-empty-line
                                     "^[^-+# \\\n]\\|" "^[^-+# \\]\\|")
                                 ;; A `unified' header is ambiguous.
                                 diff-file-header-re))
                        (`context "^[^-+#! \\]")
                        (`normal "^[^<>#\\]")
                        (_ "^[^-+#!<> \\]"))
482 483 484 485 486 487 488 489 490
                      nil t)
                     (match-beginning 0)))
      (when diff-valid-unified-empty-line
        ;; While empty lines may be valid inside hunks, they are also likely
        ;; to be unrelated to the hunk.
        (goto-char (or end (point-max)))
        (while (eq ?\n (char-before (1- (point))))
          (forward-char -1)
          (setq end (point)))))
491 492
    ;; The return value is used by easy-mmode-define-navigation.
    (goto-char (or end (point-max)))))
493

494 495 496
;; "index ", "old mode", "new mode", "new file mode" and
;; "deleted file mode" are output by git-diff.
(defconst diff-file-junk-re
497
  (concat "Index: \\|Prereq: \\|=\\{20,\\}\\|" ; SVN
Tino Calancha's avatar
Tino Calancha committed
498
          "diff \\|index \\|\\(?:deleted file\\|new\\(?: file\\)?\\|old\\) mode\\|=== modified file"))
499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526

;; If point is in a diff header, then return beginning
;; of hunk position otherwise return nil.
(defun diff--at-diff-header-p ()
  "Return non-nil if point is inside a diff header."
  (let ((regexp-hunk diff-hunk-header-re)
        (regexp-file diff-file-header-re)
        (regexp-junk diff-file-junk-re)
        (orig (point)))
    (catch 'headerp
      (save-excursion
        (forward-line 0)
        (when (looking-at regexp-hunk) ; Hunk header.
          (throw 'headerp (point)))
        (forward-line -1)
        (when (re-search-forward regexp-file (point-at-eol 4) t) ; File header.
          (forward-line 0)
          (throw 'headerp (point)))
        (goto-char orig)
        (forward-line 0)
        (when (looking-at regexp-junk) ; Git diff junk.
          (while (and (looking-at regexp-junk)
                      (not (bobp)))
            (forward-line -1))
          (re-search-forward regexp-file nil t)
          (forward-line 0)
          (throw 'headerp (point)))) nil)))

527
(defun diff-beginning-of-hunk (&optional try-harder)
Chong Yidong's avatar
Chong Yidong committed
528 529 530
  "Move back to the previous hunk beginning, and return its position.
If point is in a file header rather than a hunk, advance to the
next hunk if TRY-HARDER is non-nil; otherwise signal an error."
531
  (beginning-of-line)
532
  (if (looking-at diff-hunk-header-re) ; At hunk header.
Chong Yidong's avatar
Chong Yidong committed
533
      (point)
534 535 536 537 538 539 540 541
    (let ((pos (diff--at-diff-header-p))
          (regexp diff-hunk-header-re))
      (cond (pos ; At junk diff header.
             (if try-harder
                 (goto-char pos)
               (error "Can't find the beginning of the hunk")))
            ((re-search-backward regexp nil t)) ; In the middle of a hunk.
            ((re-search-forward regexp nil t) ; At first hunk header.
Tino Calancha's avatar
Tino Calancha committed
542 543
             (forward-line 0)
             (point))
544
            (t (error "Can't find the beginning of the hunk"))))))
545

546 547 548 549 550 551
(defun diff-unified-hunk-p ()
  (save-excursion
    (ignore-errors
      (diff-beginning-of-hunk)
      (looking-at "^@@"))))

552 553 554
(defun diff-beginning-of-file ()
  (beginning-of-line)
  (unless (looking-at diff-file-header-re)
555 556 557 558 559 560 561 562 563 564 565 566 567
    (let ((start (point))
          res)
      ;; diff-file-header-re may need to match up to 4 lines, so in case
      ;; we're inside the header, we need to move up to 3 lines forward.
      (forward-line 3)
      (if (and (setq res (re-search-backward diff-file-header-re nil t))
               ;; Maybe the 3 lines forward were too much and we matched
               ;; a file header after our starting point :-(
               (or (<= (point) start)
                   (setq res (re-search-backward diff-file-header-re nil t))))
          res
        (goto-char start)
        (error "Can't find the beginning of the file")))))
Lute Kamstra's avatar
Lute Kamstra committed
568

569 570

(defun diff-end-of-file ()
571
  (re-search-forward "^[-+#!<>0-9@* \\]" nil t)
572 573 574 575 576
  (re-search-forward (concat "^[^-+#!<>0-9@* \\]\\|" diff-file-header-re)
		     nil 'move)
  (if (match-beginning 1)
      (goto-char (match-beginning 1))
    (beginning-of-line)))
577

578 579
(defvar diff--auto-refine-data nil)

580 581
;; Define diff-{hunk,file}-{prev,next}
(easy-mmode-define-navigation
582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598
 diff-hunk diff-hunk-header-re "hunk" diff-end-of-hunk diff-restrict-view
 (when diff-auto-refine-mode
   (unless (prog1 diff--auto-refine-data
             (setq diff--auto-refine-data
                   (cons (current-buffer) (point-marker))))
     (run-at-time 0.0 nil
                  (lambda ()
                    (when diff--auto-refine-data
                      (let ((buffer (car diff--auto-refine-data))
                            (point (cdr diff--auto-refine-data)))
                        (setq diff--auto-refine-data nil)
                        (with-local-quit
                          (when (buffer-live-p buffer)
                            (with-current-buffer buffer
                              (save-excursion
                                (goto-char point)
                                (diff-refine-hunk))))))))))))
Stefan Monnier's avatar
Stefan Monnier committed
599

600
(easy-mmode-define-navigation
601
 diff-file diff-file-header-re "file" diff-end-of-file)
602

Chong Yidong's avatar
Chong Yidong committed
603 604 605 606 607 608 609 610 611
(defun diff-bounds-of-hunk ()
  "Return the bounds of the diff hunk at point.
The return value is a list (BEG END), which are the hunk's start
and end positions.  Signal an error if no hunk is found.  If
point is in a file header, return the bounds of the next hunk."
  (save-excursion
    (let ((pos (point))
	  (beg (diff-beginning-of-hunk t))
	  (end (diff-end-of-hunk)))
612
      (cond ((>= end pos)
Chong Yidong's avatar
Chong Yidong committed
613 614 615 616
	     (list beg end))
	    ;; If this hunk ends above POS, consider the next hunk.
	    ((re-search-forward diff-hunk-header-re nil t)
	     (list (match-beginning 0) (diff-end-of-hunk)))
617
	    (t (error "No hunk found"))))))
Chong Yidong's avatar
Chong Yidong committed
618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634

(defun diff-bounds-of-file ()
  "Return the bounds of the file segment at point.
The return value is a list (BEG END), which are the segment's
start and end positions."
  (save-excursion
    (let ((pos (point))
	  (beg (progn (diff-beginning-of-file-and-junk)
		      (point))))
      (diff-end-of-file)
      ;; bzr puts a newline after the last hunk.
      (while (looking-at "^\n")
	(forward-char 1))
      (if (> pos (point))
	  (error "Not inside a file diff"))
      (list beg (point)))))

635 636 637 638
(defun diff-restrict-view (&optional arg)
  "Restrict the view to the current hunk.
If the prefix ARG is given, restrict the view to the current file instead."
  (interactive "P")
Chong Yidong's avatar
Chong Yidong committed
639 640 641
  (apply 'narrow-to-region
	 (if arg (diff-bounds-of-file) (diff-bounds-of-hunk)))
  (set (make-local-variable 'diff-narrowed-to) (if arg 'file 'hunk)))
642

643 644 645 646 647
(defun diff--some-hunks-p ()
  (save-excursion
    (goto-char (point-min))
    (re-search-forward diff-hunk-header-re nil t)))

648
(defun diff-hunk-kill ()
Chong Yidong's avatar
Chong Yidong committed
649
  "Kill the hunk at point."
650
  (interactive)
651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672
  (if (not (diff--some-hunks-p))
      (error "No hunks")
    (diff-beginning-of-hunk t)
    (let* ((hunk-bounds (diff-bounds-of-hunk))
           (file-bounds (ignore-errors (diff-bounds-of-file)))
           ;; If the current hunk is the only one for its file, kill the
           ;; file header too.
           (bounds (if (and file-bounds
                            (progn (goto-char (car file-bounds))
                                   (= (progn (diff-hunk-next) (point))
                                      (car hunk-bounds)))
                            (progn (goto-char (cadr hunk-bounds))
                                   ;; bzr puts a newline after the last hunk.
                                   (while (looking-at "^\n")
                                     (forward-char 1))
                                   (= (point) (cadr file-bounds))))
                       file-bounds
                     hunk-bounds))
           (inhibit-read-only t))
      (apply 'kill-region bounds)
      (goto-char (car bounds))
      (ignore-errors (diff-beginning-of-hunk t)))))
673

674 675 676 677
(defun diff-beginning-of-file-and-junk ()
  "Go to the beginning of file-related diff-info.
This is like `diff-beginning-of-file' except it tries to skip back over leading
data such as \"Index: ...\" and such."
678 679 680 681 682 683 684
  (let* ((orig (point))
         ;; Skip forward over what might be "leading junk" so as to get
         ;; closer to the actual diff.
         (_ (progn (beginning-of-line)
                   (while (looking-at diff-file-junk-re)
                     (forward-line 1))))
         (start (point))
685 686 687 688 689 690 691 692 693 694 695 696 697
         (prevfile (condition-case err
                       (save-excursion (diff-beginning-of-file) (point))
                     (error err)))
         (err (if (consp prevfile) prevfile))
         (nextfile (ignore-errors
                     (save-excursion
                       (goto-char start) (diff-file-next) (point))))
         ;; prevhunk is one of the limits.
         (prevhunk (save-excursion
                     (ignore-errors
                       (if (numberp prevfile) (goto-char prevfile))
                       (diff-hunk-prev) (point))))
         (previndex (save-excursion
698
                      (forward-line 1)  ;In case we're looking at "Index:".
699 700 701 702 703 704 705 706
                      (re-search-backward "^Index: " prevhunk t))))
    ;; If we're in the junk, we should use nextfile instead of prevfile.
    (if (and (numberp nextfile)
             (or (not (numberp prevfile))
                 (and previndex (> previndex prevfile))))
        (setq prevfile nextfile))
    (if (and previndex (numberp prevfile) (< previndex prevfile))
        (setq prevfile previndex))
707
    (if (and (numberp prevfile) (<= prevfile start))
708 709 710 711 712 713 714 715
          (progn
            (goto-char prevfile)
            ;; Now skip backward over the leading junk we may have before the
            ;; diff itself.
            (while (save-excursion
                     (and (zerop (forward-line -1))
                          (looking-at diff-file-junk-re)))
              (forward-line -1)))
716 717
      ;; File starts *after* the starting point: we really weren't in
      ;; a file diff but elsewhere.
718
      (goto-char orig)
719
      (signal (car err) (cdr err)))))
Lute Kamstra's avatar
Lute Kamstra committed
720

721
(defun diff-file-kill ()
722 723
  "Kill current file's hunks."
  (interactive)
724 725 726 727 728 729
  (if (not (diff--some-hunks-p))
      (error "No hunks")
    (diff-beginning-of-hunk t)
    (let ((inhibit-read-only t))
      (apply 'kill-region (diff-bounds-of-file)))
    (ignore-errors (diff-beginning-of-hunk t))))
730

731 732 733 734 735 736 737 738 739 740 741 742 743 744 745
(defun diff-kill-junk ()
  "Kill spurious empty diffs."
  (interactive)
  (save-excursion
    (let ((inhibit-read-only t))
      (goto-char (point-min))
      (while (re-search-forward (concat "^\\(Index: .*\n\\)"
					"\\([^-+!* <>].*\n\\)*?"
					"\\(\\(Index:\\) \\|"
					diff-file-header-re "\\)")
				nil t)
	(delete-region (if (match-end 4) (match-beginning 0) (match-end 1))
		       (match-beginning 3))
	(beginning-of-line)))))

746 747 748 749
(defun diff-count-matches (re start end)
  (save-excursion
    (let ((n 0))
      (goto-char start)
Stefan Monnier's avatar
Stefan Monnier committed
750
      (while (re-search-forward re end t) (cl-incf n))
751 752
      n)))

753 754 755 756 757 758 759
(defun diff-splittable-p ()
  (save-excursion
    (beginning-of-line)
    (and (looking-at "^[-+ ]")
         (progn (forward-line -1) (looking-at "^[-+ ]"))
         (diff-unified-hunk-p))))

760 761 762 763 764
(defun diff-split-hunk ()
  "Split the current (unified diff) hunk at point into two hunks."
  (interactive)
  (beginning-of-line)
  (let ((pos (point))
Chong Yidong's avatar
Chong Yidong committed
765
	(start (diff-beginning-of-hunk)))
766
    (unless (looking-at diff-hunk-header-re-unified)
767 768 769
      (error "diff-split-hunk only works on unified context diffs"))
    (forward-line 1)
    (let* ((start1 (string-to-number (match-string 1)))
770
	   (start2 (string-to-number (match-string 3)))
771
	   (newstart1 (+ start1 (diff-count-matches "^[- \t]" (point) pos)))
772 773
	   (newstart2 (+ start2 (diff-count-matches "^[+ \t]" (point) pos)))
	   (inhibit-read-only t))
774 775 776 777 778 779
      (goto-char pos)
      ;; Hopefully the after-change-function will not screw us over.
      (insert "@@ -" (number-to-string newstart1) ",1 +"
	      (number-to-string newstart2) ",1 @@\n")
      ;; Fix the original hunk-header.
      (diff-fixup-modifs start pos))))
780

781

782 783 784 785
;;;;
;;;; jump to other buffers
;;;;

786
(defvar diff-remembered-files-alist nil)
787
(defvar diff-remembered-defdir nil)
788

789 790 791
(defun diff-filename-drop-dir (file)
  (when (string-match "/" file) (substring file (match-end 0))))

792 793 794
(defun diff-merge-strings (ancestor from to)
  "Merge the diff between ANCESTOR and FROM into TO.
Returns the merged string if successful or nil otherwise.
795
The strings are assumed not to contain any \"\\n\" (i.e. end of line).
796 797 798 799 800 801 802 803
If ANCESTOR = FROM, returns TO.
If ANCESTOR = TO, returns FROM.
The heuristic is simplistic and only really works for cases
like \(diff-merge-strings \"b/foo\" \"b/bar\" \"/a/c/foo\")."
  ;; Ideally, we want:
  ;;   AMB ANB CMD -> CND
  ;; but that's ambiguous if `foo' or `bar' is empty:
  ;; a/foo a/foo1 b/foo.c -> b/foo1.c but not 1b/foo.c or b/foo.c1
804
  (let ((str (concat ancestor "\n" from "\n" to)))
805
    (when (and (string-match (concat
806 807
			      "\\`\\(.*?\\)\\(.*\\)\\(.*\\)\n"
			      "\\1\\(.*\\)\\3\n"
808 809 810 811 812 813
			      "\\(.*\\(\\2\\).*\\)\\'") str)
	       (equal to (match-string 5 str)))
      (concat (substring str (match-beginning 5) (match-beginning 6))
	      (match-string 4 str)
	      (substring str (match-end 6) (match-end 5))))))

814 815 816 817 818 819 820 821
(defun diff-tell-file-name (old name)
  "Tell Emacs where the find the source file of the current hunk.
If the OLD prefix arg is passed, tell the file NAME of the old file."
  (interactive
   (let* ((old current-prefix-arg)
	  (fs (diff-hunk-file-names current-prefix-arg)))
     (unless fs (error "No file name to look for"))
     (list old (read-file-name (format "File for %s: " (car fs))
822
			       nil (diff-find-file-name old 'noprompt) t))))
823 824 825
  (let ((fs (diff-hunk-file-names old)))
    (unless fs (error "No file name to look for"))
    (push (cons fs name) diff-remembered-files-alist)))
826

827 828
(defun diff-hunk-file-names (&optional old)
  "Give the list of file names textually mentioned for the current hunk."
829 830 831 832
  (save-excursion
    (unless (looking-at diff-file-header-re)
      (or (ignore-errors (diff-beginning-of-file))
	  (re-search-forward diff-file-header-re nil t)))
833
    (let ((limit (save-excursion
834
		   (condition-case ()
835
		       (progn (diff-hunk-prev) (point))
836
		     (error (point-min)))))
837
	  (header-files
838
           ;; handle filenames with spaces;
839
           ;; cf. diff-font-lock-keywords / diff-file-header
840
	   (if (looking-at "[-*][-*][-*] \\([^\t\n]+\\).*\n[-+][-+][-+] \\([^\t\n]+\\)")
841 842
	       (list (if old (match-string 1) (match-string 2))
		     (if old (match-string 2) (match-string 1)))
843 844 845 846 847 848 849 850
	     (forward-line 1) nil)))
      (delq nil
	    (append
	     (when (and (not old)
			(save-excursion
			  (re-search-backward "^Index: \\(.+\\)" limit t)))
	       (list (match-string 1)))
	     header-files
851
             ;; this assumes that there are no spaces in filenames
852 853 854 855 856 857
	     (when (re-search-backward
		    "^diff \\(-\\S-+ +\\)*\\(\\S-+\\)\\( +\\(\\S-+\\)\\)?"
		    nil t)
	       (list (if old (match-string 2) (match-string 4))
		     (if old (match-string 4) (match-string 2)))))))))

858
(defun diff-find-file-name (&optional old noprompt prefix)
859
  "Return the file corresponding to the current patch.
860
Non-nil OLD means that we want the old file.
861
Non-nil NOPROMPT means to prefer returning nil than to prompt the user.
862
PREFIX is only used internally: don't use it."
863 864 865 866
  (unless (equal diff-remembered-defdir default-directory)
    ;; Flush diff-remembered-files-alist if the default-directory is changed.
    (set (make-local-variable 'diff-remembered-defdir) default-directory)
    (set (make-local-variable 'diff-remembered-files-alist) nil))
867
  (save-excursion
868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883
    (save-restriction
      (widen)
      (unless (looking-at diff-file-header-re)
        (or (ignore-errors (diff-beginning-of-file))
	    (re-search-forward diff-file-header-re nil t)))
      (let ((fs (diff-hunk-file-names old)))
        (if prefix (setq fs (mapcar (lambda (f) (concat prefix f)) fs)))
        (or
         ;; use any previously used preference
         (cdr (assoc fs diff-remembered-files-alist))
         ;; try to be clever and use previous choices as an inspiration
         (cl-dolist (rf diff-remembered-files-alist)
	   (let ((newfile (diff-merge-strings (caar rf) (car fs) (cdr rf))))
	     (if (and newfile (file-exists-p newfile)) (cl-return newfile))))
         ;; look for each file in turn.  If none found, try again but
         ;; ignoring the first level of directory, ...
884
         (cl-do* ((files fs (delq nil (mapcar #'diff-filename-drop-dir files)))
885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914
                  (file nil nil))
	     ((or (null files)
		  (setq file (cl-do* ((files files (cdr files))
                                      (file (car files) (car files)))
			         ;; Use file-regular-p to avoid
			         ;; /dev/null, directories, etc.
			         ((or (null file) (file-regular-p file))
				  file))))
	      file))
         ;; <foo>.rej patches implicitly apply to <foo>
         (and (string-match "\\.rej\\'" (or buffer-file-name ""))
	      (let ((file (substring buffer-file-name 0 (match-beginning 0))))
	        (when (file-exists-p file) file)))
         ;; If we haven't found the file, maybe it's because we haven't paid
         ;; attention to the PCL-CVS hint.
         (and (not prefix)
	      (boundp 'cvs-pcl-cvs-dirchange-re)
	      (save-excursion
	        (re-search-backward cvs-pcl-cvs-dirchange-re nil t))
	      (diff-find-file-name old noprompt (match-string 1)))
         ;; if all else fails, ask the user
         (unless noprompt
           (let ((file (expand-file-name (or (car fs) ""))))
	     (setq file
		   (read-file-name (format "Use file %s: " file)
				   (file-name-directory file) file t
				   (file-name-nondirectory file)))
             (set (make-local-variable 'diff-remembered-files-alist)
                  (cons (cons fs file) diff-remembered-files-alist))
             file)))))))
915

916

917 918 919
(defun diff-ediff-patch ()
  "Call `ediff-patch-file' on the current buffer."
  (interactive)
920
  (condition-case nil
921
      (ediff-patch-file nil (current-buffer))
922 923
    (wrong-number-of-arguments (ediff-patch-file))))

924
;;;;
925
;;;; Conversion functions
926
;;;;
927 928 929 930 931 932 933

;;(defvar diff-inhibit-after-change nil
;;  "Non-nil means inhibit `diff-mode's after-change functions.")

(defun diff-unified->context (start end)
  "Convert unified diffs to context diffs.
START and END are either taken from the region (if a prefix arg is given) or
934
else cover the whole buffer."
935
  (interactive (if (or current-prefix-arg (use-region-p))
936
		   (list (region-beginning) (region-end))
937
		 (list (point-min) (point-max))))
938
  (unless (markerp end) (setq end (copy-marker end t)))
939 940 941 942
  (let (;;(diff-inhibit-after-change t)
	(inhibit-read-only t))
    (save-excursion
      (goto-char start)
943 944 945 946
      (while (and (re-search-forward
                   (concat "^\\(\\(---\\) .+\n\\(\\+\\+\\+\\) .+\\|"
                           diff-hunk-header-re-unified ".*\\)$")
                   nil t)
947 948 949
		  (< (point) end))
	(combine-after-change-calls
	  (if (match-beginning 2)
950
	      ;; we matched a file header
951 952 953 954 955 956
	      (progn
		;; use reverse order to make sure the indices are kept valid
		(replace-match "---" t t nil 3)
		(replace-match "***" t t nil 2))
	    ;; we matched a hunk header
	    (let ((line1 (match-string 4))
957
		  (lines1 (or (match-string 5) "1"))
958
		  (line2 (match-string 6))
Miles Bader's avatar
Miles Bader committed
959
		  (lines2 (or (match-string 7) "1"))
960 961 962 963 964
		  ;; Variables to use the special undo function.
		  (old-undo buffer-undo-list)
		  (old-end (marker-position end))
		  (start (match-beginning 0))
		  (reversible t))
965 966 967 968
	      (replace-match
	       (concat "***************\n*** " line1 ","
		       (number-to-string (+ (string-to-number line1)
					    (string-to-number lines1)
Miles Bader's avatar
Miles Bader committed
969 970
					    -1))
		       " ****"))
971
	      (save-restriction
972 973 974 975
		(narrow-to-region (line-beginning-position 2)
                                  ;; Call diff-end-of-hunk from just before
                                  ;; the hunk header so it can use the hunk
                                  ;; header info.
976 977 978 979 980 981 982 983 984
				  (progn (diff-end-of-hunk 'unified) (point)))
		(let ((hunk (buffer-string)))
		  (goto-char (point-min))
		  (if (not (save-excursion (re-search-forward "^-" nil t)))
		      (delete-region (point) (point-max))
		    (goto-char (point-max))
		    (let ((modif nil) last-pt)
		      (while (progn (setq last-pt (point))
				    (= (forward-line -1) 0))
Stefan Monnier's avatar
Stefan Monnier committed
985
			(pcase (char-after)
Juanma Barranquero's avatar
Juanma Barranquero committed
986
			  (?\s (insert " ") (setq modif nil) (backward-char 1))
987 988
			  (?+ (delete-region (point) last-pt) (setq modif t))
			  (?- (if (not modif)
Stefan Monnier's avatar
Stefan Monnier committed
989 990 991 992 993
                                  (progn (forward-char 1)
                                         (insert " "))
                                (delete-char 1)
                                (insert "! "))