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