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