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