Show More
This diff has been collapsed as it changes many lines, (626 lines changed) Show them Hide them | |||||
@@ -0,0 +1,626 b'' | |||||
|
1 | # __init__.py - fsmonitor initialization and overrides | |||
|
2 | # | |||
|
3 | # Copyright 2013-2016 Facebook, Inc. | |||
|
4 | # | |||
|
5 | # This software may be used and distributed according to the terms of the | |||
|
6 | # GNU General Public License version 2 or any later version. | |||
|
7 | ||||
|
8 | '''Faster status operations with the Watchman file monitor (EXPERIMENTAL) | |||
|
9 | ||||
|
10 | Integrates the file-watching program Watchman with Mercurial to produce faster | |||
|
11 | status results. | |||
|
12 | ||||
|
13 | On a particular Linux system, for a real-world repository with over 400,000 | |||
|
14 | files hosted on ext4, vanilla `hg status` takes 1.3 seconds. On the same | |||
|
15 | system, with fsmonitor it takes about 0.3 seconds. | |||
|
16 | ||||
|
17 | fsmonitor requires no configuration -- it will tell Watchman about your | |||
|
18 | repository as necessary. You'll need to install Watchman from | |||
|
19 | https://facebook.github.io/watchman/ and make sure it is in your PATH. | |||
|
20 | ||||
|
21 | The following configuration options exist: | |||
|
22 | ||||
|
23 | :: | |||
|
24 | ||||
|
25 | [fsmonitor] | |||
|
26 | mode = {off, on, paranoid} | |||
|
27 | ||||
|
28 | When `mode = off`, fsmonitor will disable itself (similar to not loading the | |||
|
29 | extension at all). When `mode = on`, fsmonitor will be enabled (the default). | |||
|
30 | When `mode = paranoid`, fsmonitor will query both Watchman and the filesystem, | |||
|
31 | and ensure that the results are consistent. | |||
|
32 | ||||
|
33 | :: | |||
|
34 | ||||
|
35 | [fsmonitor] | |||
|
36 | timeout = (float) | |||
|
37 | ||||
|
38 | A value, in seconds, that determines how long fsmonitor will wait for Watchman | |||
|
39 | to return results. Defaults to `2.0`. | |||
|
40 | ||||
|
41 | :: | |||
|
42 | ||||
|
43 | [fsmonitor] | |||
|
44 | blacklistusers = (list of userids) | |||
|
45 | ||||
|
46 | A list of usernames for which fsmonitor will disable itself altogether. | |||
|
47 | ||||
|
48 | :: | |||
|
49 | ||||
|
50 | [fsmonitor] | |||
|
51 | walk_on_invalidate = (boolean) | |||
|
52 | ||||
|
53 | Whether or not to walk the whole repo ourselves when our cached state has been | |||
|
54 | invalidated, for example when Watchman has been restarted or .hgignore rules | |||
|
55 | have been changed. Walking the repo in that case can result in competing for | |||
|
56 | I/O with Watchman. For large repos it is recommended to set this value to | |||
|
57 | false. You may wish to set this to true if you have a very fast filesystem | |||
|
58 | that can outpace the IPC overhead of getting the result data for the full repo | |||
|
59 | from Watchman. Defaults to false. | |||
|
60 | ||||
|
61 | fsmonitor is incompatible with the largefiles and eol extensions, and | |||
|
62 | will disable itself if any of those are active. | |||
|
63 | ||||
|
64 | ''' | |||
|
65 | ||||
|
66 | # Platforms Supported | |||
|
67 | # =================== | |||
|
68 | # | |||
|
69 | # **Linux:** *Stable*. Watchman and fsmonitor are both known to work reliably, | |||
|
70 | # even under severe loads. | |||
|
71 | # | |||
|
72 | # **Mac OS X:** *Stable*. The Mercurial test suite passes with fsmonitor | |||
|
73 | # turned on, on case-insensitive HFS+. There has been a reasonable amount of | |||
|
74 | # user testing under normal loads. | |||
|
75 | # | |||
|
76 | # **Solaris, BSD:** *Alpha*. watchman and fsmonitor are believed to work, but | |||
|
77 | # very little testing has been done. | |||
|
78 | # | |||
|
79 | # **Windows:** *Alpha*. Not in a release version of watchman or fsmonitor yet. | |||
|
80 | # | |||
|
81 | # Known Issues | |||
|
82 | # ============ | |||
|
83 | # | |||
|
84 | # * fsmonitor will disable itself if any of the following extensions are | |||
|
85 | # enabled: largefiles, inotify, eol; or if the repository has subrepos. | |||
|
86 | # * fsmonitor will produce incorrect results if nested repos that are not | |||
|
87 | # subrepos exist. *Workaround*: add nested repo paths to your `.hgignore`. | |||
|
88 | # | |||
|
89 | # The issues related to nested repos and subrepos are probably not fundamental | |||
|
90 | # ones. Patches to fix them are welcome. | |||
|
91 | ||||
|
92 | from __future__ import absolute_import | |||
|
93 | ||||
|
94 | import os | |||
|
95 | import stat | |||
|
96 | import sys | |||
|
97 | ||||
|
98 | from mercurial import ( | |||
|
99 | context, | |||
|
100 | extensions, | |||
|
101 | localrepo, | |||
|
102 | pathutil, | |||
|
103 | scmutil, | |||
|
104 | util, | |||
|
105 | ) | |||
|
106 | from mercurial import match as matchmod | |||
|
107 | from mercurial.i18n import _ | |||
|
108 | ||||
|
109 | from . import ( | |||
|
110 | state, | |||
|
111 | watchmanclient, | |||
|
112 | ) | |||
|
113 | ||||
|
114 | # Note for extension authors: ONLY specify testedwith = 'internal' for | |||
|
115 | # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should | |||
|
116 | # be specifying the version(s) of Mercurial they are tested with, or | |||
|
117 | # leave the attribute unspecified. | |||
|
118 | testedwith = 'internal' | |||
|
119 | ||||
|
120 | # This extension is incompatible with the following blacklisted extensions | |||
|
121 | # and will disable itself when encountering one of these: | |||
|
122 | _blacklist = ['largefiles', 'eol'] | |||
|
123 | ||||
|
124 | def _handleunavailable(ui, state, ex): | |||
|
125 | """Exception handler for Watchman interaction exceptions""" | |||
|
126 | if isinstance(ex, watchmanclient.Unavailable): | |||
|
127 | if ex.warn: | |||
|
128 | ui.warn(str(ex) + '\n') | |||
|
129 | if ex.invalidate: | |||
|
130 | state.invalidate() | |||
|
131 | ui.log('fsmonitor', 'Watchman unavailable: %s\n', ex.msg) | |||
|
132 | else: | |||
|
133 | ui.log('fsmonitor', 'Watchman exception: %s\n', ex) | |||
|
134 | ||||
|
135 | def _hashignore(ignore): | |||
|
136 | """Calculate hash for ignore patterns and filenames | |||
|
137 | ||||
|
138 | If this information changes between Mercurial invocations, we can't | |||
|
139 | rely on Watchman information anymore and have to re-scan the working | |||
|
140 | copy. | |||
|
141 | ||||
|
142 | """ | |||
|
143 | sha1 = util.sha1() | |||
|
144 | if util.safehasattr(ignore, 'includepat'): | |||
|
145 | sha1.update(ignore.includepat) | |||
|
146 | sha1.update('\0\0') | |||
|
147 | if util.safehasattr(ignore, 'excludepat'): | |||
|
148 | sha1.update(ignore.excludepat) | |||
|
149 | sha1.update('\0\0') | |||
|
150 | if util.safehasattr(ignore, 'patternspat'): | |||
|
151 | sha1.update(ignore.patternspat) | |||
|
152 | sha1.update('\0\0') | |||
|
153 | if util.safehasattr(ignore, '_files'): | |||
|
154 | for f in ignore._files: | |||
|
155 | sha1.update(f) | |||
|
156 | sha1.update('\0') | |||
|
157 | return sha1.hexdigest() | |||
|
158 | ||||
|
159 | def overridewalk(orig, self, match, subrepos, unknown, ignored, full=True): | |||
|
160 | '''Replacement for dirstate.walk, hooking into Watchman. | |||
|
161 | ||||
|
162 | Whenever full is False, ignored is False, and the Watchman client is | |||
|
163 | available, use Watchman combined with saved state to possibly return only a | |||
|
164 | subset of files.''' | |||
|
165 | def bail(): | |||
|
166 | return orig(match, subrepos, unknown, ignored, full=True) | |||
|
167 | ||||
|
168 | if full or ignored or not self._watchmanclient.available(): | |||
|
169 | return bail() | |||
|
170 | state = self._fsmonitorstate | |||
|
171 | clock, ignorehash, notefiles = state.get() | |||
|
172 | if not clock: | |||
|
173 | if state.walk_on_invalidate: | |||
|
174 | return bail() | |||
|
175 | # Initial NULL clock value, see | |||
|
176 | # https://facebook.github.io/watchman/docs/clockspec.html | |||
|
177 | clock = 'c:0:0' | |||
|
178 | notefiles = [] | |||
|
179 | ||||
|
180 | def fwarn(f, msg): | |||
|
181 | self._ui.warn('%s: %s\n' % (self.pathto(f), msg)) | |||
|
182 | return False | |||
|
183 | ||||
|
184 | def badtype(mode): | |||
|
185 | kind = _('unknown') | |||
|
186 | if stat.S_ISCHR(mode): | |||
|
187 | kind = _('character device') | |||
|
188 | elif stat.S_ISBLK(mode): | |||
|
189 | kind = _('block device') | |||
|
190 | elif stat.S_ISFIFO(mode): | |||
|
191 | kind = _('fifo') | |||
|
192 | elif stat.S_ISSOCK(mode): | |||
|
193 | kind = _('socket') | |||
|
194 | elif stat.S_ISDIR(mode): | |||
|
195 | kind = _('directory') | |||
|
196 | return _('unsupported file type (type is %s)') % kind | |||
|
197 | ||||
|
198 | ignore = self._ignore | |||
|
199 | dirignore = self._dirignore | |||
|
200 | if unknown: | |||
|
201 | if _hashignore(ignore) != ignorehash and clock != 'c:0:0': | |||
|
202 | # ignore list changed -- can't rely on Watchman state any more | |||
|
203 | if state.walk_on_invalidate: | |||
|
204 | return bail() | |||
|
205 | notefiles = [] | |||
|
206 | clock = 'c:0:0' | |||
|
207 | else: | |||
|
208 | # always ignore | |||
|
209 | ignore = util.always | |||
|
210 | dirignore = util.always | |||
|
211 | ||||
|
212 | matchfn = match.matchfn | |||
|
213 | matchalways = match.always() | |||
|
214 | dmap = self._map | |||
|
215 | nonnormalset = getattr(self, '_nonnormalset', None) | |||
|
216 | ||||
|
217 | copymap = self._copymap | |||
|
218 | getkind = stat.S_IFMT | |||
|
219 | dirkind = stat.S_IFDIR | |||
|
220 | regkind = stat.S_IFREG | |||
|
221 | lnkkind = stat.S_IFLNK | |||
|
222 | join = self._join | |||
|
223 | normcase = util.normcase | |||
|
224 | fresh_instance = False | |||
|
225 | ||||
|
226 | exact = skipstep3 = False | |||
|
227 | if matchfn == match.exact: # match.exact | |||
|
228 | exact = True | |||
|
229 | dirignore = util.always # skip step 2 | |||
|
230 | elif match.files() and not match.anypats(): # match.match, no patterns | |||
|
231 | skipstep3 = True | |||
|
232 | ||||
|
233 | if not exact and self._checkcase: | |||
|
234 | # note that even though we could receive directory entries, we're only | |||
|
235 | # interested in checking if a file with the same name exists. So only | |||
|
236 | # normalize files if possible. | |||
|
237 | normalize = self._normalizefile | |||
|
238 | skipstep3 = False | |||
|
239 | else: | |||
|
240 | normalize = None | |||
|
241 | ||||
|
242 | # step 1: find all explicit files | |||
|
243 | results, work, dirsnotfound = self._walkexplicit(match, subrepos) | |||
|
244 | ||||
|
245 | skipstep3 = skipstep3 and not (work or dirsnotfound) | |||
|
246 | work = [d for d in work if not dirignore(d[0])] | |||
|
247 | ||||
|
248 | if not work and (exact or skipstep3): | |||
|
249 | for s in subrepos: | |||
|
250 | del results[s] | |||
|
251 | del results['.hg'] | |||
|
252 | return results | |||
|
253 | ||||
|
254 | # step 2: query Watchman | |||
|
255 | try: | |||
|
256 | # Use the user-configured timeout for the query. | |||
|
257 | # Add a little slack over the top of the user query to allow for | |||
|
258 | # overheads while transferring the data | |||
|
259 | self._watchmanclient.settimeout(state.timeout + 0.1) | |||
|
260 | result = self._watchmanclient.command('query', { | |||
|
261 | 'fields': ['mode', 'mtime', 'size', 'exists', 'name'], | |||
|
262 | 'since': clock, | |||
|
263 | 'expression': [ | |||
|
264 | 'not', [ | |||
|
265 | 'anyof', ['dirname', '.hg'], | |||
|
266 | ['name', '.hg', 'wholename'] | |||
|
267 | ] | |||
|
268 | ], | |||
|
269 | 'sync_timeout': int(state.timeout * 1000), | |||
|
270 | 'empty_on_fresh_instance': state.walk_on_invalidate, | |||
|
271 | }) | |||
|
272 | except Exception as ex: | |||
|
273 | _handleunavailable(self._ui, state, ex) | |||
|
274 | self._watchmanclient.clearconnection() | |||
|
275 | return bail() | |||
|
276 | else: | |||
|
277 | # We need to propagate the last observed clock up so that we | |||
|
278 | # can use it for our next query | |||
|
279 | state.setlastclock(result['clock']) | |||
|
280 | if result['is_fresh_instance']: | |||
|
281 | if state.walk_on_invalidate: | |||
|
282 | state.invalidate() | |||
|
283 | return bail() | |||
|
284 | fresh_instance = True | |||
|
285 | # Ignore any prior noteable files from the state info | |||
|
286 | notefiles = [] | |||
|
287 | ||||
|
288 | # for file paths which require normalization and we encounter a case | |||
|
289 | # collision, we store our own foldmap | |||
|
290 | if normalize: | |||
|
291 | foldmap = dict((normcase(k), k) for k in results) | |||
|
292 | ||||
|
293 | switch_slashes = os.sep == '\\' | |||
|
294 | # The order of the results is, strictly speaking, undefined. | |||
|
295 | # For case changes on a case insensitive filesystem we may receive | |||
|
296 | # two entries, one with exists=True and another with exists=False. | |||
|
297 | # The exists=True entries in the same response should be interpreted | |||
|
298 | # as being happens-after the exists=False entries due to the way that | |||
|
299 | # Watchman tracks files. We use this property to reconcile deletes | |||
|
300 | # for name case changes. | |||
|
301 | for entry in result['files']: | |||
|
302 | fname = entry['name'] | |||
|
303 | if switch_slashes: | |||
|
304 | fname = fname.replace('\\', '/') | |||
|
305 | if normalize: | |||
|
306 | normed = normcase(fname) | |||
|
307 | fname = normalize(fname, True, True) | |||
|
308 | foldmap[normed] = fname | |||
|
309 | fmode = entry['mode'] | |||
|
310 | fexists = entry['exists'] | |||
|
311 | kind = getkind(fmode) | |||
|
312 | ||||
|
313 | if not fexists: | |||
|
314 | # if marked as deleted and we don't already have a change | |||
|
315 | # record, mark it as deleted. If we already have an entry | |||
|
316 | # for fname then it was either part of walkexplicit or was | |||
|
317 | # an earlier result that was a case change | |||
|
318 | if fname not in results and fname in dmap and ( | |||
|
319 | matchalways or matchfn(fname)): | |||
|
320 | results[fname] = None | |||
|
321 | elif kind == dirkind: | |||
|
322 | if fname in dmap and (matchalways or matchfn(fname)): | |||
|
323 | results[fname] = None | |||
|
324 | elif kind == regkind or kind == lnkkind: | |||
|
325 | if fname in dmap: | |||
|
326 | if matchalways or matchfn(fname): | |||
|
327 | results[fname] = entry | |||
|
328 | elif (matchalways or matchfn(fname)) and not ignore(fname): | |||
|
329 | results[fname] = entry | |||
|
330 | elif fname in dmap and (matchalways or matchfn(fname)): | |||
|
331 | results[fname] = None | |||
|
332 | ||||
|
333 | # step 3: query notable files we don't already know about | |||
|
334 | # XXX try not to iterate over the entire dmap | |||
|
335 | if normalize: | |||
|
336 | # any notable files that have changed case will already be handled | |||
|
337 | # above, so just check membership in the foldmap | |||
|
338 | notefiles = set((normalize(f, True, True) for f in notefiles | |||
|
339 | if normcase(f) not in foldmap)) | |||
|
340 | visit = set((f for f in notefiles if (f not in results and matchfn(f) | |||
|
341 | and (f in dmap or not ignore(f))))) | |||
|
342 | ||||
|
343 | if nonnormalset is not None and not fresh_instance: | |||
|
344 | if matchalways: | |||
|
345 | visit.update(f for f in nonnormalset if f not in results) | |||
|
346 | visit.update(f for f in copymap if f not in results) | |||
|
347 | else: | |||
|
348 | visit.update(f for f in nonnormalset | |||
|
349 | if f not in results and matchfn(f)) | |||
|
350 | visit.update(f for f in copymap | |||
|
351 | if f not in results and matchfn(f)) | |||
|
352 | else: | |||
|
353 | if matchalways: | |||
|
354 | visit.update(f for f, st in dmap.iteritems() | |||
|
355 | if (f not in results and | |||
|
356 | (st[2] < 0 or st[0] != 'n' or fresh_instance))) | |||
|
357 | visit.update(f for f in copymap if f not in results) | |||
|
358 | else: | |||
|
359 | visit.update(f for f, st in dmap.iteritems() | |||
|
360 | if (f not in results and | |||
|
361 | (st[2] < 0 or st[0] != 'n' or fresh_instance) | |||
|
362 | and matchfn(f))) | |||
|
363 | visit.update(f for f in copymap | |||
|
364 | if f not in results and matchfn(f)) | |||
|
365 | ||||
|
366 | audit = pathutil.pathauditor(self._root).check | |||
|
367 | auditpass = [f for f in visit if audit(f)] | |||
|
368 | auditpass.sort() | |||
|
369 | auditfail = visit.difference(auditpass) | |||
|
370 | for f in auditfail: | |||
|
371 | results[f] = None | |||
|
372 | ||||
|
373 | nf = iter(auditpass).next | |||
|
374 | for st in util.statfiles([join(f) for f in auditpass]): | |||
|
375 | f = nf() | |||
|
376 | if st or f in dmap: | |||
|
377 | results[f] = st | |||
|
378 | ||||
|
379 | for s in subrepos: | |||
|
380 | del results[s] | |||
|
381 | del results['.hg'] | |||
|
382 | return results | |||
|
383 | ||||
|
384 | def overridestatus( | |||
|
385 | orig, self, node1='.', node2=None, match=None, ignored=False, | |||
|
386 | clean=False, unknown=False, listsubrepos=False): | |||
|
387 | listignored = ignored | |||
|
388 | listclean = clean | |||
|
389 | listunknown = unknown | |||
|
390 | ||||
|
391 | def _cmpsets(l1, l2): | |||
|
392 | try: | |||
|
393 | if 'FSMONITOR_LOG_FILE' in os.environ: | |||
|
394 | fn = os.environ['FSMONITOR_LOG_FILE'] | |||
|
395 | f = open(fn, 'wb') | |||
|
396 | else: | |||
|
397 | fn = 'fsmonitorfail.log' | |||
|
398 | f = self.opener(fn, 'wb') | |||
|
399 | except (IOError, OSError): | |||
|
400 | self.ui.warn(_('warning: unable to write to %s\n') % fn) | |||
|
401 | return | |||
|
402 | ||||
|
403 | try: | |||
|
404 | for i, (s1, s2) in enumerate(zip(l1, l2)): | |||
|
405 | if set(s1) != set(s2): | |||
|
406 | f.write('sets at position %d are unequal\n' % i) | |||
|
407 | f.write('watchman returned: %s\n' % s1) | |||
|
408 | f.write('stat returned: %s\n' % s2) | |||
|
409 | finally: | |||
|
410 | f.close() | |||
|
411 | ||||
|
412 | if isinstance(node1, context.changectx): | |||
|
413 | ctx1 = node1 | |||
|
414 | else: | |||
|
415 | ctx1 = self[node1] | |||
|
416 | if isinstance(node2, context.changectx): | |||
|
417 | ctx2 = node2 | |||
|
418 | else: | |||
|
419 | ctx2 = self[node2] | |||
|
420 | ||||
|
421 | working = ctx2.rev() is None | |||
|
422 | parentworking = working and ctx1 == self['.'] | |||
|
423 | match = match or matchmod.always(self.root, self.getcwd()) | |||
|
424 | ||||
|
425 | # Maybe we can use this opportunity to update Watchman's state. | |||
|
426 | # Mercurial uses workingcommitctx and/or memctx to represent the part of | |||
|
427 | # the workingctx that is to be committed. So don't update the state in | |||
|
428 | # that case. | |||
|
429 | # HG_PENDING is set in the environment when the dirstate is being updated | |||
|
430 | # in the middle of a transaction; we must not update our state in that | |||
|
431 | # case, or we risk forgetting about changes in the working copy. | |||
|
432 | updatestate = (parentworking and match.always() and | |||
|
433 | not isinstance(ctx2, (context.workingcommitctx, | |||
|
434 | context.memctx)) and | |||
|
435 | 'HG_PENDING' not in os.environ) | |||
|
436 | ||||
|
437 | try: | |||
|
438 | if self._fsmonitorstate.walk_on_invalidate: | |||
|
439 | # Use a short timeout to query the current clock. If that | |||
|
440 | # takes too long then we assume that the service will be slow | |||
|
441 | # to answer our query. | |||
|
442 | # walk_on_invalidate indicates that we prefer to walk the | |||
|
443 | # tree ourselves because we can ignore portions that Watchman | |||
|
444 | # cannot and we tend to be faster in the warmer buffer cache | |||
|
445 | # cases. | |||
|
446 | self._watchmanclient.settimeout(0.1) | |||
|
447 | else: | |||
|
448 | # Give Watchman more time to potentially complete its walk | |||
|
449 | # and return the initial clock. In this mode we assume that | |||
|
450 | # the filesystem will be slower than parsing a potentially | |||
|
451 | # very large Watchman result set. | |||
|
452 | self._watchmanclient.settimeout( | |||
|
453 | self._fsmonitorstate.timeout + 0.1) | |||
|
454 | startclock = self._watchmanclient.getcurrentclock() | |||
|
455 | except Exception as ex: | |||
|
456 | self._watchmanclient.clearconnection() | |||
|
457 | _handleunavailable(self.ui, self._fsmonitorstate, ex) | |||
|
458 | # boo, Watchman failed. bail | |||
|
459 | return orig(node1, node2, match, listignored, listclean, | |||
|
460 | listunknown, listsubrepos) | |||
|
461 | ||||
|
462 | if updatestate: | |||
|
463 | # We need info about unknown files. This may make things slower the | |||
|
464 | # first time, but whatever. | |||
|
465 | stateunknown = True | |||
|
466 | else: | |||
|
467 | stateunknown = listunknown | |||
|
468 | ||||
|
469 | r = orig(node1, node2, match, listignored, listclean, stateunknown, | |||
|
470 | listsubrepos) | |||
|
471 | modified, added, removed, deleted, unknown, ignored, clean = r | |||
|
472 | ||||
|
473 | if updatestate: | |||
|
474 | notefiles = modified + added + removed + deleted + unknown | |||
|
475 | self._fsmonitorstate.set( | |||
|
476 | self._fsmonitorstate.getlastclock() or startclock, | |||
|
477 | _hashignore(self.dirstate._ignore), | |||
|
478 | notefiles) | |||
|
479 | ||||
|
480 | if not listunknown: | |||
|
481 | unknown = [] | |||
|
482 | ||||
|
483 | # don't do paranoid checks if we're not going to query Watchman anyway | |||
|
484 | full = listclean or match.traversedir is not None | |||
|
485 | if self._fsmonitorstate.mode == 'paranoid' and not full: | |||
|
486 | # run status again and fall back to the old walk this time | |||
|
487 | self.dirstate._fsmonitordisable = True | |||
|
488 | ||||
|
489 | # shut the UI up | |||
|
490 | quiet = self.ui.quiet | |||
|
491 | self.ui.quiet = True | |||
|
492 | fout, ferr = self.ui.fout, self.ui.ferr | |||
|
493 | self.ui.fout = self.ui.ferr = open(os.devnull, 'wb') | |||
|
494 | ||||
|
495 | try: | |||
|
496 | rv2 = orig( | |||
|
497 | node1, node2, match, listignored, listclean, listunknown, | |||
|
498 | listsubrepos) | |||
|
499 | finally: | |||
|
500 | self.dirstate._fsmonitordisable = False | |||
|
501 | self.ui.quiet = quiet | |||
|
502 | self.ui.fout, self.ui.ferr = fout, ferr | |||
|
503 | ||||
|
504 | # clean isn't tested since it's set to True above | |||
|
505 | _cmpsets([modified, added, removed, deleted, unknown, ignored, clean], | |||
|
506 | rv2) | |||
|
507 | modified, added, removed, deleted, unknown, ignored, clean = rv2 | |||
|
508 | ||||
|
509 | return scmutil.status( | |||
|
510 | modified, added, removed, deleted, unknown, ignored, clean) | |||
|
511 | ||||
|
512 | def makedirstate(cls): | |||
|
513 | class fsmonitordirstate(cls): | |||
|
514 | def _fsmonitorinit(self, fsmonitorstate, watchmanclient): | |||
|
515 | # _fsmonitordisable is used in paranoid mode | |||
|
516 | self._fsmonitordisable = False | |||
|
517 | self._fsmonitorstate = fsmonitorstate | |||
|
518 | self._watchmanclient = watchmanclient | |||
|
519 | ||||
|
520 | def walk(self, *args, **kwargs): | |||
|
521 | orig = super(fsmonitordirstate, self).walk | |||
|
522 | if self._fsmonitordisable: | |||
|
523 | return orig(*args, **kwargs) | |||
|
524 | return overridewalk(orig, self, *args, **kwargs) | |||
|
525 | ||||
|
526 | def rebuild(self, *args, **kwargs): | |||
|
527 | self._fsmonitorstate.invalidate() | |||
|
528 | return super(fsmonitordirstate, self).rebuild(*args, **kwargs) | |||
|
529 | ||||
|
530 | def invalidate(self, *args, **kwargs): | |||
|
531 | self._fsmonitorstate.invalidate() | |||
|
532 | return super(fsmonitordirstate, self).invalidate(*args, **kwargs) | |||
|
533 | ||||
|
534 | return fsmonitordirstate | |||
|
535 | ||||
|
536 | def wrapdirstate(orig, self): | |||
|
537 | ds = orig(self) | |||
|
538 | # only override the dirstate when Watchman is available for the repo | |||
|
539 | if util.safehasattr(self, '_fsmonitorstate'): | |||
|
540 | ds.__class__ = makedirstate(ds.__class__) | |||
|
541 | ds._fsmonitorinit(self._fsmonitorstate, self._watchmanclient) | |||
|
542 | return ds | |||
|
543 | ||||
|
544 | def extsetup(ui): | |||
|
545 | wrapfilecache(localrepo.localrepository, 'dirstate', wrapdirstate) | |||
|
546 | if sys.platform == 'darwin': | |||
|
547 | # An assist for avoiding the dangling-symlink fsevents bug | |||
|
548 | extensions.wrapfunction(os, 'symlink', wrapsymlink) | |||
|
549 | ||||
|
550 | def wrapsymlink(orig, source, link_name): | |||
|
551 | ''' if we create a dangling symlink, also touch the parent dir | |||
|
552 | to encourage fsevents notifications to work more correctly ''' | |||
|
553 | try: | |||
|
554 | return orig(source, link_name) | |||
|
555 | finally: | |||
|
556 | try: | |||
|
557 | os.utime(os.path.dirname(link_name), None) | |||
|
558 | except OSError: | |||
|
559 | pass | |||
|
560 | ||||
|
561 | def reposetup(ui, repo): | |||
|
562 | # We don't work with largefiles or inotify | |||
|
563 | exts = extensions.enabled() | |||
|
564 | for ext in _blacklist: | |||
|
565 | if ext in exts: | |||
|
566 | ui.warn(_('The fsmonitor extension is incompatible with the %s ' | |||
|
567 | 'extension and has been disabled.\n') % ext) | |||
|
568 | return | |||
|
569 | ||||
|
570 | if util.safehasattr(repo, 'dirstate'): | |||
|
571 | # We don't work with subrepos either. Note that we can get passed in | |||
|
572 | # e.g. a statichttprepo, which throws on trying to access the substate. | |||
|
573 | # XXX This sucks. | |||
|
574 | try: | |||
|
575 | # if repo[None].substate can cause a dirstate parse, which is too | |||
|
576 | # slow. Instead, look for a file called hgsubstate, | |||
|
577 | if repo.wvfs.exists('.hgsubstate') or repo.wvfs.exists('.hgsub'): | |||
|
578 | return | |||
|
579 | except AttributeError: | |||
|
580 | return | |||
|
581 | ||||
|
582 | fsmonitorstate = state.state(repo) | |||
|
583 | if fsmonitorstate.mode == 'off': | |||
|
584 | return | |||
|
585 | ||||
|
586 | try: | |||
|
587 | client = watchmanclient.client(repo) | |||
|
588 | except Exception as ex: | |||
|
589 | _handleunavailable(ui, fsmonitorstate, ex) | |||
|
590 | return | |||
|
591 | ||||
|
592 | repo._fsmonitorstate = fsmonitorstate | |||
|
593 | repo._watchmanclient = client | |||
|
594 | ||||
|
595 | # at this point since fsmonitorstate wasn't present, repo.dirstate is | |||
|
596 | # not a fsmonitordirstate | |||
|
597 | repo.dirstate.__class__ = makedirstate(repo.dirstate.__class__) | |||
|
598 | # nuke the dirstate so that _fsmonitorinit and subsequent configuration | |||
|
599 | # changes take effect on it | |||
|
600 | del repo._filecache['dirstate'] | |||
|
601 | delattr(repo.unfiltered(), 'dirstate') | |||
|
602 | ||||
|
603 | class fsmonitorrepo(repo.__class__): | |||
|
604 | def status(self, *args, **kwargs): | |||
|
605 | orig = super(fsmonitorrepo, self).status | |||
|
606 | return overridestatus(orig, self, *args, **kwargs) | |||
|
607 | ||||
|
608 | repo.__class__ = fsmonitorrepo | |||
|
609 | ||||
|
610 | def wrapfilecache(cls, propname, wrapper): | |||
|
611 | """Wraps a filecache property. These can't be wrapped using the normal | |||
|
612 | wrapfunction. This should eventually go into upstream Mercurial. | |||
|
613 | """ | |||
|
614 | assert callable(wrapper) | |||
|
615 | for currcls in cls.__mro__: | |||
|
616 | if propname in currcls.__dict__: | |||
|
617 | origfn = currcls.__dict__[propname].func | |||
|
618 | assert callable(origfn) | |||
|
619 | def wrap(*args, **kwargs): | |||
|
620 | return wrapper(origfn, *args, **kwargs) | |||
|
621 | currcls.__dict__[propname].func = wrap | |||
|
622 | break | |||
|
623 | ||||
|
624 | if currcls is object: | |||
|
625 | raise AttributeError( | |||
|
626 | _("type '%s' has no property '%s'") % (cls, propname)) |
@@ -0,0 +1,115 b'' | |||||
|
1 | # state.py - fsmonitor persistent state | |||
|
2 | # | |||
|
3 | # Copyright 2013-2016 Facebook, Inc. | |||
|
4 | # | |||
|
5 | # This software may be used and distributed according to the terms of the | |||
|
6 | # GNU General Public License version 2 or any later version. | |||
|
7 | ||||
|
8 | from __future__ import absolute_import | |||
|
9 | ||||
|
10 | import errno | |||
|
11 | import os | |||
|
12 | import socket | |||
|
13 | import struct | |||
|
14 | ||||
|
15 | from mercurial import pathutil | |||
|
16 | from mercurial.i18n import _ | |||
|
17 | ||||
|
18 | _version = 4 | |||
|
19 | _versionformat = ">I" | |||
|
20 | ||||
|
21 | class state(object): | |||
|
22 | def __init__(self, repo): | |||
|
23 | self._opener = repo.opener | |||
|
24 | self._ui = repo.ui | |||
|
25 | self._rootdir = pathutil.normasprefix(repo.root) | |||
|
26 | self._lastclock = None | |||
|
27 | ||||
|
28 | self.mode = self._ui.config('fsmonitor', 'mode', default='on') | |||
|
29 | self.walk_on_invalidate = self._ui.configbool( | |||
|
30 | 'fsmonitor', 'walk_on_invalidate', False) | |||
|
31 | self.timeout = float(self._ui.config( | |||
|
32 | 'fsmonitor', 'timeout', default='2')) | |||
|
33 | ||||
|
34 | def get(self): | |||
|
35 | try: | |||
|
36 | file = self._opener('fsmonitor.state', 'rb') | |||
|
37 | except IOError as inst: | |||
|
38 | if inst.errno != errno.ENOENT: | |||
|
39 | raise | |||
|
40 | return None, None, None | |||
|
41 | ||||
|
42 | versionbytes = file.read(4) | |||
|
43 | if len(versionbytes) < 4: | |||
|
44 | self._ui.log( | |||
|
45 | 'fsmonitor', 'fsmonitor: state file only has %d bytes, ' | |||
|
46 | 'nuking state\n' % len(versionbytes)) | |||
|
47 | self.invalidate() | |||
|
48 | return None, None, None | |||
|
49 | try: | |||
|
50 | diskversion = struct.unpack(_versionformat, versionbytes)[0] | |||
|
51 | if diskversion != _version: | |||
|
52 | # different version, nuke state and start over | |||
|
53 | self._ui.log( | |||
|
54 | 'fsmonitor', 'fsmonitor: version switch from %d to ' | |||
|
55 | '%d, nuking state\n' % (diskversion, _version)) | |||
|
56 | self.invalidate() | |||
|
57 | return None, None, None | |||
|
58 | ||||
|
59 | state = file.read().split('\0') | |||
|
60 | # state = hostname\0clock\0ignorehash\0 + list of files, each | |||
|
61 | # followed by a \0 | |||
|
62 | diskhostname = state[0] | |||
|
63 | hostname = socket.gethostname() | |||
|
64 | if diskhostname != hostname: | |||
|
65 | # file got moved to a different host | |||
|
66 | self._ui.log('fsmonitor', 'fsmonitor: stored hostname "%s" ' | |||
|
67 | 'different from current "%s", nuking state\n' % | |||
|
68 | (diskhostname, hostname)) | |||
|
69 | self.invalidate() | |||
|
70 | return None, None, None | |||
|
71 | ||||
|
72 | clock = state[1] | |||
|
73 | ignorehash = state[2] | |||
|
74 | # discard the value after the last \0 | |||
|
75 | notefiles = state[3:-1] | |||
|
76 | ||||
|
77 | finally: | |||
|
78 | file.close() | |||
|
79 | ||||
|
80 | return clock, ignorehash, notefiles | |||
|
81 | ||||
|
82 | def set(self, clock, ignorehash, notefiles): | |||
|
83 | if clock is None: | |||
|
84 | self.invalidate() | |||
|
85 | return | |||
|
86 | ||||
|
87 | try: | |||
|
88 | file = self._opener('fsmonitor.state', 'wb') | |||
|
89 | except (IOError, OSError): | |||
|
90 | self._ui.warn(_("warning: unable to write out fsmonitor state\n")) | |||
|
91 | return | |||
|
92 | ||||
|
93 | try: | |||
|
94 | file.write(struct.pack(_versionformat, _version)) | |||
|
95 | file.write(socket.gethostname() + '\0') | |||
|
96 | file.write(clock + '\0') | |||
|
97 | file.write(ignorehash + '\0') | |||
|
98 | if notefiles: | |||
|
99 | file.write('\0'.join(notefiles)) | |||
|
100 | file.write('\0') | |||
|
101 | finally: | |||
|
102 | file.close() | |||
|
103 | ||||
|
104 | def invalidate(self): | |||
|
105 | try: | |||
|
106 | os.unlink(os.path.join(self._rootdir, '.hg', 'fsmonitor.state')) | |||
|
107 | except OSError as inst: | |||
|
108 | if inst.errno != errno.ENOENT: | |||
|
109 | raise | |||
|
110 | ||||
|
111 | def setlastclock(self, clock): | |||
|
112 | self._lastclock = clock | |||
|
113 | ||||
|
114 | def getlastclock(self): | |||
|
115 | return self._lastclock |
@@ -0,0 +1,109 b'' | |||||
|
1 | # watchmanclient.py - Watchman client for the fsmonitor extension | |||
|
2 | # | |||
|
3 | # Copyright 2013-2016 Facebook, Inc. | |||
|
4 | # | |||
|
5 | # This software may be used and distributed according to the terms of the | |||
|
6 | # GNU General Public License version 2 or any later version. | |||
|
7 | ||||
|
8 | from __future__ import absolute_import | |||
|
9 | ||||
|
10 | import getpass | |||
|
11 | ||||
|
12 | from mercurial import util | |||
|
13 | ||||
|
14 | from . import pywatchman | |||
|
15 | ||||
|
16 | class Unavailable(Exception): | |||
|
17 | def __init__(self, msg, warn=True, invalidate=False): | |||
|
18 | self.msg = msg | |||
|
19 | self.warn = warn | |||
|
20 | if self.msg == 'timed out waiting for response': | |||
|
21 | self.warn = False | |||
|
22 | self.invalidate = invalidate | |||
|
23 | ||||
|
24 | def __str__(self): | |||
|
25 | if self.warn: | |||
|
26 | return 'warning: Watchman unavailable: %s' % self.msg | |||
|
27 | else: | |||
|
28 | return 'Watchman unavailable: %s' % self.msg | |||
|
29 | ||||
|
30 | class WatchmanNoRoot(Unavailable): | |||
|
31 | def __init__(self, root, msg): | |||
|
32 | self.root = root | |||
|
33 | super(WatchmanNoRoot, self).__init__(msg) | |||
|
34 | ||||
|
35 | class client(object): | |||
|
36 | def __init__(self, repo, timeout=1.0): | |||
|
37 | err = None | |||
|
38 | if not self._user: | |||
|
39 | err = "couldn't get user" | |||
|
40 | warn = True | |||
|
41 | if self._user in repo.ui.configlist('fsmonitor', 'blacklistusers'): | |||
|
42 | err = 'user %s in blacklist' % self._user | |||
|
43 | warn = False | |||
|
44 | ||||
|
45 | if err: | |||
|
46 | raise Unavailable(err, warn) | |||
|
47 | ||||
|
48 | self._timeout = timeout | |||
|
49 | self._watchmanclient = None | |||
|
50 | self._root = repo.root | |||
|
51 | self._ui = repo.ui | |||
|
52 | self._firsttime = True | |||
|
53 | ||||
|
54 | def settimeout(self, timeout): | |||
|
55 | self._timeout = timeout | |||
|
56 | if self._watchmanclient is not None: | |||
|
57 | self._watchmanclient.setTimeout(timeout) | |||
|
58 | ||||
|
59 | def getcurrentclock(self): | |||
|
60 | result = self.command('clock') | |||
|
61 | if not util.safehasattr(result, 'clock'): | |||
|
62 | raise Unavailable('clock result is missing clock value', | |||
|
63 | invalidate=True) | |||
|
64 | return result.clock | |||
|
65 | ||||
|
66 | def clearconnection(self): | |||
|
67 | self._watchmanclient = None | |||
|
68 | ||||
|
69 | def available(self): | |||
|
70 | return self._watchmanclient is not None or self._firsttime | |||
|
71 | ||||
|
72 | @util.propertycache | |||
|
73 | def _user(self): | |||
|
74 | try: | |||
|
75 | return getpass.getuser() | |||
|
76 | except KeyError: | |||
|
77 | # couldn't figure out our user | |||
|
78 | return None | |||
|
79 | ||||
|
80 | def _command(self, *args): | |||
|
81 | watchmanargs = (args[0], self._root) + args[1:] | |||
|
82 | try: | |||
|
83 | if self._watchmanclient is None: | |||
|
84 | self._firsttime = False | |||
|
85 | self._watchmanclient = pywatchman.client( | |||
|
86 | timeout=self._timeout, | |||
|
87 | useImmutableBser=True) | |||
|
88 | return self._watchmanclient.query(*watchmanargs) | |||
|
89 | except pywatchman.CommandError as ex: | |||
|
90 | if ex.msg.startswith('unable to resolve root'): | |||
|
91 | raise WatchmanNoRoot(self._root, ex.msg) | |||
|
92 | raise Unavailable(ex.msg) | |||
|
93 | except pywatchman.WatchmanError as ex: | |||
|
94 | raise Unavailable(str(ex)) | |||
|
95 | ||||
|
96 | def command(self, *args): | |||
|
97 | try: | |||
|
98 | try: | |||
|
99 | return self._command(*args) | |||
|
100 | except WatchmanNoRoot: | |||
|
101 | # this 'watch' command can also raise a WatchmanNoRoot if | |||
|
102 | # watchman refuses to accept this root | |||
|
103 | self._command('watch') | |||
|
104 | return self._command(*args) | |||
|
105 | except Unavailable: | |||
|
106 | # this is in an outer scope to catch Unavailable form any of the | |||
|
107 | # above _command calls | |||
|
108 | self._watchmanclient = None | |||
|
109 | raise |
@@ -0,0 +1,52 b'' | |||||
|
1 | # Blacklist for a full testsuite run with fsmonitor enabled. | |||
|
2 | # Use with | |||
|
3 | # run-tests --blacklist=blacklists/fsmonitor \ | |||
|
4 | # --extra-config="extensions.fsmonitor=" | |||
|
5 | # The following tests all fail because they either use extensions that conflict | |||
|
6 | # with fsmonitor, use subrepositories, or don't anticipate the extra file in | |||
|
7 | # the .hg directory that fsmonitor adds. | |||
|
8 | test-basic.t | |||
|
9 | test-blackbox.t | |||
|
10 | test-check-commit.t | |||
|
11 | test-commandserver.t | |||
|
12 | test-copy.t | |||
|
13 | test-debugextensions.t | |||
|
14 | test-eol-add.t | |||
|
15 | test-eol-clone.t | |||
|
16 | test-eol-hook.t | |||
|
17 | test-eol-patch.t | |||
|
18 | test-eol-tag.t | |||
|
19 | test-eol-update.t | |||
|
20 | test-eol.t | |||
|
21 | test-eolfilename.t | |||
|
22 | test-extension.t | |||
|
23 | test-fncache.t | |||
|
24 | test-hardlinks.t | |||
|
25 | test-help.t | |||
|
26 | test-inherit-mode.t | |||
|
27 | test-issue3084.t | |||
|
28 | test-largefiles-cache.t | |||
|
29 | test-largefiles-misc.t | |||
|
30 | test-largefiles-small-disk.t | |||
|
31 | test-largefiles-update.t | |||
|
32 | test-largefiles-wireproto.t | |||
|
33 | test-largefiles.t | |||
|
34 | test-lfconvert.t | |||
|
35 | test-merge-tools.t | |||
|
36 | test-nested-repo.t | |||
|
37 | test-permissions.t | |||
|
38 | test-push-warn.t | |||
|
39 | test-subrepo-deep-nested-change.t | |||
|
40 | test-subrepo-recursion.t | |||
|
41 | test-subrepo.t | |||
|
42 | test-tags.t | |||
|
43 | ||||
|
44 | # The following tests remain enabled; they fail *too*, but only because they | |||
|
45 | # occasionally use blacklisted extensions and don't anticipate the warning | |||
|
46 | # generated. | |||
|
47 | #test-log.t | |||
|
48 | #test-hook.t | |||
|
49 | #test-rename.t | |||
|
50 | #test-histedit-fold.t | |||
|
51 | #test-fileset-generated.t | |||
|
52 | #test-init.t |
General Comments 0
You need to be logged in to leave comments.
Login now