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