##// END OF EJS Templates
fsmonitor: fix silly "*kwargs" bug in merge.update() override...
Martin von Zweigbergk -
r32405:bdc4861f default
parent child Browse files
Show More
@@ -1,741 +1,741 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 match.isexact(): # match.exact
257 257 exact = True
258 258 dirignore = util.always # skip step 2
259 259 elif match.prefix(): # 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 self._lock = None
604 604 self.need_leave = False
605 605
606 606 def __enter__(self):
607 607 # We explicitly need to take a lock here, before we proceed to update
608 608 # watchman about the update operation, so that we don't race with
609 609 # some other actor. merge.update is going to take the wlock almost
610 610 # immediately anyway, so this is effectively extending the lock
611 611 # around a couple of short sanity checks.
612 612 self._lock = self.repo.wlock()
613 613 self.need_leave = self._state('state-enter')
614 614 return self
615 615
616 616 def __exit__(self, type_, value, tb):
617 617 try:
618 618 if self.need_leave:
619 619 status = 'ok' if type_ is None else 'failed'
620 620 self._state('state-leave', status=status)
621 621 finally:
622 622 if self._lock:
623 623 self._lock.release()
624 624
625 625 def _state(self, cmd, status='ok'):
626 626 if not util.safehasattr(self.repo, '_watchmanclient'):
627 627 return False
628 628 try:
629 629 commithash = self.repo[self.node].hex()
630 630 self.repo._watchmanclient.command(cmd, {
631 631 'name': 'hg.update',
632 632 'metadata': {
633 633 # the target revision
634 634 'rev': commithash,
635 635 # approximate number of commits between current and target
636 636 'distance': self.distance,
637 637 # success/failure (only really meaningful for state-leave)
638 638 'status': status,
639 639 # whether the working copy parent is changing
640 640 'partial': self.partial,
641 641 }})
642 642 return True
643 643 except Exception as e:
644 644 # Swallow any errors; fire and forget
645 645 self.repo.ui.log(
646 646 'watchman', 'Exception %s while running %s\n', e, cmd)
647 647 return False
648 648
649 649 # Bracket working copy updates with calls to the watchman state-enter
650 650 # and state-leave commands. This allows clients to perform more intelligent
651 651 # settling during bulk file change scenarios
652 652 # https://facebook.github.io/watchman/docs/cmd/subscribe.html#advanced-settling
653 653 def wrapupdate(orig, repo, node, branchmerge, force, ancestor=None,
654 654 mergeancestor=False, labels=None, matcher=None, **kwargs):
655 655
656 656 distance = 0
657 657 partial = True
658 658 if matcher is None or matcher.always():
659 659 partial = False
660 660 wc = repo[None]
661 661 parents = wc.parents()
662 662 if len(parents) == 2:
663 663 anc = repo.changelog.ancestor(parents[0].node(), parents[1].node())
664 664 ancrev = repo[anc].rev()
665 665 distance = abs(repo[node].rev() - ancrev)
666 666 elif len(parents) == 1:
667 667 distance = abs(repo[node].rev() - parents[0].rev())
668 668
669 669 with state_update(repo, node, distance, partial):
670 670 return orig(
671 671 repo, node, branchmerge, force, ancestor, mergeancestor,
672 labels, matcher, *kwargs)
672 labels, matcher, **kwargs)
673 673
674 674 def reposetup(ui, repo):
675 675 # We don't work with largefiles or inotify
676 676 exts = extensions.enabled()
677 677 for ext in _blacklist:
678 678 if ext in exts:
679 679 ui.warn(_('The fsmonitor extension is incompatible with the %s '
680 680 'extension and has been disabled.\n') % ext)
681 681 return
682 682
683 683 if util.safehasattr(repo, 'dirstate'):
684 684 # We don't work with subrepos either. Note that we can get passed in
685 685 # e.g. a statichttprepo, which throws on trying to access the substate.
686 686 # XXX This sucks.
687 687 try:
688 688 # if repo[None].substate can cause a dirstate parse, which is too
689 689 # slow. Instead, look for a file called hgsubstate,
690 690 if repo.wvfs.exists('.hgsubstate') or repo.wvfs.exists('.hgsub'):
691 691 return
692 692 except AttributeError:
693 693 return
694 694
695 695 fsmonitorstate = state.state(repo)
696 696 if fsmonitorstate.mode == 'off':
697 697 return
698 698
699 699 try:
700 700 client = watchmanclient.client(repo)
701 701 except Exception as ex:
702 702 _handleunavailable(ui, fsmonitorstate, ex)
703 703 return
704 704
705 705 repo._fsmonitorstate = fsmonitorstate
706 706 repo._watchmanclient = client
707 707
708 708 # at this point since fsmonitorstate wasn't present, repo.dirstate is
709 709 # not a fsmonitordirstate
710 710 dirstate = repo.dirstate
711 711 dirstate.__class__ = makedirstate(dirstate.__class__)
712 712 dirstate._fsmonitorinit(fsmonitorstate, client)
713 713 # invalidate property cache, but keep filecache which contains the
714 714 # wrapped dirstate object
715 715 del repo.unfiltered().__dict__['dirstate']
716 716 assert dirstate is repo._filecache['dirstate'].obj
717 717
718 718 class fsmonitorrepo(repo.__class__):
719 719 def status(self, *args, **kwargs):
720 720 orig = super(fsmonitorrepo, self).status
721 721 return overridestatus(orig, self, *args, **kwargs)
722 722
723 723 repo.__class__ = fsmonitorrepo
724 724
725 725 def wrapfilecache(cls, propname, wrapper):
726 726 """Wraps a filecache property. These can't be wrapped using the normal
727 727 wrapfunction. This should eventually go into upstream Mercurial.
728 728 """
729 729 assert callable(wrapper)
730 730 for currcls in cls.__mro__:
731 731 if propname in currcls.__dict__:
732 732 origfn = currcls.__dict__[propname].func
733 733 assert callable(origfn)
734 734 def wrap(*args, **kwargs):
735 735 return wrapper(origfn, *args, **kwargs)
736 736 currcls.__dict__[propname].func = wrap
737 737 break
738 738
739 739 if currcls is object:
740 740 raise AttributeError(
741 741 _("type '%s' has no property '%s'") % (cls, propname))
General Comments 0
You need to be logged in to leave comments. Login now