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