Subject: Re: bug#26028: 26.0.50; epatch for multifile patches

From: Tino Calancha <[email-masked]>
Date: Tue, 23 May 2017 20:26:55 +0900

Arseny Sher <[email-masked]> writes:

> Eli Zaretskii <[email-masked]> writes:
>
>> I think you should point it to ~/tmp, not ~/tmp/old.  IOW, the
>> directory should be the one relative to which the file names in the
>> patch file will correctly point to the files.
>
> Okay, it might be not very convenient (as you might have several
> versions of project inside ~/tmp, and ediff will ask you which one is to
> be patched), but it works. However, how then I am expected to apply
> patches generated by VCS, where paths are prefixed with a/ and b/?
> Again, let's consider some simple example:
>
> mkdir -p proj/src
> cd proj
> echo "void main() {}" > src/hello.c
> git init
> git add src/ && git commit -m "commit"
> echo "int main() { return 0; }" > src/hello.c
> git diff > ../tmp.patch
> git reset --hard HEAD
>
> cat ../tmp.patch
> diff --git a/src/hello.c b/src/hello.c
> index ab73b3a..76e8197 100644
> --- a/src/hello.c
> +++ b/src/hello.c
> @@ -1 +1 @@
> -void main() {}
> +int main() { return 0; }
>
> How should I apply tmp.patch to proj?
Recently i am not using `epatch', but maybe following patch
is of interest for someone:
--8<-----------------------------cut here---------------start------------->8---
commit 3f4561b160f2cc9e2b47d16e7f27fbb9b86c3f40
Author: Tino Calancha <[email-masked]>
Date:   Tue May 23 19:21:42 2017 +0900

    epatch: multi-patch enhancement
    
    Handle multi-patches with files belonging to different
    subdirectories.
    (ediff-fixup-patch-map): For a multi-patch, set base-dir1 to a/
    and base-dir2 to b/ if it is a Git patch; otherwise, set both to .
    (ediff-map-patch-buffer): Use buffer-substring-no-properties.
    * lisp/vc/ediff.el (ediff-patch-file): Show differente prompt
    for multi-patches.  For single patches, use the patch header to guess the
    file to patch, and ensure the input matches an existing file.
    * test/lisp/vc/ediff-ptch-tests.el (ediff-ptch-test-bug26028): Add test.

diff --git a/lisp/vc/ediff-ptch.el b/lisp/vc/ediff-ptch.el
index 0340672da2..0d53b2aff4 100644
--- a/lisp/vc/ediff-ptch.el
+++ b/lisp/vc/ediff-ptch.el
@@ -222,10 +222,10 @@ ediff-map-patch-buffer
 	    ;;     (filename-from-1st-header-line . filename-from-2nd-line)
 	    (setq possible-file-names
 		  (cons (if (and beg1 end1)
-			    (buffer-substring beg1 end1)
+			    (buffer-substring-no-properties beg1 end1)
 			  "/dev/null")
 			(if (and beg2 end2)
-			    (buffer-substring beg2 end2)
+			    (buffer-substring-no-properties beg2 end2)
 			  "/dev/null")))
             ;; Remove file junk (Bug#26084).
             (while (re-search-backward
@@ -290,18 +290,25 @@ ediff-fixup-patch-map
 		    (or (file-name-directory (cdr proposed-file-names))
 			""))
 		   )
-	      ;; If both base-dir1 and base-dir2 are relative and exist,
-	      ;; assume that
-	      ;; these dirs lead to the actual files starting at the present
-	      ;; directory. So, we don't strip these relative dirs from the
-	      ;; file names. This is a heuristic intended to improve guessing
 	      (let ((default-directory (file-name-directory filename)))
-		(unless (or (file-name-absolute-p base-dir1)
-			    (file-name-absolute-p base-dir2)
-			    (not (file-exists-p base-dir1))
-			    (not (file-exists-p base-dir2)))
-		  (setq base-dir1 ""
-			base-dir2 "")))
+                (cond (multi-patch-p
+                       ;; Git diffs appends 'a/' '/b' to the files.
+                       (if (and (string-match-p "\\`a/" base-dir1)
+                                (string-match-p "\\`b/" base-dir2))
+                           (setq base-dir1 "a/" base-dir2 "b/")
+                         (setq base-dir1 "" base-dir2 "")))
+                      (t
+                       ;; If both base-dir1 and base-dir2 are relative and exist,
+                       ;; assume that
+                       ;; these dirs lead to the actual files starting at the present
+                       ;; directory. So, we don't strip these relative dirs from the
+                       ;; file names. This is a heuristic intended to improve guessing
+                       (unless (or (file-name-absolute-p base-dir1)
+                                   (file-name-absolute-p base-dir2)
+                                   (not (file-exists-p base-dir1))
+                                   (not (file-exists-p base-dir2)))
+                         (setq base-dir1 ""
+                               base-dir2 "")))))
 	      (or (string= (car proposed-file-names) "/dev/null")
 		  (setcar proposed-file-names
 			  (ediff-file-name-sans-prefix
diff --git a/lisp/vc/ediff.el b/lisp/vc/ediff.el
index 4751bb6ddc..e41839e968 100644
--- a/lisp/vc/ediff.el
+++ b/lisp/vc/ediff.el
@@ -121,6 +121,7 @@ ediff-date
 
 (require 'ediff-init)
 (require 'ediff-mult)  ; required because of the registry stuff
+(require 'diff-mode) ; diff-hunk-file-names
 
 (defgroup ediff nil
   "Comprehensive visual interface to `diff' and `patch'."
@@ -1355,6 +1356,7 @@ ediff-patch-default-directory
 (declare-function ediff-dispatch-file-patching-job "ediff-ptch"
                   (patch-buf filename &optional startup-hooks))
 
+(defvar ediff-patch-map)
 ;;;###autoload
 (defun ediff-patch-file (&optional arg patch-buf)
   "Query for a file name, and then run Ediff by patching that file.
@@ -1376,11 +1378,26 @@ ediff-patch-file
 			     (expand-file-name
 			      (buffer-file-name patch-buf))))
 			   (t default-directory)))
-    (setq source-file
-	  (read-file-name
-	   "File to patch (directory, if multifile patch): "
-	   ;; use an explicit initial file
-	   source-dir nil nil (ediff-get-default-file-name)))
+    (let ((multi-patch-p (with-current-buffer patch-buf (cdr ediff-patch-map))))
+      (cond ((not multi-patch-p)
+             (let* ((files (with-current-buffer patch-buf
+                             (diff-hunk-file-names 'old-first)))
+                    (def (if (and (string-match "\\`a/" (car files))
+                                  (string-match "\\`b/" (cadr files)))
+                             (expand-file-name
+                              (substring-no-properties (car files) 2)
+                              default-directory)
+                           (car files))))
+               (setq source-file
+                     (read-file-name
+                      "Single file to patch: "
+                      ;; use an explicit initial file
+                      source-dir nil 'mustmatch def))))
+            (t ; multi-patch
+             (setq source-file
+                   (read-file-name
+                    "Directory to patch, use root project dir: "
+                    source-dir)))))
     (ediff-dispatch-file-patching-job patch-buf source-file)))
 
 (declare-function ediff-patch-buffer-internal "ediff-ptch"
diff --git a/test/lisp/vc/ediff-ptch-tests.el b/test/lisp/vc/ediff-ptch-tests.el
index 387786ced0..74db053c97 100644
--- a/test/lisp/vc/ediff-ptch-tests.el
+++ b/test/lisp/vc/ediff-ptch-tests.el
@@ -21,6 +21,8 @@
 
 (require 'ert)
 (require 'ediff-ptch)
+(require 'ediff-diff) ; For `ediff-diff-program'.
+(eval-when-compile (require 'cl-lib))
 
 (ert-deftest ediff-ptch-test-bug25010 ()
   "Test for http://debbugs.gnu.org/25010 ."
@@ -104,6 +106,151 @@
           (delete-directory tmpdir 'recursive)
           (delete-file patch)))))
 
+(ert-deftest ediff-ptch-test-bug26028 ()
+  "Test for http://debbugs.gnu.org/26028 ."
+  (skip-unless (executable-find "git"))
+  (skip-unless (executable-find ediff-patch-program))
+  (skip-unless (executable-find ediff-diff-program))
+  (let ((git-program (executable-find "git"))
+        (default-dir default-directory)
+        tmpdir buffers)
+    ;;; Simple patch: old/src/hello.c /new/src/hello.c
+    (unwind-protect
+        (let* ((dir (make-temp-file "multipatch-test" t))
+               (file1 (expand-file-name "old/src/hello.c" dir))
+               (file2 (expand-file-name "new/src/hello.c" dir))
+               (patch (expand-file-name "tmp.patch" dir))
+               (default-directory (file-name-as-directory dir)))
+          (setq tmpdir dir)
+          (make-directory (expand-file-name "old/src/" dir) 'parents)
+          (make-directory (expand-file-name "new/src/" dir) 'parents)
+          (with-temp-buffer
+            (insert "void main() { }\n")
+            (write-region nil nil file1 nil 'silent)
+            (erase-buffer)
+            (insert "int main() { return 0; }\n")
+            (write-region nil nil file2 nil 'silent)
+            (erase-buffer)
+            (call-process ediff-diff-program nil t nil "-cr" "old" "new")
+            (write-region nil nil patch nil 'silent)
+            (cl-letf (((symbol-function 'y-or-n-p) (lambda (x) nil))
+                      ((symbol-function 'ediff-prompt-for-patch-file)
+                       (lambda (&rest x) (find-file-noselect patch)))
+                      ((symbol-function 'read-file-name) (lambda (x1 x2 x3 x4 x5) x5))
+                      ((symbol-function 'ediff-dispatch-file-patching-job)
+                       (lambda (x y) y)))
+              (should (equal (file-relative-name file1) (epatch nil patch)))
+              (push (get-file-buffer patch) buffers))))
+      (when (file-exists-p tmpdir)
+        (setq default-directory default-dir)
+        (delete-directory tmpdir 'recursive))
+      (mapc (lambda (b)
+              (when (buffer-live-p b) (kill-buffer b)))
+            buffers)
+      (setq buffers nil))
+    ;;; Simple Git patch: proj/src/hello.c
+    (unwind-protect
+        (let* ((dir (make-temp-file "multipatch-test" t))
+               (rootdir (expand-file-name "proj/src/" dir))
+               (file (expand-file-name "hello.c" rootdir))
+               (patch (expand-file-name "tmp.patch" dir))
+               (default-directory (file-name-as-directory rootdir)))
+          (make-directory rootdir 'parents)
+          (setq tmpdir dir)
+          (with-temp-buffer
+            (insert "void main() { }\n")
+            (write-region nil nil file nil 'silent)
+            (call-process git-program nil nil nil "init")
+            (call-process git-program nil nil nil "add" ".")
+            (call-process git-program nil nil nil "commit" "-m" "test repository.")
+            (erase-buffer)
+            (insert "int main() { return 0; }\n")
+            (write-region nil nil file nil 'silent)
+            (call-process git-program nil `(:file ,patch) nil "diff")
+            (call-process git-program nil nil nil "reset" "--hard" "head")
+            (cl-letf (((symbol-function 'y-or-n-p) (lambda (x) nil))
+                      ((symbol-function 'ediff-prompt-for-patch-file)
+                       (lambda (&rest x) (find-file-noselect patch)))
+                      ((symbol-function 'read-file-name) (lambda (&rest x) file))
+                      ((symbol-function 'read-file-name) (lambda (x1 x2 x3 x4 x5) x5))
+                      ((symbol-function 'ediff-dispatch-file-patching-job)
+                       (lambda (x y) y)))
+              (should (equal file (epatch nil patch)))))
+          (push (get-file-buffer patch) buffers))
+      ;; clean up
+      (when (file-exists-p tmpdir)
+        (setq default-directory default-dir)
+        (delete-directory tmpdir 'recursive))
+      (mapc (lambda (b)
+              (when (buffer-live-p b) (kill-buffer b)))
+            buffers)
+      (setq buffers nil))
+    ;;; Git multipatch.
+    (unwind-protect
+        (let* ((dir (make-temp-file "multipatch-test" t))
+               (file1 (expand-file-name "proj/src/hello.c" dir))
+               (file2 (expand-file-name "proj/src/bye.c" dir))
+               (file3 (expand-file-name "proj/lisp/foo.el" dir))
+               (file4 (expand-file-name "proj/lisp/bar.el" dir))
+               (file5 (expand-file-name "proj/etc/news" dir))
+               (patch (expand-file-name "tmp.patch" dir))
+               (default-directory (expand-file-name "proj" dir)))
+          (setq tmpdir dir)
+          (dolist (d '("src" "lisp" "etc"))
+            (setq rootdir (expand-file-name (concat "proj/" d) dir))
+            (make-directory rootdir 'parents))
+          (with-temp-buffer
+            (insert "void main() { }\n")
+            (write-region nil nil file1 nil 'silent)
+            (write-region nil nil file2 nil 'silent)
+            (erase-buffer)
+            (insert "(defun foo () nil)\n")
+            (write-region nil nil file3 nil 'silent)
+            (erase-buffer)
+            (insert "(defun bar () nil)\n")
+            (write-region nil nil file4 nil 'silent)
+            (erase-buffer)
+            (insert "new functions 'foo' and 'bar'\n")
+            (write-region nil nil file5 nil 'silent)
+            (call-process git-program nil nil nil "init")
+            (call-process git-program nil nil nil "add" "src" "lisp" "etc")
+            (call-process git-program nil nil nil "commit" "-m" "test repository.");)
+            (erase-buffer)
+            (insert "int main() { return 0;}\n")
+            (write-region nil nil file1 nil 'silent)
+            (write-region nil nil file2 nil 'silent)
+            (erase-buffer)
+            (insert "(defun qux () nil)\n")
+            (write-region nil nil file3 nil 'silent)
+            (erase-buffer)
+            (insert "(defun quux () nil)\n")
+            (write-region nil nil file4 nil 'silent)
+            (erase-buffer)
+            (insert "new functions 'qux' and 'quux'\n")
+            (write-region nil nil file5 nil 'silent)
+            (call-process git-program nil `(:file ,patch) nil "diff")
+            (call-process git-program nil nil nil "reset" "--hard" "head"))
+          (cl-letf (((symbol-function 'y-or-n-p) (lambda (x) nil))
+                    ((symbol-function 'ediff-get-patch-file) (lambda (&rest x) patch))
+                    ((symbol-function 'read-file-name) (lambda (&rest x) patch)))
+            (epatch nil patch)
+            (with-current-buffer "*Ediff Session Group Panel*"
+              (push (get-file-buffer patch) buffers)
+              (should (= 5 (length (cdr ediff-meta-list))))
+              ;; don't ask confirmation to exit.
+              (cl-letf (((symbol-function 'y-or-n-p) (lambda (x) t)))
+                (ediff-quit-meta-buffer)))))
+      ;; clean up
+      (when (file-exists-p tmpdir)
+        (setq default-directory default-dir)
+        (delete-directory tmpdir 'recursive))
+      (when ediff-registry-buffer
+        (push ediff-registry-buffer buffers))
+      (mapc (lambda (b)
+              (when (buffer-live-p b) (kill-buffer b)))
+            buffers)
+      (setq buffers nil))))
+
 
 (provide 'ediff-ptch-tests)
 ;;; ediff-ptch-tests.el ends here
--8<-----------------------------cut here---------------end--------------->8---
In GNU Emacs 26.0.50 (build 1, x86_64-pc-linux-gnu, GTK+ Version 3.22.11)
 of 2017-05-23
Repository revision: 4a485410ce74cafd4e9c344e31f7575464a16113