##// END OF EJS Templates
watchman: detect nested mercurial repositories and abort...
Boris Feld -
r41630:5125f0a9 default
parent child Browse files
Show More
@@ -1,814 +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 164 configitem('hgwatchman', '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 178 # experimental config: hgwatchman.verbose
179 179 if ex.warn and ui.configbool('hgwatchman', 'verbose'):
180 180 ui.warn(str(ex) + '\n')
181 181 if ex.invalidate:
182 182 state.invalidate()
183 183 # experimental config: hgwatchman.verbose
184 184 if ui.configbool('hgwatchman','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 if '/.hg/' in fname or fname.endswith('/.hg'):
371 return bail('nested-repo-detected')
372
370 373 if not fexists:
371 374 # if marked as deleted and we don't already have a change
372 375 # record, mark it as deleted. If we already have an entry
373 376 # for fname then it was either part of walkexplicit or was
374 377 # an earlier result that was a case change
375 378 if fname not in results and fname in dmap and (
376 379 matchalways or matchfn(fname)):
377 380 results[fname] = None
378 381 elif kind == dirkind:
379 382 if fname in dmap and (matchalways or matchfn(fname)):
380 383 results[fname] = None
381 384 elif kind == regkind or kind == lnkkind:
382 385 if fname in dmap:
383 386 if matchalways or matchfn(fname):
384 387 results[fname] = entry
385 388 elif (matchalways or matchfn(fname)) and not ignore(fname):
386 389 results[fname] = entry
387 390 elif fname in dmap and (matchalways or matchfn(fname)):
388 391 results[fname] = None
389 392
390 393 # step 3: query notable files we don't already know about
391 394 # XXX try not to iterate over the entire dmap
392 395 if normalize:
393 396 # any notable files that have changed case will already be handled
394 397 # above, so just check membership in the foldmap
395 398 notefiles = set((normalize(f, True, True) for f in notefiles
396 399 if normcase(f) not in foldmap))
397 400 visit = set((f for f in notefiles if (f not in results and matchfn(f)
398 401 and (f in dmap or not ignore(f)))))
399 402
400 403 if not fresh_instance:
401 404 if matchalways:
402 405 visit.update(f for f in nonnormalset if f not in results)
403 406 visit.update(f for f in copymap if f not in results)
404 407 else:
405 408 visit.update(f for f in nonnormalset
406 409 if f not in results and matchfn(f))
407 410 visit.update(f for f in copymap
408 411 if f not in results and matchfn(f))
409 412 else:
410 413 if matchalways:
411 414 visit.update(f for f, st in dmap.iteritems() 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, st in dmap.iteritems()
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
419 422 audit = pathutil.pathauditor(self._root, cached=True).check
420 423 auditpass = [f for f in visit if audit(f)]
421 424 auditpass.sort()
422 425 auditfail = visit.difference(auditpass)
423 426 for f in auditfail:
424 427 results[f] = None
425 428
426 429 nf = iter(auditpass).next
427 430 for st in util.statfiles([join(f) for f in auditpass]):
428 431 f = nf()
429 432 if st or f in dmap:
430 433 results[f] = st
431 434
432 435 for s in subrepos:
433 436 del results[s]
434 437 del results['.hg']
435 438 return results
436 439
437 440 def overridestatus(
438 441 orig, self, node1='.', node2=None, match=None, ignored=False,
439 442 clean=False, unknown=False, listsubrepos=False):
440 443 listignored = ignored
441 444 listclean = clean
442 445 listunknown = unknown
443 446
444 447 def _cmpsets(l1, l2):
445 448 try:
446 449 if 'FSMONITOR_LOG_FILE' in encoding.environ:
447 450 fn = encoding.environ['FSMONITOR_LOG_FILE']
448 451 f = open(fn, 'wb')
449 452 else:
450 453 fn = 'fsmonitorfail.log'
451 454 f = self.vfs.open(fn, 'wb')
452 455 except (IOError, OSError):
453 456 self.ui.warn(_('warning: unable to write to %s\n') % fn)
454 457 return
455 458
456 459 try:
457 460 for i, (s1, s2) in enumerate(zip(l1, l2)):
458 461 if set(s1) != set(s2):
459 462 f.write('sets at position %d are unequal\n' % i)
460 463 f.write('watchman returned: %s\n' % s1)
461 464 f.write('stat returned: %s\n' % s2)
462 465 finally:
463 466 f.close()
464 467
465 468 if isinstance(node1, context.changectx):
466 469 ctx1 = node1
467 470 else:
468 471 ctx1 = self[node1]
469 472 if isinstance(node2, context.changectx):
470 473 ctx2 = node2
471 474 else:
472 475 ctx2 = self[node2]
473 476
474 477 working = ctx2.rev() is None
475 478 parentworking = working and ctx1 == self['.']
476 479 match = match or matchmod.always(self.root, self.getcwd())
477 480
478 481 # Maybe we can use this opportunity to update Watchman's state.
479 482 # Mercurial uses workingcommitctx and/or memctx to represent the part of
480 483 # the workingctx that is to be committed. So don't update the state in
481 484 # that case.
482 485 # HG_PENDING is set in the environment when the dirstate is being updated
483 486 # in the middle of a transaction; we must not update our state in that
484 487 # case, or we risk forgetting about changes in the working copy.
485 488 updatestate = (parentworking and match.always() and
486 489 not isinstance(ctx2, (context.workingcommitctx,
487 490 context.memctx)) and
488 491 'HG_PENDING' not in encoding.environ)
489 492
490 493 try:
491 494 if self._fsmonitorstate.walk_on_invalidate:
492 495 # Use a short timeout to query the current clock. If that
493 496 # takes too long then we assume that the service will be slow
494 497 # to answer our query.
495 498 # walk_on_invalidate indicates that we prefer to walk the
496 499 # tree ourselves because we can ignore portions that Watchman
497 500 # cannot and we tend to be faster in the warmer buffer cache
498 501 # cases.
499 502 self._watchmanclient.settimeout(0.1)
500 503 else:
501 504 # Give Watchman more time to potentially complete its walk
502 505 # and return the initial clock. In this mode we assume that
503 506 # the filesystem will be slower than parsing a potentially
504 507 # very large Watchman result set.
505 508 self._watchmanclient.settimeout(
506 509 self._fsmonitorstate.timeout + 0.1)
507 510 startclock = self._watchmanclient.getcurrentclock()
508 511 except Exception as ex:
509 512 self._watchmanclient.clearconnection()
510 513 _handleunavailable(self.ui, self._fsmonitorstate, ex)
511 514 # boo, Watchman failed. bail
512 515 return orig(node1, node2, match, listignored, listclean,
513 516 listunknown, listsubrepos)
514 517
515 518 if updatestate:
516 519 # We need info about unknown files. This may make things slower the
517 520 # first time, but whatever.
518 521 stateunknown = True
519 522 else:
520 523 stateunknown = listunknown
521 524
522 525 if updatestate:
523 526 ps = poststatus(startclock)
524 527 self.addpostdsstatus(ps)
525 528
526 529 r = orig(node1, node2, match, listignored, listclean, stateunknown,
527 530 listsubrepos)
528 531 modified, added, removed, deleted, unknown, ignored, clean = r
529 532
530 533 if not listunknown:
531 534 unknown = []
532 535
533 536 # don't do paranoid checks if we're not going to query Watchman anyway
534 537 full = listclean or match.traversedir is not None
535 538 if self._fsmonitorstate.mode == 'paranoid' and not full:
536 539 # run status again and fall back to the old walk this time
537 540 self.dirstate._fsmonitordisable = True
538 541
539 542 # shut the UI up
540 543 quiet = self.ui.quiet
541 544 self.ui.quiet = True
542 545 fout, ferr = self.ui.fout, self.ui.ferr
543 546 self.ui.fout = self.ui.ferr = open(os.devnull, 'wb')
544 547
545 548 try:
546 549 rv2 = orig(
547 550 node1, node2, match, listignored, listclean, listunknown,
548 551 listsubrepos)
549 552 finally:
550 553 self.dirstate._fsmonitordisable = False
551 554 self.ui.quiet = quiet
552 555 self.ui.fout, self.ui.ferr = fout, ferr
553 556
554 557 # clean isn't tested since it's set to True above
555 558 with self.wlock():
556 559 _cmpsets(
557 560 [modified, added, removed, deleted, unknown, ignored, clean],
558 561 rv2)
559 562 modified, added, removed, deleted, unknown, ignored, clean = rv2
560 563
561 564 return scmutil.status(
562 565 modified, added, removed, deleted, unknown, ignored, clean)
563 566
564 567 class poststatus(object):
565 568 def __init__(self, startclock):
566 569 self._startclock = startclock
567 570
568 571 def __call__(self, wctx, status):
569 572 clock = wctx.repo()._fsmonitorstate.getlastclock() or self._startclock
570 573 hashignore = _hashignore(wctx.repo().dirstate._ignore)
571 574 notefiles = (status.modified + status.added + status.removed +
572 575 status.deleted + status.unknown)
573 576 wctx.repo()._fsmonitorstate.set(clock, hashignore, notefiles)
574 577
575 578 def makedirstate(repo, dirstate):
576 579 class fsmonitordirstate(dirstate.__class__):
577 580 def _fsmonitorinit(self, repo):
578 581 # _fsmonitordisable is used in paranoid mode
579 582 self._fsmonitordisable = False
580 583 self._fsmonitorstate = repo._fsmonitorstate
581 584 self._watchmanclient = repo._watchmanclient
582 585 self._repo = weakref.proxy(repo)
583 586
584 587 def walk(self, *args, **kwargs):
585 588 orig = super(fsmonitordirstate, self).walk
586 589 if self._fsmonitordisable:
587 590 return orig(*args, **kwargs)
588 591 return overridewalk(orig, self, *args, **kwargs)
589 592
590 593 def rebuild(self, *args, **kwargs):
591 594 self._fsmonitorstate.invalidate()
592 595 return super(fsmonitordirstate, self).rebuild(*args, **kwargs)
593 596
594 597 def invalidate(self, *args, **kwargs):
595 598 self._fsmonitorstate.invalidate()
596 599 return super(fsmonitordirstate, self).invalidate(*args, **kwargs)
597 600
598 601 dirstate.__class__ = fsmonitordirstate
599 602 dirstate._fsmonitorinit(repo)
600 603
601 604 def wrapdirstate(orig, self):
602 605 ds = orig(self)
603 606 # only override the dirstate when Watchman is available for the repo
604 607 if util.safehasattr(self, '_fsmonitorstate'):
605 608 makedirstate(self, ds)
606 609 return ds
607 610
608 611 def extsetup(ui):
609 612 extensions.wrapfilecache(
610 613 localrepo.localrepository, 'dirstate', wrapdirstate)
611 614 if pycompat.isdarwin:
612 615 # An assist for avoiding the dangling-symlink fsevents bug
613 616 extensions.wrapfunction(os, 'symlink', wrapsymlink)
614 617
615 618 extensions.wrapfunction(merge, 'update', wrapupdate)
616 619
617 620 def wrapsymlink(orig, source, link_name):
618 621 ''' if we create a dangling symlink, also touch the parent dir
619 622 to encourage fsevents notifications to work more correctly '''
620 623 try:
621 624 return orig(source, link_name)
622 625 finally:
623 626 try:
624 627 os.utime(os.path.dirname(link_name), None)
625 628 except OSError:
626 629 pass
627 630
628 631 class state_update(object):
629 632 ''' This context manager is responsible for dispatching the state-enter
630 633 and state-leave signals to the watchman service. The enter and leave
631 634 methods can be invoked manually (for scenarios where context manager
632 635 semantics are not possible). If parameters oldnode and newnode are None,
633 636 they will be populated based on current working copy in enter and
634 637 leave, respectively. Similarly, if the distance is none, it will be
635 638 calculated based on the oldnode and newnode in the leave method.'''
636 639
637 640 def __init__(self, repo, name, oldnode=None, newnode=None, distance=None,
638 641 partial=False):
639 642 self.repo = repo.unfiltered()
640 643 self.name = name
641 644 self.oldnode = oldnode
642 645 self.newnode = newnode
643 646 self.distance = distance
644 647 self.partial = partial
645 648 self._lock = None
646 649 self.need_leave = False
647 650
648 651 def __enter__(self):
649 652 self.enter()
650 653
651 654 def enter(self):
652 655 # Make sure we have a wlock prior to sending notifications to watchman.
653 656 # We don't want to race with other actors. In the update case,
654 657 # merge.update is going to take the wlock almost immediately. We are
655 658 # effectively extending the lock around several short sanity checks.
656 659 if self.oldnode is None:
657 660 self.oldnode = self.repo['.'].node()
658 661
659 662 if self.repo.currentwlock() is None:
660 663 if util.safehasattr(self.repo, 'wlocknostateupdate'):
661 664 self._lock = self.repo.wlocknostateupdate()
662 665 else:
663 666 self._lock = self.repo.wlock()
664 667 self.need_leave = self._state(
665 668 'state-enter',
666 669 hex(self.oldnode))
667 670 return self
668 671
669 672 def __exit__(self, type_, value, tb):
670 673 abort = True if type_ else False
671 674 self.exit(abort=abort)
672 675
673 676 def exit(self, abort=False):
674 677 try:
675 678 if self.need_leave:
676 679 status = 'failed' if abort else 'ok'
677 680 if self.newnode is None:
678 681 self.newnode = self.repo['.'].node()
679 682 if self.distance is None:
680 683 self.distance = calcdistance(
681 684 self.repo, self.oldnode, self.newnode)
682 685 self._state(
683 686 'state-leave',
684 687 hex(self.newnode),
685 688 status=status)
686 689 finally:
687 690 self.need_leave = False
688 691 if self._lock:
689 692 self._lock.release()
690 693
691 694 def _state(self, cmd, commithash, status='ok'):
692 695 if not util.safehasattr(self.repo, '_watchmanclient'):
693 696 return False
694 697 try:
695 698 self.repo._watchmanclient.command(cmd, {
696 699 'name': self.name,
697 700 'metadata': {
698 701 # the target revision
699 702 'rev': commithash,
700 703 # approximate number of commits between current and target
701 704 'distance': self.distance if self.distance else 0,
702 705 # success/failure (only really meaningful for state-leave)
703 706 'status': status,
704 707 # whether the working copy parent is changing
705 708 'partial': self.partial,
706 709 }})
707 710 return True
708 711 except Exception as e:
709 712 # Swallow any errors; fire and forget
710 713 self.repo.ui.log(
711 714 'watchman', 'Exception %s while running %s\n', e, cmd)
712 715 return False
713 716
714 717 # Estimate the distance between two nodes
715 718 def calcdistance(repo, oldnode, newnode):
716 719 anc = repo.changelog.ancestor(oldnode, newnode)
717 720 ancrev = repo[anc].rev()
718 721 distance = (abs(repo[oldnode].rev() - ancrev)
719 722 + abs(repo[newnode].rev() - ancrev))
720 723 return distance
721 724
722 725 # Bracket working copy updates with calls to the watchman state-enter
723 726 # and state-leave commands. This allows clients to perform more intelligent
724 727 # settling during bulk file change scenarios
725 728 # https://facebook.github.io/watchman/docs/cmd/subscribe.html#advanced-settling
726 729 def wrapupdate(orig, repo, node, branchmerge, force, ancestor=None,
727 730 mergeancestor=False, labels=None, matcher=None, **kwargs):
728 731
729 732 distance = 0
730 733 partial = True
731 734 oldnode = repo['.'].node()
732 735 newnode = repo[node].node()
733 736 if matcher is None or matcher.always():
734 737 partial = False
735 738 distance = calcdistance(repo.unfiltered(), oldnode, newnode)
736 739
737 740 with state_update(repo, name="hg.update", oldnode=oldnode, newnode=newnode,
738 741 distance=distance, partial=partial):
739 742 return orig(
740 743 repo, node, branchmerge, force, ancestor, mergeancestor,
741 744 labels, matcher, **kwargs)
742 745
746 def repo_has_depth_one_nested_repo(repo):
747 for f in repo.wvfs.listdir():
748 if os.path.isdir(os.path.join(repo.root, f, '.hg')):
749 msg = 'fsmonitor: sub-repository %r detected, fsmonitor disabled\n'
750 repo.ui.debug(msg % f)
751 return True
752 return False
753
743 754 def reposetup(ui, repo):
744 755 # We don't work with largefiles or inotify
745 756 exts = extensions.enabled()
746 757 for ext in _blacklist:
747 758 if ext in exts:
748 759 ui.warn(_('The fsmonitor extension is incompatible with the %s '
749 760 'extension and has been disabled.\n') % ext)
750 761 return
751 762
752 763 if repo.local():
753 764 # We don't work with subrepos either.
754 765 #
755 766 # if repo[None].substate can cause a dirstate parse, which is too
756 767 # slow. Instead, look for a file called hgsubstate,
757 768 if repo.wvfs.exists('.hgsubstate') or repo.wvfs.exists('.hgsub'):
758 769 return
759 770
771 if repo_has_depth_one_nested_repo(repo):
772 return
773
760 774 fsmonitorstate = state.state(repo)
761 775 if fsmonitorstate.mode == 'off':
762 776 return
763 777
764 778 try:
765 779 client = watchmanclient.client(repo)
766 780 except Exception as ex:
767 781 _handleunavailable(ui, fsmonitorstate, ex)
768 782 return
769 783
770 784 repo._fsmonitorstate = fsmonitorstate
771 785 repo._watchmanclient = client
772 786
773 787 dirstate, cached = localrepo.isfilecached(repo, 'dirstate')
774 788 if cached:
775 789 # at this point since fsmonitorstate wasn't present,
776 790 # repo.dirstate is not a fsmonitordirstate
777 791 makedirstate(repo, dirstate)
778 792
779 793 class fsmonitorrepo(repo.__class__):
780 794 def status(self, *args, **kwargs):
781 795 orig = super(fsmonitorrepo, self).status
782 796 return overridestatus(orig, self, *args, **kwargs)
783 797
784 798 def wlocknostateupdate(self, *args, **kwargs):
785 799 return super(fsmonitorrepo, self).wlock(*args, **kwargs)
786 800
787 801 def wlock(self, *args, **kwargs):
788 802 l = super(fsmonitorrepo, self).wlock(*args, **kwargs)
789 803 if not ui.configbool(
790 804 "experimental", "fsmonitor.transaction_notify"):
791 805 return l
792 806 if l.held != 1:
793 807 return l
794 808 origrelease = l.releasefn
795 809
796 810 def staterelease():
797 811 if origrelease:
798 812 origrelease()
799 813 if l.stateupdate:
800 814 l.stateupdate.exit()
801 815 l.stateupdate = None
802 816
803 817 try:
804 818 l.stateupdate = None
805 819 l.stateupdate = state_update(self, name="hg.transaction")
806 820 l.stateupdate.enter()
807 821 l.releasefn = staterelease
808 822 except Exception as e:
809 823 # Swallow any errors; fire and forget
810 824 self.ui.log(
811 825 'watchman', 'Exception in state update %s\n', e)
812 826 return l
813 827
814 828 repo.__class__ = fsmonitorrepo
General Comments 0
You need to be logged in to leave comments. Login now