31

If I want to see how foo.bar looked like in some certain commit <COMMIT_ID> then I can invoke:

git show <COMMIT_ID>:foo.bar

Nice... How can I do it in emacs? Using magit? Using vc? Say I am visiting the file foo.bar and I want to look up how it looked in <COMMIT_ID>.

Dror
  • 12,174
  • 21
  • 90
  • 160

4 Answers4

33

The canonical way to do that in Emacs is to use VC: C-x v ~ from the file's buffer will ask you for a revision and then show that file as it was at that revision. It should work for any control system supported by VC, such as Git, Bzr, ...

Stefan
  • 27,908
  • 4
  • 53
  • 82
  • It doesn't work for moved files with the `--follow` option. – choroba Sep 29 '15 at 14:23
  • I guess "doesn't work" depends on how you look at it. But I agree that it would be nice if you could ask Emacs to tell Git to follow renames when trying to find the state of "the file" at a particular revision. I suggest you `M-x report-emacs-bug` to request this feature. – Stefan Sep 29 '15 at 14:30
  • Q: How to make `vc-revision-other-window` (`C-x v ~`) not create the file (`filename.~branch~`) containing the shown version of the original file (`filename`)? – Mekeor Melire Sep 16 '16 at 09:42
  • I was expecting TAB completion to generate a list of commits on the prompt, but it didn't (you need to know and enter/paste the commit ID manually), so the first 3 lines of phils' answer below were a bit more helpful to me – Chrisuu Nov 25 '22 at 20:49
26
  • C-xvl to view the file's history.
  • n and p to move between commits.
  • f to visit the file as of the commit at point.

That's bound to log-view-find-revision, and if we look at the code we see the critical bit is:

(switch-to-buffer (vc-find-revision file revision)))

So we could wrap that in a custom function like so:

(defun my-vc-visit-file-revision (file revision)
  "Visit FILE as it was at REVISION."
  (interactive
   (list (expand-file-name
          (read-file-name (if (buffer-file-name)
                              (format "File (%s): " (file-name-nondirectory
                                                     (buffer-file-name)))
                            "File: ")))
         (read-string "Revision: ")))
  (require 'vc)
  (switch-to-buffer
   (vc-find-revision file revision)))

Edit: Stefan has provided a better answer, but if you liked being able to select the file as well as the revision, here's a version of my function which maintains the interactive file selection, but uses the code from vc-revision-other-window for the revision handling.

I concluded that using other-window by default does indeed make more sense, so I've done the same here -- unless you provide a prefix argument in which case it uses the current window.

(defun my-vc-visit-file-revision (file rev)
  "Visit revision REV of FILE in another window.
With prefix argument, uses the current window instead.
If the current file is named `F', the revision is named `F.~REV~'.
If `F.~REV~' already exists, use it instead of checking it out again."
  ;; based on `vc-revision-other-window'.
  (interactive
   (let ((file (expand-file-name
                (read-file-name
                 (if (buffer-file-name)
                     (format "File (%s): " (file-name-nondirectory
                                            (buffer-file-name)))
                   "File: ")))))
     (require 'vc)
     (list file (if (vc-backend file)
                    (vc-read-revision
                     "Revision to visit (default is working revision): "
                     (list file))
                  (vc-read-revision "Revision to visit: " t
                                    (or (vc-deduce-backend)
                                        (vc-responsible-backend file)))))))
  (require 'vc)
  (let ((revision (if (string-equal rev "")
                      (if (vc-backend file)
                          (vc-working-revision file)
                        (error "No revision specified for unregistered file %s"
                               file))
                    rev))
        (backend (or (vc-backend file)
                     (vc-deduce-backend)
                     (vc-responsible-backend file)))
        (visit (if current-prefix-arg
                   'switch-to-buffer
                 'switch-to-buffer-other-window)))
    (condition-case err
        (funcall visit (vc-find-revision file revision backend))
      ;; The errors which can result when we request an invalid combination of
      ;; file and revision tend to be opaque side-effects of some unexpected
      ;; failure within the backend; so we simply trap everything and signal a
      ;; replacement error indicting the assumed cause.
      (error (error "File not found at revision %s: %s" revision file)))))

I bind this command to C-xvC-f

phils
  • 71,335
  • 11
  • 153
  • 198
  • 1
    This is such an extremely useful answer! Stefan may have more precisely addressed the question, but I'm grateful to have run across this invaluable feature. – taranaki Apr 29 '15 at 11:37
  • `my-vc-visit-file-revision` now supports visiting old revisions of historical file paths which no longer exist. – phils May 27 '20 at 03:29
  • 1
    Magit users can use `M-x magit-find-file`, which likewise supports historical revisions, and furthermore provides completion for tracked paths for the revision you specify (as it reads the revision first). Very nice! – phils May 27 '20 at 03:47
9

There's a package called git-timemachine that makes the process of viewing previous versions of a file almost completely seamless; see the link for installation instructions and a demo. (If you are already using MELPA, just do M-x package-install RET git-timemachine RET).

The way it works is, you call M-x git-timemachine RET from a buffer visiting a tracked file. Then you can:

  • p Visit previous historic version
  • n Visit next historic version
  • w Copy the abbreviated hash of the current historic version
  • W Copy the full hash of the current historic version
  • q Exit the time machine.

Note that if you know the hash of the commit you want to visit, the custom command from @phils' solution will serve you better for that specific use case. But for navigating between different versions of a file I find that using git-timemachine is even easier than using the functionality that VC provides.

You can of course bind git-timemachine to a key binding of your choice.

itsjeyd
  • 5,070
  • 2
  • 30
  • 49
7

If you are viewing the commit in magit you can just press Enter on the file or part of the file you are interested in.

Robin Green
  • 32,079
  • 16
  • 104
  • 187
  • Yes you can! But is there a way you can look visit a file in a commit that hasn't been modified for the commit you're looking at? – kristianlm Dec 26 '16 at 15:00
  • Yes, copy the commit hash and then check out that commit using the b command. If you have uncommitted changes, either commit them, stash them, or create a new work tree. – Robin Green Dec 26 '16 at 15:03
  • Thanks @Robin, but I was hoping there was a way to look at file in the buffer without checkout it out. We know it's possible since you can look at the files that are modified be simply pressing enter. I ended up looking for the most recent commit that contains the file and then opening up the file from that commit. – kristianlm Dec 28 '16 at 14:27