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