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