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