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