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