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