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