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