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