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