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