##// END OF EJS Templates
fsmonitor: make _hashignore compatible with Python 3...
Gregory Szorc -
r43711:bdebc7b5 stable
parent child Browse files
Show More
@@ -1,981 +1,981 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 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 sha1.update(repr(ignore))
240 return sha1.hexdigest()
239 sha1.update(pycompat.byterepr(ignore))
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 410 if _fixencoding:
411 411 fname = _watchmantofsencoding(fname)
412 412 if switch_slashes:
413 413 fname = fname.replace(b'\\', b'/')
414 414 if normalize:
415 415 normed = normcase(fname)
416 416 fname = normalize(fname, True, True)
417 417 foldmap[normed] = fname
418 418 fmode = entry[b'mode']
419 419 fexists = entry[b'exists']
420 420 kind = getkind(fmode)
421 421
422 422 if b'/.hg/' in fname or fname.endswith(b'/.hg'):
423 423 return bail(b'nested-repo-detected')
424 424
425 425 if not fexists:
426 426 # if marked as deleted and we don't already have a change
427 427 # record, mark it as deleted. If we already have an entry
428 428 # for fname then it was either part of walkexplicit or was
429 429 # an earlier result that was a case change
430 430 if (
431 431 fname not in results
432 432 and fname in dmap
433 433 and (matchalways or matchfn(fname))
434 434 ):
435 435 results[fname] = None
436 436 elif kind == dirkind:
437 437 if fname in dmap and (matchalways or matchfn(fname)):
438 438 results[fname] = None
439 439 elif kind == regkind or kind == lnkkind:
440 440 if fname in dmap:
441 441 if matchalways or matchfn(fname):
442 442 results[fname] = entry
443 443 elif (matchalways or matchfn(fname)) and not ignore(fname):
444 444 results[fname] = entry
445 445 elif fname in dmap and (matchalways or matchfn(fname)):
446 446 results[fname] = None
447 447
448 448 # step 3: query notable files we don't already know about
449 449 # XXX try not to iterate over the entire dmap
450 450 if normalize:
451 451 # any notable files that have changed case will already be handled
452 452 # above, so just check membership in the foldmap
453 453 notefiles = set(
454 454 (
455 455 normalize(f, True, True)
456 456 for f in notefiles
457 457 if normcase(f) not in foldmap
458 458 )
459 459 )
460 460 visit = set(
461 461 (
462 462 f
463 463 for f in notefiles
464 464 if (
465 465 f not in results and matchfn(f) and (f in dmap or not ignore(f))
466 466 )
467 467 )
468 468 )
469 469
470 470 if not fresh_instance:
471 471 if matchalways:
472 472 visit.update(f for f in nonnormalset if f not in results)
473 473 visit.update(f for f in copymap if f not in results)
474 474 else:
475 475 visit.update(
476 476 f for f in nonnormalset if f not in results and matchfn(f)
477 477 )
478 478 visit.update(f for f in copymap if f not in results and matchfn(f))
479 479 else:
480 480 if matchalways:
481 481 visit.update(
482 482 f for f, st in pycompat.iteritems(dmap) if f not in results
483 483 )
484 484 visit.update(f for f in copymap if f not in results)
485 485 else:
486 486 visit.update(
487 487 f
488 488 for f, st in pycompat.iteritems(dmap)
489 489 if f not in results and matchfn(f)
490 490 )
491 491 visit.update(f for f in copymap if f not in results and matchfn(f))
492 492
493 493 audit = pathutil.pathauditor(self._root, cached=True).check
494 494 auditpass = [f for f in visit if audit(f)]
495 495 auditpass.sort()
496 496 auditfail = visit.difference(auditpass)
497 497 for f in auditfail:
498 498 results[f] = None
499 499
500 500 nf = iter(auditpass).next
501 501 for st in util.statfiles([join(f) for f in auditpass]):
502 502 f = nf()
503 503 if st or f in dmap:
504 504 results[f] = st
505 505
506 506 for s in subrepos:
507 507 del results[s]
508 508 del results[b'.hg']
509 509 return results
510 510
511 511
512 512 def overridestatus(
513 513 orig,
514 514 self,
515 515 node1=b'.',
516 516 node2=None,
517 517 match=None,
518 518 ignored=False,
519 519 clean=False,
520 520 unknown=False,
521 521 listsubrepos=False,
522 522 ):
523 523 listignored = ignored
524 524 listclean = clean
525 525 listunknown = unknown
526 526
527 527 def _cmpsets(l1, l2):
528 528 try:
529 529 if b'FSMONITOR_LOG_FILE' in encoding.environ:
530 530 fn = encoding.environ[b'FSMONITOR_LOG_FILE']
531 531 f = open(fn, b'wb')
532 532 else:
533 533 fn = b'fsmonitorfail.log'
534 534 f = self.vfs.open(fn, b'wb')
535 535 except (IOError, OSError):
536 536 self.ui.warn(_(b'warning: unable to write to %s\n') % fn)
537 537 return
538 538
539 539 try:
540 540 for i, (s1, s2) in enumerate(zip(l1, l2)):
541 541 if set(s1) != set(s2):
542 542 f.write(b'sets at position %d are unequal\n' % i)
543 543 f.write(b'watchman returned: %s\n' % s1)
544 544 f.write(b'stat returned: %s\n' % s2)
545 545 finally:
546 546 f.close()
547 547
548 548 if isinstance(node1, context.changectx):
549 549 ctx1 = node1
550 550 else:
551 551 ctx1 = self[node1]
552 552 if isinstance(node2, context.changectx):
553 553 ctx2 = node2
554 554 else:
555 555 ctx2 = self[node2]
556 556
557 557 working = ctx2.rev() is None
558 558 parentworking = working and ctx1 == self[b'.']
559 559 match = match or matchmod.always()
560 560
561 561 # Maybe we can use this opportunity to update Watchman's state.
562 562 # Mercurial uses workingcommitctx and/or memctx to represent the part of
563 563 # the workingctx that is to be committed. So don't update the state in
564 564 # that case.
565 565 # HG_PENDING is set in the environment when the dirstate is being updated
566 566 # in the middle of a transaction; we must not update our state in that
567 567 # case, or we risk forgetting about changes in the working copy.
568 568 updatestate = (
569 569 parentworking
570 570 and match.always()
571 571 and not isinstance(ctx2, (context.workingcommitctx, context.memctx))
572 572 and b'HG_PENDING' not in encoding.environ
573 573 )
574 574
575 575 try:
576 576 if self._fsmonitorstate.walk_on_invalidate:
577 577 # Use a short timeout to query the current clock. If that
578 578 # takes too long then we assume that the service will be slow
579 579 # to answer our query.
580 580 # walk_on_invalidate indicates that we prefer to walk the
581 581 # tree ourselves because we can ignore portions that Watchman
582 582 # cannot and we tend to be faster in the warmer buffer cache
583 583 # cases.
584 584 self._watchmanclient.settimeout(0.1)
585 585 else:
586 586 # Give Watchman more time to potentially complete its walk
587 587 # and return the initial clock. In this mode we assume that
588 588 # the filesystem will be slower than parsing a potentially
589 589 # very large Watchman result set.
590 590 self._watchmanclient.settimeout(self._fsmonitorstate.timeout + 0.1)
591 591 startclock = self._watchmanclient.getcurrentclock()
592 592 except Exception as ex:
593 593 self._watchmanclient.clearconnection()
594 594 _handleunavailable(self.ui, self._fsmonitorstate, ex)
595 595 # boo, Watchman failed. bail
596 596 return orig(
597 597 node1,
598 598 node2,
599 599 match,
600 600 listignored,
601 601 listclean,
602 602 listunknown,
603 603 listsubrepos,
604 604 )
605 605
606 606 if updatestate:
607 607 # We need info about unknown files. This may make things slower the
608 608 # first time, but whatever.
609 609 stateunknown = True
610 610 else:
611 611 stateunknown = listunknown
612 612
613 613 if updatestate:
614 614 ps = poststatus(startclock)
615 615 self.addpostdsstatus(ps)
616 616
617 617 r = orig(
618 618 node1, node2, match, listignored, listclean, stateunknown, listsubrepos
619 619 )
620 620 modified, added, removed, deleted, unknown, ignored, clean = r
621 621
622 622 if not listunknown:
623 623 unknown = []
624 624
625 625 # don't do paranoid checks if we're not going to query Watchman anyway
626 626 full = listclean or match.traversedir is not None
627 627 if self._fsmonitorstate.mode == b'paranoid' and not full:
628 628 # run status again and fall back to the old walk this time
629 629 self.dirstate._fsmonitordisable = True
630 630
631 631 # shut the UI up
632 632 quiet = self.ui.quiet
633 633 self.ui.quiet = True
634 634 fout, ferr = self.ui.fout, self.ui.ferr
635 635 self.ui.fout = self.ui.ferr = open(os.devnull, b'wb')
636 636
637 637 try:
638 638 rv2 = orig(
639 639 node1,
640 640 node2,
641 641 match,
642 642 listignored,
643 643 listclean,
644 644 listunknown,
645 645 listsubrepos,
646 646 )
647 647 finally:
648 648 self.dirstate._fsmonitordisable = False
649 649 self.ui.quiet = quiet
650 650 self.ui.fout, self.ui.ferr = fout, ferr
651 651
652 652 # clean isn't tested since it's set to True above
653 653 with self.wlock():
654 654 _cmpsets(
655 655 [modified, added, removed, deleted, unknown, ignored, clean],
656 656 rv2,
657 657 )
658 658 modified, added, removed, deleted, unknown, ignored, clean = rv2
659 659
660 660 return scmutil.status(
661 661 modified, added, removed, deleted, unknown, ignored, clean
662 662 )
663 663
664 664
665 665 class poststatus(object):
666 666 def __init__(self, startclock):
667 667 self._startclock = startclock
668 668
669 669 def __call__(self, wctx, status):
670 670 clock = wctx.repo()._fsmonitorstate.getlastclock() or self._startclock
671 671 hashignore = _hashignore(wctx.repo().dirstate._ignore)
672 672 notefiles = (
673 673 status.modified
674 674 + status.added
675 675 + status.removed
676 676 + status.deleted
677 677 + status.unknown
678 678 )
679 679 wctx.repo()._fsmonitorstate.set(clock, hashignore, notefiles)
680 680
681 681
682 682 def makedirstate(repo, dirstate):
683 683 class fsmonitordirstate(dirstate.__class__):
684 684 def _fsmonitorinit(self, repo):
685 685 # _fsmonitordisable is used in paranoid mode
686 686 self._fsmonitordisable = False
687 687 self._fsmonitorstate = repo._fsmonitorstate
688 688 self._watchmanclient = repo._watchmanclient
689 689 self._repo = weakref.proxy(repo)
690 690
691 691 def walk(self, *args, **kwargs):
692 692 orig = super(fsmonitordirstate, self).walk
693 693 if self._fsmonitordisable:
694 694 return orig(*args, **kwargs)
695 695 return overridewalk(orig, self, *args, **kwargs)
696 696
697 697 def rebuild(self, *args, **kwargs):
698 698 self._fsmonitorstate.invalidate()
699 699 return super(fsmonitordirstate, self).rebuild(*args, **kwargs)
700 700
701 701 def invalidate(self, *args, **kwargs):
702 702 self._fsmonitorstate.invalidate()
703 703 return super(fsmonitordirstate, self).invalidate(*args, **kwargs)
704 704
705 705 dirstate.__class__ = fsmonitordirstate
706 706 dirstate._fsmonitorinit(repo)
707 707
708 708
709 709 def wrapdirstate(orig, self):
710 710 ds = orig(self)
711 711 # only override the dirstate when Watchman is available for the repo
712 712 if util.safehasattr(self, b'_fsmonitorstate'):
713 713 makedirstate(self, ds)
714 714 return ds
715 715
716 716
717 717 def extsetup(ui):
718 718 extensions.wrapfilecache(
719 719 localrepo.localrepository, b'dirstate', wrapdirstate
720 720 )
721 721 if pycompat.isdarwin:
722 722 # An assist for avoiding the dangling-symlink fsevents bug
723 723 extensions.wrapfunction(os, b'symlink', wrapsymlink)
724 724
725 725 extensions.wrapfunction(merge, b'update', wrapupdate)
726 726
727 727
728 728 def wrapsymlink(orig, source, link_name):
729 729 ''' if we create a dangling symlink, also touch the parent dir
730 730 to encourage fsevents notifications to work more correctly '''
731 731 try:
732 732 return orig(source, link_name)
733 733 finally:
734 734 try:
735 735 os.utime(os.path.dirname(link_name), None)
736 736 except OSError:
737 737 pass
738 738
739 739
740 740 class state_update(object):
741 741 ''' This context manager is responsible for dispatching the state-enter
742 742 and state-leave signals to the watchman service. The enter and leave
743 743 methods can be invoked manually (for scenarios where context manager
744 744 semantics are not possible). If parameters oldnode and newnode are None,
745 745 they will be populated based on current working copy in enter and
746 746 leave, respectively. Similarly, if the distance is none, it will be
747 747 calculated based on the oldnode and newnode in the leave method.'''
748 748
749 749 def __init__(
750 750 self,
751 751 repo,
752 752 name,
753 753 oldnode=None,
754 754 newnode=None,
755 755 distance=None,
756 756 partial=False,
757 757 ):
758 758 self.repo = repo.unfiltered()
759 759 self.name = name
760 760 self.oldnode = oldnode
761 761 self.newnode = newnode
762 762 self.distance = distance
763 763 self.partial = partial
764 764 self._lock = None
765 765 self.need_leave = False
766 766
767 767 def __enter__(self):
768 768 self.enter()
769 769
770 770 def enter(self):
771 771 # Make sure we have a wlock prior to sending notifications to watchman.
772 772 # We don't want to race with other actors. In the update case,
773 773 # merge.update is going to take the wlock almost immediately. We are
774 774 # effectively extending the lock around several short sanity checks.
775 775 if self.oldnode is None:
776 776 self.oldnode = self.repo[b'.'].node()
777 777
778 778 if self.repo.currentwlock() is None:
779 779 if util.safehasattr(self.repo, b'wlocknostateupdate'):
780 780 self._lock = self.repo.wlocknostateupdate()
781 781 else:
782 782 self._lock = self.repo.wlock()
783 783 self.need_leave = self._state(b'state-enter', hex(self.oldnode))
784 784 return self
785 785
786 786 def __exit__(self, type_, value, tb):
787 787 abort = True if type_ else False
788 788 self.exit(abort=abort)
789 789
790 790 def exit(self, abort=False):
791 791 try:
792 792 if self.need_leave:
793 793 status = b'failed' if abort else b'ok'
794 794 if self.newnode is None:
795 795 self.newnode = self.repo[b'.'].node()
796 796 if self.distance is None:
797 797 self.distance = calcdistance(
798 798 self.repo, self.oldnode, self.newnode
799 799 )
800 800 self._state(b'state-leave', hex(self.newnode), status=status)
801 801 finally:
802 802 self.need_leave = False
803 803 if self._lock:
804 804 self._lock.release()
805 805
806 806 def _state(self, cmd, commithash, status=b'ok'):
807 807 if not util.safehasattr(self.repo, b'_watchmanclient'):
808 808 return False
809 809 try:
810 810 self.repo._watchmanclient.command(
811 811 cmd,
812 812 {
813 813 b'name': self.name,
814 814 b'metadata': {
815 815 # the target revision
816 816 b'rev': commithash,
817 817 # approximate number of commits between current and target
818 818 b'distance': self.distance if self.distance else 0,
819 819 # success/failure (only really meaningful for state-leave)
820 820 b'status': status,
821 821 # whether the working copy parent is changing
822 822 b'partial': self.partial,
823 823 },
824 824 },
825 825 )
826 826 return True
827 827 except Exception as e:
828 828 # Swallow any errors; fire and forget
829 829 self.repo.ui.log(
830 830 b'watchman', b'Exception %s while running %s\n', e, cmd
831 831 )
832 832 return False
833 833
834 834
835 835 # Estimate the distance between two nodes
836 836 def calcdistance(repo, oldnode, newnode):
837 837 anc = repo.changelog.ancestor(oldnode, newnode)
838 838 ancrev = repo[anc].rev()
839 839 distance = abs(repo[oldnode].rev() - ancrev) + abs(
840 840 repo[newnode].rev() - ancrev
841 841 )
842 842 return distance
843 843
844 844
845 845 # Bracket working copy updates with calls to the watchman state-enter
846 846 # and state-leave commands. This allows clients to perform more intelligent
847 847 # settling during bulk file change scenarios
848 848 # https://facebook.github.io/watchman/docs/cmd/subscribe.html#advanced-settling
849 849 def wrapupdate(
850 850 orig,
851 851 repo,
852 852 node,
853 853 branchmerge,
854 854 force,
855 855 ancestor=None,
856 856 mergeancestor=False,
857 857 labels=None,
858 858 matcher=None,
859 859 **kwargs
860 860 ):
861 861
862 862 distance = 0
863 863 partial = True
864 864 oldnode = repo[b'.'].node()
865 865 newnode = repo[node].node()
866 866 if matcher is None or matcher.always():
867 867 partial = False
868 868 distance = calcdistance(repo.unfiltered(), oldnode, newnode)
869 869
870 870 with state_update(
871 871 repo,
872 872 name=b"hg.update",
873 873 oldnode=oldnode,
874 874 newnode=newnode,
875 875 distance=distance,
876 876 partial=partial,
877 877 ):
878 878 return orig(
879 879 repo,
880 880 node,
881 881 branchmerge,
882 882 force,
883 883 ancestor,
884 884 mergeancestor,
885 885 labels,
886 886 matcher,
887 887 **kwargs
888 888 )
889 889
890 890
891 891 def repo_has_depth_one_nested_repo(repo):
892 892 for f in repo.wvfs.listdir():
893 893 if os.path.isdir(os.path.join(repo.root, f, b'.hg')):
894 894 msg = b'fsmonitor: sub-repository %r detected, fsmonitor disabled\n'
895 895 repo.ui.debug(msg % f)
896 896 return True
897 897 return False
898 898
899 899
900 900 def reposetup(ui, repo):
901 901 # We don't work with largefiles or inotify
902 902 exts = extensions.enabled()
903 903 for ext in _blacklist:
904 904 if ext in exts:
905 905 ui.warn(
906 906 _(
907 907 b'The fsmonitor extension is incompatible with the %s '
908 908 b'extension and has been disabled.\n'
909 909 )
910 910 % ext
911 911 )
912 912 return
913 913
914 914 if repo.local():
915 915 # We don't work with subrepos either.
916 916 #
917 917 # if repo[None].substate can cause a dirstate parse, which is too
918 918 # slow. Instead, look for a file called hgsubstate,
919 919 if repo.wvfs.exists(b'.hgsubstate') or repo.wvfs.exists(b'.hgsub'):
920 920 return
921 921
922 922 if repo_has_depth_one_nested_repo(repo):
923 923 return
924 924
925 925 fsmonitorstate = state.state(repo)
926 926 if fsmonitorstate.mode == b'off':
927 927 return
928 928
929 929 try:
930 930 client = watchmanclient.client(repo.ui, repo.root)
931 931 except Exception as ex:
932 932 _handleunavailable(ui, fsmonitorstate, ex)
933 933 return
934 934
935 935 repo._fsmonitorstate = fsmonitorstate
936 936 repo._watchmanclient = client
937 937
938 938 dirstate, cached = localrepo.isfilecached(repo, b'dirstate')
939 939 if cached:
940 940 # at this point since fsmonitorstate wasn't present,
941 941 # repo.dirstate is not a fsmonitordirstate
942 942 makedirstate(repo, dirstate)
943 943
944 944 class fsmonitorrepo(repo.__class__):
945 945 def status(self, *args, **kwargs):
946 946 orig = super(fsmonitorrepo, self).status
947 947 return overridestatus(orig, self, *args, **kwargs)
948 948
949 949 def wlocknostateupdate(self, *args, **kwargs):
950 950 return super(fsmonitorrepo, self).wlock(*args, **kwargs)
951 951
952 952 def wlock(self, *args, **kwargs):
953 953 l = super(fsmonitorrepo, self).wlock(*args, **kwargs)
954 954 if not ui.configbool(
955 955 b"experimental", b"fsmonitor.transaction_notify"
956 956 ):
957 957 return l
958 958 if l.held != 1:
959 959 return l
960 960 origrelease = l.releasefn
961 961
962 962 def staterelease():
963 963 if origrelease:
964 964 origrelease()
965 965 if l.stateupdate:
966 966 l.stateupdate.exit()
967 967 l.stateupdate = None
968 968
969 969 try:
970 970 l.stateupdate = None
971 971 l.stateupdate = state_update(self, name=b"hg.transaction")
972 972 l.stateupdate.enter()
973 973 l.releasefn = staterelease
974 974 except Exception as e:
975 975 # Swallow any errors; fire and forget
976 976 self.ui.log(
977 977 b'watchman', b'Exception in state update %s\n', e
978 978 )
979 979 return l
980 980
981 981 repo.__class__ = fsmonitorrepo
General Comments 0
You need to be logged in to leave comments. Login now