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