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