##// END OF EJS Templates
fsmonitor: match watchman and filesystem encoding...
Olivier Trempe -
r31846:1064a296 default
parent child Browse files
Show More
@@ -1,696 +1,724 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 The following configuration options exist:
21 The following configuration options exist:
22
22
23 ::
23 ::
24
24
25 [fsmonitor]
25 [fsmonitor]
26 mode = {off, on, paranoid}
26 mode = {off, on, paranoid}
27
27
28 When `mode = off`, fsmonitor will disable itself (similar to not loading the
28 When `mode = off`, fsmonitor will disable itself (similar to not loading the
29 extension at all). When `mode = on`, fsmonitor will be enabled (the default).
29 extension at all). When `mode = on`, fsmonitor will be enabled (the default).
30 When `mode = paranoid`, fsmonitor will query both Watchman and the filesystem,
30 When `mode = paranoid`, fsmonitor will query both Watchman and the filesystem,
31 and ensure that the results are consistent.
31 and ensure that the results are consistent.
32
32
33 ::
33 ::
34
34
35 [fsmonitor]
35 [fsmonitor]
36 timeout = (float)
36 timeout = (float)
37
37
38 A value, in seconds, that determines how long fsmonitor will wait for Watchman
38 A value, in seconds, that determines how long fsmonitor will wait for Watchman
39 to return results. Defaults to `2.0`.
39 to return results. Defaults to `2.0`.
40
40
41 ::
41 ::
42
42
43 [fsmonitor]
43 [fsmonitor]
44 blacklistusers = (list of userids)
44 blacklistusers = (list of userids)
45
45
46 A list of usernames for which fsmonitor will disable itself altogether.
46 A list of usernames for which fsmonitor will disable itself altogether.
47
47
48 ::
48 ::
49
49
50 [fsmonitor]
50 [fsmonitor]
51 walk_on_invalidate = (boolean)
51 walk_on_invalidate = (boolean)
52
52
53 Whether or not to walk the whole repo ourselves when our cached state has been
53 Whether or not to walk the whole repo ourselves when our cached state has been
54 invalidated, for example when Watchman has been restarted or .hgignore rules
54 invalidated, for example when Watchman has been restarted or .hgignore rules
55 have been changed. Walking the repo in that case can result in competing for
55 have been changed. Walking the repo in that case can result in competing for
56 I/O with Watchman. For large repos it is recommended to set this value to
56 I/O with Watchman. For large repos it is recommended to set this value to
57 false. You may wish to set this to true if you have a very fast filesystem
57 false. You may wish to set this to true if you have a very fast filesystem
58 that can outpace the IPC overhead of getting the result data for the full repo
58 that can outpace the IPC overhead of getting the result data for the full repo
59 from Watchman. Defaults to false.
59 from Watchman. Defaults to false.
60
60
61 fsmonitor is incompatible with the largefiles and eol extensions, and
61 fsmonitor is incompatible with the largefiles and eol extensions, and
62 will disable itself if any of those are active.
62 will disable itself if any of those are active.
63
63
64 '''
64 '''
65
65
66 # Platforms Supported
66 # Platforms Supported
67 # ===================
67 # ===================
68 #
68 #
69 # **Linux:** *Stable*. Watchman and fsmonitor are both known to work reliably,
69 # **Linux:** *Stable*. Watchman and fsmonitor are both known to work reliably,
70 # even under severe loads.
70 # even under severe loads.
71 #
71 #
72 # **Mac OS X:** *Stable*. The Mercurial test suite passes with fsmonitor
72 # **Mac OS X:** *Stable*. The Mercurial test suite passes with fsmonitor
73 # turned on, on case-insensitive HFS+. There has been a reasonable amount of
73 # turned on, on case-insensitive HFS+. There has been a reasonable amount of
74 # user testing under normal loads.
74 # user testing under normal loads.
75 #
75 #
76 # **Solaris, BSD:** *Alpha*. watchman and fsmonitor are believed to work, but
76 # **Solaris, BSD:** *Alpha*. watchman and fsmonitor are believed to work, but
77 # very little testing has been done.
77 # very little testing has been done.
78 #
78 #
79 # **Windows:** *Alpha*. Not in a release version of watchman or fsmonitor yet.
79 # **Windows:** *Alpha*. Not in a release version of watchman or fsmonitor yet.
80 #
80 #
81 # Known Issues
81 # Known Issues
82 # ============
82 # ============
83 #
83 #
84 # * fsmonitor will disable itself if any of the following extensions are
84 # * fsmonitor will disable itself if any of the following extensions are
85 # enabled: largefiles, inotify, eol; or if the repository has subrepos.
85 # enabled: largefiles, inotify, eol; or if the repository has subrepos.
86 # * fsmonitor will produce incorrect results if nested repos that are not
86 # * fsmonitor will produce incorrect results if nested repos that are not
87 # subrepos exist. *Workaround*: add nested repo paths to your `.hgignore`.
87 # subrepos exist. *Workaround*: add nested repo paths to your `.hgignore`.
88 #
88 #
89 # The issues related to nested repos and subrepos are probably not fundamental
89 # The issues related to nested repos and subrepos are probably not fundamental
90 # ones. Patches to fix them are welcome.
90 # ones. Patches to fix them are welcome.
91
91
92 from __future__ import absolute_import
92 from __future__ import absolute_import
93
93
94 import codecs
94 import hashlib
95 import hashlib
95 import os
96 import os
96 import stat
97 import stat
98 import sys
97
99
98 from mercurial.i18n import _
100 from mercurial.i18n import _
99 from mercurial import (
101 from mercurial import (
100 context,
102 context,
101 encoding,
103 encoding,
104 error,
102 extensions,
105 extensions,
103 localrepo,
106 localrepo,
104 merge,
107 merge,
105 pathutil,
108 pathutil,
106 pycompat,
109 pycompat,
107 scmutil,
110 scmutil,
108 util,
111 util,
109 )
112 )
110 from mercurial import match as matchmod
113 from mercurial import match as matchmod
111
114
112 from . import (
115 from . import (
116 pywatchman,
113 state,
117 state,
114 watchmanclient,
118 watchmanclient,
115 )
119 )
116
120
117 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
121 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
118 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
122 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
119 # be specifying the version(s) of Mercurial they are tested with, or
123 # be specifying the version(s) of Mercurial they are tested with, or
120 # leave the attribute unspecified.
124 # leave the attribute unspecified.
121 testedwith = 'ships-with-hg-core'
125 testedwith = 'ships-with-hg-core'
122
126
123 # This extension is incompatible with the following blacklisted extensions
127 # This extension is incompatible with the following blacklisted extensions
124 # and will disable itself when encountering one of these:
128 # and will disable itself when encountering one of these:
125 _blacklist = ['largefiles', 'eol']
129 _blacklist = ['largefiles', 'eol']
126
130
127 def _handleunavailable(ui, state, ex):
131 def _handleunavailable(ui, state, ex):
128 """Exception handler for Watchman interaction exceptions"""
132 """Exception handler for Watchman interaction exceptions"""
129 if isinstance(ex, watchmanclient.Unavailable):
133 if isinstance(ex, watchmanclient.Unavailable):
130 if ex.warn:
134 if ex.warn:
131 ui.warn(str(ex) + '\n')
135 ui.warn(str(ex) + '\n')
132 if ex.invalidate:
136 if ex.invalidate:
133 state.invalidate()
137 state.invalidate()
134 ui.log('fsmonitor', 'Watchman unavailable: %s\n', ex.msg)
138 ui.log('fsmonitor', 'Watchman unavailable: %s\n', ex.msg)
135 else:
139 else:
136 ui.log('fsmonitor', 'Watchman exception: %s\n', ex)
140 ui.log('fsmonitor', 'Watchman exception: %s\n', ex)
137
141
138 def _hashignore(ignore):
142 def _hashignore(ignore):
139 """Calculate hash for ignore patterns and filenames
143 """Calculate hash for ignore patterns and filenames
140
144
141 If this information changes between Mercurial invocations, we can't
145 If this information changes between Mercurial invocations, we can't
142 rely on Watchman information anymore and have to re-scan the working
146 rely on Watchman information anymore and have to re-scan the working
143 copy.
147 copy.
144
148
145 """
149 """
146 sha1 = hashlib.sha1()
150 sha1 = hashlib.sha1()
147 if util.safehasattr(ignore, 'includepat'):
151 if util.safehasattr(ignore, 'includepat'):
148 sha1.update(ignore.includepat)
152 sha1.update(ignore.includepat)
149 sha1.update('\0\0')
153 sha1.update('\0\0')
150 if util.safehasattr(ignore, 'excludepat'):
154 if util.safehasattr(ignore, 'excludepat'):
151 sha1.update(ignore.excludepat)
155 sha1.update(ignore.excludepat)
152 sha1.update('\0\0')
156 sha1.update('\0\0')
153 if util.safehasattr(ignore, 'patternspat'):
157 if util.safehasattr(ignore, 'patternspat'):
154 sha1.update(ignore.patternspat)
158 sha1.update(ignore.patternspat)
155 sha1.update('\0\0')
159 sha1.update('\0\0')
156 if util.safehasattr(ignore, '_files'):
160 if util.safehasattr(ignore, '_files'):
157 for f in ignore._files:
161 for f in ignore._files:
158 sha1.update(f)
162 sha1.update(f)
159 sha1.update('\0')
163 sha1.update('\0')
160 return sha1.hexdigest()
164 return sha1.hexdigest()
161
165
166 _watchmanencoding = pywatchman.encoding.get_local_encoding()
167 _fsencoding = sys.getfilesystemencoding() or sys.getdefaultencoding()
168 _fixencoding = codecs.lookup(_watchmanencoding) != codecs.lookup(_fsencoding)
169
170 def _watchmantofsencoding(path):
171 """Fix path to match watchman and local filesystem encoding
172
173 watchman's paths encoding can differ from filesystem encoding. For example,
174 on Windows, it's always utf-8.
175 """
176 try:
177 decoded = path.decode(_watchmanencoding)
178 except UnicodeDecodeError as e:
179 raise error.Abort(str(e), hint='watchman encoding error')
180
181 try:
182 encoded = decoded.encode(_fsencoding, 'strict')
183 except UnicodeEncodeError as e:
184 raise error.Abort(str(e))
185
186 return encoded
187
162 def overridewalk(orig, self, match, subrepos, unknown, ignored, full=True):
188 def overridewalk(orig, self, match, subrepos, unknown, ignored, full=True):
163 '''Replacement for dirstate.walk, hooking into Watchman.
189 '''Replacement for dirstate.walk, hooking into Watchman.
164
190
165 Whenever full is False, ignored is False, and the Watchman client is
191 Whenever full is False, ignored is False, and the Watchman client is
166 available, use Watchman combined with saved state to possibly return only a
192 available, use Watchman combined with saved state to possibly return only a
167 subset of files.'''
193 subset of files.'''
168 def bail():
194 def bail():
169 return orig(match, subrepos, unknown, ignored, full=True)
195 return orig(match, subrepos, unknown, ignored, full=True)
170
196
171 if full or ignored or not self._watchmanclient.available():
197 if full or ignored or not self._watchmanclient.available():
172 return bail()
198 return bail()
173 state = self._fsmonitorstate
199 state = self._fsmonitorstate
174 clock, ignorehash, notefiles = state.get()
200 clock, ignorehash, notefiles = state.get()
175 if not clock:
201 if not clock:
176 if state.walk_on_invalidate:
202 if state.walk_on_invalidate:
177 return bail()
203 return bail()
178 # Initial NULL clock value, see
204 # Initial NULL clock value, see
179 # https://facebook.github.io/watchman/docs/clockspec.html
205 # https://facebook.github.io/watchman/docs/clockspec.html
180 clock = 'c:0:0'
206 clock = 'c:0:0'
181 notefiles = []
207 notefiles = []
182
208
183 def fwarn(f, msg):
209 def fwarn(f, msg):
184 self._ui.warn('%s: %s\n' % (self.pathto(f), msg))
210 self._ui.warn('%s: %s\n' % (self.pathto(f), msg))
185 return False
211 return False
186
212
187 def badtype(mode):
213 def badtype(mode):
188 kind = _('unknown')
214 kind = _('unknown')
189 if stat.S_ISCHR(mode):
215 if stat.S_ISCHR(mode):
190 kind = _('character device')
216 kind = _('character device')
191 elif stat.S_ISBLK(mode):
217 elif stat.S_ISBLK(mode):
192 kind = _('block device')
218 kind = _('block device')
193 elif stat.S_ISFIFO(mode):
219 elif stat.S_ISFIFO(mode):
194 kind = _('fifo')
220 kind = _('fifo')
195 elif stat.S_ISSOCK(mode):
221 elif stat.S_ISSOCK(mode):
196 kind = _('socket')
222 kind = _('socket')
197 elif stat.S_ISDIR(mode):
223 elif stat.S_ISDIR(mode):
198 kind = _('directory')
224 kind = _('directory')
199 return _('unsupported file type (type is %s)') % kind
225 return _('unsupported file type (type is %s)') % kind
200
226
201 ignore = self._ignore
227 ignore = self._ignore
202 dirignore = self._dirignore
228 dirignore = self._dirignore
203 if unknown:
229 if unknown:
204 if _hashignore(ignore) != ignorehash and clock != 'c:0:0':
230 if _hashignore(ignore) != ignorehash and clock != 'c:0:0':
205 # ignore list changed -- can't rely on Watchman state any more
231 # ignore list changed -- can't rely on Watchman state any more
206 if state.walk_on_invalidate:
232 if state.walk_on_invalidate:
207 return bail()
233 return bail()
208 notefiles = []
234 notefiles = []
209 clock = 'c:0:0'
235 clock = 'c:0:0'
210 else:
236 else:
211 # always ignore
237 # always ignore
212 ignore = util.always
238 ignore = util.always
213 dirignore = util.always
239 dirignore = util.always
214
240
215 matchfn = match.matchfn
241 matchfn = match.matchfn
216 matchalways = match.always()
242 matchalways = match.always()
217 dmap = self._map
243 dmap = self._map
218 nonnormalset = getattr(self, '_nonnormalset', None)
244 nonnormalset = getattr(self, '_nonnormalset', None)
219
245
220 copymap = self._copymap
246 copymap = self._copymap
221 getkind = stat.S_IFMT
247 getkind = stat.S_IFMT
222 dirkind = stat.S_IFDIR
248 dirkind = stat.S_IFDIR
223 regkind = stat.S_IFREG
249 regkind = stat.S_IFREG
224 lnkkind = stat.S_IFLNK
250 lnkkind = stat.S_IFLNK
225 join = self._join
251 join = self._join
226 normcase = util.normcase
252 normcase = util.normcase
227 fresh_instance = False
253 fresh_instance = False
228
254
229 exact = skipstep3 = False
255 exact = skipstep3 = False
230 if matchfn == match.exact: # match.exact
256 if matchfn == match.exact: # match.exact
231 exact = True
257 exact = True
232 dirignore = util.always # skip step 2
258 dirignore = util.always # skip step 2
233 elif match.files() and not match.anypats(): # match.match, no patterns
259 elif match.files() and not match.anypats(): # match.match, no patterns
234 skipstep3 = True
260 skipstep3 = True
235
261
236 if not exact and self._checkcase:
262 if not exact and self._checkcase:
237 # note that even though we could receive directory entries, we're only
263 # note that even though we could receive directory entries, we're only
238 # interested in checking if a file with the same name exists. So only
264 # interested in checking if a file with the same name exists. So only
239 # normalize files if possible.
265 # normalize files if possible.
240 normalize = self._normalizefile
266 normalize = self._normalizefile
241 skipstep3 = False
267 skipstep3 = False
242 else:
268 else:
243 normalize = None
269 normalize = None
244
270
245 # step 1: find all explicit files
271 # step 1: find all explicit files
246 results, work, dirsnotfound = self._walkexplicit(match, subrepos)
272 results, work, dirsnotfound = self._walkexplicit(match, subrepos)
247
273
248 skipstep3 = skipstep3 and not (work or dirsnotfound)
274 skipstep3 = skipstep3 and not (work or dirsnotfound)
249 work = [d for d in work if not dirignore(d[0])]
275 work = [d for d in work if not dirignore(d[0])]
250
276
251 if not work and (exact or skipstep3):
277 if not work and (exact or skipstep3):
252 for s in subrepos:
278 for s in subrepos:
253 del results[s]
279 del results[s]
254 del results['.hg']
280 del results['.hg']
255 return results
281 return results
256
282
257 # step 2: query Watchman
283 # step 2: query Watchman
258 try:
284 try:
259 # Use the user-configured timeout for the query.
285 # Use the user-configured timeout for the query.
260 # Add a little slack over the top of the user query to allow for
286 # Add a little slack over the top of the user query to allow for
261 # overheads while transferring the data
287 # overheads while transferring the data
262 self._watchmanclient.settimeout(state.timeout + 0.1)
288 self._watchmanclient.settimeout(state.timeout + 0.1)
263 result = self._watchmanclient.command('query', {
289 result = self._watchmanclient.command('query', {
264 'fields': ['mode', 'mtime', 'size', 'exists', 'name'],
290 'fields': ['mode', 'mtime', 'size', 'exists', 'name'],
265 'since': clock,
291 'since': clock,
266 'expression': [
292 'expression': [
267 'not', [
293 'not', [
268 'anyof', ['dirname', '.hg'],
294 'anyof', ['dirname', '.hg'],
269 ['name', '.hg', 'wholename']
295 ['name', '.hg', 'wholename']
270 ]
296 ]
271 ],
297 ],
272 'sync_timeout': int(state.timeout * 1000),
298 'sync_timeout': int(state.timeout * 1000),
273 'empty_on_fresh_instance': state.walk_on_invalidate,
299 'empty_on_fresh_instance': state.walk_on_invalidate,
274 })
300 })
275 except Exception as ex:
301 except Exception as ex:
276 _handleunavailable(self._ui, state, ex)
302 _handleunavailable(self._ui, state, ex)
277 self._watchmanclient.clearconnection()
303 self._watchmanclient.clearconnection()
278 return bail()
304 return bail()
279 else:
305 else:
280 # We need to propagate the last observed clock up so that we
306 # We need to propagate the last observed clock up so that we
281 # can use it for our next query
307 # can use it for our next query
282 state.setlastclock(result['clock'])
308 state.setlastclock(result['clock'])
283 if result['is_fresh_instance']:
309 if result['is_fresh_instance']:
284 if state.walk_on_invalidate:
310 if state.walk_on_invalidate:
285 state.invalidate()
311 state.invalidate()
286 return bail()
312 return bail()
287 fresh_instance = True
313 fresh_instance = True
288 # Ignore any prior noteable files from the state info
314 # Ignore any prior noteable files from the state info
289 notefiles = []
315 notefiles = []
290
316
291 # for file paths which require normalization and we encounter a case
317 # for file paths which require normalization and we encounter a case
292 # collision, we store our own foldmap
318 # collision, we store our own foldmap
293 if normalize:
319 if normalize:
294 foldmap = dict((normcase(k), k) for k in results)
320 foldmap = dict((normcase(k), k) for k in results)
295
321
296 switch_slashes = pycompat.ossep == '\\'
322 switch_slashes = pycompat.ossep == '\\'
297 # The order of the results is, strictly speaking, undefined.
323 # The order of the results is, strictly speaking, undefined.
298 # For case changes on a case insensitive filesystem we may receive
324 # For case changes on a case insensitive filesystem we may receive
299 # two entries, one with exists=True and another with exists=False.
325 # two entries, one with exists=True and another with exists=False.
300 # The exists=True entries in the same response should be interpreted
326 # The exists=True entries in the same response should be interpreted
301 # as being happens-after the exists=False entries due to the way that
327 # as being happens-after the exists=False entries due to the way that
302 # Watchman tracks files. We use this property to reconcile deletes
328 # Watchman tracks files. We use this property to reconcile deletes
303 # for name case changes.
329 # for name case changes.
304 for entry in result['files']:
330 for entry in result['files']:
305 fname = entry['name']
331 fname = entry['name']
332 if _fixencoding:
333 fname = _watchmantofsencoding(fname)
306 if switch_slashes:
334 if switch_slashes:
307 fname = fname.replace('\\', '/')
335 fname = fname.replace('\\', '/')
308 if normalize:
336 if normalize:
309 normed = normcase(fname)
337 normed = normcase(fname)
310 fname = normalize(fname, True, True)
338 fname = normalize(fname, True, True)
311 foldmap[normed] = fname
339 foldmap[normed] = fname
312 fmode = entry['mode']
340 fmode = entry['mode']
313 fexists = entry['exists']
341 fexists = entry['exists']
314 kind = getkind(fmode)
342 kind = getkind(fmode)
315
343
316 if not fexists:
344 if not fexists:
317 # if marked as deleted and we don't already have a change
345 # if marked as deleted and we don't already have a change
318 # record, mark it as deleted. If we already have an entry
346 # record, mark it as deleted. If we already have an entry
319 # for fname then it was either part of walkexplicit or was
347 # for fname then it was either part of walkexplicit or was
320 # an earlier result that was a case change
348 # an earlier result that was a case change
321 if fname not in results and fname in dmap and (
349 if fname not in results and fname in dmap and (
322 matchalways or matchfn(fname)):
350 matchalways or matchfn(fname)):
323 results[fname] = None
351 results[fname] = None
324 elif kind == dirkind:
352 elif kind == dirkind:
325 if fname in dmap and (matchalways or matchfn(fname)):
353 if fname in dmap and (matchalways or matchfn(fname)):
326 results[fname] = None
354 results[fname] = None
327 elif kind == regkind or kind == lnkkind:
355 elif kind == regkind or kind == lnkkind:
328 if fname in dmap:
356 if fname in dmap:
329 if matchalways or matchfn(fname):
357 if matchalways or matchfn(fname):
330 results[fname] = entry
358 results[fname] = entry
331 elif (matchalways or matchfn(fname)) and not ignore(fname):
359 elif (matchalways or matchfn(fname)) and not ignore(fname):
332 results[fname] = entry
360 results[fname] = entry
333 elif fname in dmap and (matchalways or matchfn(fname)):
361 elif fname in dmap and (matchalways or matchfn(fname)):
334 results[fname] = None
362 results[fname] = None
335
363
336 # step 3: query notable files we don't already know about
364 # step 3: query notable files we don't already know about
337 # XXX try not to iterate over the entire dmap
365 # XXX try not to iterate over the entire dmap
338 if normalize:
366 if normalize:
339 # any notable files that have changed case will already be handled
367 # any notable files that have changed case will already be handled
340 # above, so just check membership in the foldmap
368 # above, so just check membership in the foldmap
341 notefiles = set((normalize(f, True, True) for f in notefiles
369 notefiles = set((normalize(f, True, True) for f in notefiles
342 if normcase(f) not in foldmap))
370 if normcase(f) not in foldmap))
343 visit = set((f for f in notefiles if (f not in results and matchfn(f)
371 visit = set((f for f in notefiles if (f not in results and matchfn(f)
344 and (f in dmap or not ignore(f)))))
372 and (f in dmap or not ignore(f)))))
345
373
346 if nonnormalset is not None and not fresh_instance:
374 if nonnormalset is not None and not fresh_instance:
347 if matchalways:
375 if matchalways:
348 visit.update(f for f in nonnormalset if f not in results)
376 visit.update(f for f in nonnormalset if f not in results)
349 visit.update(f for f in copymap if f not in results)
377 visit.update(f for f in copymap if f not in results)
350 else:
378 else:
351 visit.update(f for f in nonnormalset
379 visit.update(f for f in nonnormalset
352 if f not in results and matchfn(f))
380 if f not in results and matchfn(f))
353 visit.update(f for f in copymap
381 visit.update(f for f in copymap
354 if f not in results and matchfn(f))
382 if f not in results and matchfn(f))
355 else:
383 else:
356 if matchalways:
384 if matchalways:
357 visit.update(f for f, st in dmap.iteritems()
385 visit.update(f for f, st in dmap.iteritems()
358 if (f not in results and
386 if (f not in results and
359 (st[2] < 0 or st[0] != 'n' or fresh_instance)))
387 (st[2] < 0 or st[0] != 'n' or fresh_instance)))
360 visit.update(f for f in copymap if f not in results)
388 visit.update(f for f in copymap if f not in results)
361 else:
389 else:
362 visit.update(f for f, st in dmap.iteritems()
390 visit.update(f for f, st in dmap.iteritems()
363 if (f not in results and
391 if (f not in results and
364 (st[2] < 0 or st[0] != 'n' or fresh_instance)
392 (st[2] < 0 or st[0] != 'n' or fresh_instance)
365 and matchfn(f)))
393 and matchfn(f)))
366 visit.update(f for f in copymap
394 visit.update(f for f in copymap
367 if f not in results and matchfn(f))
395 if f not in results and matchfn(f))
368
396
369 audit = pathutil.pathauditor(self._root).check
397 audit = pathutil.pathauditor(self._root).check
370 auditpass = [f for f in visit if audit(f)]
398 auditpass = [f for f in visit if audit(f)]
371 auditpass.sort()
399 auditpass.sort()
372 auditfail = visit.difference(auditpass)
400 auditfail = visit.difference(auditpass)
373 for f in auditfail:
401 for f in auditfail:
374 results[f] = None
402 results[f] = None
375
403
376 nf = iter(auditpass).next
404 nf = iter(auditpass).next
377 for st in util.statfiles([join(f) for f in auditpass]):
405 for st in util.statfiles([join(f) for f in auditpass]):
378 f = nf()
406 f = nf()
379 if st or f in dmap:
407 if st or f in dmap:
380 results[f] = st
408 results[f] = st
381
409
382 for s in subrepos:
410 for s in subrepos:
383 del results[s]
411 del results[s]
384 del results['.hg']
412 del results['.hg']
385 return results
413 return results
386
414
387 def overridestatus(
415 def overridestatus(
388 orig, self, node1='.', node2=None, match=None, ignored=False,
416 orig, self, node1='.', node2=None, match=None, ignored=False,
389 clean=False, unknown=False, listsubrepos=False):
417 clean=False, unknown=False, listsubrepos=False):
390 listignored = ignored
418 listignored = ignored
391 listclean = clean
419 listclean = clean
392 listunknown = unknown
420 listunknown = unknown
393
421
394 def _cmpsets(l1, l2):
422 def _cmpsets(l1, l2):
395 try:
423 try:
396 if 'FSMONITOR_LOG_FILE' in encoding.environ:
424 if 'FSMONITOR_LOG_FILE' in encoding.environ:
397 fn = encoding.environ['FSMONITOR_LOG_FILE']
425 fn = encoding.environ['FSMONITOR_LOG_FILE']
398 f = open(fn, 'wb')
426 f = open(fn, 'wb')
399 else:
427 else:
400 fn = 'fsmonitorfail.log'
428 fn = 'fsmonitorfail.log'
401 f = self.opener(fn, 'wb')
429 f = self.opener(fn, 'wb')
402 except (IOError, OSError):
430 except (IOError, OSError):
403 self.ui.warn(_('warning: unable to write to %s\n') % fn)
431 self.ui.warn(_('warning: unable to write to %s\n') % fn)
404 return
432 return
405
433
406 try:
434 try:
407 for i, (s1, s2) in enumerate(zip(l1, l2)):
435 for i, (s1, s2) in enumerate(zip(l1, l2)):
408 if set(s1) != set(s2):
436 if set(s1) != set(s2):
409 f.write('sets at position %d are unequal\n' % i)
437 f.write('sets at position %d are unequal\n' % i)
410 f.write('watchman returned: %s\n' % s1)
438 f.write('watchman returned: %s\n' % s1)
411 f.write('stat returned: %s\n' % s2)
439 f.write('stat returned: %s\n' % s2)
412 finally:
440 finally:
413 f.close()
441 f.close()
414
442
415 if isinstance(node1, context.changectx):
443 if isinstance(node1, context.changectx):
416 ctx1 = node1
444 ctx1 = node1
417 else:
445 else:
418 ctx1 = self[node1]
446 ctx1 = self[node1]
419 if isinstance(node2, context.changectx):
447 if isinstance(node2, context.changectx):
420 ctx2 = node2
448 ctx2 = node2
421 else:
449 else:
422 ctx2 = self[node2]
450 ctx2 = self[node2]
423
451
424 working = ctx2.rev() is None
452 working = ctx2.rev() is None
425 parentworking = working and ctx1 == self['.']
453 parentworking = working and ctx1 == self['.']
426 match = match or matchmod.always(self.root, self.getcwd())
454 match = match or matchmod.always(self.root, self.getcwd())
427
455
428 # Maybe we can use this opportunity to update Watchman's state.
456 # Maybe we can use this opportunity to update Watchman's state.
429 # Mercurial uses workingcommitctx and/or memctx to represent the part of
457 # Mercurial uses workingcommitctx and/or memctx to represent the part of
430 # the workingctx that is to be committed. So don't update the state in
458 # the workingctx that is to be committed. So don't update the state in
431 # that case.
459 # that case.
432 # HG_PENDING is set in the environment when the dirstate is being updated
460 # HG_PENDING is set in the environment when the dirstate is being updated
433 # in the middle of a transaction; we must not update our state in that
461 # in the middle of a transaction; we must not update our state in that
434 # case, or we risk forgetting about changes in the working copy.
462 # case, or we risk forgetting about changes in the working copy.
435 updatestate = (parentworking and match.always() and
463 updatestate = (parentworking and match.always() and
436 not isinstance(ctx2, (context.workingcommitctx,
464 not isinstance(ctx2, (context.workingcommitctx,
437 context.memctx)) and
465 context.memctx)) and
438 'HG_PENDING' not in encoding.environ)
466 'HG_PENDING' not in encoding.environ)
439
467
440 try:
468 try:
441 if self._fsmonitorstate.walk_on_invalidate:
469 if self._fsmonitorstate.walk_on_invalidate:
442 # Use a short timeout to query the current clock. If that
470 # Use a short timeout to query the current clock. If that
443 # takes too long then we assume that the service will be slow
471 # takes too long then we assume that the service will be slow
444 # to answer our query.
472 # to answer our query.
445 # walk_on_invalidate indicates that we prefer to walk the
473 # walk_on_invalidate indicates that we prefer to walk the
446 # tree ourselves because we can ignore portions that Watchman
474 # tree ourselves because we can ignore portions that Watchman
447 # cannot and we tend to be faster in the warmer buffer cache
475 # cannot and we tend to be faster in the warmer buffer cache
448 # cases.
476 # cases.
449 self._watchmanclient.settimeout(0.1)
477 self._watchmanclient.settimeout(0.1)
450 else:
478 else:
451 # Give Watchman more time to potentially complete its walk
479 # Give Watchman more time to potentially complete its walk
452 # and return the initial clock. In this mode we assume that
480 # and return the initial clock. In this mode we assume that
453 # the filesystem will be slower than parsing a potentially
481 # the filesystem will be slower than parsing a potentially
454 # very large Watchman result set.
482 # very large Watchman result set.
455 self._watchmanclient.settimeout(
483 self._watchmanclient.settimeout(
456 self._fsmonitorstate.timeout + 0.1)
484 self._fsmonitorstate.timeout + 0.1)
457 startclock = self._watchmanclient.getcurrentclock()
485 startclock = self._watchmanclient.getcurrentclock()
458 except Exception as ex:
486 except Exception as ex:
459 self._watchmanclient.clearconnection()
487 self._watchmanclient.clearconnection()
460 _handleunavailable(self.ui, self._fsmonitorstate, ex)
488 _handleunavailable(self.ui, self._fsmonitorstate, ex)
461 # boo, Watchman failed. bail
489 # boo, Watchman failed. bail
462 return orig(node1, node2, match, listignored, listclean,
490 return orig(node1, node2, match, listignored, listclean,
463 listunknown, listsubrepos)
491 listunknown, listsubrepos)
464
492
465 if updatestate:
493 if updatestate:
466 # We need info about unknown files. This may make things slower the
494 # We need info about unknown files. This may make things slower the
467 # first time, but whatever.
495 # first time, but whatever.
468 stateunknown = True
496 stateunknown = True
469 else:
497 else:
470 stateunknown = listunknown
498 stateunknown = listunknown
471
499
472 r = orig(node1, node2, match, listignored, listclean, stateunknown,
500 r = orig(node1, node2, match, listignored, listclean, stateunknown,
473 listsubrepos)
501 listsubrepos)
474 modified, added, removed, deleted, unknown, ignored, clean = r
502 modified, added, removed, deleted, unknown, ignored, clean = r
475
503
476 if updatestate:
504 if updatestate:
477 notefiles = modified + added + removed + deleted + unknown
505 notefiles = modified + added + removed + deleted + unknown
478 self._fsmonitorstate.set(
506 self._fsmonitorstate.set(
479 self._fsmonitorstate.getlastclock() or startclock,
507 self._fsmonitorstate.getlastclock() or startclock,
480 _hashignore(self.dirstate._ignore),
508 _hashignore(self.dirstate._ignore),
481 notefiles)
509 notefiles)
482
510
483 if not listunknown:
511 if not listunknown:
484 unknown = []
512 unknown = []
485
513
486 # don't do paranoid checks if we're not going to query Watchman anyway
514 # don't do paranoid checks if we're not going to query Watchman anyway
487 full = listclean or match.traversedir is not None
515 full = listclean or match.traversedir is not None
488 if self._fsmonitorstate.mode == 'paranoid' and not full:
516 if self._fsmonitorstate.mode == 'paranoid' and not full:
489 # run status again and fall back to the old walk this time
517 # run status again and fall back to the old walk this time
490 self.dirstate._fsmonitordisable = True
518 self.dirstate._fsmonitordisable = True
491
519
492 # shut the UI up
520 # shut the UI up
493 quiet = self.ui.quiet
521 quiet = self.ui.quiet
494 self.ui.quiet = True
522 self.ui.quiet = True
495 fout, ferr = self.ui.fout, self.ui.ferr
523 fout, ferr = self.ui.fout, self.ui.ferr
496 self.ui.fout = self.ui.ferr = open(os.devnull, 'wb')
524 self.ui.fout = self.ui.ferr = open(os.devnull, 'wb')
497
525
498 try:
526 try:
499 rv2 = orig(
527 rv2 = orig(
500 node1, node2, match, listignored, listclean, listunknown,
528 node1, node2, match, listignored, listclean, listunknown,
501 listsubrepos)
529 listsubrepos)
502 finally:
530 finally:
503 self.dirstate._fsmonitordisable = False
531 self.dirstate._fsmonitordisable = False
504 self.ui.quiet = quiet
532 self.ui.quiet = quiet
505 self.ui.fout, self.ui.ferr = fout, ferr
533 self.ui.fout, self.ui.ferr = fout, ferr
506
534
507 # clean isn't tested since it's set to True above
535 # clean isn't tested since it's set to True above
508 _cmpsets([modified, added, removed, deleted, unknown, ignored, clean],
536 _cmpsets([modified, added, removed, deleted, unknown, ignored, clean],
509 rv2)
537 rv2)
510 modified, added, removed, deleted, unknown, ignored, clean = rv2
538 modified, added, removed, deleted, unknown, ignored, clean = rv2
511
539
512 return scmutil.status(
540 return scmutil.status(
513 modified, added, removed, deleted, unknown, ignored, clean)
541 modified, added, removed, deleted, unknown, ignored, clean)
514
542
515 def makedirstate(cls):
543 def makedirstate(cls):
516 class fsmonitordirstate(cls):
544 class fsmonitordirstate(cls):
517 def _fsmonitorinit(self, fsmonitorstate, watchmanclient):
545 def _fsmonitorinit(self, fsmonitorstate, watchmanclient):
518 # _fsmonitordisable is used in paranoid mode
546 # _fsmonitordisable is used in paranoid mode
519 self._fsmonitordisable = False
547 self._fsmonitordisable = False
520 self._fsmonitorstate = fsmonitorstate
548 self._fsmonitorstate = fsmonitorstate
521 self._watchmanclient = watchmanclient
549 self._watchmanclient = watchmanclient
522
550
523 def walk(self, *args, **kwargs):
551 def walk(self, *args, **kwargs):
524 orig = super(fsmonitordirstate, self).walk
552 orig = super(fsmonitordirstate, self).walk
525 if self._fsmonitordisable:
553 if self._fsmonitordisable:
526 return orig(*args, **kwargs)
554 return orig(*args, **kwargs)
527 return overridewalk(orig, self, *args, **kwargs)
555 return overridewalk(orig, self, *args, **kwargs)
528
556
529 def rebuild(self, *args, **kwargs):
557 def rebuild(self, *args, **kwargs):
530 self._fsmonitorstate.invalidate()
558 self._fsmonitorstate.invalidate()
531 return super(fsmonitordirstate, self).rebuild(*args, **kwargs)
559 return super(fsmonitordirstate, self).rebuild(*args, **kwargs)
532
560
533 def invalidate(self, *args, **kwargs):
561 def invalidate(self, *args, **kwargs):
534 self._fsmonitorstate.invalidate()
562 self._fsmonitorstate.invalidate()
535 return super(fsmonitordirstate, self).invalidate(*args, **kwargs)
563 return super(fsmonitordirstate, self).invalidate(*args, **kwargs)
536
564
537 return fsmonitordirstate
565 return fsmonitordirstate
538
566
539 def wrapdirstate(orig, self):
567 def wrapdirstate(orig, self):
540 ds = orig(self)
568 ds = orig(self)
541 # only override the dirstate when Watchman is available for the repo
569 # only override the dirstate when Watchman is available for the repo
542 if util.safehasattr(self, '_fsmonitorstate'):
570 if util.safehasattr(self, '_fsmonitorstate'):
543 ds.__class__ = makedirstate(ds.__class__)
571 ds.__class__ = makedirstate(ds.__class__)
544 ds._fsmonitorinit(self._fsmonitorstate, self._watchmanclient)
572 ds._fsmonitorinit(self._fsmonitorstate, self._watchmanclient)
545 return ds
573 return ds
546
574
547 def extsetup(ui):
575 def extsetup(ui):
548 wrapfilecache(localrepo.localrepository, 'dirstate', wrapdirstate)
576 wrapfilecache(localrepo.localrepository, 'dirstate', wrapdirstate)
549 if pycompat.sysplatform == 'darwin':
577 if pycompat.sysplatform == 'darwin':
550 # An assist for avoiding the dangling-symlink fsevents bug
578 # An assist for avoiding the dangling-symlink fsevents bug
551 extensions.wrapfunction(os, 'symlink', wrapsymlink)
579 extensions.wrapfunction(os, 'symlink', wrapsymlink)
552
580
553 extensions.wrapfunction(merge, 'update', wrapupdate)
581 extensions.wrapfunction(merge, 'update', wrapupdate)
554
582
555 def wrapsymlink(orig, source, link_name):
583 def wrapsymlink(orig, source, link_name):
556 ''' if we create a dangling symlink, also touch the parent dir
584 ''' if we create a dangling symlink, also touch the parent dir
557 to encourage fsevents notifications to work more correctly '''
585 to encourage fsevents notifications to work more correctly '''
558 try:
586 try:
559 return orig(source, link_name)
587 return orig(source, link_name)
560 finally:
588 finally:
561 try:
589 try:
562 os.utime(os.path.dirname(link_name), None)
590 os.utime(os.path.dirname(link_name), None)
563 except OSError:
591 except OSError:
564 pass
592 pass
565
593
566 class state_update(object):
594 class state_update(object):
567 ''' This context manager is responsible for dispatching the state-enter
595 ''' This context manager is responsible for dispatching the state-enter
568 and state-leave signals to the watchman service '''
596 and state-leave signals to the watchman service '''
569
597
570 def __init__(self, repo, node, distance, partial):
598 def __init__(self, repo, node, distance, partial):
571 self.repo = repo
599 self.repo = repo
572 self.node = node
600 self.node = node
573 self.distance = distance
601 self.distance = distance
574 self.partial = partial
602 self.partial = partial
575
603
576 def __enter__(self):
604 def __enter__(self):
577 self._state('state-enter')
605 self._state('state-enter')
578 return self
606 return self
579
607
580 def __exit__(self, type_, value, tb):
608 def __exit__(self, type_, value, tb):
581 status = 'ok' if type_ is None else 'failed'
609 status = 'ok' if type_ is None else 'failed'
582 self._state('state-leave', status=status)
610 self._state('state-leave', status=status)
583
611
584 def _state(self, cmd, status='ok'):
612 def _state(self, cmd, status='ok'):
585 if not util.safehasattr(self.repo, '_watchmanclient'):
613 if not util.safehasattr(self.repo, '_watchmanclient'):
586 return
614 return
587 try:
615 try:
588 commithash = self.repo[self.node].hex()
616 commithash = self.repo[self.node].hex()
589 self.repo._watchmanclient.command(cmd, {
617 self.repo._watchmanclient.command(cmd, {
590 'name': 'hg.update',
618 'name': 'hg.update',
591 'metadata': {
619 'metadata': {
592 # the target revision
620 # the target revision
593 'rev': commithash,
621 'rev': commithash,
594 # approximate number of commits between current and target
622 # approximate number of commits between current and target
595 'distance': self.distance,
623 'distance': self.distance,
596 # success/failure (only really meaningful for state-leave)
624 # success/failure (only really meaningful for state-leave)
597 'status': status,
625 'status': status,
598 # whether the working copy parent is changing
626 # whether the working copy parent is changing
599 'partial': self.partial,
627 'partial': self.partial,
600 }})
628 }})
601 except Exception as e:
629 except Exception as e:
602 # Swallow any errors; fire and forget
630 # Swallow any errors; fire and forget
603 self.repo.ui.log(
631 self.repo.ui.log(
604 'watchman', 'Exception %s while running %s\n', e, cmd)
632 'watchman', 'Exception %s while running %s\n', e, cmd)
605
633
606 # Bracket working copy updates with calls to the watchman state-enter
634 # Bracket working copy updates with calls to the watchman state-enter
607 # and state-leave commands. This allows clients to perform more intelligent
635 # and state-leave commands. This allows clients to perform more intelligent
608 # settling during bulk file change scenarios
636 # settling during bulk file change scenarios
609 # https://facebook.github.io/watchman/docs/cmd/subscribe.html#advanced-settling
637 # https://facebook.github.io/watchman/docs/cmd/subscribe.html#advanced-settling
610 def wrapupdate(orig, repo, node, branchmerge, force, ancestor=None,
638 def wrapupdate(orig, repo, node, branchmerge, force, ancestor=None,
611 mergeancestor=False, labels=None, matcher=None, **kwargs):
639 mergeancestor=False, labels=None, matcher=None, **kwargs):
612
640
613 distance = 0
641 distance = 0
614 partial = True
642 partial = True
615 if matcher is None or matcher.always():
643 if matcher is None or matcher.always():
616 partial = False
644 partial = False
617 wc = repo[None]
645 wc = repo[None]
618 parents = wc.parents()
646 parents = wc.parents()
619 if len(parents) == 2:
647 if len(parents) == 2:
620 anc = repo.changelog.ancestor(parents[0].node(), parents[1].node())
648 anc = repo.changelog.ancestor(parents[0].node(), parents[1].node())
621 ancrev = repo[anc].rev()
649 ancrev = repo[anc].rev()
622 distance = abs(repo[node].rev() - ancrev)
650 distance = abs(repo[node].rev() - ancrev)
623 elif len(parents) == 1:
651 elif len(parents) == 1:
624 distance = abs(repo[node].rev() - parents[0].rev())
652 distance = abs(repo[node].rev() - parents[0].rev())
625
653
626 with state_update(repo, node, distance, partial):
654 with state_update(repo, node, distance, partial):
627 return orig(
655 return orig(
628 repo, node, branchmerge, force, ancestor, mergeancestor,
656 repo, node, branchmerge, force, ancestor, mergeancestor,
629 labels, matcher, *kwargs)
657 labels, matcher, *kwargs)
630
658
631 def reposetup(ui, repo):
659 def reposetup(ui, repo):
632 # We don't work with largefiles or inotify
660 # We don't work with largefiles or inotify
633 exts = extensions.enabled()
661 exts = extensions.enabled()
634 for ext in _blacklist:
662 for ext in _blacklist:
635 if ext in exts:
663 if ext in exts:
636 ui.warn(_('The fsmonitor extension is incompatible with the %s '
664 ui.warn(_('The fsmonitor extension is incompatible with the %s '
637 'extension and has been disabled.\n') % ext)
665 'extension and has been disabled.\n') % ext)
638 return
666 return
639
667
640 if util.safehasattr(repo, 'dirstate'):
668 if util.safehasattr(repo, 'dirstate'):
641 # We don't work with subrepos either. Note that we can get passed in
669 # We don't work with subrepos either. Note that we can get passed in
642 # e.g. a statichttprepo, which throws on trying to access the substate.
670 # e.g. a statichttprepo, which throws on trying to access the substate.
643 # XXX This sucks.
671 # XXX This sucks.
644 try:
672 try:
645 # if repo[None].substate can cause a dirstate parse, which is too
673 # if repo[None].substate can cause a dirstate parse, which is too
646 # slow. Instead, look for a file called hgsubstate,
674 # slow. Instead, look for a file called hgsubstate,
647 if repo.wvfs.exists('.hgsubstate') or repo.wvfs.exists('.hgsub'):
675 if repo.wvfs.exists('.hgsubstate') or repo.wvfs.exists('.hgsub'):
648 return
676 return
649 except AttributeError:
677 except AttributeError:
650 return
678 return
651
679
652 fsmonitorstate = state.state(repo)
680 fsmonitorstate = state.state(repo)
653 if fsmonitorstate.mode == 'off':
681 if fsmonitorstate.mode == 'off':
654 return
682 return
655
683
656 try:
684 try:
657 client = watchmanclient.client(repo)
685 client = watchmanclient.client(repo)
658 except Exception as ex:
686 except Exception as ex:
659 _handleunavailable(ui, fsmonitorstate, ex)
687 _handleunavailable(ui, fsmonitorstate, ex)
660 return
688 return
661
689
662 repo._fsmonitorstate = fsmonitorstate
690 repo._fsmonitorstate = fsmonitorstate
663 repo._watchmanclient = client
691 repo._watchmanclient = client
664
692
665 # at this point since fsmonitorstate wasn't present, repo.dirstate is
693 # at this point since fsmonitorstate wasn't present, repo.dirstate is
666 # not a fsmonitordirstate
694 # not a fsmonitordirstate
667 repo.dirstate.__class__ = makedirstate(repo.dirstate.__class__)
695 repo.dirstate.__class__ = makedirstate(repo.dirstate.__class__)
668 # nuke the dirstate so that _fsmonitorinit and subsequent configuration
696 # nuke the dirstate so that _fsmonitorinit and subsequent configuration
669 # changes take effect on it
697 # changes take effect on it
670 del repo._filecache['dirstate']
698 del repo._filecache['dirstate']
671 delattr(repo.unfiltered(), 'dirstate')
699 delattr(repo.unfiltered(), 'dirstate')
672
700
673 class fsmonitorrepo(repo.__class__):
701 class fsmonitorrepo(repo.__class__):
674 def status(self, *args, **kwargs):
702 def status(self, *args, **kwargs):
675 orig = super(fsmonitorrepo, self).status
703 orig = super(fsmonitorrepo, self).status
676 return overridestatus(orig, self, *args, **kwargs)
704 return overridestatus(orig, self, *args, **kwargs)
677
705
678 repo.__class__ = fsmonitorrepo
706 repo.__class__ = fsmonitorrepo
679
707
680 def wrapfilecache(cls, propname, wrapper):
708 def wrapfilecache(cls, propname, wrapper):
681 """Wraps a filecache property. These can't be wrapped using the normal
709 """Wraps a filecache property. These can't be wrapped using the normal
682 wrapfunction. This should eventually go into upstream Mercurial.
710 wrapfunction. This should eventually go into upstream Mercurial.
683 """
711 """
684 assert callable(wrapper)
712 assert callable(wrapper)
685 for currcls in cls.__mro__:
713 for currcls in cls.__mro__:
686 if propname in currcls.__dict__:
714 if propname in currcls.__dict__:
687 origfn = currcls.__dict__[propname].func
715 origfn = currcls.__dict__[propname].func
688 assert callable(origfn)
716 assert callable(origfn)
689 def wrap(*args, **kwargs):
717 def wrap(*args, **kwargs):
690 return wrapper(origfn, *args, **kwargs)
718 return wrapper(origfn, *args, **kwargs)
691 currcls.__dict__[propname].func = wrap
719 currcls.__dict__[propname].func = wrap
692 break
720 break
693
721
694 if currcls is object:
722 if currcls is object:
695 raise AttributeError(
723 raise AttributeError(
696 _("type '%s' has no property '%s'") % (cls, propname))
724 _("type '%s' has no property '%s'") % (cls, propname))
@@ -1,44 +1,44 b''
1 #require test-repo
1 #require test-repo
2
2
3 $ . "$TESTDIR/helpers-testrepo.sh"
3 $ . "$TESTDIR/helpers-testrepo.sh"
4 $ cd "$TESTDIR"/..
4 $ cd "$TESTDIR"/..
5
5
6 $ hg files 'set:(**.py)' | sed 's|\\|/|g' | xargs python contrib/check-py3-compat.py
6 $ hg files 'set:(**.py)' | sed 's|\\|/|g' | xargs python contrib/check-py3-compat.py
7 contrib/python-zstandard/setup.py not using absolute_import
7 contrib/python-zstandard/setup.py not using absolute_import
8 contrib/python-zstandard/setup_zstd.py not using absolute_import
8 contrib/python-zstandard/setup_zstd.py not using absolute_import
9 contrib/python-zstandard/tests/common.py not using absolute_import
9 contrib/python-zstandard/tests/common.py not using absolute_import
10 contrib/python-zstandard/tests/test_buffer_util.py not using absolute_import
10 contrib/python-zstandard/tests/test_buffer_util.py not using absolute_import
11 contrib/python-zstandard/tests/test_compressor.py not using absolute_import
11 contrib/python-zstandard/tests/test_compressor.py not using absolute_import
12 contrib/python-zstandard/tests/test_compressor_fuzzing.py not using absolute_import
12 contrib/python-zstandard/tests/test_compressor_fuzzing.py not using absolute_import
13 contrib/python-zstandard/tests/test_data_structures.py not using absolute_import
13 contrib/python-zstandard/tests/test_data_structures.py not using absolute_import
14 contrib/python-zstandard/tests/test_data_structures_fuzzing.py not using absolute_import
14 contrib/python-zstandard/tests/test_data_structures_fuzzing.py not using absolute_import
15 contrib/python-zstandard/tests/test_decompressor.py not using absolute_import
15 contrib/python-zstandard/tests/test_decompressor.py not using absolute_import
16 contrib/python-zstandard/tests/test_decompressor_fuzzing.py not using absolute_import
16 contrib/python-zstandard/tests/test_decompressor_fuzzing.py not using absolute_import
17 contrib/python-zstandard/tests/test_estimate_sizes.py not using absolute_import
17 contrib/python-zstandard/tests/test_estimate_sizes.py not using absolute_import
18 contrib/python-zstandard/tests/test_module_attributes.py not using absolute_import
18 contrib/python-zstandard/tests/test_module_attributes.py not using absolute_import
19 contrib/python-zstandard/tests/test_train_dictionary.py not using absolute_import
19 contrib/python-zstandard/tests/test_train_dictionary.py not using absolute_import
20 i18n/check-translation.py not using absolute_import
20 i18n/check-translation.py not using absolute_import
21 setup.py not using absolute_import
21 setup.py not using absolute_import
22 tests/test-demandimport.py not using absolute_import
22 tests/test-demandimport.py not using absolute_import
23
23
24 #if py3exe
24 #if py3exe
25 $ hg files 'set:(**.py) - grep(pygments)' -X hgext/fsmonitor/pywatchman \
25 $ hg files 'set:(**.py) - grep(pygments)' -X hgext/fsmonitor/pywatchman \
26 > | sed 's|\\|/|g' | xargs $PYTHON3 contrib/check-py3-compat.py \
26 > | sed 's|\\|/|g' | xargs $PYTHON3 contrib/check-py3-compat.py \
27 > | sed 's/[0-9][0-9]*)$/*)/'
27 > | sed 's/[0-9][0-9]*)$/*)/'
28 hgext/convert/transport.py: error importing: <*Error> No module named 'svn.client' (error at transport.py:*) (glob)
28 hgext/convert/transport.py: error importing: <*Error> No module named 'svn.client' (error at transport.py:*) (glob)
29 hgext/fsmonitor/state.py: error importing: <SyntaxError> from __future__ imports must occur at the beginning of the file (__init__.py, line 30) (error at watchmanclient.py:*)
29 hgext/fsmonitor/state.py: error importing: <SyntaxError> from __future__ imports must occur at the beginning of the file (__init__.py, line 30) (error at __init__.py:*)
30 hgext/fsmonitor/watchmanclient.py: error importing: <SyntaxError> from __future__ imports must occur at the beginning of the file (__init__.py, line 30) (error at watchmanclient.py:*)
30 hgext/fsmonitor/watchmanclient.py: error importing: <SyntaxError> from __future__ imports must occur at the beginning of the file (__init__.py, line 30) (error at __init__.py:*)
31 mercurial/cffi/bdiff.py: error importing: <*Error> No module named 'mercurial.cffi' (error at check-py3-compat.py:*) (glob)
31 mercurial/cffi/bdiff.py: error importing: <*Error> No module named 'mercurial.cffi' (error at check-py3-compat.py:*) (glob)
32 mercurial/cffi/mpatch.py: error importing: <*Error> No module named 'mercurial.cffi' (error at check-py3-compat.py:*) (glob)
32 mercurial/cffi/mpatch.py: error importing: <*Error> No module named 'mercurial.cffi' (error at check-py3-compat.py:*) (glob)
33 mercurial/cffi/osutil.py: error importing: <*Error> No module named 'mercurial.cffi' (error at check-py3-compat.py:*) (glob)
33 mercurial/cffi/osutil.py: error importing: <*Error> No module named 'mercurial.cffi' (error at check-py3-compat.py:*) (glob)
34 mercurial/scmwindows.py: error importing: <*Error> No module named 'msvcrt' (error at win32.py:*) (glob)
34 mercurial/scmwindows.py: error importing: <*Error> No module named 'msvcrt' (error at win32.py:*) (glob)
35 mercurial/win32.py: error importing: <*Error> No module named 'msvcrt' (error at win32.py:*) (glob)
35 mercurial/win32.py: error importing: <*Error> No module named 'msvcrt' (error at win32.py:*) (glob)
36 mercurial/windows.py: error importing: <*Error> No module named 'msvcrt' (error at windows.py:*) (glob)
36 mercurial/windows.py: error importing: <*Error> No module named 'msvcrt' (error at windows.py:*) (glob)
37
37
38 #endif
38 #endif
39
39
40 #if py3exe py3pygments
40 #if py3exe py3pygments
41 $ hg files 'set:(**.py) and grep(pygments)' | sed 's|\\|/|g' \
41 $ hg files 'set:(**.py) and grep(pygments)' | sed 's|\\|/|g' \
42 > | xargs $PYTHON3 contrib/check-py3-compat.py \
42 > | xargs $PYTHON3 contrib/check-py3-compat.py \
43 > | sed 's/[0-9][0-9]*)$/*)/'
43 > | sed 's/[0-9][0-9]*)$/*)/'
44 #endif
44 #endif
General Comments 0
You need to be logged in to leave comments. Login now