;;; diff-mode.el --- a mode for viewing/editing context diffs
;; Copyright (C) 1998, 1999, 2000, 2001, 2002, 2003, 2004,
-;; 2005, 2006 Free Software Foundation, Inc.
+;; 2005, 2006, 2007, 2008 Free Software Foundation, Inc.
;; Author: Stefan Monnier <monnier@iro.umontreal.ca>
;; Keywords: convenience patch diff
;; 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)
+;; the Free Software Foundation; either version 3, or (at your option)
;; any later version.
;; GNU Emacs is distributed in the hope that it will be useful,
;; 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".
+;; 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.
;;; Code:
-
(eval-when-compile (require 'cl))
(defvar add-log-buffer-file-name-function)
("N" . diff-file-next)
("p" . diff-hunk-prev)
("P" . diff-file-prev)
+ ("\t" . diff-hunk-next)
+ ([backtab] . diff-hunk-prev)
("k" . diff-hunk-kill)
("K" . diff-file-kill)
;; From compilation-minor-mode.
("\C-m" . diff-goto-source)
([mouse-2] . diff-goto-source)
;; From XEmacs' diff-mode.
-;; Standard M-w is useful, so don't change M-W.
-;; ("W" . widen)
+ ;; Standard M-w is useful, so don't change M-W.
+ ;;("W" . widen)
;;("." . diff-goto-source) ;display-buffer
;;("f" . diff-goto-source) ;find-file
("o" . diff-goto-source) ;other-window
;;("h" . diff-show-header)
;;("j" . diff-show-difference) ;jump to Nth diff
;;("q" . diff-quit)
-;; Not useful if you have to metafy them.
-;; (" " . scroll-up)
-;; ("\177" . scroll-down)
-;; Standard M-a is useful, so don't change M-A.
-;; ("A" . diff-ediff-patch)
-;; Standard M-r is useful, so don't change M-r or M-R.
-;; ("r" . diff-restrict-view)
-;; ("R" . diff-reverse-direction)
+ ;; Not useful if you have to metafy them.
+ ;;(" " . scroll-up)
+ ;;("\177" . scroll-down)
+ ;; Standard M-a is useful, so don't change M-A.
+ ;;("A" . diff-ediff-patch)
+ ;; Standard M-r is useful, so don't change M-r or M-R.
+ ;;("r" . diff-restrict-view)
+ ;;("R" . diff-reverse-direction)
("q" . quit-window))
"Basic keymap for `diff-mode', bound to various prefix keys.")
("\C-c\C-s" . diff-split-hunk)
("\C-c\C-t" . diff-test-hunk)
("\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-f" . next-error-follow-minor-mode))
"Keymap for `diff-mode'. See also `diff-mode-shared-map'.")
'("Diff"
["Jump to Source" diff-goto-source t]
["Apply hunk" diff-apply-hunk t]
+ ["Test applying hunk" diff-test-hunk t]
["Apply diff with Ediff" diff-ediff-patch t]
- ["-----" nil nil]
+ "-----"
["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]
+ ["Kill current hunk" diff-hunk-kill t]
+ ["Kill current file's hunks" diff-file-kill t]
+ "-----"
+ ["Previous Hunk" diff-hunk-prev t]
+ ["Next Hunk" diff-hunk-next t]
+ ["Previous File" diff-file-prev t]
+ ["Next File" diff-file-next t]
))
(defcustom diff-minor-mode-prefix "\C-c="
;; The return value is used by easy-mmode-define-navigation.
(goto-char (or end (point-max)))))
-(defun diff-beginning-of-hunk ()
+(defun diff-beginning-of-hunk (&optional try-harder)
+ "Move back to beginning of hunk.
+If TRY-HARDER is non-nil, try to cater to the case where we're not in a hunk
+but in the file header instead, in which case move forward to the first hunk."
(beginning-of-line)
(unless (looking-at diff-hunk-header-re)
(forward-line 1)
(condition-case ()
(re-search-backward diff-hunk-header-re)
- (error (error "Can't find the beginning of the hunk")))))
+ (error
+ (if (not try-harder)
+ (error "Can't find the beginning of the hunk")
+ (diff-beginning-of-file-and-junk)
+ (diff-hunk-next))))))
(defun diff-beginning-of-file ()
(beginning-of-line)
If the prefix ARG is given, restrict the view to the current file instead."
(interactive "P")
(save-excursion
- (if arg (diff-beginning-of-file) (diff-beginning-of-hunk))
+ (if arg (diff-beginning-of-file) (diff-beginning-of-hunk 'try-harder))
(narrow-to-region (point)
(progn (if arg (diff-end-of-file) (diff-end-of-hunk))
(point)))
(diff-end-of-hunk)
(kill-region start (point)))))
+(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))))))
+
(defun diff-file-kill ()
"Kill current file's hunks."
(interactive)
- (diff-beginning-of-file)
+ (diff-beginning-of-file-and-junk)
(let* ((start (point))
- (prevhunk (save-excursion
- (ignore-errors
- (diff-hunk-prev) (point))))
- (index (save-excursion
- (re-search-backward "^Index: " prevhunk t)))
(inhibit-read-only t))
- (when index (setq start index))
(diff-end-of-file)
(if (looking-at "^\n") (forward-char 1)) ;`tla' generates such diffs.
(kill-region start (point))))
(list (if old (match-string 2) (match-string 4))
(if old (match-string 4) (match-string 2)))))))))
-(defun diff-find-file-name (&optional old)
+(defun diff-find-file-name (&optional old prefix)
"Return the file corresponding to the current patch.
-Non-nil OLD means that we want the old file."
+Non-nil OLD means that we want the old file.
+PREFIX is only used internally: don't use it."
(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)))
(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))
((or (null files)
(setq file (do* ((files files (cdr files))
(file (car files) (car files)))
- ((or (null file) (file-exists-p file))
+ ;; 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 (match-string 1)))
;; 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))))
(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
-else cover the whole bufer."
+else cover the whole buffer."
(interactive (if (or current-prefix-arg (and transient-mark-mode mark-active))
(list (region-beginning) (region-end))
(list (point-min) (point-max))))
- (unless (markerp end) (setq end (copy-marker end)))
+ (unless (markerp end) (setq end (copy-marker end t)))
(let (;;(diff-inhibit-after-change t)
(inhibit-read-only t))
(save-excursion
(list (point-min) (point-max) current-prefix-arg)))
(if to-context
(diff-unified->context start end)
- (unless (markerp end) (setq end (copy-marker end)))
+ (unless (markerp end) (setq end (copy-marker end t)))
(let ( ;;(diff-inhibit-after-change t)
(inhibit-read-only t))
(save-excursion
(defun diff-reverse-direction (start end)
"Reverse the direction of the diffs.
START and END are either taken from the region (if a prefix arg is given) or
-else cover the whole bufer."
+else cover the whole buffer."
(interactive (if (or current-prefix-arg (and transient-mark-mode mark-active))
(list (region-beginning) (region-end))
(list (point-min) (point-max))))
- (unless (markerp end) (setq end (copy-marker end)))
+ (unless (markerp end) (setq end (copy-marker end t)))
(let (;;(diff-inhibit-after-change t)
(inhibit-read-only t))
(save-excursion
(defun diff-fixup-modifs (start end)
"Fixup the hunk headers (in case the buffer was modified).
START and END are either taken from the region (if a prefix arg is given) or
-else cover the whole bufer."
+else cover the whole buffer."
(interactive (if (or current-prefix-arg (and transient-mark-mode mark-active))
(list (region-beginning) (region-end))
(list (point-min) (point-max))))
"Major mode for viewing/editing context diffs.
Supports unified and context diffs as well as (to a lesser extent)
normal diffs.
+
When the buffer is read-only, the ESC prefix is not necessary.
If you edit the buffer manually, diff-mode will try to update the hunk
headers for you on-the-fly.
You can also switch between context diff and unified diff with \\[diff-context->unified],
or vice versa with \\[diff-unified->context] and you can also reverse the direction of
-a diff with \\[diff-reverse-direction]."
+a diff with \\[diff-reverse-direction].
+
+ \\{diff-mode-map}"
+
(set (make-local-variable 'font-lock-defaults) diff-font-lock-defaults)
(set (make-local-variable 'outline-regexp) diff-outline-regexp)
(set (make-local-variable 'imenu-generic-expression)
(add-hook 'after-change-functions 'diff-after-change-function nil t)
(add-hook 'post-command-hook 'diff-post-command-hook nil t))
;; Neat trick from Dave Love to add more bindings in read-only mode:
- (let ((ro-bind (cons 'buffer-read-only diff-mode-shared-map)))
+ (lexical-let ((ro-bind (cons 'buffer-read-only diff-mode-shared-map)))
(add-to-list 'minor-mode-overriding-map-alist ro-bind)
;; Turn off this little trick in case the buffer is put in view-mode.
(add-hook 'view-mode-hook
- `(lambda ()
- (setq minor-mode-overriding-map-alist
- (delq ',ro-bind minor-mode-overriding-map-alist)))
+ (lambda ()
+ (setq minor-mode-overriding-map-alist
+ (delq ro-bind minor-mode-overriding-map-alist)))
nil t))
;; add-log support
(set (make-local-variable 'add-log-current-defun-function)
(add-hook 'after-change-functions 'diff-after-change-function nil t)
(add-hook 'post-command-hook 'diff-post-command-hook nil t)))
-;;; Handy hook functions ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;;; Handy hook functions ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defun diff-delete-if-empty ()
;; An empty diff file means there's no more diffs to integrate, so we
nil t)
(equal (match-string 1) (match-string 2)))))
+(defun diff-sanity-check-context-hunk-half (lines)
+ (let ((count lines))
+ (while
+ (cond
+ ((and (memq (char-after) '(?\s ?! ?+ ?-))
+ (memq (char-after (1+ (point))) '(?\s ?\t)))
+ (decf count) t)
+ ((or (zerop count) (= count lines)) nil)
+ ((memq (char-after) '(?! ?+ ?-))
+ (if (not (and (eq (char-after (1+ (point))) ?\n)
+ (y-or-n-p "Try to auto-fix whitespace loss damage? ")))
+ (error "End of hunk ambiguously marked")
+ (forward-char 1) (insert " ") (forward-line -1) t))
+ ((< lines 0)
+ (error "End of hunk ambiguously marked"))
+ ((not (y-or-n-p "Try to auto-fix whitespace loss and word-wrap damage? "))
+ (error "Abort!"))
+ ((eolp) (insert " ") (forward-line -1) t)
+ (t (insert " ") (delete-region (- (point) 2) (- (point) 1)) t))
+ (forward-line))))
+
+(defun diff-sanity-check-hunk ()
+ (let (;; Every modification is protected by a y-or-n-p, so it's probably
+ ;; OK to override a read-only setting.
+ (inhibit-read-only t))
+ (save-excursion
+ (cond
+ ((not (looking-at diff-hunk-header-re))
+ (error "Not recognizable hunk header"))
+
+ ;; A context diff.
+ ((eq (char-after) ?*)
+ (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]+\\) ----$"))
+ (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))))))))
+
+ ;; A unified diff.
+ ((eq (char-after) ?@)
+ (if (not (looking-at
+ "@@ -[0-9]+,\\([0-9]+\\) \\+[0-9]+,\\([0-9]+\\) @@"))
+ (error "Unrecognized unified diff hunk header format")
+ (let ((before (string-to-number (match-string 1)))
+ (after (string-to-number (match-string 2))))
+ (forward-line)
+ (while
+ (case (char-after)
+ (?\s (decf before) (decf after) t)
+ (?-
+ (if (and (looking-at diff-file-header-re)
+ (zerop before) (zerop after))
+ ;; No need to query: this is a case where two patches
+ ;; are concatenated and only counting the lines will
+ ;; give the right result. Let's just add an empty
+ ;; line so that our code which doesn't count lines
+ ;; will not get confused.
+ (progn (save-excursion (insert "\n")) nil)
+ (decf before) t))
+ (?+ (decf after) t)
+ (t
+ (cond
+ ((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? "))
+ (error "Abort!"))
+ ((eolp) (insert " ") (forward-line -1) t)
+ (t (insert " ")
+ (delete-region (- (point) 2) (- (point) 1)) t))))
+ (forward-line)))))
+
+ ;; A plain diff.
+ (t
+ ;; TODO.
+ )))))
+
(defun diff-hunk-text (hunk destp char-offset)
"Return the literal source text from HUNK as (TEXT . OFFSET).
If DESTP is nil, TEXT is the source, otherwise the destination text.
SWITCHED is non-nil if the patch is already applied."
(save-excursion
(let* ((other (diff-xor other-file diff-jump-to-old-file))
- (char-offset (- (point) (progn (diff-beginning-of-hunk) (point))))
+ (char-offset (- (point) (progn (diff-beginning-of-hunk 'try-harder)
+ (point))))
+ ;; Check that the hunk is well-formed. Otherwise diff-mode and
+ ;; the user may disagree on what constitutes the hunk
+ ;; (e.g. because an empty line truncates the hunk mid-course),
+ ;; leading to potentially nasty surprises for the user.
+ (_ (diff-sanity-check-hunk))
(hunk (buffer-substring (point)
(save-excursion (diff-end-of-hunk) (point))))
(old (diff-hunk-text hunk reverse char-offset))
(t "Hunk %s at offset %d lines"))
msg line-offset)))
+(defvar diff-apply-hunk-to-backup-file nil)
(defun diff-apply-hunk (&optional reverse)
"Apply the current hunk to the source file and go to the next.
(cond
((null line-offset)
(error "Can't find the text to patch"))
+ ((with-current-buffer buf
+ (and buffer-file-name
+ (backup-file-name-p buffer-file-name)
+ (not diff-apply-hunk-to-backup-file)
+ (not (set (make-local-variable 'diff-apply-hunk-to-backup-file)
+ (yes-or-no-p (format "Really apply this hunk to %s? "
+ (file-name-nondirectory
+ buffer-file-name)))))))
+ (error "%s"
+ (substitute-command-keys
+ (format "Use %s\\[diff-apply-hunk] to apply it to the other file"
+ (if (not reverse) "\\[universal-argument] ")))))
((and switched
;; A reversed patch was detected, perhaps apply it in reverse.
(not (save-window-excursion
(defun diff-refine-hunk ()
"Refine the current hunk by ignoring space differences."
(interactive)
- (let* ((char-offset (- (point) (progn (diff-beginning-of-hunk) (point))))
+ (let* ((char-offset (- (point) (progn (diff-beginning-of-hunk 'try-harder)
+ (point))))
(opts (case (char-after) (?@ "-bu") (?* "-bc") (t "-b")))
(line-nb (and (or (looking-at "[^0-9]+\\([0-9]+\\)")
(error "Can't find line number"))