├── .gitignore ├── README.org └── magit-filenotify.el /.gitignore: -------------------------------------------------------------------------------- 1 | *.elc 2 | -------------------------------------------------------------------------------- /README.org: -------------------------------------------------------------------------------- 1 | # -*- mode:org; mode:auto-fill; fill-column:80; coding:utf-8; -*- 2 | * Refresh status buffer when git tree changes 3 | 4 | This extension to [[http://magit.vc/][Magit]] provides a minor-mode which, when enabled, automatically 5 | updates a repository's status buffer whenever a file inside that repository's 6 | working tree has changed. It uses the file notification support that was added 7 | in Emacs v24.4. 8 | 9 | ** Should I use it? 10 | 11 | Note that Magit itself refreshes the appropriate status buffer whenever you run 12 | a Magit command. (Before v2.3, Magit did only do so when the major-mode of the 13 | current buffer derives from ~magit-mode~, but now it does so whenever you run a 14 | Magit command, regardless of context.) 15 | 16 | Additionally, also starting with v2.3, Magit can be configured to also refresh a 17 | repository's status buffer whenever you save a buffer which visits a file which 18 | is being tracked in that repository. Due to performance concerns that feature 19 | is not enabled by default, to enable it add this to your init file: 20 | 21 | #+BEGIN_SRC emacs-lisp 22 | (add-hook 'after-save-hook 'magit-after-save-refresh-status) 23 | #+END_SRC 24 | 25 | After you have done that and assuming that you mostly interact with Git using 26 | Magit, then you don't need this package. If a status buffer is out-of-date due 27 | to outside changes, you can always refresh it by simply pressing =g=. 28 | 29 | You can find more information about how Magit automatically refreshes buffers 30 | and about how to do it explicitly in its manual, in the node [[http://magit.vc/manual/magit/Automatic-refresh-and-revert.html][Automatic refresh 31 | and revert]]. 32 | 33 | However if you often use Git on the command line and inside Magit in parallel, 34 | then you might want to avoid having to press =g= when coming back to the status 35 | buffer from the command line. Enabling this mode avoids that. 36 | 37 | But also note that refreshing the status buffer can still be slow in big 38 | repositories. If you are not satisfied with Magit's performance then you should 39 | probably not enable this mode, as that would refresh the status buffer even more 40 | often, in many cases needlessly. 41 | 42 | ** How do I use it? 43 | 44 | To activate the minor-mode for a repository, open the status buffer for that 45 | repository and then call =M-x magit-filenotify-mode RET=. Repeat the same step 46 | to deactivate the mode again. To automatically enable the mode when opening 47 | a status buffer, add this to your init file: 48 | 49 | #+BEGIN_SRC emacs-lisp 50 | (add-hook 'magit-status-mode-hook 'magit-filenotify-mode) 51 | #+END_SRC 52 | -------------------------------------------------------------------------------- /magit-filenotify.el: -------------------------------------------------------------------------------- 1 | ;;; magit-filenotify.el --- Refresh status buffer when git tree changes -*- lexical-binding: t -*- 2 | 3 | ;; Copyright (C) 2013-2015 Rüdiger Sonderfeld 4 | 5 | ;; Author: Rüdiger Sonderfeld 6 | ;; Created: 17 Jul 2013 7 | ;; Keywords: tools 8 | ;; Package-Requires: ((magit "1.3.0") (emacs "24.4")) 9 | 10 | ;; This file is NOT part of GNU Emacs. 11 | 12 | ;; This program is free software: you can redistribute it and/or modify 13 | ;; it under the terms of the GNU General Public License as published by 14 | ;; the Free Software Foundation, either version 3 of the License, or 15 | ;; (at your option) any later version. 16 | 17 | ;; This program is distributed in the hope that it will be useful, 18 | ;; but WITHOUT ANY WARRANTY; without even the implied warranty of 19 | ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 20 | ;; GNU General Public License for more details. 21 | 22 | ;; You should have received a copy of the GNU General Public License 23 | ;; along with this program. If not, see . 24 | 25 | ;;; Commentary: 26 | 27 | ;; This module comes with a minor mode `magit-filenotify' which tracks 28 | ;; changes in the source tree using `file-notify' and refreshes the magit 29 | ;; status buffer. Emacs 24.4 with `file-notify-support' is required for 30 | ;; it to work. 31 | 32 | ;; Also see https://github.com/magit/magit-filenotify for more information. 33 | 34 | ;;; Code: 35 | 36 | (require 'magit) 37 | (require 'cl-lib) 38 | (require 'filenotify) 39 | 40 | (defgroup magit-filenotify nil 41 | "Refresh status buffer if source tree changes" 42 | :prefix "magit-filenotify" 43 | :group 'magit-extensions) 44 | 45 | (defcustom magit-filenotify-ignored '("\\`\\.#" "\\`flycheck_") 46 | "A list of regexp for filenames that will be ignored by the callback." 47 | :group 'magit-filenotify 48 | :type '(repeat regexp)) 49 | 50 | (defun magit-filenotify--directories () 51 | "List all directories containing files watched by git." 52 | (cons 53 | default-directory 54 | (cl-remove-duplicates 55 | (cl-loop for file in (magit-git-lines "ls-files") 56 | for dir = (file-name-directory (magit-decode-git-path file)) 57 | when dir 58 | collect (expand-file-name dir)) 59 | :test #'string=))) 60 | 61 | ;; Use #'equal as test because watch-descriptors aren't always integers. With 62 | ;; inotify, descriptors can be lists like (17). This is properly documented in 63 | ;; (info "(elisp)File Notifications") which states "It [the descriptor] should 64 | ;; be used for comparison by `equal' only". 65 | (defvar magit-filenotify-data (make-hash-table :test #'equal) 66 | "A hash table to map watch-descriptors to a list (DIRECTORY STATUS-BUFFER).") 67 | 68 | (defvar magit-filenotify--idle-timer nil 69 | "Timer which will refresh buffers when Emacs becomes idle.") 70 | 71 | (defcustom magit-filenotify-idle-delay 1.57 72 | "Number of seconds to wait before refreshing out-of-date buffers." 73 | :group 'magit-filenotify 74 | :type 'number) 75 | 76 | (defcustom magit-filenotify-instant-refresh-time 1.73 77 | "Minimum number of seconds for an instant refresh. 78 | When an file-notify event occurs for some repository and the 79 | previous event is more distant than this value, the corresponding 80 | magit status buffer will be refreshed immediately instead of 81 | delaying the refresh according to `magit-filenotify-idle-delay'. 82 | 83 | Note that setting this option to a too low value will cause very 84 | frequent refreshes which might seem to make Emacs hang in case 85 | frequent changes occur to files, e.g., during the compilation of 86 | a large project." 87 | :group 'magit-filenotify 88 | :type 'number) 89 | 90 | (defvar magit-filenotify--buffers nil 91 | "List of magit status buffers to be refreshed. 92 | Those will be refreshed after `magit-filenotify-idle-delay' seconds.") 93 | 94 | (defun magit-filenotify--refresh-buffer (buffer) 95 | "Refresh the given magit status BUFFER." 96 | (when (buffer-live-p buffer) 97 | (with-current-buffer buffer 98 | ;; `magit-refresh' runs the functions in `magit-pre-refresh-hook' which 99 | ;; contains `magit-maybe-save-repository-buffers'. This function 100 | ;; queries the user to save repository buffers. That's nice for 101 | ;; interactive use but it's bad here because when you edit, save, and 102 | ;; start editing again, you'll get that query after 103 | ;; `magit-filenotify-idle-delay'. 104 | ;; 105 | ;; Workaround Emacs bug#21311. As the bug states, this is actually not 106 | ;; an Emacs bug but a bug in Magit. All hooks should be declared using 107 | ;; `defvar' nowadays. This has been fixed already in Magit (see 108 | ;; https://github.com/magit/magit/issues/2198) but let's keep that here 109 | ;; for compatibility with older Magit versions. 110 | (defvar magit-pre-refresh-hook) 111 | (let ((magit-pre-refresh-hook nil)) 112 | (magit-refresh)))) 113 | (setq magit-filenotify--buffers (delq buffer magit-filenotify--buffers))) 114 | 115 | (defun magit-filenotify--refresh-all () 116 | "Refresh all magit status buffers in `magit-filenotify--buffers'. 117 | Those are all status buffers for which file change notifications 118 | have been received since the last refresh." 119 | (mapc #'magit-filenotify--refresh-buffer magit-filenotify--buffers)) 120 | 121 | (defun magit-filenotify--register-buffer (buffer) 122 | "Register BUFFER as being out-of-date. 123 | BUFFER is some magit status buffer where some file-notify change 124 | event has been received for some of the repository's 125 | directories. 126 | 127 | All out-of-date magit status buffers are collected in 128 | `magit-filenotify--buffers' and will be refreshed automatically 129 | when Emacs has been idle for `magit-filenotify-idle-delay' 130 | seconds." 131 | (cl-pushnew buffer magit-filenotify--buffers) 132 | (if magit-filenotify--idle-timer 133 | (progn 134 | (cancel-timer magit-filenotify--idle-timer) 135 | (timer-activate-when-idle magit-filenotify--idle-timer t)) 136 | (setq magit-filenotify--idle-timer 137 | (run-with-idle-timer magit-filenotify-idle-delay 138 | nil #'magit-filenotify--refresh-all)))) 139 | 140 | (defvar magit-filenotify--last-event-times (make-hash-table) 141 | "A hash-table from status buffers to the last event for the buffers.") 142 | 143 | (defun magit-filenotify--rm-watch (desc) 144 | "Remove the directory watch DESC." 145 | ;; At least when using inotify as `file-notify--library' there will be an 146 | ;; error when calling `file-notify-rm-watch' on a descriptor of a directory 147 | ;; which has been deleted (as per git rm -rf some/dir/). 148 | ;; 149 | ;; Actually, it would be even better to handle deletions and creations of 150 | ;; directories directly in `magit-filenotify--callback', i.e., if a watched 151 | ;; dir is deleted, remove its entry (and all subdir entries) from 152 | ;; `magit-filenotify-data'. If some new directory is created as a 153 | ;; subdirectory of a watched directory, start watching it. However, one 154 | ;; problem is that renamings can be either reported as one `renamed' events 155 | ;; or a sequence of `created' and `deleted' events in any order depending on 156 | ;; `file-notify--library' (and maybe also `system-type'). 157 | (condition-case var 158 | (file-notify-rm-watch desc) 159 | (file-notify-error (message "File notify error: %S" (cdr var))))) 160 | 161 | (defun magit-filenotify--callback (ev) 162 | "Handle file-notify callbacks. 163 | Argument EV contains the watch data." 164 | (unless 165 | (let ((file (nth 2 ev)) res) 166 | (dolist (rx magit-filenotify-ignored res) 167 | (when (string-match rx (file-name-nondirectory file)) 168 | (setq res t)))) 169 | (let* ((wd (car ev)) 170 | (data (gethash wd magit-filenotify-data)) 171 | (buffer (cadr data)) 172 | (now (current-time))) 173 | (if (buffer-live-p buffer) 174 | (let ((last-event-time (gethash buffer magit-filenotify--last-event-times))) 175 | (puthash buffer now magit-filenotify--last-event-times) 176 | (if (and last-event-time 177 | (> (time-to-seconds (time-subtract now last-event-time)) 178 | magit-filenotify-instant-refresh-time)) 179 | ;; Fast path: The last event concerning this status buffer is 180 | ;; quite some time back in the past, so refresh immediately. 181 | ;; This should basically catch all cases where a user manually 182 | ;; modifies a file, e.g. by saving a buffer. 183 | (magit-filenotify--refresh-buffer buffer) 184 | ;; Delayed path: We're receiving bursts of events which probably 185 | ;; means that some kind of compilation is ongoing. So defer the 186 | ;; refreshes into the future in order not to lock up emacs. 187 | (magit-filenotify--register-buffer buffer))) 188 | (magit-filenotify--rm-watch wd) 189 | (remhash wd magit-filenotify-data) 190 | (remhash buffer magit-filenotify--last-event-times))))) 191 | 192 | (defun magit-filenotify-start () 193 | "Start watching for changes to the source tree using filenotify. 194 | This can only be called from a magit status buffer." 195 | (unless (derived-mode-p 'magit-status-mode) 196 | (error "Only works in magit status buffer")) 197 | (dolist (dir (magit-filenotify--directories)) 198 | (when (file-exists-p dir) 199 | (puthash (file-notify-add-watch dir 200 | '(change attribute-change) 201 | #'magit-filenotify--callback) 202 | (list dir (current-buffer)) 203 | magit-filenotify-data)))) 204 | 205 | (defun magit-filenotify-stop () 206 | "Stop watching for changes to the source tree using filenotify. 207 | This can only be called from a magit status buffer." 208 | (unless (derived-mode-p 'magit-status-mode) 209 | (error "Only works in magit status buffer")) 210 | (maphash 211 | (lambda (k v) 212 | (when (or (equal (cadr v) (current-buffer)) ; or use buffer? 213 | ;; Also remove watches for source trees where the magit status 214 | ;; buffer has been killed. 215 | (not (buffer-live-p (cadr v)))) 216 | (magit-filenotify--rm-watch k) 217 | (remhash k magit-filenotify-data) 218 | (remhash (cadr v) magit-filenotify--last-event-times))) 219 | magit-filenotify-data)) 220 | 221 | (defun magit-filenotify-watching-p () 222 | "Return non-nil if current source tree is watched." 223 | (unless (derived-mode-p 'magit-status-mode) 224 | (error "Only works in magit status buffer")) 225 | (let (ret) 226 | (maphash (lambda (_k v) 227 | (when (and (not ret) 228 | (equal (cadr v) (current-buffer))) 229 | (setq ret t))) 230 | magit-filenotify-data) 231 | ret)) 232 | 233 | (defcustom magit-filenotify-lighter " MagitFilenotify" 234 | "String to display in mode line when `magit-filenotify-mode' is active." 235 | :group 'magit-filenotify 236 | :type 'string) 237 | 238 | ;;;###autoload 239 | (define-minor-mode magit-filenotify-mode 240 | "Refresh status buffer if source tree changes." 241 | :lighter magit-filenotify-lighter 242 | :group 'magit-filenotify 243 | (if magit-filenotify-mode 244 | (progn 245 | (magit-filenotify-start) 246 | (add-hook 'kill-buffer-hook #'magit-filenotify-stop nil t)) 247 | (magit-filenotify-stop))) 248 | 249 | (defun magit-filenotify-stop-all () 250 | "Stop watching for changes in all git trees." 251 | (interactive) 252 | (maphash 253 | (lambda (k _v) (magit-filenotify--rm-watch k)) 254 | magit-filenotify-data) 255 | (clrhash magit-filenotify-data)) 256 | 257 | ;;; Loading 258 | (easy-menu-add-item magit-mode-menu nil 259 | ["Auto Refresh" magit-filenotify-mode 260 | :style toggle 261 | :visible (derived-mode-p 'magit-status-mode) 262 | :selected (magit-filenotify-watching-p) 263 | :help "Refresh magit status buffer when source tree updates"] 264 | "Refresh") 265 | 266 | (custom-add-option 'magit-status-mode-hook #'magit-filenotify-mode) 267 | 268 | (defun magit-filenotify-unload-function () 269 | "Cleanup when module is unloaded." 270 | (magit-filenotify-stop-all) 271 | (easy-menu-remove-item magit-mode-menu nil "Auto Refresh")) 272 | 273 | (provide 'magit-filenotify) 274 | ;; Local Variables: 275 | ;; indent-tabs-mode: nil 276 | ;; End: 277 | ;;; magit-filenotify.el ends here 278 | --------------------------------------------------------------------------------