Subject: bug#8756: vc-git.el doesn't use --follow argument in vc-git-print-log

From: Dmitry Dzhus <[email-masked]>
Date: Thu, 31 Jul 2014 00:35:32 +0400

How are you gentlemen,

Following renames is nice no matter what Git upstream thinks,
so I gave this bug a try. I'd like to share some progress.

Diffing (`d`), paging (`f`) and annotating a file (`a`) revision works.
I've tested it with Git version 1.8.5.4 and 2.0.2.

Parallel git log with hash/file information is run when
a log is queried. (vc-git-file-shalist) provides access to
a list of SHA1's/historical file names for the file open
in the *vc-change-log* buffer.

The bad part is that whole-changeset (`D`) diffing is broken
(wrong revisions are selected for diff).
The culprit is `vc-git-previous-revision`.
Using HASH^ to obtain the parent of HASH revision produces
unexpected results when file renames are involved. However,
for whole-changeset diffing HASH^ seems to be exactly what
we need. However, `vc-git-previous-revision` doesn't seem
to enable any inspection of the context in which we use it
(in other words, if we hit `d` or `D` in the log buffer.)
Any ideas?

I also have not tackled revision navigation in the annotation
mode, but this can likely be solved by plugging calls to 
`(vc-git-rev-to-filename)` somewhere in the annotation mode.

=== modified file 'lisp/vc/vc-git.el'
--- lisp/vc/vc-git.el	2014-06-29 20:48:55 +0000
+++ lisp/vc/vc-git.el	2014-07-30 20:32:01 +0000
@@ -691,13 +691,7 @@
 	 (coding-system-for-read 'binary)
 	 (coding-system-for-write 'binary)
 	 (fullname
-	  (let ((fn (vc-git--run-command-string
-		     file "ls-files" "-z" "--full-name" "--")))
-	    ;; ls-files does not return anything when looking for a
-	    ;; revision of a file that has been renamed or removed.
-	    (if (string= fn "")
-		(file-relative-name file (vc-git-root default-directory))
-	      (substring fn 0 -1)))))
+          (vc-git-rev-to-filename rev)))
     (vc-git-command
      buffer 0
      nil
@@ -786,20 +780,53 @@
     ;; If the buffer exists from a previous invocation it might be
     ;; read-only.
     (let ((inhibit-read-only t))
+      ;; Clean SHA1 list caches whenever we query a new change log
       (with-current-buffer
           buffer
-	(apply 'vc-git-command buffer
-	       'async files
-	       (append
-		'("log" "--no-color")
-		(when shortlog
-		  `("--graph" "--decorate" "--date=short"
+        (if (boundp 'vc-git-file-shalist-raw)
+            (setq vc-git-file-shalist-raw nil)
+          (set (make-local-variable 'vc-git-file-shalist-raw) nil))
+        (if (boundp 'vc-git-file-shalist)
+            (setq vc-git-file-shalist nil))
+        (when (vc-git-single-file files)
+          ;; Store newline-separated list of revision hashes and file
+          ;; names in vc-git-file-shalist-raw buffer-local variable
+          (with-temp-buffer
+            (set-process-filter
+             (apply 'vc-git-command nil
+                    'async files
+                    (append
+                     '("log"
+                       "--follow"
+                       "--name-only"
+                       "--pretty=tformat:%H"
+                       "--no-color")
+                     ;; Tail revision must now its parent
+                     (when limit (list "-n" (format "%s" (1+ limit))))
+                     (when start-revision (lsit start-revision))
+                     '("--")))
+             (lambda (p s)
+               (with-current-buffer buffer
+                 (setq
+                  vc-git-file-shalist-raw
+                  (replace-regexp-in-string
+                   "\n\n" "\n"
+                   (concat (if (boundp 'vc-git-file-shalist-raw)
+                               vc-git-file-shalist-raw "") s))))))))
+        (apply 'vc-git-command buffer
+               'async files
+               (append
+                (if (vc-git-single-file files)
+                    '("log" "--follow" "--no-color")
+                  '("log" "--no-color"))
+                (when shortlog
+                  `("--graph" "--decorate" "--date=short"
                     ,(format "--pretty=tformat:%s"
-			     (car vc-git-root-log-format))
-		    "--abbrev-commit"))
-		(when limit (list "-n" (format "%s" limit)))
-		(when start-revision (list start-revision))
-		'("--")))))))
+                             (car vc-git-root-log-format))
+                    "--abbrev-commit"))
+                (when limit (list "-n" (format "%s" limit)))
+                (when start-revision (list start-revision))
+                '("--")))))))
 
 (defun vc-git-log-outgoing (buffer remote-location)
   (interactive)
@@ -904,11 +931,21 @@
 (defun vc-git-diff (files &optional rev1 rev2 buffer)
   "Get a difference report using Git between two revisions of FILES."
   (let (process-file-side-effects)
-    (apply #'vc-git-command (or buffer "*vc-diff*") 1 files
-	   (if (and rev1 rev2) "diff-tree" "diff-index")
-	   "--exit-code"
-	   (append (vc-switches 'git 'diff)
-		   (list "-p" (or rev1 "HEAD") rev2 "--")))))
+    (with-current-buffer (or buffer "*vc-diff*")
+      ;; Run diff from the repository root because our file names are
+      ;; relative to it
+      (setq default-directory (vc-git-root default-directory))
+      (apply #'vc-git-command (or buffer "*vc-diff*") 1
+             (if (vc-git-single-file files)
+                 (list
+                  (vc-git-rev-to-filename rev1)
+                  (vc-git-rev-to-filename rev2))
+               files)
+             (if (and rev1 rev2) "diff-tree" "diff-index")
+             "--exit-code"
+             "-M"
+             (append (vc-switches 'git 'diff)
+                     (list "-p" (or rev1 "HEAD") rev2 "--"))))))
 
 (defun vc-git-revision-table (_files)
   ;; What about `files'?!?  --Stef
@@ -928,7 +965,8 @@
     table))
 
 (defun vc-git-annotate-command (file buf &optional rev)
-  (let ((name (file-relative-name file)))
+  (setq default-directory (vc-git-root default-directory))
+  (let ((name (vc-git-rev-to-filename rev)))
     (vc-git-command buf 'async nil "blame" "--date=iso" "-C" "-C" rev "--" name)))
 
 (declare-function vc-annotate-convert-time "vc-annotate" (time))
@@ -987,7 +1025,11 @@
                            (point)
                            (1- (point-max)))))))
         (or (vc-git-symbolic-commit prev-rev) prev-rev))
-    (vc-git--rev-parse (concat rev "^"))))
+    ;; Use historical data for the file if possible.
+    ;; FIXME: This breaks whole-changeset diffing.
+    (if (vc-git-file-shalist)
+        (car (cddr (member rev (vc-git-file-shalist))))
+      (vc-git--rev-parse (concat rev "^")))))
 
 (defun vc-git--rev-parse (rev)
   (with-temp-buffer
@@ -995,6 +1037,26 @@
      (vc-git--out-ok "rev-parse" rev)
      (buffer-substring-no-properties (point-min) (+ (point-min) 40)))))
 
+(defun vc-git-single-file (files)
+  "Return t if FILES contains a single non-directory file."
+  (and (eq (length files) 1)
+       (not (file-directory-p (car files)))))
+
+(defun vc-git-file-shalist ()
+  "Return alternating list of SHA1 hashes and file names.
+The list contains commit hashes and historical names for a file
+in the current change log buffer."
+  (cond
+   ((and (boundp 'vc-git-file-shalist) vc-git-file-shalist)
+    vc-git-file-shalist)
+   ((and (boundp 'vc-git-file-shalist-raw) vc-git-file-shalist-raw)
+    (set (make-local-variable 'vc-git-file-shalist)
+         (split-string vc-git-file-shalist-raw "\n")))))
+
+(defun vc-git-rev-to-filename (rev)
+  "Return a historical file name for the file in REV."
+  (cadr (member rev (vc-git-file-shalist))))
+
 (defun vc-git-next-revision (file rev)
   "Git-specific version of `vc-next-revision'."
   (let* ((default-directory (file-name-directory