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