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