Emacs Reverts Buffer to Weird Previous State with Git-Rebase

微笑、不失礼 提交于 2019-12-05 20:20:17

I'm the original author of autorevert, however, I have to dig quite deep into my memory as I haven't worked with it for quite some time.

I believe this boils down to a problem with the Emacs core function verify-visited-file-modtime, which auto-revert use to check if a buffers needs to be refreshed.

This could be caused by a number of reasons:

  • This could be a race condition, where Emacs reads a file before it was completely written, but sets the buffer modtime to the end time. (When this occurs, Emacs has auto-reverted the buffer half-way, but a new auto-revert does not trigger to revert the final version of the file.)

  • If the two variants of the file have the same time stamp, then Emacs will not see the difference.

I don't think that there is little you can do on the lisp side to get this working properly (without resolving to brute-force solutions). Emacs really need to atomically get a fingerprint of the file that has been opened and read into a buffer, and this can only be done from the core (which is something outside my area). Also, some file systems don't have a higher timestap resolution than one second, making it inherently difficult to use the timestamp to detect if a file has been changed.

It is a race condition as mentioned before. "git pull --rebase" may modify the same file multiple times as it replays your commits. If the following occurs it's game over:

  1. git changes file "foo"
  2. auto-revert kicks in and reloads "foo"
  3. git changes file "foo" again in the same second

Since unix file times have second resolution there's no way for emacs to tell that the second change occurred. I recently noticed I lost a bunch of changes that way so I decided to try to address the problem in emacs. I have patched the emacs code as follows. If the time the file is modified is the same as the current system time, I postpone the auto-revert until the next interval:

;;
;; Fix the auto-revert-handler so that if the system time is the
;; same as the new modified time for a file, skip it on this
;; iteration. This should fix race conditions when a file is changed
;; multiple times within the same second.
;; 

(defun file-change-too-close-for-comfort ()
  (let* ((file-time-raw (nth 5 (file-attributes (buffer-file-name))))
         (file-time (+ (lsh (nth 0 file-time-raw) 16) (nth 1 file-time-raw)))
         (current-time (+ (lsh (nth 0 (current-time)) 16) (nth 1 (current-time)))))
    (and (eq current-time file-time)
         (message "%s: postpone revert" (buffer-name))
         t)))


(defun auto-revert-handler ()
  "Revert current buffer, if appropriate.
This is an internal function used by Auto-Revert Mode."
  (when (or auto-revert-tail-mode (not (buffer-modified-p)))
    (let* ((buffer (current-buffer)) size
           (revert
            (or (and buffer-file-name
                     (file-readable-p buffer-file-name)
                     (if auto-revert-tail-mode
                         ;; Tramp caches the file attributes.  Setting
                         ;; `tramp-cache-inhibit' forces Tramp to
                         ;; reread the values.
                         (let ((tramp-cache-inhibit-cache t))
                           (/= auto-revert-tail-pos
                               (setq size
                                     (nth 7 (file-attributes
                                             buffer-file-name)))))
                       (and (not (file-remote-p buffer-file-name))
                            (not (verify-visited-file-modtime buffer))
                            (not (file-change-too-close-for-comfort)))))
                (and (or auto-revert-mode
                         global-auto-revert-non-file-buffers)
                     revert-buffer-function
                     (boundp 'buffer-stale-function)
                     (functionp buffer-stale-function)
                     (funcall buffer-stale-function t))))
           eob eoblist)
      (when revert
        (when (and auto-revert-verbose
                   (not (eq revert 'fast)))
          (message "Reverting buffer `%s'." (buffer-name)))
        ;; If point (or a window point) is at the end of the buffer,
        ;; we want to keep it at the end after reverting.  This allows
        ;; to tail a file.
        (when buffer-file-name
          (setq eob (eobp))
          (walk-windows
           #'(lambda (window)
               (and (eq (window-buffer window) buffer)
                    (= (window-point window) (point-max))
                    (push window eoblist)))
           'no-mini t))
        (if auto-revert-tail-mode
            (auto-revert-tail-handler size)
          ;; Bind buffer-read-only in case user has done C-x C-q,
          ;; so as not to forget that.  This gives undesirable results
          ;; when the file's mode changes, but that is less common.
          (let ((buffer-read-only buffer-read-only))
            (revert-buffer 'ignore-auto 'dont-ask 'preserve-modes)))
        (when buffer-file-name
          (when eob (goto-char (point-max)))
          (dolist (window eoblist)
            (set-window-point window (point-max)))))
      ;; `preserve-modes' avoids changing the (minor) modes.  But we
      ;; do want to reset the mode for VC, so we do it manually.
      (when (or revert auto-revert-check-vc-info)
        (vc-find-file-hook)))))

auto-revert-mode decides whether to revert based solely on the file's modification date. My guess is that git manipulates the modification date of the rebased files during the rebasing process, stopping auto-revert-mode from seeing every change.

Possible solutions include touching files after rebasing (yuck!), writing a replacement version of the auto-revert-handler function which checks for changes in file size, or rebasing within magit, which is a rather wonderful emacs git interface.

it may be a timing issue with multiple modifications to the file in quick succession. does it always happen, or is it random?

also, maybe setting auto-revert-check-vc-info to t could help (assuming you are using a git mode which is vc compatible in emacs).

as all the answers have mentioned thus far, it's probably a timing issue. it may seem counter intuitive, but maybe setting the autorevert interval to something longer may fix the problem (thus increasing the chances that when the buffer is reverted, it is reverted to its final state).

Another solution would be to close Emacs when doing Git operations and reopen after. I am almost ready to resort to that if I can find a way to preserve the opened buffers from one Emacs session to the other.

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!