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