Fix "diff-apply-buffer applies to the wrong file"

* lisp/vc/diff-mode.el (diff-find-file-name): Inhibit the "drop
dir" behavior in diffs produced by Git and Hg (bug#81210).
Except for the virtual subdirectories like a/b/etc, which is
moved and happens for all such diffs.  Tighten the check for
such virtual directory names, though (characters are the chars
used by the configuration option 'diff.mnemonicPrefix').
(diff-setup-buffer-type): Recognize Hg diffs where only one
revision is specified, too.

* test/lisp/vc/diff-mode-tests.el
(diff-mode-test-setup-buffer-type)
(diff-mode-test-find-file-name-create): New tests.
This commit is contained in:
Dmitry Gutov 2026-06-19 06:35:44 +03:00
parent 14bf2a707c
commit b6e7962700
2 changed files with 57 additions and 6 deletions

View file

@ -1176,6 +1176,10 @@ PREFIX is only used internally: don't use it."
(or (ignore-errors (diff-beginning-of-file))
(re-search-forward diff-file-header-re nil t)))
(let ((fs (diff-hunk-file-names old)))
(when (memq diff-buffer-type '(git hg))
(setq fs
(mapcar (lambda (f) (replace-regexp-in-string "\\`[icoawib]/" "" f))
fs)))
(if prefix (setq fs (mapcar (lambda (f) (concat prefix f)) fs)))
(or
;; use any previously used preference
@ -1186,7 +1190,10 @@ PREFIX is only used internally: don't use it."
(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, ...
(cl-do* ((files fs (delq nil (mapcar #'diff-filename-drop-dir files)))
(cl-do* ((files fs (and (not (and (memq diff-buffer-type '(git hg))
(not old)
(equal null-device (cadr files))))
(delq nil (mapcar #'diff-filename-drop-dir files))))
(file nil nil))
((or (null files)
(setq file (cl-do* ((files files (cdr files))
@ -1212,10 +1219,6 @@ PREFIX is only used internally: don't use it."
(let ((file (or (car fs) ""))
(creation (equal null-device
(car (diff-hunk-file-names (not old))))))
(when (and (memq diff-buffer-type '(git hg))
(string-match "/" file))
;; Strip the dst prefix (like b/) if diff is from Git/Hg.
(setq file (substring file (match-end 0))))
(setq file (expand-file-name file))
(setq file
(read-file-name (format "Use file %s: " file)
@ -1822,7 +1825,7 @@ modified lines of the diff."
(setq-local diff-buffer-type
(if (re-search-forward "^diff --git" nil t)
'git
(if (re-search-forward "^diff -r.*-r" nil t)
(if (re-search-forward "^diff -r " nil t)
'hg
nil))))
(when (eq diff-buffer-type 'git)

View file

@ -744,5 +744,53 @@ plum
(set-buffer-modified-p nil)
(kill-buffer buf-after))))))
(ert-deftest diff-mode-test-setup-buffer-type ()
"Check that `diff-setup-buffer-type' recognizes the diff's origin."
(with-temp-buffer
(insert "diff --git a/foo b/foo\n--- a/foo\n+++ b/foo\n")
(diff-mode)
(should (eq diff-buffer-type 'git)))
(with-temp-buffer
(insert "diff -r 0123456789ab foo\n--- a/foo\n+++ b/foo\n")
(diff-mode)
(should (eq diff-buffer-type 'hg)))
(with-temp-buffer
(insert "--- foo\n+++ foo\n@@ -1 +1 @@\n-a\n+b\n")
(diff-mode)
(should (eq diff-buffer-type nil))))
(ert-deftest diff-mode-test-find-file-name-create ()
"Check `diff-find-file-name' for a Git/Hg file creation.
It should not use existing file without subdirectory."
(ert-with-temp-directory temp-dir
(let ((default-directory temp-dir)
call-dir call-initial call-mustmatch)
;; A decoy with the same basename as the created file, but in a
;; different directory.
(with-temp-file (expand-file-name "created.txt" temp-dir)
(insert "decoy\n"))
(with-temp-buffer
(insert "diff --git a/sub/created.txt b/sub/created.txt
new file mode 100644
index 0000000..3456789
--- /dev/null
+++ b/sub/created.txt
@@ -0,0 +1 @@
+new
")
(diff-mode)
(goto-char (point-min))
(let ((read-file-name-function
(lambda (_p &optional dir _def mustmatch initial _pred)
(setq call-dir dir
call-initial initial
call-mustmatch mustmatch)
"magic")))
(should (equal (diff-find-file-name)
"magic"))
(should (equal call-dir (expand-file-name "sub/" temp-dir)))
(should (equal call-initial "created.txt"))
(should (equal call-mustmatch nil)))))))
(provide 'diff-mode-tests)
;;; diff-mode-tests.el ends here