]> code.delx.au - gnu-emacs/blobdiff - lisp/diff-mode.el
Merge from emacs--rel--22
[gnu-emacs] / lisp / diff-mode.el
index a088cd6ccbaa7c1423ad5458c1e15445097c5258..afd9399d1e56f42394fbc5bd317fa9fbf15c85ee 100644 (file)
@@ -39,6 +39,9 @@
 
 ;; Todo:
 
+;; - Improve `diff-add-change-log-entries-other-window',
+;;   it is very simplistic now.
+;;  
 ;; - Add a `delete-after-apply' so C-c C-a automatically deletes hunks.
 ;;   Also allow C-c C-a to delete already-applied hunks.
 ;;
@@ -48,8 +51,6 @@
 ;;   Or maybe just make it into a ".rej to diff3-markers converter".
 ;;   Maybe just use `wiggle' (by Neil Brown) to do it for us.
 ;;
-;; - Refine hunk on a word-by-word basis.
-;; 
 ;; - in diff-apply-hunk, strip context in replace-match to better
 ;;   preserve markers and spacing.
 ;; - Handle `diff -b' output in context->unified.
@@ -72,7 +73,7 @@
   :group 'diff-mode)
 
 (defcustom diff-jump-to-old-file nil
-  "*Non-nil means `diff-goto-source' jumps to the old file.
+  "Non-nil means `diff-goto-source' jumps to the old file.
 Else, it jumps to the new file."
   :type 'boolean
   :group 'diff-mode)
@@ -92,6 +93,10 @@ when editing big diffs)."
   :type 'boolean
   :group 'diff-mode)
 
+(defcustom diff-auto-refine t
+  "Automatically highlight changes in detail as the user visits hunks."
+  :type 'boolean
+  :group 'diff-mode)
 
 (defcustom diff-mode-hook nil
   "Run after setting up the `diff-mode' major mode."
@@ -147,17 +152,20 @@ when editing big diffs)."
   `(("\e" . ,diff-mode-shared-map)
     ;; From compilation-minor-mode.
     ("\C-c\C-c" . diff-goto-source)
+    ;; By analogy with the global C-x 4 a binding.
+    ("\C-x4A" . diff-add-change-log-entries-other-window)
     ;; Misc operations.
     ("\C-c\C-a" . diff-apply-hunk)
     ("\C-c\C-e" . diff-ediff-patch)
     ("\C-c\C-n" . diff-restrict-view)
-    ("\C-c\C-r" . diff-reverse-direction)
     ("\C-c\C-s" . diff-split-hunk)
     ("\C-c\C-t" . diff-test-hunk)
+    ("\C-c\C-r" . diff-reverse-direction)
     ("\C-c\C-u" . diff-context->unified)
     ;; `d' because it duplicates the context :-(  --Stef
     ("\C-c\C-d" . diff-unified->context)
-    ("\C-c\C-w" . diff-refine-hunk)
+    ("\C-c\C-w" . diff-ignore-whitespace-hunk)
+    ("\C-c\C-b" . diff-refine-hunk)  ;No reason for `b' :-(
     ("\C-c\C-f" . next-error-follow-minor-mode))
   "Keymap for `diff-mode'.  See also `diff-mode-shared-map'.")
 
@@ -168,14 +176,17 @@ when editing big diffs)."
     ["Apply hunk"              diff-apply-hunk         t]
     ["Test applying hunk"      diff-test-hunk          t]
     ["Apply diff with Ediff"   diff-ediff-patch        t]
+    ["Create Change Log entries" diff-add-change-log-entries-other-window
+     :help "Create ChangeLog entries for the changes in the diff buffer"]
     "-----"
     ["Reverse direction"       diff-reverse-direction  t]
     ["Context -> Unified"      diff-context->unified   t]
     ["Unified -> Context"      diff-unified->context   t]
     ;;["Fixup Headers"         diff-fixup-modifs       (not buffer-read-only)]
     "-----"
-    ["Split hunk"              diff-split-hunk         t]
-    ["Refine hunk"             diff-refine-hunk        t]
+    ["Split hunk"              diff-split-hunk         (diff-splittable-p)]
+    ["Ignore whitespace changes" diff-ignore-whitespace-hunk t]
+    ["Highlight fine changes"  diff-refine-hunk        t]
     ["Kill current hunk"       diff-hunk-kill          t]
     ["Kill current file's hunks" diff-file-kill        t]
     "-----"
@@ -341,18 +352,23 @@ when editing big diffs)."
          (while (re-search-backward re start t)
            (replace-match "" t t)))))))
 
+(defconst diff-hunk-header-re-unified
+  "^@@ -\\([0-9]+\\)\\(?:,\\([0-9]+\\)\\)? \\+\\([0-9]+\\)\\(?:,\\([0-9]+\\)\\) @@")
 
 (defvar diff-font-lock-keywords
-  `(("^\\(@@ -[0-9,]+ \\+[0-9,]+ @@\\)\\(.*\\)$"          ;unified
-     (1 diff-hunk-header-face) (2 diff-function-face))
+  `((,(concat "\\(" diff-hunk-header-re-unified "\\)\\(.*\\)$")
+     (1 diff-hunk-header-face) (6 diff-function-face))
     ("^\\(\\*\\{15\\}\\)\\(.*\\)$"                        ;context
      (1 diff-hunk-header-face) (2 diff-function-face))
     ("^\\*\\*\\* .+ \\*\\*\\*\\*". diff-hunk-header-face) ;context
     ("^--- .+ ----$"             . diff-hunk-header-face) ;context
     ("^[0-9,]+[acd][0-9,]+$"     . diff-hunk-header-face) ;normal
     ("^---$"                     . diff-hunk-header-face) ;normal
-    ("^\\(---\\|\\+\\+\\+\\|\\*\\*\\*\\) \\(\\S-+\\)\\(.*[^*-]\\)?\n"
-     (0 diff-header-face) (2 diff-file-header-face prepend))
+    ;; For file headers, accept files with spaces, but be careful to rule
+    ;; out false-positives when matching hunk headers.
+    ("^\\(---\\|\\+\\+\\+\\|\\*\\*\\*\\) \\([^\t\n]+?\\)\\(?:\t.*\\| \\(\\*\\*\\*\\*\\|----\\)\\)?\n"
+     (0 diff-header-face)
+     (2 (if (not (match-end 3)) diff-file-header-face) prepend))
     ("^\\([-<]\\)\\(.*\n\\)"
      (1 diff-indicator-removed-face) (2 diff-removed-face))
     ("^\\([+>]\\)\\(.*\n\\)"
@@ -381,29 +397,56 @@ when editing big diffs)."
 ;;;; Movement
 ;;;;
 
-(defconst diff-hunk-header-re "^\\(@@ -[0-9,]+ \\+[0-9,]+ @@.*\\|\\*\\{15\\}.*\n\\*\\*\\* .+ \\*\\*\\*\\*\\|[0-9]+\\(,[0-9]+\\)?[acd][0-9]+\\(,[0-9]+\\)?\\)$")
+(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.
+See http://lists.gnu.org/archive/html/emacs-devel/2007-11/msg01990.html")
+
+(defconst diff-hunk-header-re
+  (concat "^\\(?:" diff-hunk-header-re-unified ".*\\|\\*\\{15\\}.*\n\\*\\*\\* .+ \\*\\*\\*\\*\\|[0-9]+\\(,[0-9]+\\)?[acd][0-9]+\\(,[0-9]+\\)?\\)$"))
 (defconst diff-file-header-re (concat "^\\(--- .+\n\\+\\+\\+ \\|\\*\\*\\* .+\n--- \\|[^-+!<>0-9@* ]\\).+\n" (substring diff-hunk-header-re 1)))
 (defvar diff-narrowed-to nil)
 
-(defun diff-end-of-hunk (&optional style)
+(defun diff-hunk-style (&optional style)
   (when (looking-at diff-hunk-header-re)
-    (unless style
-      ;; Especially important for unified (because headers are ambiguous).
-      (setq style (cdr (assq (char-after) '((?@ . unified) (?* . context))))))
+    (setq style (cdr (assq (char-after) '((?@ . unified) (?* . context)))))
     (goto-char (match-end 0)))
-  ;; Some versions of diff replace all-blank context lines in unified
-  ;; format with empty lines. The use of \n below avoids matching such
-  ;; lines as headers.
-  ;; http://lists.gnu.org/archive/html/emacs-devel/2007-11/msg01990.html
-  (let ((end (and (re-search-forward (case style
-                                      ;; A `unified' header is ambiguous.
-                                      (unified (concat "^[^-+# \\\n]\\|"
-                                                       diff-file-header-re))
-                                      (context "^[^-+#! \\]")
-                                      (normal "^[^<>#\\]")
-                                      (t "^[^-+#!<> \\]"))
-                                    nil t)
-                 (match-beginning 0))))
+  style)
+
+(defun diff-end-of-hunk (&optional style donttrustheader)
+  (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))
+        (save-excursion
+          (re-search-forward (if diff-valid-unified-empty-line
+                                 "^[- \n]" "^[- ]")
+                             nil t
+                             (string-to-number (match-string 2)))
+          (setq end (line-beginning-position 2)))))
+    ;; We may have a first evaluation of `end' thanks to the hunk header.
+    (unless end
+      (setq end (and (re-search-forward
+                      (case style
+                        (unified (concat (if diff-valid-unified-empty-line
+                                             "^[^-+# \\\n]\\|" "^[^-+# \\]\\|")
+                                         ;; A `unified' header is ambiguous.
+                                         diff-file-header-re))
+                        (context "^[^-+#! \\]")
+                        (normal "^[^<>#\\]")
+                        (t "^[^-+#!<> \\]"))
+                      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)))))
     ;; The return value is used by easy-mmode-define-navigation.
     (goto-char (or end (point-max)))))
 
@@ -422,13 +465,29 @@ but in the file header instead, in which case move forward to the first hunk."
          (diff-beginning-of-file-and-junk)
          (diff-hunk-next))))))
 
+(defun diff-unified-hunk-p ()
+  (save-excursion
+    (ignore-errors
+      (diff-beginning-of-hunk)
+      (looking-at "^@@"))))
+
 (defun diff-beginning-of-file ()
   (beginning-of-line)
   (unless (looking-at diff-file-header-re)
-    (forward-line 2)
-    (condition-case ()
-       (re-search-backward diff-file-header-re)
-      (error (error "Can't find the beginning of the file")))))
+    (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")))))
+        
 
 (defun diff-end-of-file ()
   (re-search-forward "^[-+#!<>0-9@* \\]" nil t)
@@ -440,7 +499,10 @@ but in the file header instead, in which case move forward to the first hunk."
 
 ;; Define diff-{hunk,file}-{prev,next}
 (easy-mmode-define-navigation
- diff-hunk diff-hunk-header-re "hunk" diff-end-of-hunk diff-restrict-view)
+ diff-hunk diff-hunk-header-re "hunk" diff-end-of-hunk diff-restrict-view
+ (if diff-auto-refine
+     (condition-case-no-debug nil (diff-refine-hunk) (error nil))))
+
 (easy-mmode-define-navigation
  diff-file diff-file-header-re "file" diff-end-of-hunk)
 
@@ -477,39 +539,64 @@ If the prefix ARG is given, restrict the view to the current file instead."
       (diff-end-of-hunk)
       (kill-region start (point)))))
 
+(defconst diff-file-junk-re "diff \\|index ") ; "index " is output by git-diff.
+
 (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."
-  (let ((start (point))
-        (file (condition-case err (progn (diff-beginning-of-file) (point))
-                (error err)))
-        ;; prevhunk is one of the limits.
-        (prevhunk (save-excursion (ignore-errors (diff-hunk-prev) (point))))
-        err)
-    (when (consp file)
-      ;; Presumably, we started before the file header, in the leading junk.
-      (setq err file)
-      (diff-file-next)
-      (setq file (point)))
-    (let ((index (save-excursion
-                   (re-search-backward "^Index: " prevhunk t))))
-      (when index (setq file index))
-      (if (<= file start)
-          (goto-char file)
-        ;; File starts *after* the starting point: we really weren't in
-        ;; a file diff but elsewhere.
-        (goto-char start)
-        (signal (car err) (cdr err))))))
+  (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))
+         (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
+                      (forward-line 1)  ;In case we're looking at "Index:".
+                      (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))
+    (if (and (numberp prevfile) (<= prevfile start))
+          (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)))
+      ;; File starts *after* the starting point: we really weren't in
+      ;; a file diff but elsewhere.
+      (goto-char orig)
+      (signal (car err) (cdr err)))))
           
 (defun diff-file-kill ()
   "Kill current file's hunks."
   (interactive)
-  (diff-beginning-of-file-and-junk)
-  (let* ((start (point))
+  (let ((orig (point))
+        (start (progn (diff-beginning-of-file-and-junk) (point)))
         (inhibit-read-only t))
     (diff-end-of-file)
     (if (looking-at "^\n") (forward-char 1)) ;`tla' generates such diffs.
+    (if (> orig (point)) (error "Not inside a file diff"))
     (kill-region start (point))))
 
 (defun diff-kill-junk ()
@@ -534,17 +621,24 @@ data such as \"Index: ...\" and such."
       (while (re-search-forward re end t) (incf n))
       n)))
 
+(defun diff-splittable-p ()
+  (save-excursion
+    (beginning-of-line)
+    (and (looking-at "^[-+ ]")
+         (progn (forward-line -1) (looking-at "^[-+ ]"))
+         (diff-unified-hunk-p))))
+
 (defun diff-split-hunk ()
   "Split the current (unified diff) hunk at point into two hunks."
   (interactive)
   (beginning-of-line)
   (let ((pos (point))
        (start (progn (diff-beginning-of-hunk) (point))))
-    (unless (looking-at "@@ -\\([0-9]+\\),[0-9]+ \\+\\([0-9]+\\),[0-9]+ @@")
+    (unless (looking-at diff-hunk-header-re-unified)
       (error "diff-split-hunk only works on unified context diffs"))
     (forward-line 1)
     (let* ((start1 (string-to-number (match-string 1)))
-          (start2 (string-to-number (match-string 2)))
+          (start2 (string-to-number (match-string 3)))
           (newstart1 (+ start1 (diff-count-matches "^[- \t]" (point) pos)))
           (newstart2 (+ start2 (diff-count-matches "^[+ \t]" (point) pos)))
           (inhibit-read-only t))
@@ -628,9 +722,11 @@ If the OLD prefix arg is passed, tell the file NAME of the old file."
               (list (if old (match-string 2) (match-string 4))
                     (if old (match-string 4) (match-string 2)))))))))
 
-(defun diff-find-file-name (&optional old prefix)
+(defun diff-find-file-name (&optional old batch prefix)
   "Return the file corresponding to the current patch.
 Non-nil OLD means that we want the old file.
+Non-nil BATCH means to prefer returning an incorrect answer than to prompt
+the user.
 PREFIX is only used internally: don't use it."
   (save-excursion
     (unless (looking-at diff-file-header-re)
@@ -667,7 +763,10 @@ PREFIX is only used internally: don't use it."
            (boundp 'cvs-pcl-cvs-dirchange-re)
            (save-excursion
              (re-search-backward cvs-pcl-cvs-dirchange-re nil t))
-           (diff-find-file-name old (match-string 1)))
+           (diff-find-file-name old batch (match-string 1)))
+       ;; Invent something, if necessary.
+       (when batch
+         (or (car fs) default-directory))
        ;; if all else fails, ask the user
        (let ((file (read-file-name (format "Use file %s: " (or (first fs) ""))
                                   nil (first fs) t (first fs))))
@@ -702,7 +801,10 @@ else cover the whole buffer."
        (inhibit-read-only t))
     (save-excursion
       (goto-char start)
-      (while (and (re-search-forward "^\\(\\(---\\) .+\n\\(\\+\\+\\+\\) .+\\|@@ -\\([0-9]+\\),\\([0-9]+\\) \\+\\([0-9]+\\),\\([0-9]+\\) @@.*\\)$" nil t)
+      (while (and (re-search-forward
+                   (concat "^\\(\\(---\\) .+\n\\(\\+\\+\\+\\) .+\\|"
+                           diff-hunk-header-re-unified ".*\\)$")
+                   nil t)
                  (< (point) end))
        (combine-after-change-calls
          (if (match-beginning 2)
@@ -713,17 +815,25 @@ else cover the whole buffer."
                (replace-match "***" t t nil 2))
            ;; we matched a hunk header
            (let ((line1 (match-string 4))
-                 (lines1 (match-string 5))
+                 (lines1 (if (match-end 5)
+                              (string-to-number (match-string 5)) 1))
                  (line2 (match-string 6))
-                 (lines2 (match-string 7)))
+                 (lines2 (if (match-end 7)
+                              (string-to-number (match-string 7)) 1))
+                 ;; Variables to use the special undo function.
+                 (old-undo buffer-undo-list)
+                 (old-end (marker-position end))
+                 (start (match-beginning 0))
+                 (reversible t))
              (replace-match
               (concat "***************\n*** " line1 ","
                       (number-to-string (+ (string-to-number line1)
-                                           (string-to-number lines1)
-                                           -1)) " ****"))
-             (forward-line 1)
+                                           lines1 -1)) " ****"))
              (save-restriction
-               (narrow-to-region (point)
+               (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.
                                  (progn (diff-end-of-hunk 'unified) (point)))
                (let ((hunk (buffer-string)))
                  (goto-char (point-min))
@@ -745,18 +855,28 @@ else cover the whole buffer."
                          (?\\ (when (save-excursion (forward-line -1)
                                                     (= (char-after) ?+))
                                 (delete-region (point) last-pt) (setq modif t)))
+                          ;; diff-valid-unified-empty-line.
+                          (?\n (insert "  ") (setq modif nil) (backward-char 2))
                          (t (setq modif nil))))))
                  (goto-char (point-max))
                  (save-excursion
                    (insert "--- " line2 ","
                            (number-to-string (+ (string-to-number line2)
-                                                (string-to-number lines2)
-                                                -1)) " ----\n" hunk))
+                                                lines2 -1))
+                            " ----\n" hunk))
                  ;;(goto-char (point-min))
                  (forward-line 1)
                  (if (not (save-excursion (re-search-forward "^+" nil t)))
                      (delete-region (point) (point-max))
                    (let ((modif nil) (delete nil))
+                     (if (save-excursion (re-search-forward "^\\+.*\n-" nil t))
+                          ;; Normally, lines in a substitution come with
+                          ;; first the removals and then the additions, and
+                          ;; the context->unified function follows this
+                          ;; convention, of course.  Yet, other alternatives
+                          ;; are valid as well, but they preclude the use of
+                          ;; context->unified as an undo command.
+                         (setq reversible nil))
                      (while (not (eobp))
                        (case (char-after)
                          (?\s (insert " ") (setq modif nil) (backward-char 1))
@@ -770,12 +890,23 @@ else cover the whole buffer."
                          (?\\ (when (save-excursion (forward-line 1)
                                                     (not (eobp)))
                                 (setq delete t) (setq modif t)))
+                          ;; diff-valid-unified-empty-line.
+                          (?\n (insert "  ") (setq modif nil) (backward-char 2)
+                               (setq reversible nil))
                          (t (setq modif nil)))
                        (let ((last-pt (point)))
                          (forward-line 1)
                          (when delete
                            (delete-region last-pt (point))
-                           (setq delete nil)))))))))))))))
+                           (setq delete nil)))))))
+               (unless (or (not reversible) (eq buffer-undo-list t))
+                  ;; Drop the many undo entries and replace them with
+                  ;; a single entry that uses diff-context->unified to do
+                  ;; the work.
+                 (setq buffer-undo-list
+                       (cons (list 'apply (- old-end end) start (point-max)
+                                   'diff-context->unified start (point-max))
+                             old-undo)))))))))))
 
 (defun diff-context->unified (start end &optional to-context)
   "Convert context diffs to unified diffs.
@@ -789,68 +920,89 @@ With a prefix argument, convert unified format to context format."
       (diff-unified->context start end)
     (unless (markerp end) (setq end (copy-marker end t)))
     (let ( ;;(diff-inhibit-after-change t)
-         (inhibit-read-only t))
+          (inhibit-read-only t))
       (save-excursion
-       (goto-char start)
-       (while (and (re-search-forward "^\\(\\(\\*\\*\\*\\) .+\n\\(---\\) .+\\|\\*\\{15\\}.*\n\\*\\*\\* \\([0-9]+\\),\\(-?[0-9]+\\) \\*\\*\\*\\*\\)$" nil t)
-                   (< (point) end))
-         (combine-after-change-calls
-           (if (match-beginning 2)
-               ;; we matched a file header
-               (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 ((line1s (match-string 4))
-                   (line1e (match-string 5))
-                   (pt1 (match-beginning 0)))
-               (replace-match "")
-               (unless (re-search-forward
-                        "^--- \\([0-9]+\\),\\(-?[0-9]+\\) ----$" nil t)
-                 (error "Can't find matching `--- n1,n2 ----' line"))
-               (let ((line2s (match-string 1))
-                     (line2e (match-string 2))
-                     (pt2 (progn
-                            (delete-region (progn (beginning-of-line) (point))
-                                           (progn (forward-line 1) (point)))
-                            (point-marker))))
-                 (goto-char pt1)
-                 (forward-line 1)
-                 (while (< (point) pt2)
-                   (case (char-after)
-                     ((?! ?-) (delete-char 2) (insert "-") (forward-line 1))
-                     (?\s     ;merge with the other half of the chunk
-                      (let* ((endline2
-                              (save-excursion
-                                (goto-char pt2) (forward-line 1) (point)))
-                             (c (char-after pt2)))
-                        (case c
-                          ((?! ?+)
-                           (insert "+"
-                                   (prog1 (buffer-substring (+ pt2 2) endline2)
-                                     (delete-region pt2 endline2))))
-                          (?\s         ;FIXME: check consistency
-                           (delete-region pt2 endline2)
-                           (delete-char 1)
-                           (forward-line 1))
-                          (?\\ (forward-line 1))
-                          (t (delete-char 1) (forward-line 1)))))
-                     (t (forward-line 1))))
-                 (while (looking-at "[+! ] ")
-                   (if (/= (char-after) ?!) (forward-char 1)
-                     (delete-char 1) (insert "+"))
-                   (delete-char 1) (forward-line 1))
-                 (save-excursion
-                   (goto-char pt1)
-                   (insert "@@ -" line1s ","
-                           (number-to-string (- (string-to-number line1e)
-                                                (string-to-number line1s)
-                                                -1))
-                           " +" line2s ","
-                           (number-to-string (- (string-to-number line2e)
-                                                (string-to-number line2s)
-                                                -1)) " @@")))))))))))
+        (goto-char start)
+        (while (and (re-search-forward "^\\(\\(\\*\\*\\*\\) .+\n\\(---\\) .+\\|\\*\\{15\\}.*\n\\*\\*\\* \\([0-9]+\\),\\(-?[0-9]+\\) \\*\\*\\*\\*\\)$" nil t)
+                    (< (point) end))
+          (combine-after-change-calls
+            (if (match-beginning 2)
+                ;; we matched a file header
+                (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 ((line1s (match-string 4))
+                    (line1e (match-string 5))
+                    (pt1 (match-beginning 0))
+                    ;; Variables to use the special undo function.
+                    (old-undo buffer-undo-list)
+                    (old-end (marker-position end))
+                    (reversible t))
+                (replace-match "")
+                (unless (re-search-forward
+                         "^--- \\([0-9]+\\),\\(-?[0-9]+\\) ----$" nil t)
+                  (error "Can't find matching `--- n1,n2 ----' line"))
+                (let ((line2s (match-string 1))
+                      (line2e (match-string 2))
+                      (pt2 (progn
+                             (delete-region (progn (beginning-of-line) (point))
+                                            (progn (forward-line 1) (point)))
+                             (point-marker))))
+                  (goto-char pt1)
+                  (forward-line 1)
+                  (while (< (point) pt2)
+                    (case (char-after)
+                      (?! (delete-char 2) (insert "-") (forward-line 1))
+                      (?- (forward-char 1) (delete-char 1) (forward-line 1))
+                      (?\s           ;merge with the other half of the chunk
+                       (let* ((endline2
+                               (save-excursion
+                                 (goto-char pt2) (forward-line 1) (point))))
+                         (case (char-after pt2)
+                           ((?! ?+)
+                            (insert "+"
+                                    (prog1 (buffer-substring (+ pt2 2) endline2)
+                                      (delete-region pt2 endline2))))
+                           (?\s
+                            (unless (= (- endline2 pt2)
+                                       (- (line-beginning-position 2) (point)))
+                              ;; If the two lines we're merging don't have the
+                              ;; same length (can happen with "diff -b"), then
+                              ;; diff-unified->context will not properly undo
+                              ;; this operation.
+                              (setq reversible nil))
+                            (delete-region pt2 endline2)
+                            (delete-char 1)
+                            (forward-line 1))
+                           (?\\ (forward-line 1))
+                           (t (setq reversible nil)
+                              (delete-char 1) (forward-line 1)))))
+                      (t (setq reversible nil) (forward-line 1))))
+                  (while (looking-at "[+! ] ")
+                    (if (/= (char-after) ?!) (forward-char 1)
+                      (delete-char 1) (insert "+"))
+                    (delete-char 1) (forward-line 1))
+                  (save-excursion
+                    (goto-char pt1)
+                    (insert "@@ -" line1s ","
+                            (number-to-string (- (string-to-number line1e)
+                                                 (string-to-number line1s)
+                                                 -1))
+                            " +" line2s ","
+                            (number-to-string (- (string-to-number line2e)
+                                                 (string-to-number line2s)
+                                                 -1)) " @@"))
+                  (set-marker pt2 nil)
+                  ;; The whole procedure succeeded, let's replace the myriad
+                  ;; of undo elements with just a single special one.
+                  (unless (or (not reversible) (eq buffer-undo-list t))
+                    (setq buffer-undo-list
+                          (cons (list 'apply (- old-end end) pt1 (point)
+                                      'diff-unified->context pt1 (point))
+                                old-undo)))
+                  )))))))))
 
 (defun diff-reverse-direction (start end)
   "Reverse the direction of the diffs.
@@ -911,7 +1063,8 @@ else cover the whole buffer."
                       (t (when (and first last (< first last))
                            (insert (delete-and-extract-region first last)))
                          (setq first nil last nil)
-                         (equal ?\s c)))
+                         (memq c (if diff-valid-unified-empty-line
+                                      '(?\s ?\n) '(?\s)))))
                (forward-line 1))))))))))
 
 (defun diff-fixup-modifs (start end)
@@ -923,11 +1076,11 @@ else cover the whole buffer."
                 (list (point-min) (point-max))))
   (let ((inhibit-read-only t))
     (save-excursion
-      (goto-char end) (diff-end-of-hunk)
+      (goto-char end) (diff-end-of-hunk nil 'donttrustheader)
       (let ((plus 0) (minus 0) (space 0) (bang 0))
        (while (and (= (forward-line -1) 0) (<= start (point)))
          (if (not (looking-at
-                   (concat "@@ -[0-9,]+ \\+[0-9,]+ @@"
+                   (concat diff-hunk-header-re-unified
                            "\\|[-*][-*][-*] [0-9,]+ [-*][-*][-*][-*]$"
                            "\\|--- .+\n\\+\\+\\+ ")))
              (case (char-after)
@@ -938,13 +1091,17 @@ else cover the whole buffer."
                ((?\\ ?#) nil)
                (t  (setq space 0 plus 0 minus 0 bang 0)))
            (cond
-            ((looking-at "@@ -[0-9]+,\\([0-9]*\\) \\+[0-9]+,\\([0-9]*\\) @@.*$")
-             (let* ((old1 (match-string 1))
-                    (old2 (match-string 2))
+            ((looking-at diff-hunk-header-re-unified)
+             (let* ((old1 (match-string 2))
+                    (old2 (match-string 4))
                     (new1 (number-to-string (+ space minus)))
                     (new2 (number-to-string (+ space plus))))
-               (unless (string= new2 old2) (replace-match new2 t t nil 2))
-               (unless (string= new1 old1) (replace-match new1 t t nil 1))))
+                (if old2
+                    (unless (string= new2 old2) (replace-match new2 t t nil 4))
+                  (goto-char (match-end 4)) (insert "," new2))
+                (if old1
+                    (unless (string= new1 old1) (replace-match new1 t t nil 2))
+                  (goto-char (match-end 2)) (insert "," new1))))
             ((looking-at "--- \\([0-9]+\\),\\([0-9]*\\) ----$")
              (when (> (+ space bang plus) 0)
                (let* ((old1 (match-string 1))
@@ -1012,7 +1169,7 @@ See `after-change-functions' for the meaning of BEG, END and LEN."
        ;; (diff-fixup-modifs (point) (cdr diff-unhandled-changes))
        (diff-beginning-of-hunk)
        (when (save-excursion
-               (diff-end-of-hunk)
+               (diff-end-of-hunk nil 'donttrustheader)
                (>= (point) (cdr diff-unhandled-changes)))
          (diff-fixup-modifs (point) (cdr diff-unhandled-changes)))))
     (setq diff-unhandled-changes nil)))
@@ -1127,9 +1284,8 @@ a diff with \\[diff-reverse-direction].
 Only works for unified diffs."
   (interactive)
   (while
-      (and (re-search-forward "^@@ [-0-9]+,\\([0-9]+\\) [+0-9]+,\\([0-9]+\\) @@"
-                             nil t)
-          (equal (match-string 1) (match-string 2)))))
+      (and (re-search-forward diff-hunk-header-re-unified nil t)
+          (equal (match-string 2) (match-string 4)))))
 
 (defun diff-sanity-check-context-hunk-half (lines)
   (let ((count lines))
@@ -1163,26 +1319,31 @@ Only works for unified diffs."
 
        ;; A context diff.
        ((eq (char-after) ?*)
-        (if (not (looking-at "\\*\\{15\\}\\(?: .*\\)?\n\\*\\*\\* \\([0-9]+\\),\\([0-9]+\\) \\*\\*\\*\\*"))
+        (if (not (looking-at "\\*\\{15\\}\\(?: .*\\)?\n\\*\\*\\* \\([0-9]+\\)\\(?:,\\([0-9]+\\)\\)? \\*\\*\\*\\*"))
             (error "Unrecognized context diff first hunk header format")
           (forward-line 2)
           (diff-sanity-check-context-hunk-half
-           (1+ (- (string-to-number (match-string 2))
-                  (string-to-number (match-string 1)))))
-          (if (not (looking-at "--- \\([0-9]+\\),\\([0-9]+\\) ----$"))
+          (if (match-end 2)
+              (1+ (- (string-to-number (match-string 2))
+                     (string-to-number (match-string 1))))
+            1))
+          (if (not (looking-at "--- \\([0-9]+\\)\\(?:,\\([0-9]+\\)\\)? ----$"))
               (error "Unrecognized context diff second hunk header format")
             (forward-line)
             (diff-sanity-check-context-hunk-half
-             (1+ (- (string-to-number (match-string 2))
-                    (string-to-number (match-string 1))))))))
+            (if (match-end 2)
+                (1+ (- (string-to-number (match-string 2))
+                       (string-to-number (match-string 1))))
+              1)))))
 
        ;; A unified diff.
        ((eq (char-after) ?@)
-        (if (not (looking-at
-                  "@@ -[0-9]+,\\([0-9]+\\) \\+[0-9]+,\\([0-9]+\\) @@"))
+        (if (not (looking-at diff-hunk-header-re-unified))
             (error "Unrecognized unified diff hunk header format")
-          (let ((before (string-to-number (match-string 1)))
-                (after (string-to-number (match-string 2))))
+          (let ((before (if (match-end 2)
+                            (string-to-number (match-string 2)) 1))
+                (after (if (match-end 4)
+                           (string-to-number (match-string 4)) 1)))
             (forward-line)
             (while
                 (case (char-after)
@@ -1200,12 +1361,17 @@ Only works for unified diffs."
                   (?+ (decf after) t)
                   (t
                    (cond
+                    ((and diff-valid-unified-empty-line
+                          ;; Not just (eolp) so we don't infloop at eob.
+                          (eq (char-after) ?\n)
+                          (> before 0) (> after 0))
+                     (decf before) (decf after) t)
                     ((and (zerop before) (zerop after)) nil)
                     ((or (< before 0) (< after 0))
                      (error (if (or (zerop before) (zerop after))
                                 "End of hunk ambiguously marked"
                               "Hunk seriously messed up")))
-                    ((not (y-or-n-p "Try to auto-fix whitespace loss and word-wrap damage? "))
+                    ((not (y-or-n-p (concat "Try to auto-fix " (if (eolp) "whitespace loss" "word-wrap damage") "? ")))
                      (error "Abort!"))
                     ((eolp) (insert " ") (forward-line -1) t)
                     (t (insert " ")
@@ -1330,7 +1496,7 @@ Whitespace differences are ignored."
        (if (> (- (car forw) orig) (- orig (car back))) back forw)
       (or back forw))))
 
-(defsubst diff-xor (a b) (if a (not b) b))
+(defsubst diff-xor (a b) (if a (if (not b) a) b))
 
 (defun diff-find-source-location (&optional other-file reverse)
   "Find out (BUF LINE-OFFSET POS SRC DST SWITCHED).
@@ -1413,8 +1579,15 @@ the value of this variable when given an appropriate prefix argument).
 With a prefix argument, REVERSE the hunk."
   (interactive "P")
   (destructuring-bind (buf line-offset pos old new &optional switched)
-      ;; If REVERSE go to the new file, otherwise go to the old.
-      (diff-find-source-location (not reverse) reverse)
+      ;; Sometimes we'd like to have the following behavior: if REVERSE go
+      ;; to the new file, otherwise go to the old.  But that means that by
+      ;; default we use the old file, which is the opposite of the default
+      ;; for diff-goto-source, and is thus confusing.  Also when you don't
+      ;; know about it it's pretty surprising.
+      ;; TODO: make it possible to ask explicitly for this behavior.
+      ;; 
+      ;; This is duplicated in diff-test-hunk.
+      (diff-find-source-location nil reverse)
     (cond
      ((null line-offset)
       (error "Can't find the text to patch"))
@@ -1458,8 +1631,7 @@ With a prefix argument, REVERSE the hunk."
 With a prefix argument, try to REVERSE the hunk."
   (interactive "P")
   (destructuring-bind (buf line-offset pos src dst &optional switched)
-      ;; If REVERSE go to the new file, otherwise go to the old.
-      (diff-find-source-location (not reverse) reverse)
+      (diff-find-source-location nil reverse)
     (set-window-point (display-buffer buf) (+ (car pos) (cdr src)))
     (diff-hunk-status-msg line-offset (diff-xor reverse switched) t)))
 
@@ -1488,6 +1660,10 @@ then `diff-jump-to-old-file' is also set, for the next invocations."
 (defun diff-current-defun ()
   "Find the name of function at point.
 For use in `add-log-current-defun-function'."
+  ;; Kill change-log-default-name so it gets recomputed each time, since
+  ;; each hunk may belong to another file which may belong to another
+  ;; directory and hence have a different ChangeLog file.
+  (kill-local-variable 'change-log-default-name)
   (save-excursion
     (when (looking-at diff-hunk-header-re)
       (forward-line 1)
@@ -1510,8 +1686,8 @@ For use in `add-log-current-defun-function'."
            (goto-char (+ (car pos) (cdr src)))
            (add-log-current-defun))))))
 
-(defun diff-refine-hunk ()
-  "Refine the current hunk by ignoring space differences."
+(defun diff-ignore-whitespace-hunk ()
+  "Re-diff the current hunk, ignoring whitespace differences."
   (interactive)
   (let* ((char-offset (- (point) (progn (diff-beginning-of-hunk 'try-harder)
                                         (point))))
@@ -1555,6 +1731,106 @@ For use in `add-log-current-defun-function'."
       (delete-file file1)
       (delete-file file2))))
 
+;;; Fine change highlighting.
+
+(defface diff-refine-change
+  '((((class color) (min-colors 88) (background light))
+     :background "grey90")
+    (((class color) (min-colors 88) (background dark))
+     :background "grey40")
+    (((class color) (background light))
+     :background "yellow")
+    (((class color) (background dark))
+     :background "green")
+    (t :weight bold))
+  "Face used for char-based changes shown by `diff-refine-hunk'."
+  :group 'diff-mode)
+
+(defun diff-refine-preproc ()
+  (while (re-search-forward "^[+>]" nil t)
+    ;; Remove spurious changes due to the fact that one side of the hunk is
+    ;; marked with leading + or > and the other with leading - or <.
+    ;; We used to replace all the prefix chars with " " but this only worked
+    ;; when we did char-based refinement (or when using
+    ;; smerge-refine-weight-hack) since otherwise, the `forward' motion done
+    ;; in chopup do not necessarily do the same as the ones in highlight
+    ;; since the "_" is not treated the same as " ".
+    (replace-match (cdr (assq (char-before) '((?+ . "-") (?> . "<"))))))
+  )
+
+(defun diff-refine-hunk ()
+  "Highlight changes of hunk at point at a finer granularity."
+  (interactive)
+  (eval-and-compile (require 'smerge-mode))
+  (save-excursion
+    (diff-beginning-of-hunk 'try-harder)
+    (let* ((style (diff-hunk-style))    ;Skips the hunk header as well.
+           (beg (point))
+           (props '((diff-mode . fine) (face diff-refine-change)))
+           (end (progn (diff-end-of-hunk) (point))))
+
+      (remove-overlays beg end 'diff-mode 'fine)
+
+      (goto-char beg)
+      (case style
+        (unified
+         (while (re-search-forward "^\\(?:-.*\n\\)+\\(\\)\\(?:\\+.*\n\\)+"
+                                   end t)
+           (smerge-refine-subst (match-beginning 0) (match-end 1)
+                                (match-end 1) (match-end 0)
+                                props 'diff-refine-preproc)))
+        (context
+         (let* ((middle (save-excursion (re-search-forward "^---")))
+                (other middle))
+           (while (re-search-forward "^\\(?:!.*\n\\)+" middle t)
+             (smerge-refine-subst (match-beginning 0) (match-end 0)
+                                  (save-excursion
+                                    (goto-char other)
+                                    (re-search-forward "^\\(?:!.*\n\\)+" end)
+                                    (setq other (match-end 0))
+                                    (match-beginning 0))
+                                  other
+                                  props 'diff-refine-preproc))))
+        (t ;; Normal diffs.
+         (let ((beg1 (1+ (point))))
+           (when (re-search-forward "^---.*\n" end t)
+             ;; It's a combined add&remove, so there's something to do.
+             (smerge-refine-subst beg1 (match-beginning 0)
+                                  (match-end 0) end
+                                  props 'diff-refine-preproc))))))))
+
+
+(defun diff-add-change-log-entries-other-window ()
+  "Iterate through the current diff and create ChangeLog entries.
+I.e. like `add-change-log-entry-other-window' but applied to all hunks."
+  (interactive)
+  ;; XXX: Currently add-change-log-entry-other-window is only called
+  ;; once per hunk.  Some hunks have multiple changes, it would be
+  ;; good to call it for each change.
+  (save-excursion
+    (goto-char (point-min))
+    (let ((orig-buffer (current-buffer)))
+      (condition-case nil
+         ;; Call add-change-log-entry-other-window for each hunk in
+         ;; the diff buffer.
+         (while (progn
+                   (diff-hunk-next)
+                   ;; Move to where the changes are,
+                   ;; `add-change-log-entry-other-window' works better in
+                   ;; that case.
+                   (re-search-forward
+                    (concat "\n[!+-<>]"
+                            ;; If the hunk is a context hunk with an empty first
+                            ;; half, recognize the "--- NNN,MMM ----" line
+                            "\\(-- [0-9]+\\(,[0-9]+\\)? ----\n"
+                            ;; and skip to the next non-context line.
+                            "\\( .*\n\\)*[+]\\)?")
+                    nil t))
+            (save-excursion
+              (add-change-log-entry nil nil t t)))
+        ;; When there's no more hunks, diff-hunk-next signals an error.
+       (error nil)))))
+
 ;; provide the package
 (provide 'diff-mode)