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