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