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