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