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