##// END OF EJS Templates
fsmonitor: change the distance calculation...
Eamonn Kent -
r34565:b79f5942 default
parent child Browse files
Show More
@@ -1,729 +1,731 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 The following configuration options exist:
22 22
23 23 ::
24 24
25 25 [fsmonitor]
26 26 mode = {off, on, paranoid}
27 27
28 28 When `mode = off`, fsmonitor will disable itself (similar to not loading the
29 29 extension at all). When `mode = on`, fsmonitor will be enabled (the default).
30 30 When `mode = paranoid`, fsmonitor will query both Watchman and the filesystem,
31 31 and ensure that the results are consistent.
32 32
33 33 ::
34 34
35 35 [fsmonitor]
36 36 timeout = (float)
37 37
38 38 A value, in seconds, that determines how long fsmonitor will wait for Watchman
39 39 to return results. Defaults to `2.0`.
40 40
41 41 ::
42 42
43 43 [fsmonitor]
44 44 blacklistusers = (list of userids)
45 45
46 46 A list of usernames for which fsmonitor will disable itself altogether.
47 47
48 48 ::
49 49
50 50 [fsmonitor]
51 51 walk_on_invalidate = (boolean)
52 52
53 53 Whether or not to walk the whole repo ourselves when our cached state has been
54 54 invalidated, for example when Watchman has been restarted or .hgignore rules
55 55 have been changed. Walking the repo in that case can result in competing for
56 56 I/O with Watchman. For large repos it is recommended to set this value to
57 57 false. You may wish to set this to true if you have a very fast filesystem
58 58 that can outpace the IPC overhead of getting the result data for the full repo
59 59 from Watchman. Defaults to false.
60 60
61 61 fsmonitor is incompatible with the largefiles and eol extensions, and
62 62 will disable itself if any of those are active.
63 63
64 64 '''
65 65
66 66 # Platforms Supported
67 67 # ===================
68 68 #
69 69 # **Linux:** *Stable*. Watchman and fsmonitor are both known to work reliably,
70 70 # even under severe loads.
71 71 #
72 72 # **Mac OS X:** *Stable*. The Mercurial test suite passes with fsmonitor
73 73 # turned on, on case-insensitive HFS+. There has been a reasonable amount of
74 74 # user testing under normal loads.
75 75 #
76 76 # **Solaris, BSD:** *Alpha*. watchman and fsmonitor are believed to work, but
77 77 # very little testing has been done.
78 78 #
79 79 # **Windows:** *Alpha*. Not in a release version of watchman or fsmonitor yet.
80 80 #
81 81 # Known Issues
82 82 # ============
83 83 #
84 84 # * fsmonitor will disable itself if any of the following extensions are
85 85 # enabled: largefiles, inotify, eol; or if the repository has subrepos.
86 86 # * fsmonitor will produce incorrect results if nested repos that are not
87 87 # subrepos exist. *Workaround*: add nested repo paths to your `.hgignore`.
88 88 #
89 89 # The issues related to nested repos and subrepos are probably not fundamental
90 90 # ones. Patches to fix them are welcome.
91 91
92 92 from __future__ import absolute_import
93 93
94 94 import codecs
95 95 import hashlib
96 96 import os
97 97 import stat
98 98 import sys
99 99
100 100 from mercurial.i18n import _
101 101 from mercurial import (
102 102 context,
103 103 encoding,
104 104 error,
105 105 extensions,
106 106 localrepo,
107 107 merge,
108 108 pathutil,
109 109 pycompat,
110 110 registrar,
111 111 scmutil,
112 112 util,
113 113 )
114 114 from mercurial import match as matchmod
115 115
116 116 from . import (
117 117 pywatchman,
118 118 state,
119 119 watchmanclient,
120 120 )
121 121
122 122 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
123 123 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
124 124 # be specifying the version(s) of Mercurial they are tested with, or
125 125 # leave the attribute unspecified.
126 126 testedwith = 'ships-with-hg-core'
127 127
128 128 configtable = {}
129 129 configitem = registrar.configitem(configtable)
130 130
131 131 configitem('fsmonitor', 'mode',
132 132 default='on',
133 133 )
134 134 configitem('fsmonitor', 'walk_on_invalidate',
135 135 default=False,
136 136 )
137 137 configitem('fsmonitor', 'timeout',
138 138 default='2',
139 139 )
140 140 configitem('fsmonitor', 'blacklistusers',
141 141 default=list,
142 142 )
143 143
144 144 # This extension is incompatible with the following blacklisted extensions
145 145 # and will disable itself when encountering one of these:
146 146 _blacklist = ['largefiles', 'eol']
147 147
148 148 def _handleunavailable(ui, state, ex):
149 149 """Exception handler for Watchman interaction exceptions"""
150 150 if isinstance(ex, watchmanclient.Unavailable):
151 151 if ex.warn:
152 152 ui.warn(str(ex) + '\n')
153 153 if ex.invalidate:
154 154 state.invalidate()
155 155 ui.log('fsmonitor', 'Watchman unavailable: %s\n', ex.msg)
156 156 else:
157 157 ui.log('fsmonitor', 'Watchman exception: %s\n', ex)
158 158
159 159 def _hashignore(ignore):
160 160 """Calculate hash for ignore patterns and filenames
161 161
162 162 If this information changes between Mercurial invocations, we can't
163 163 rely on Watchman information anymore and have to re-scan the working
164 164 copy.
165 165
166 166 """
167 167 sha1 = hashlib.sha1()
168 168 sha1.update(repr(ignore))
169 169 return sha1.hexdigest()
170 170
171 171 _watchmanencoding = pywatchman.encoding.get_local_encoding()
172 172 _fsencoding = sys.getfilesystemencoding() or sys.getdefaultencoding()
173 173 _fixencoding = codecs.lookup(_watchmanencoding) != codecs.lookup(_fsencoding)
174 174
175 175 def _watchmantofsencoding(path):
176 176 """Fix path to match watchman and local filesystem encoding
177 177
178 178 watchman's paths encoding can differ from filesystem encoding. For example,
179 179 on Windows, it's always utf-8.
180 180 """
181 181 try:
182 182 decoded = path.decode(_watchmanencoding)
183 183 except UnicodeDecodeError as e:
184 184 raise error.Abort(str(e), hint='watchman encoding error')
185 185
186 186 try:
187 187 encoded = decoded.encode(_fsencoding, 'strict')
188 188 except UnicodeEncodeError as e:
189 189 raise error.Abort(str(e))
190 190
191 191 return encoded
192 192
193 193 def overridewalk(orig, self, match, subrepos, unknown, ignored, full=True):
194 194 '''Replacement for dirstate.walk, hooking into Watchman.
195 195
196 196 Whenever full is False, ignored is False, and the Watchman client is
197 197 available, use Watchman combined with saved state to possibly return only a
198 198 subset of files.'''
199 199 def bail():
200 200 return orig(match, subrepos, unknown, ignored, full=True)
201 201
202 202 if full or ignored or not self._watchmanclient.available():
203 203 return bail()
204 204 state = self._fsmonitorstate
205 205 clock, ignorehash, notefiles = state.get()
206 206 if not clock:
207 207 if state.walk_on_invalidate:
208 208 return bail()
209 209 # Initial NULL clock value, see
210 210 # https://facebook.github.io/watchman/docs/clockspec.html
211 211 clock = 'c:0:0'
212 212 notefiles = []
213 213
214 214 def fwarn(f, msg):
215 215 self._ui.warn('%s: %s\n' % (self.pathto(f), msg))
216 216 return False
217 217
218 218 def badtype(mode):
219 219 kind = _('unknown')
220 220 if stat.S_ISCHR(mode):
221 221 kind = _('character device')
222 222 elif stat.S_ISBLK(mode):
223 223 kind = _('block device')
224 224 elif stat.S_ISFIFO(mode):
225 225 kind = _('fifo')
226 226 elif stat.S_ISSOCK(mode):
227 227 kind = _('socket')
228 228 elif stat.S_ISDIR(mode):
229 229 kind = _('directory')
230 230 return _('unsupported file type (type is %s)') % kind
231 231
232 232 ignore = self._ignore
233 233 dirignore = self._dirignore
234 234 if unknown:
235 235 if _hashignore(ignore) != ignorehash and clock != 'c:0:0':
236 236 # ignore list changed -- can't rely on Watchman state any more
237 237 if state.walk_on_invalidate:
238 238 return bail()
239 239 notefiles = []
240 240 clock = 'c:0:0'
241 241 else:
242 242 # always ignore
243 243 ignore = util.always
244 244 dirignore = util.always
245 245
246 246 matchfn = match.matchfn
247 247 matchalways = match.always()
248 248 dmap = self._map
249 249 nonnormalset = getattr(self, '_nonnormalset', None)
250 250
251 251 copymap = self._map.copymap
252 252 getkind = stat.S_IFMT
253 253 dirkind = stat.S_IFDIR
254 254 regkind = stat.S_IFREG
255 255 lnkkind = stat.S_IFLNK
256 256 join = self._join
257 257 normcase = util.normcase
258 258 fresh_instance = False
259 259
260 260 exact = skipstep3 = False
261 261 if match.isexact(): # match.exact
262 262 exact = True
263 263 dirignore = util.always # skip step 2
264 264 elif match.prefix(): # match.match, no patterns
265 265 skipstep3 = True
266 266
267 267 if not exact and self._checkcase:
268 268 # note that even though we could receive directory entries, we're only
269 269 # interested in checking if a file with the same name exists. So only
270 270 # normalize files if possible.
271 271 normalize = self._normalizefile
272 272 skipstep3 = False
273 273 else:
274 274 normalize = None
275 275
276 276 # step 1: find all explicit files
277 277 results, work, dirsnotfound = self._walkexplicit(match, subrepos)
278 278
279 279 skipstep3 = skipstep3 and not (work or dirsnotfound)
280 280 work = [d for d in work if not dirignore(d[0])]
281 281
282 282 if not work and (exact or skipstep3):
283 283 for s in subrepos:
284 284 del results[s]
285 285 del results['.hg']
286 286 return results
287 287
288 288 # step 2: query Watchman
289 289 try:
290 290 # Use the user-configured timeout for the query.
291 291 # Add a little slack over the top of the user query to allow for
292 292 # overheads while transferring the data
293 293 self._watchmanclient.settimeout(state.timeout + 0.1)
294 294 result = self._watchmanclient.command('query', {
295 295 'fields': ['mode', 'mtime', 'size', 'exists', 'name'],
296 296 'since': clock,
297 297 'expression': [
298 298 'not', [
299 299 'anyof', ['dirname', '.hg'],
300 300 ['name', '.hg', 'wholename']
301 301 ]
302 302 ],
303 303 'sync_timeout': int(state.timeout * 1000),
304 304 'empty_on_fresh_instance': state.walk_on_invalidate,
305 305 })
306 306 except Exception as ex:
307 307 _handleunavailable(self._ui, state, ex)
308 308 self._watchmanclient.clearconnection()
309 309 return bail()
310 310 else:
311 311 # We need to propagate the last observed clock up so that we
312 312 # can use it for our next query
313 313 state.setlastclock(result['clock'])
314 314 if result['is_fresh_instance']:
315 315 if state.walk_on_invalidate:
316 316 state.invalidate()
317 317 return bail()
318 318 fresh_instance = True
319 319 # Ignore any prior noteable files from the state info
320 320 notefiles = []
321 321
322 322 # for file paths which require normalization and we encounter a case
323 323 # collision, we store our own foldmap
324 324 if normalize:
325 325 foldmap = dict((normcase(k), k) for k in results)
326 326
327 327 switch_slashes = pycompat.ossep == '\\'
328 328 # The order of the results is, strictly speaking, undefined.
329 329 # For case changes on a case insensitive filesystem we may receive
330 330 # two entries, one with exists=True and another with exists=False.
331 331 # The exists=True entries in the same response should be interpreted
332 332 # as being happens-after the exists=False entries due to the way that
333 333 # Watchman tracks files. We use this property to reconcile deletes
334 334 # for name case changes.
335 335 for entry in result['files']:
336 336 fname = entry['name']
337 337 if _fixencoding:
338 338 fname = _watchmantofsencoding(fname)
339 339 if switch_slashes:
340 340 fname = fname.replace('\\', '/')
341 341 if normalize:
342 342 normed = normcase(fname)
343 343 fname = normalize(fname, True, True)
344 344 foldmap[normed] = fname
345 345 fmode = entry['mode']
346 346 fexists = entry['exists']
347 347 kind = getkind(fmode)
348 348
349 349 if not fexists:
350 350 # if marked as deleted and we don't already have a change
351 351 # record, mark it as deleted. If we already have an entry
352 352 # for fname then it was either part of walkexplicit or was
353 353 # an earlier result that was a case change
354 354 if fname not in results and fname in dmap and (
355 355 matchalways or matchfn(fname)):
356 356 results[fname] = None
357 357 elif kind == dirkind:
358 358 if fname in dmap and (matchalways or matchfn(fname)):
359 359 results[fname] = None
360 360 elif kind == regkind or kind == lnkkind:
361 361 if fname in dmap:
362 362 if matchalways or matchfn(fname):
363 363 results[fname] = entry
364 364 elif (matchalways or matchfn(fname)) and not ignore(fname):
365 365 results[fname] = entry
366 366 elif fname in dmap and (matchalways or matchfn(fname)):
367 367 results[fname] = None
368 368
369 369 # step 3: query notable files we don't already know about
370 370 # XXX try not to iterate over the entire dmap
371 371 if normalize:
372 372 # any notable files that have changed case will already be handled
373 373 # above, so just check membership in the foldmap
374 374 notefiles = set((normalize(f, True, True) for f in notefiles
375 375 if normcase(f) not in foldmap))
376 376 visit = set((f for f in notefiles if (f not in results and matchfn(f)
377 377 and (f in dmap or not ignore(f)))))
378 378
379 379 if nonnormalset is not None and not fresh_instance:
380 380 if matchalways:
381 381 visit.update(f for f in nonnormalset if f not in results)
382 382 visit.update(f for f in copymap if f not in results)
383 383 else:
384 384 visit.update(f for f in nonnormalset
385 385 if f not in results and matchfn(f))
386 386 visit.update(f for f in copymap
387 387 if f not in results and matchfn(f))
388 388 else:
389 389 if matchalways:
390 390 visit.update(f for f, st in dmap.iteritems()
391 391 if (f not in results and
392 392 (st[2] < 0 or st[0] != 'n' or fresh_instance)))
393 393 visit.update(f for f in copymap if f not in results)
394 394 else:
395 395 visit.update(f for f, st in dmap.iteritems()
396 396 if (f not in results and
397 397 (st[2] < 0 or st[0] != 'n' or fresh_instance)
398 398 and matchfn(f)))
399 399 visit.update(f for f in copymap
400 400 if f not in results and matchfn(f))
401 401
402 402 audit = pathutil.pathauditor(self._root, cached=True).check
403 403 auditpass = [f for f in visit if audit(f)]
404 404 auditpass.sort()
405 405 auditfail = visit.difference(auditpass)
406 406 for f in auditfail:
407 407 results[f] = None
408 408
409 409 nf = iter(auditpass).next
410 410 for st in util.statfiles([join(f) for f in auditpass]):
411 411 f = nf()
412 412 if st or f in dmap:
413 413 results[f] = st
414 414
415 415 for s in subrepos:
416 416 del results[s]
417 417 del results['.hg']
418 418 return results
419 419
420 420 def overridestatus(
421 421 orig, self, node1='.', node2=None, match=None, ignored=False,
422 422 clean=False, unknown=False, listsubrepos=False):
423 423 listignored = ignored
424 424 listclean = clean
425 425 listunknown = unknown
426 426
427 427 def _cmpsets(l1, l2):
428 428 try:
429 429 if 'FSMONITOR_LOG_FILE' in encoding.environ:
430 430 fn = encoding.environ['FSMONITOR_LOG_FILE']
431 431 f = open(fn, 'wb')
432 432 else:
433 433 fn = 'fsmonitorfail.log'
434 434 f = self.opener(fn, 'wb')
435 435 except (IOError, OSError):
436 436 self.ui.warn(_('warning: unable to write to %s\n') % fn)
437 437 return
438 438
439 439 try:
440 440 for i, (s1, s2) in enumerate(zip(l1, l2)):
441 441 if set(s1) != set(s2):
442 442 f.write('sets at position %d are unequal\n' % i)
443 443 f.write('watchman returned: %s\n' % s1)
444 444 f.write('stat returned: %s\n' % s2)
445 445 finally:
446 446 f.close()
447 447
448 448 if isinstance(node1, context.changectx):
449 449 ctx1 = node1
450 450 else:
451 451 ctx1 = self[node1]
452 452 if isinstance(node2, context.changectx):
453 453 ctx2 = node2
454 454 else:
455 455 ctx2 = self[node2]
456 456
457 457 working = ctx2.rev() is None
458 458 parentworking = working and ctx1 == self['.']
459 459 match = match or matchmod.always(self.root, self.getcwd())
460 460
461 461 # Maybe we can use this opportunity to update Watchman's state.
462 462 # Mercurial uses workingcommitctx and/or memctx to represent the part of
463 463 # the workingctx that is to be committed. So don't update the state in
464 464 # that case.
465 465 # HG_PENDING is set in the environment when the dirstate is being updated
466 466 # in the middle of a transaction; we must not update our state in that
467 467 # case, or we risk forgetting about changes in the working copy.
468 468 updatestate = (parentworking and match.always() and
469 469 not isinstance(ctx2, (context.workingcommitctx,
470 470 context.memctx)) and
471 471 'HG_PENDING' not in encoding.environ)
472 472
473 473 try:
474 474 if self._fsmonitorstate.walk_on_invalidate:
475 475 # Use a short timeout to query the current clock. If that
476 476 # takes too long then we assume that the service will be slow
477 477 # to answer our query.
478 478 # walk_on_invalidate indicates that we prefer to walk the
479 479 # tree ourselves because we can ignore portions that Watchman
480 480 # cannot and we tend to be faster in the warmer buffer cache
481 481 # cases.
482 482 self._watchmanclient.settimeout(0.1)
483 483 else:
484 484 # Give Watchman more time to potentially complete its walk
485 485 # and return the initial clock. In this mode we assume that
486 486 # the filesystem will be slower than parsing a potentially
487 487 # very large Watchman result set.
488 488 self._watchmanclient.settimeout(
489 489 self._fsmonitorstate.timeout + 0.1)
490 490 startclock = self._watchmanclient.getcurrentclock()
491 491 except Exception as ex:
492 492 self._watchmanclient.clearconnection()
493 493 _handleunavailable(self.ui, self._fsmonitorstate, ex)
494 494 # boo, Watchman failed. bail
495 495 return orig(node1, node2, match, listignored, listclean,
496 496 listunknown, listsubrepos)
497 497
498 498 if updatestate:
499 499 # We need info about unknown files. This may make things slower the
500 500 # first time, but whatever.
501 501 stateunknown = True
502 502 else:
503 503 stateunknown = listunknown
504 504
505 505 if updatestate:
506 506 ps = poststatus(startclock)
507 507 self.addpostdsstatus(ps)
508 508
509 509 r = orig(node1, node2, match, listignored, listclean, stateunknown,
510 510 listsubrepos)
511 511 modified, added, removed, deleted, unknown, ignored, clean = r
512 512
513 513 if not listunknown:
514 514 unknown = []
515 515
516 516 # don't do paranoid checks if we're not going to query Watchman anyway
517 517 full = listclean or match.traversedir is not None
518 518 if self._fsmonitorstate.mode == 'paranoid' and not full:
519 519 # run status again and fall back to the old walk this time
520 520 self.dirstate._fsmonitordisable = True
521 521
522 522 # shut the UI up
523 523 quiet = self.ui.quiet
524 524 self.ui.quiet = True
525 525 fout, ferr = self.ui.fout, self.ui.ferr
526 526 self.ui.fout = self.ui.ferr = open(os.devnull, 'wb')
527 527
528 528 try:
529 529 rv2 = orig(
530 530 node1, node2, match, listignored, listclean, listunknown,
531 531 listsubrepos)
532 532 finally:
533 533 self.dirstate._fsmonitordisable = False
534 534 self.ui.quiet = quiet
535 535 self.ui.fout, self.ui.ferr = fout, ferr
536 536
537 537 # clean isn't tested since it's set to True above
538 538 _cmpsets([modified, added, removed, deleted, unknown, ignored, clean],
539 539 rv2)
540 540 modified, added, removed, deleted, unknown, ignored, clean = rv2
541 541
542 542 return scmutil.status(
543 543 modified, added, removed, deleted, unknown, ignored, clean)
544 544
545 545 class poststatus(object):
546 546 def __init__(self, startclock):
547 547 self._startclock = startclock
548 548
549 549 def __call__(self, wctx, status):
550 550 clock = wctx.repo()._fsmonitorstate.getlastclock() or self._startclock
551 551 hashignore = _hashignore(wctx.repo().dirstate._ignore)
552 552 notefiles = (status.modified + status.added + status.removed +
553 553 status.deleted + status.unknown)
554 554 wctx.repo()._fsmonitorstate.set(clock, hashignore, notefiles)
555 555
556 556 def makedirstate(repo, dirstate):
557 557 class fsmonitordirstate(dirstate.__class__):
558 558 def _fsmonitorinit(self, fsmonitorstate, watchmanclient):
559 559 # _fsmonitordisable is used in paranoid mode
560 560 self._fsmonitordisable = False
561 561 self._fsmonitorstate = fsmonitorstate
562 562 self._watchmanclient = watchmanclient
563 563
564 564 def walk(self, *args, **kwargs):
565 565 orig = super(fsmonitordirstate, self).walk
566 566 if self._fsmonitordisable:
567 567 return orig(*args, **kwargs)
568 568 return overridewalk(orig, self, *args, **kwargs)
569 569
570 570 def rebuild(self, *args, **kwargs):
571 571 self._fsmonitorstate.invalidate()
572 572 return super(fsmonitordirstate, self).rebuild(*args, **kwargs)
573 573
574 574 def invalidate(self, *args, **kwargs):
575 575 self._fsmonitorstate.invalidate()
576 576 return super(fsmonitordirstate, self).invalidate(*args, **kwargs)
577 577
578 578 dirstate.__class__ = fsmonitordirstate
579 579 dirstate._fsmonitorinit(repo._fsmonitorstate, repo._watchmanclient)
580 580
581 581 def wrapdirstate(orig, self):
582 582 ds = orig(self)
583 583 # only override the dirstate when Watchman is available for the repo
584 584 if util.safehasattr(self, '_fsmonitorstate'):
585 585 makedirstate(self, ds)
586 586 return ds
587 587
588 588 def extsetup(ui):
589 589 extensions.wrapfilecache(
590 590 localrepo.localrepository, 'dirstate', wrapdirstate)
591 591 if pycompat.sysplatform == 'darwin':
592 592 # An assist for avoiding the dangling-symlink fsevents bug
593 593 extensions.wrapfunction(os, 'symlink', wrapsymlink)
594 594
595 595 extensions.wrapfunction(merge, 'update', wrapupdate)
596 596
597 597 def wrapsymlink(orig, source, link_name):
598 598 ''' if we create a dangling symlink, also touch the parent dir
599 599 to encourage fsevents notifications to work more correctly '''
600 600 try:
601 601 return orig(source, link_name)
602 602 finally:
603 603 try:
604 604 os.utime(os.path.dirname(link_name), None)
605 605 except OSError:
606 606 pass
607 607
608 608 class state_update(object):
609 609 ''' This context manager is responsible for dispatching the state-enter
610 610 and state-leave signals to the watchman service '''
611 611
612 612 def __init__(self, repo, node, distance, partial):
613 613 self.repo = repo
614 614 self.node = node
615 615 self.distance = distance
616 616 self.partial = partial
617 617 self._lock = None
618 618 self.need_leave = False
619 619
620 620 def __enter__(self):
621 621 # We explicitly need to take a lock here, before we proceed to update
622 622 # watchman about the update operation, so that we don't race with
623 623 # some other actor. merge.update is going to take the wlock almost
624 624 # immediately anyway, so this is effectively extending the lock
625 625 # around a couple of short sanity checks.
626 626 self._lock = self.repo.wlock()
627 627 self.need_leave = self._state('state-enter')
628 628 return self
629 629
630 630 def __exit__(self, type_, value, tb):
631 631 try:
632 632 if self.need_leave:
633 633 status = 'ok' if type_ is None else 'failed'
634 634 self._state('state-leave', status=status)
635 635 finally:
636 636 if self._lock:
637 637 self._lock.release()
638 638
639 639 def _state(self, cmd, status='ok'):
640 640 if not util.safehasattr(self.repo, '_watchmanclient'):
641 641 return False
642 642 try:
643 643 commithash = self.repo[self.node].hex()
644 644 self.repo._watchmanclient.command(cmd, {
645 645 'name': 'hg.update',
646 646 'metadata': {
647 647 # the target revision
648 648 'rev': commithash,
649 649 # approximate number of commits between current and target
650 650 'distance': self.distance,
651 651 # success/failure (only really meaningful for state-leave)
652 652 'status': status,
653 653 # whether the working copy parent is changing
654 654 'partial': self.partial,
655 655 }})
656 656 return True
657 657 except Exception as e:
658 658 # Swallow any errors; fire and forget
659 659 self.repo.ui.log(
660 660 'watchman', 'Exception %s while running %s\n', e, cmd)
661 661 return False
662 662
663 # Estimate the distance between two nodes
664 def calcdistance(repo, oldnode, newnode):
665 anc = repo.changelog.ancestor(oldnode, newnode)
666 ancrev = repo[anc].rev()
667 distance = (abs(repo[oldnode].rev() - ancrev)
668 + abs(repo[newnode].rev() - ancrev))
669 return distance
670
663 671 # Bracket working copy updates with calls to the watchman state-enter
664 672 # and state-leave commands. This allows clients to perform more intelligent
665 673 # settling during bulk file change scenarios
666 674 # https://facebook.github.io/watchman/docs/cmd/subscribe.html#advanced-settling
667 675 def wrapupdate(orig, repo, node, branchmerge, force, ancestor=None,
668 676 mergeancestor=False, labels=None, matcher=None, **kwargs):
669 677
670 678 distance = 0
671 679 partial = True
672 680 if matcher is None or matcher.always():
673 681 partial = False
674 wc = repo[None]
675 parents = wc.parents()
676 if len(parents) == 2:
677 anc = repo.changelog.ancestor(parents[0].node(), parents[1].node())
678 ancrev = repo[anc].rev()
679 distance = abs(repo[node].rev() - ancrev)
680 elif len(parents) == 1:
681 distance = abs(repo[node].rev() - parents[0].rev())
682 distance = calcdistance(repo.unfiltered(), repo['.'].node(),
683 repo[node].node())
682 684
683 685 with state_update(repo, node, distance, partial):
684 686 return orig(
685 687 repo, node, branchmerge, force, ancestor, mergeancestor,
686 688 labels, matcher, **kwargs)
687 689
688 690 def reposetup(ui, repo):
689 691 # We don't work with largefiles or inotify
690 692 exts = extensions.enabled()
691 693 for ext in _blacklist:
692 694 if ext in exts:
693 695 ui.warn(_('The fsmonitor extension is incompatible with the %s '
694 696 'extension and has been disabled.\n') % ext)
695 697 return
696 698
697 699 if repo.local():
698 700 # We don't work with subrepos either.
699 701 #
700 702 # if repo[None].substate can cause a dirstate parse, which is too
701 703 # slow. Instead, look for a file called hgsubstate,
702 704 if repo.wvfs.exists('.hgsubstate') or repo.wvfs.exists('.hgsub'):
703 705 return
704 706
705 707 fsmonitorstate = state.state(repo)
706 708 if fsmonitorstate.mode == 'off':
707 709 return
708 710
709 711 try:
710 712 client = watchmanclient.client(repo)
711 713 except Exception as ex:
712 714 _handleunavailable(ui, fsmonitorstate, ex)
713 715 return
714 716
715 717 repo._fsmonitorstate = fsmonitorstate
716 718 repo._watchmanclient = client
717 719
718 720 dirstate, cached = localrepo.isfilecached(repo, 'dirstate')
719 721 if cached:
720 722 # at this point since fsmonitorstate wasn't present,
721 723 # repo.dirstate is not a fsmonitordirstate
722 724 makedirstate(repo, dirstate)
723 725
724 726 class fsmonitorrepo(repo.__class__):
725 727 def status(self, *args, **kwargs):
726 728 orig = super(fsmonitorrepo, self).status
727 729 return overridestatus(orig, self, *args, **kwargs)
728 730
729 731 repo.__class__ = fsmonitorrepo
General Comments 0
You need to be logged in to leave comments. Login now