Subject: bug#62731: 29.0.60; diff-apply-hunk doesn't work for creating new files

From: Dmitry Gutov <[email-masked]>
Date: Wed, 2 Oct 2024 03:41:05 +0300

Hi!

On 09/04/2023 04:14, [email-masked] wrote:
> 1. emacs -Q
> 2. Put the following content in a diff-mode buffer:
> diff --git a/foo b/foo
> new file mode 100644
> --- /dev/null
> +++ b/foo
> @@ -0,0 +1,1 @@
> +content
> 3. C-c C-a
> 
> Expected behavior: A file called "foo" with content "content" is
> created.
> 
> Observed behavior: diff-mode prompts for the location of "b/foo", and
> doesn't allow specifying the location as a non-existent file, meaning
> the file can't actaully be created.

This is annoying indeed.

The attached patch should handle this:

* When OLD equals to /dev/null, allow reading non-existing file name.
* When NEW starts with b/ or /a, slice that off if such dir does not exist.
* Bonus: when the diff is applied in reverse, the checked file names are 
switched. That helps undo deletions as well. Or renames.

It makes some assumptions, though, (such as that default-directory fits 
the file names in the diff, which is normal for vc diffs but maybe not 
others), so some testing would be welcome, especially from people who 
deal with diffs produced otherwise.

diff --git a/lisp/vc/diff-mode.el b/lisp/vc/diff-mode.el
index 25c6238765d..98f77f1a1d7 100644
--- a/lisp/vc/diff-mode.el
+++ b/lisp/vc/diff-mode.el
@@ -1055,13 +1055,24 @@ diff-find-file-name
 	      (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) ""))))
+           (let ((file (or (car fs) ""))
+                 (creation (equal null-device
+                                  (car (diff-hunk-file-names (not old))))))
+             (when (and (string-match-p "\\`[ab]/" file)
+                        (not (file-directory-p (substring file 0 1))))
+               ;; Strip the common prefix a/ or /b if no such dir exists.
+               (setq file (substring file 2)))
+             (setq file (expand-file-name file))
 	     (setq file
 		   (read-file-name (format "Use file %s: " file)
-				   (file-name-directory file) file t
+				   (file-name-directory file) file
+                                   ;; Allow non-matching for creation.
+                                   (not creation)
 				   (file-name-nondirectory file)))
-             (setq-local diff-remembered-files-alist
-                         (cons (cons fs file) diff-remembered-files-alist))
+             (when (or (not creation) (file-exists-p file))
+               ;; Only remember files that exist. User might have mistyped.
+               (setq-local diff-remembered-files-alist
+                           (cons (cons fs file) diff-remembered-files-alist)))
              file)))))))

@@ -1921,7 +1932,7 @@ diff-find-source-location
                                 diff-context-mid-hunk-header-re nil t)
 			 (error "Can't find the hunk separator"))
 		       (match-string 1)))))
-	   (file (or (diff-find-file-name other noprompt)
+	   (file (or (diff-find-file-name (xor other reverse) noprompt)
                      (error "Can't find the file")))
 	   (revision (and other diff-vc-backend
                           (if reverse (nth 1 diff-vc-revisions)