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