##// END OF EJS Templates
match: write forceincludematcher using unionmatcher...
Martin von Zweigbergk -
r33447:6f4e5e59 default
parent child Browse files
Show More
@@ -1,1027 +1,1013
1 # match.py - filename matching
1 # match.py - filename matching
2 #
2 #
3 # Copyright 2008, 2009 Matt Mackall <mpm@selenic.com> and others
3 # Copyright 2008, 2009 Matt Mackall <mpm@selenic.com> and others
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 from __future__ import absolute_import
8 from __future__ import absolute_import
9
9
10 import copy
10 import copy
11 import os
11 import os
12 import re
12 import re
13
13
14 from .i18n import _
14 from .i18n import _
15 from . import (
15 from . import (
16 error,
16 error,
17 pathutil,
17 pathutil,
18 util,
18 util,
19 )
19 )
20
20
21 propertycache = util.propertycache
21 propertycache = util.propertycache
22
22
23 def _rematcher(regex):
23 def _rematcher(regex):
24 '''compile the regexp with the best available regexp engine and return a
24 '''compile the regexp with the best available regexp engine and return a
25 matcher function'''
25 matcher function'''
26 m = util.re.compile(regex)
26 m = util.re.compile(regex)
27 try:
27 try:
28 # slightly faster, provided by facebook's re2 bindings
28 # slightly faster, provided by facebook's re2 bindings
29 return m.test_match
29 return m.test_match
30 except AttributeError:
30 except AttributeError:
31 return m.match
31 return m.match
32
32
33 def _expandsets(kindpats, ctx, listsubrepos):
33 def _expandsets(kindpats, ctx, listsubrepos):
34 '''Returns the kindpats list with the 'set' patterns expanded.'''
34 '''Returns the kindpats list with the 'set' patterns expanded.'''
35 fset = set()
35 fset = set()
36 other = []
36 other = []
37
37
38 for kind, pat, source in kindpats:
38 for kind, pat, source in kindpats:
39 if kind == 'set':
39 if kind == 'set':
40 if not ctx:
40 if not ctx:
41 raise error.ProgrammingError("fileset expression with no "
41 raise error.ProgrammingError("fileset expression with no "
42 "context")
42 "context")
43 s = ctx.getfileset(pat)
43 s = ctx.getfileset(pat)
44 fset.update(s)
44 fset.update(s)
45
45
46 if listsubrepos:
46 if listsubrepos:
47 for subpath in ctx.substate:
47 for subpath in ctx.substate:
48 s = ctx.sub(subpath).getfileset(pat)
48 s = ctx.sub(subpath).getfileset(pat)
49 fset.update(subpath + '/' + f for f in s)
49 fset.update(subpath + '/' + f for f in s)
50
50
51 continue
51 continue
52 other.append((kind, pat, source))
52 other.append((kind, pat, source))
53 return fset, other
53 return fset, other
54
54
55 def _expandsubinclude(kindpats, root):
55 def _expandsubinclude(kindpats, root):
56 '''Returns the list of subinclude matcher args and the kindpats without the
56 '''Returns the list of subinclude matcher args and the kindpats without the
57 subincludes in it.'''
57 subincludes in it.'''
58 relmatchers = []
58 relmatchers = []
59 other = []
59 other = []
60
60
61 for kind, pat, source in kindpats:
61 for kind, pat, source in kindpats:
62 if kind == 'subinclude':
62 if kind == 'subinclude':
63 sourceroot = pathutil.dirname(util.normpath(source))
63 sourceroot = pathutil.dirname(util.normpath(source))
64 pat = util.pconvert(pat)
64 pat = util.pconvert(pat)
65 path = pathutil.join(sourceroot, pat)
65 path = pathutil.join(sourceroot, pat)
66
66
67 newroot = pathutil.dirname(path)
67 newroot = pathutil.dirname(path)
68 matcherargs = (newroot, '', [], ['include:%s' % path])
68 matcherargs = (newroot, '', [], ['include:%s' % path])
69
69
70 prefix = pathutil.canonpath(root, root, newroot)
70 prefix = pathutil.canonpath(root, root, newroot)
71 if prefix:
71 if prefix:
72 prefix += '/'
72 prefix += '/'
73 relmatchers.append((prefix, matcherargs))
73 relmatchers.append((prefix, matcherargs))
74 else:
74 else:
75 other.append((kind, pat, source))
75 other.append((kind, pat, source))
76
76
77 return relmatchers, other
77 return relmatchers, other
78
78
79 def _kindpatsalwaysmatch(kindpats):
79 def _kindpatsalwaysmatch(kindpats):
80 """"Checks whether the kindspats match everything, as e.g.
80 """"Checks whether the kindspats match everything, as e.g.
81 'relpath:.' does.
81 'relpath:.' does.
82 """
82 """
83 for kind, pat, source in kindpats:
83 for kind, pat, source in kindpats:
84 if pat != '' or kind not in ['relpath', 'glob']:
84 if pat != '' or kind not in ['relpath', 'glob']:
85 return False
85 return False
86 return True
86 return True
87
87
88 def match(root, cwd, patterns=None, include=None, exclude=None, default='glob',
88 def match(root, cwd, patterns=None, include=None, exclude=None, default='glob',
89 exact=False, auditor=None, ctx=None, listsubrepos=False, warn=None,
89 exact=False, auditor=None, ctx=None, listsubrepos=False, warn=None,
90 badfn=None, icasefs=False):
90 badfn=None, icasefs=False):
91 """build an object to match a set of file patterns
91 """build an object to match a set of file patterns
92
92
93 arguments:
93 arguments:
94 root - the canonical root of the tree you're matching against
94 root - the canonical root of the tree you're matching against
95 cwd - the current working directory, if relevant
95 cwd - the current working directory, if relevant
96 patterns - patterns to find
96 patterns - patterns to find
97 include - patterns to include (unless they are excluded)
97 include - patterns to include (unless they are excluded)
98 exclude - patterns to exclude (even if they are included)
98 exclude - patterns to exclude (even if they are included)
99 default - if a pattern in patterns has no explicit type, assume this one
99 default - if a pattern in patterns has no explicit type, assume this one
100 exact - patterns are actually filenames (include/exclude still apply)
100 exact - patterns are actually filenames (include/exclude still apply)
101 warn - optional function used for printing warnings
101 warn - optional function used for printing warnings
102 badfn - optional bad() callback for this matcher instead of the default
102 badfn - optional bad() callback for this matcher instead of the default
103 icasefs - make a matcher for wdir on case insensitive filesystems, which
103 icasefs - make a matcher for wdir on case insensitive filesystems, which
104 normalizes the given patterns to the case in the filesystem
104 normalizes the given patterns to the case in the filesystem
105
105
106 a pattern is one of:
106 a pattern is one of:
107 'glob:<glob>' - a glob relative to cwd
107 'glob:<glob>' - a glob relative to cwd
108 're:<regexp>' - a regular expression
108 're:<regexp>' - a regular expression
109 'path:<path>' - a path relative to repository root, which is matched
109 'path:<path>' - a path relative to repository root, which is matched
110 recursively
110 recursively
111 'rootfilesin:<path>' - a path relative to repository root, which is
111 'rootfilesin:<path>' - a path relative to repository root, which is
112 matched non-recursively (will not match subdirectories)
112 matched non-recursively (will not match subdirectories)
113 'relglob:<glob>' - an unrooted glob (*.c matches C files in all dirs)
113 'relglob:<glob>' - an unrooted glob (*.c matches C files in all dirs)
114 'relpath:<path>' - a path relative to cwd
114 'relpath:<path>' - a path relative to cwd
115 'relre:<regexp>' - a regexp that needn't match the start of a name
115 'relre:<regexp>' - a regexp that needn't match the start of a name
116 'set:<fileset>' - a fileset expression
116 'set:<fileset>' - a fileset expression
117 'include:<path>' - a file of patterns to read and include
117 'include:<path>' - a file of patterns to read and include
118 'subinclude:<path>' - a file of patterns to match against files under
118 'subinclude:<path>' - a file of patterns to match against files under
119 the same directory
119 the same directory
120 '<something>' - a pattern of the specified default type
120 '<something>' - a pattern of the specified default type
121 """
121 """
122 normalize = _donormalize
122 normalize = _donormalize
123 if icasefs:
123 if icasefs:
124 if exact:
124 if exact:
125 raise error.ProgrammingError("a case-insensitive exact matcher "
125 raise error.ProgrammingError("a case-insensitive exact matcher "
126 "doesn't make sense")
126 "doesn't make sense")
127 dirstate = ctx.repo().dirstate
127 dirstate = ctx.repo().dirstate
128 dsnormalize = dirstate.normalize
128 dsnormalize = dirstate.normalize
129
129
130 def normalize(patterns, default, root, cwd, auditor, warn):
130 def normalize(patterns, default, root, cwd, auditor, warn):
131 kp = _donormalize(patterns, default, root, cwd, auditor, warn)
131 kp = _donormalize(patterns, default, root, cwd, auditor, warn)
132 kindpats = []
132 kindpats = []
133 for kind, pats, source in kp:
133 for kind, pats, source in kp:
134 if kind not in ('re', 'relre'): # regex can't be normalized
134 if kind not in ('re', 'relre'): # regex can't be normalized
135 p = pats
135 p = pats
136 pats = dsnormalize(pats)
136 pats = dsnormalize(pats)
137
137
138 # Preserve the original to handle a case only rename.
138 # Preserve the original to handle a case only rename.
139 if p != pats and p in dirstate:
139 if p != pats and p in dirstate:
140 kindpats.append((kind, p, source))
140 kindpats.append((kind, p, source))
141
141
142 kindpats.append((kind, pats, source))
142 kindpats.append((kind, pats, source))
143 return kindpats
143 return kindpats
144
144
145 if exact:
145 if exact:
146 m = exactmatcher(root, cwd, patterns, badfn)
146 m = exactmatcher(root, cwd, patterns, badfn)
147 elif patterns:
147 elif patterns:
148 kindpats = normalize(patterns, default, root, cwd, auditor, warn)
148 kindpats = normalize(patterns, default, root, cwd, auditor, warn)
149 if _kindpatsalwaysmatch(kindpats):
149 if _kindpatsalwaysmatch(kindpats):
150 m = alwaysmatcher(root, cwd, badfn, relativeuipath=True)
150 m = alwaysmatcher(root, cwd, badfn, relativeuipath=True)
151 else:
151 else:
152 m = patternmatcher(root, cwd, kindpats, ctx=ctx,
152 m = patternmatcher(root, cwd, kindpats, ctx=ctx,
153 listsubrepos=listsubrepos, badfn=badfn)
153 listsubrepos=listsubrepos, badfn=badfn)
154 else:
154 else:
155 # It's a little strange that no patterns means to match everything.
155 # It's a little strange that no patterns means to match everything.
156 # Consider changing this to match nothing (probably using nevermatcher).
156 # Consider changing this to match nothing (probably using nevermatcher).
157 m = alwaysmatcher(root, cwd, badfn)
157 m = alwaysmatcher(root, cwd, badfn)
158
158
159 if include:
159 if include:
160 kindpats = normalize(include, 'glob', root, cwd, auditor, warn)
160 kindpats = normalize(include, 'glob', root, cwd, auditor, warn)
161 im = includematcher(root, cwd, kindpats, ctx=ctx,
161 im = includematcher(root, cwd, kindpats, ctx=ctx,
162 listsubrepos=listsubrepos, badfn=None)
162 listsubrepos=listsubrepos, badfn=None)
163 m = intersectmatchers(m, im)
163 m = intersectmatchers(m, im)
164 if exclude:
164 if exclude:
165 kindpats = normalize(exclude, 'glob', root, cwd, auditor, warn)
165 kindpats = normalize(exclude, 'glob', root, cwd, auditor, warn)
166 em = includematcher(root, cwd, kindpats, ctx=ctx,
166 em = includematcher(root, cwd, kindpats, ctx=ctx,
167 listsubrepos=listsubrepos, badfn=None)
167 listsubrepos=listsubrepos, badfn=None)
168 m = differencematcher(m, em)
168 m = differencematcher(m, em)
169 return m
169 return m
170
170
171 def exact(root, cwd, files, badfn=None):
171 def exact(root, cwd, files, badfn=None):
172 return exactmatcher(root, cwd, files, badfn=badfn)
172 return exactmatcher(root, cwd, files, badfn=badfn)
173
173
174 def always(root, cwd):
174 def always(root, cwd):
175 return alwaysmatcher(root, cwd)
175 return alwaysmatcher(root, cwd)
176
176
177 def never(root, cwd):
177 def never(root, cwd):
178 return nevermatcher(root, cwd)
178 return nevermatcher(root, cwd)
179
179
180 def badmatch(match, badfn):
180 def badmatch(match, badfn):
181 """Make a copy of the given matcher, replacing its bad method with the given
181 """Make a copy of the given matcher, replacing its bad method with the given
182 one.
182 one.
183 """
183 """
184 m = copy.copy(match)
184 m = copy.copy(match)
185 m.bad = badfn
185 m.bad = badfn
186 return m
186 return m
187
187
188 def _donormalize(patterns, default, root, cwd, auditor, warn):
188 def _donormalize(patterns, default, root, cwd, auditor, warn):
189 '''Convert 'kind:pat' from the patterns list to tuples with kind and
189 '''Convert 'kind:pat' from the patterns list to tuples with kind and
190 normalized and rooted patterns and with listfiles expanded.'''
190 normalized and rooted patterns and with listfiles expanded.'''
191 kindpats = []
191 kindpats = []
192 for kind, pat in [_patsplit(p, default) for p in patterns]:
192 for kind, pat in [_patsplit(p, default) for p in patterns]:
193 if kind in ('glob', 'relpath'):
193 if kind in ('glob', 'relpath'):
194 pat = pathutil.canonpath(root, cwd, pat, auditor)
194 pat = pathutil.canonpath(root, cwd, pat, auditor)
195 elif kind in ('relglob', 'path', 'rootfilesin'):
195 elif kind in ('relglob', 'path', 'rootfilesin'):
196 pat = util.normpath(pat)
196 pat = util.normpath(pat)
197 elif kind in ('listfile', 'listfile0'):
197 elif kind in ('listfile', 'listfile0'):
198 try:
198 try:
199 files = util.readfile(pat)
199 files = util.readfile(pat)
200 if kind == 'listfile0':
200 if kind == 'listfile0':
201 files = files.split('\0')
201 files = files.split('\0')
202 else:
202 else:
203 files = files.splitlines()
203 files = files.splitlines()
204 files = [f for f in files if f]
204 files = [f for f in files if f]
205 except EnvironmentError:
205 except EnvironmentError:
206 raise error.Abort(_("unable to read file list (%s)") % pat)
206 raise error.Abort(_("unable to read file list (%s)") % pat)
207 for k, p, source in _donormalize(files, default, root, cwd,
207 for k, p, source in _donormalize(files, default, root, cwd,
208 auditor, warn):
208 auditor, warn):
209 kindpats.append((k, p, pat))
209 kindpats.append((k, p, pat))
210 continue
210 continue
211 elif kind == 'include':
211 elif kind == 'include':
212 try:
212 try:
213 fullpath = os.path.join(root, util.localpath(pat))
213 fullpath = os.path.join(root, util.localpath(pat))
214 includepats = readpatternfile(fullpath, warn)
214 includepats = readpatternfile(fullpath, warn)
215 for k, p, source in _donormalize(includepats, default,
215 for k, p, source in _donormalize(includepats, default,
216 root, cwd, auditor, warn):
216 root, cwd, auditor, warn):
217 kindpats.append((k, p, source or pat))
217 kindpats.append((k, p, source or pat))
218 except error.Abort as inst:
218 except error.Abort as inst:
219 raise error.Abort('%s: %s' % (pat, inst[0]))
219 raise error.Abort('%s: %s' % (pat, inst[0]))
220 except IOError as inst:
220 except IOError as inst:
221 if warn:
221 if warn:
222 warn(_("skipping unreadable pattern file '%s': %s\n") %
222 warn(_("skipping unreadable pattern file '%s': %s\n") %
223 (pat, inst.strerror))
223 (pat, inst.strerror))
224 continue
224 continue
225 # else: re or relre - which cannot be normalized
225 # else: re or relre - which cannot be normalized
226 kindpats.append((kind, pat, ''))
226 kindpats.append((kind, pat, ''))
227 return kindpats
227 return kindpats
228
228
229 class basematcher(object):
229 class basematcher(object):
230
230
231 def __init__(self, root, cwd, badfn=None, relativeuipath=True):
231 def __init__(self, root, cwd, badfn=None, relativeuipath=True):
232 self._root = root
232 self._root = root
233 self._cwd = cwd
233 self._cwd = cwd
234 if badfn is not None:
234 if badfn is not None:
235 self.bad = badfn
235 self.bad = badfn
236 self._relativeuipath = relativeuipath
236 self._relativeuipath = relativeuipath
237
237
238 def __call__(self, fn):
238 def __call__(self, fn):
239 return self.matchfn(fn)
239 return self.matchfn(fn)
240 def __iter__(self):
240 def __iter__(self):
241 for f in self._files:
241 for f in self._files:
242 yield f
242 yield f
243 # Callbacks related to how the matcher is used by dirstate.walk.
243 # Callbacks related to how the matcher is used by dirstate.walk.
244 # Subscribers to these events must monkeypatch the matcher object.
244 # Subscribers to these events must monkeypatch the matcher object.
245 def bad(self, f, msg):
245 def bad(self, f, msg):
246 '''Callback from dirstate.walk for each explicit file that can't be
246 '''Callback from dirstate.walk for each explicit file that can't be
247 found/accessed, with an error message.'''
247 found/accessed, with an error message.'''
248 pass
248 pass
249
249
250 # If an explicitdir is set, it will be called when an explicitly listed
250 # If an explicitdir is set, it will be called when an explicitly listed
251 # directory is visited.
251 # directory is visited.
252 explicitdir = None
252 explicitdir = None
253
253
254 # If an traversedir is set, it will be called when a directory discovered
254 # If an traversedir is set, it will be called when a directory discovered
255 # by recursive traversal is visited.
255 # by recursive traversal is visited.
256 traversedir = None
256 traversedir = None
257
257
258 def abs(self, f):
258 def abs(self, f):
259 '''Convert a repo path back to path that is relative to the root of the
259 '''Convert a repo path back to path that is relative to the root of the
260 matcher.'''
260 matcher.'''
261 return f
261 return f
262
262
263 def rel(self, f):
263 def rel(self, f):
264 '''Convert repo path back to path that is relative to cwd of matcher.'''
264 '''Convert repo path back to path that is relative to cwd of matcher.'''
265 return util.pathto(self._root, self._cwd, f)
265 return util.pathto(self._root, self._cwd, f)
266
266
267 def uipath(self, f):
267 def uipath(self, f):
268 '''Convert repo path to a display path. If patterns or -I/-X were used
268 '''Convert repo path to a display path. If patterns or -I/-X were used
269 to create this matcher, the display path will be relative to cwd.
269 to create this matcher, the display path will be relative to cwd.
270 Otherwise it is relative to the root of the repo.'''
270 Otherwise it is relative to the root of the repo.'''
271 return (self._relativeuipath and self.rel(f)) or self.abs(f)
271 return (self._relativeuipath and self.rel(f)) or self.abs(f)
272
272
273 @propertycache
273 @propertycache
274 def _files(self):
274 def _files(self):
275 return []
275 return []
276
276
277 def files(self):
277 def files(self):
278 '''Explicitly listed files or patterns or roots:
278 '''Explicitly listed files or patterns or roots:
279 if no patterns or .always(): empty list,
279 if no patterns or .always(): empty list,
280 if exact: list exact files,
280 if exact: list exact files,
281 if not .anypats(): list all files and dirs,
281 if not .anypats(): list all files and dirs,
282 else: optimal roots'''
282 else: optimal roots'''
283 return self._files
283 return self._files
284
284
285 @propertycache
285 @propertycache
286 def _fileset(self):
286 def _fileset(self):
287 return set(self._files)
287 return set(self._files)
288
288
289 def exact(self, f):
289 def exact(self, f):
290 '''Returns True if f is in .files().'''
290 '''Returns True if f is in .files().'''
291 return f in self._fileset
291 return f in self._fileset
292
292
293 def matchfn(self, f):
293 def matchfn(self, f):
294 return False
294 return False
295
295
296 def visitdir(self, dir):
296 def visitdir(self, dir):
297 '''Decides whether a directory should be visited based on whether it
297 '''Decides whether a directory should be visited based on whether it
298 has potential matches in it or one of its subdirectories. This is
298 has potential matches in it or one of its subdirectories. This is
299 based on the match's primary, included, and excluded patterns.
299 based on the match's primary, included, and excluded patterns.
300
300
301 Returns the string 'all' if the given directory and all subdirectories
301 Returns the string 'all' if the given directory and all subdirectories
302 should be visited. Otherwise returns True or False indicating whether
302 should be visited. Otherwise returns True or False indicating whether
303 the given directory should be visited.
303 the given directory should be visited.
304
304
305 This function's behavior is undefined if it has returned False for
305 This function's behavior is undefined if it has returned False for
306 one of the dir's parent directories.
306 one of the dir's parent directories.
307 '''
307 '''
308 return False
308 return False
309
309
310 def always(self):
310 def always(self):
311 '''Matcher will match everything and .files() will be empty --
311 '''Matcher will match everything and .files() will be empty --
312 optimization might be possible.'''
312 optimization might be possible.'''
313 return False
313 return False
314
314
315 def isexact(self):
315 def isexact(self):
316 '''Matcher will match exactly the list of files in .files() --
316 '''Matcher will match exactly the list of files in .files() --
317 optimization might be possible.'''
317 optimization might be possible.'''
318 return False
318 return False
319
319
320 def prefix(self):
320 def prefix(self):
321 '''Matcher will match the paths in .files() recursively --
321 '''Matcher will match the paths in .files() recursively --
322 optimization might be possible.'''
322 optimization might be possible.'''
323 return False
323 return False
324
324
325 def anypats(self):
325 def anypats(self):
326 '''None of .always(), .isexact(), and .prefix() is true --
326 '''None of .always(), .isexact(), and .prefix() is true --
327 optimizations will be difficult.'''
327 optimizations will be difficult.'''
328 return not self.always() and not self.isexact() and not self.prefix()
328 return not self.always() and not self.isexact() and not self.prefix()
329
329
330 class alwaysmatcher(basematcher):
330 class alwaysmatcher(basematcher):
331 '''Matches everything.'''
331 '''Matches everything.'''
332
332
333 def __init__(self, root, cwd, badfn=None, relativeuipath=False):
333 def __init__(self, root, cwd, badfn=None, relativeuipath=False):
334 super(alwaysmatcher, self).__init__(root, cwd, badfn,
334 super(alwaysmatcher, self).__init__(root, cwd, badfn,
335 relativeuipath=relativeuipath)
335 relativeuipath=relativeuipath)
336
336
337 def always(self):
337 def always(self):
338 return True
338 return True
339
339
340 def matchfn(self, f):
340 def matchfn(self, f):
341 return True
341 return True
342
342
343 def visitdir(self, dir):
343 def visitdir(self, dir):
344 return 'all'
344 return 'all'
345
345
346 def __repr__(self):
346 def __repr__(self):
347 return '<alwaysmatcher>'
347 return '<alwaysmatcher>'
348
348
349 class nevermatcher(basematcher):
349 class nevermatcher(basematcher):
350 '''Matches nothing.'''
350 '''Matches nothing.'''
351
351
352 def __init__(self, root, cwd, badfn=None):
352 def __init__(self, root, cwd, badfn=None):
353 super(nevermatcher, self).__init__(root, cwd, badfn)
353 super(nevermatcher, self).__init__(root, cwd, badfn)
354
354
355 # It's a little weird to say that the nevermatcher is an exact matcher
355 # It's a little weird to say that the nevermatcher is an exact matcher
356 # or a prefix matcher, but it seems to make sense to let callers take
356 # or a prefix matcher, but it seems to make sense to let callers take
357 # fast paths based on either. There will be no exact matches, nor any
357 # fast paths based on either. There will be no exact matches, nor any
358 # prefixes (files() returns []), so fast paths iterating over them should
358 # prefixes (files() returns []), so fast paths iterating over them should
359 # be efficient (and correct).
359 # be efficient (and correct).
360 def isexact(self):
360 def isexact(self):
361 return True
361 return True
362
362
363 def prefix(self):
363 def prefix(self):
364 return True
364 return True
365
365
366 def __repr__(self):
366 def __repr__(self):
367 return '<nevermatcher>'
367 return '<nevermatcher>'
368
368
369 class patternmatcher(basematcher):
369 class patternmatcher(basematcher):
370
370
371 def __init__(self, root, cwd, kindpats, ctx=None, listsubrepos=False,
371 def __init__(self, root, cwd, kindpats, ctx=None, listsubrepos=False,
372 badfn=None):
372 badfn=None):
373 super(patternmatcher, self).__init__(root, cwd, badfn)
373 super(patternmatcher, self).__init__(root, cwd, badfn)
374
374
375 self._files = _explicitfiles(kindpats)
375 self._files = _explicitfiles(kindpats)
376 self._prefix = _prefix(kindpats)
376 self._prefix = _prefix(kindpats)
377 self._pats, self.matchfn = _buildmatch(ctx, kindpats, '$', listsubrepos,
377 self._pats, self.matchfn = _buildmatch(ctx, kindpats, '$', listsubrepos,
378 root)
378 root)
379
379
380 @propertycache
380 @propertycache
381 def _dirs(self):
381 def _dirs(self):
382 return set(util.dirs(self._fileset)) | {'.'}
382 return set(util.dirs(self._fileset)) | {'.'}
383
383
384 def visitdir(self, dir):
384 def visitdir(self, dir):
385 if self._prefix and dir in self._fileset:
385 if self._prefix and dir in self._fileset:
386 return 'all'
386 return 'all'
387 return ('.' in self._fileset or
387 return ('.' in self._fileset or
388 dir in self._fileset or
388 dir in self._fileset or
389 dir in self._dirs or
389 dir in self._dirs or
390 any(parentdir in self._fileset
390 any(parentdir in self._fileset
391 for parentdir in util.finddirs(dir)))
391 for parentdir in util.finddirs(dir)))
392
392
393 def prefix(self):
393 def prefix(self):
394 return self._prefix
394 return self._prefix
395
395
396 def __repr__(self):
396 def __repr__(self):
397 return ('<patternmatcher patterns=%r>' % self._pats)
397 return ('<patternmatcher patterns=%r>' % self._pats)
398
398
399 class includematcher(basematcher):
399 class includematcher(basematcher):
400
400
401 def __init__(self, root, cwd, kindpats, ctx=None, listsubrepos=False,
401 def __init__(self, root, cwd, kindpats, ctx=None, listsubrepos=False,
402 badfn=None):
402 badfn=None):
403 super(includematcher, self).__init__(root, cwd, badfn)
403 super(includematcher, self).__init__(root, cwd, badfn)
404
404
405 self._pats, self.matchfn = _buildmatch(ctx, kindpats, '(?:/|$)',
405 self._pats, self.matchfn = _buildmatch(ctx, kindpats, '(?:/|$)',
406 listsubrepos, root)
406 listsubrepos, root)
407 self._prefix = _prefix(kindpats)
407 self._prefix = _prefix(kindpats)
408 roots, dirs = _rootsanddirs(kindpats)
408 roots, dirs = _rootsanddirs(kindpats)
409 # roots are directories which are recursively included.
409 # roots are directories which are recursively included.
410 self._roots = set(roots)
410 self._roots = set(roots)
411 # dirs are directories which are non-recursively included.
411 # dirs are directories which are non-recursively included.
412 self._dirs = set(dirs)
412 self._dirs = set(dirs)
413
413
414 def visitdir(self, dir):
414 def visitdir(self, dir):
415 if self._prefix and dir in self._roots:
415 if self._prefix and dir in self._roots:
416 return 'all'
416 return 'all'
417 return ('.' in self._roots or
417 return ('.' in self._roots or
418 dir in self._roots or
418 dir in self._roots or
419 dir in self._dirs or
419 dir in self._dirs or
420 any(parentdir in self._roots
420 any(parentdir in self._roots
421 for parentdir in util.finddirs(dir)))
421 for parentdir in util.finddirs(dir)))
422
422
423 def __repr__(self):
423 def __repr__(self):
424 return ('<includematcher includes=%r>' % self._pats)
424 return ('<includematcher includes=%r>' % self._pats)
425
425
426 class exactmatcher(basematcher):
426 class exactmatcher(basematcher):
427 '''Matches the input files exactly. They are interpreted as paths, not
427 '''Matches the input files exactly. They are interpreted as paths, not
428 patterns (so no kind-prefixes).
428 patterns (so no kind-prefixes).
429 '''
429 '''
430
430
431 def __init__(self, root, cwd, files, badfn=None):
431 def __init__(self, root, cwd, files, badfn=None):
432 super(exactmatcher, self).__init__(root, cwd, badfn)
432 super(exactmatcher, self).__init__(root, cwd, badfn)
433
433
434 if isinstance(files, list):
434 if isinstance(files, list):
435 self._files = files
435 self._files = files
436 else:
436 else:
437 self._files = list(files)
437 self._files = list(files)
438
438
439 matchfn = basematcher.exact
439 matchfn = basematcher.exact
440
440
441 @propertycache
441 @propertycache
442 def _dirs(self):
442 def _dirs(self):
443 return set(util.dirs(self._fileset)) | {'.'}
443 return set(util.dirs(self._fileset)) | {'.'}
444
444
445 def visitdir(self, dir):
445 def visitdir(self, dir):
446 return dir in self._dirs
446 return dir in self._dirs
447
447
448 def isexact(self):
448 def isexact(self):
449 return True
449 return True
450
450
451 def __repr__(self):
451 def __repr__(self):
452 return ('<exactmatcher files=%r>' % self._files)
452 return ('<exactmatcher files=%r>' % self._files)
453
453
454 class differencematcher(basematcher):
454 class differencematcher(basematcher):
455 '''Composes two matchers by matching if the first matches and the second
455 '''Composes two matchers by matching if the first matches and the second
456 does not. Well, almost... If the user provides a pattern like "-X foo foo",
456 does not. Well, almost... If the user provides a pattern like "-X foo foo",
457 Mercurial actually does match "foo" against that. That's because exact
457 Mercurial actually does match "foo" against that. That's because exact
458 matches are treated specially. So, since this differencematcher is used for
458 matches are treated specially. So, since this differencematcher is used for
459 excludes, it needs to special-case exact matching.
459 excludes, it needs to special-case exact matching.
460
460
461 The second matcher's non-matching-attributes (root, cwd, bad, explicitdir,
461 The second matcher's non-matching-attributes (root, cwd, bad, explicitdir,
462 traversedir) are ignored.
462 traversedir) are ignored.
463
463
464 TODO: If we want to keep the behavior described above for exact matches, we
464 TODO: If we want to keep the behavior described above for exact matches, we
465 should consider instead treating the above case something like this:
465 should consider instead treating the above case something like this:
466 union(exact(foo), difference(pattern(foo), include(foo)))
466 union(exact(foo), difference(pattern(foo), include(foo)))
467 '''
467 '''
468 def __init__(self, m1, m2):
468 def __init__(self, m1, m2):
469 super(differencematcher, self).__init__(m1._root, m1._cwd)
469 super(differencematcher, self).__init__(m1._root, m1._cwd)
470 self._m1 = m1
470 self._m1 = m1
471 self._m2 = m2
471 self._m2 = m2
472 self.bad = m1.bad
472 self.bad = m1.bad
473 self.explicitdir = m1.explicitdir
473 self.explicitdir = m1.explicitdir
474 self.traversedir = m1.traversedir
474 self.traversedir = m1.traversedir
475
475
476 def matchfn(self, f):
476 def matchfn(self, f):
477 return self._m1(f) and (not self._m2(f) or self._m1.exact(f))
477 return self._m1(f) and (not self._m2(f) or self._m1.exact(f))
478
478
479 @propertycache
479 @propertycache
480 def _files(self):
480 def _files(self):
481 if self.isexact():
481 if self.isexact():
482 return [f for f in self._m1.files() if self(f)]
482 return [f for f in self._m1.files() if self(f)]
483 # If m1 is not an exact matcher, we can't easily figure out the set of
483 # If m1 is not an exact matcher, we can't easily figure out the set of
484 # files, because its files() are not always files. For example, if
484 # files, because its files() are not always files. For example, if
485 # m1 is "path:dir" and m2 is "rootfileins:.", we don't
485 # m1 is "path:dir" and m2 is "rootfileins:.", we don't
486 # want to remove "dir" from the set even though it would match m2,
486 # want to remove "dir" from the set even though it would match m2,
487 # because the "dir" in m1 may not be a file.
487 # because the "dir" in m1 may not be a file.
488 return self._m1.files()
488 return self._m1.files()
489
489
490 def visitdir(self, dir):
490 def visitdir(self, dir):
491 if self._m2.visitdir(dir) == 'all':
491 if self._m2.visitdir(dir) == 'all':
492 # There's a bug here: If m1 matches file 'dir/file' and m2 excludes
492 # There's a bug here: If m1 matches file 'dir/file' and m2 excludes
493 # 'dir' (recursively), we should still visit 'dir' due to the
493 # 'dir' (recursively), we should still visit 'dir' due to the
494 # exception we have for exact matches.
494 # exception we have for exact matches.
495 return False
495 return False
496 return bool(self._m1.visitdir(dir))
496 return bool(self._m1.visitdir(dir))
497
497
498 def isexact(self):
498 def isexact(self):
499 return self._m1.isexact()
499 return self._m1.isexact()
500
500
501 def __repr__(self):
501 def __repr__(self):
502 return ('<differencematcher m1=%r, m2=%r>' % (self._m1, self._m2))
502 return ('<differencematcher m1=%r, m2=%r>' % (self._m1, self._m2))
503
503
504 def intersectmatchers(m1, m2):
504 def intersectmatchers(m1, m2):
505 '''Composes two matchers by matching if both of them match.
505 '''Composes two matchers by matching if both of them match.
506
506
507 The second matcher's non-matching-attributes (root, cwd, bad, explicitdir,
507 The second matcher's non-matching-attributes (root, cwd, bad, explicitdir,
508 traversedir) are ignored.
508 traversedir) are ignored.
509 '''
509 '''
510 if m1 is None or m2 is None:
510 if m1 is None or m2 is None:
511 return m1 or m2
511 return m1 or m2
512 if m1.always():
512 if m1.always():
513 m = copy.copy(m2)
513 m = copy.copy(m2)
514 # TODO: Consider encapsulating these things in a class so there's only
514 # TODO: Consider encapsulating these things in a class so there's only
515 # one thing to copy from m1.
515 # one thing to copy from m1.
516 m.bad = m1.bad
516 m.bad = m1.bad
517 m.explicitdir = m1.explicitdir
517 m.explicitdir = m1.explicitdir
518 m.traversedir = m1.traversedir
518 m.traversedir = m1.traversedir
519 m.abs = m1.abs
519 m.abs = m1.abs
520 m.rel = m1.rel
520 m.rel = m1.rel
521 m._relativeuipath |= m1._relativeuipath
521 m._relativeuipath |= m1._relativeuipath
522 return m
522 return m
523 if m2.always():
523 if m2.always():
524 m = copy.copy(m1)
524 m = copy.copy(m1)
525 m._relativeuipath |= m2._relativeuipath
525 m._relativeuipath |= m2._relativeuipath
526 return m
526 return m
527 return intersectionmatcher(m1, m2)
527 return intersectionmatcher(m1, m2)
528
528
529 class intersectionmatcher(basematcher):
529 class intersectionmatcher(basematcher):
530 def __init__(self, m1, m2):
530 def __init__(self, m1, m2):
531 super(intersectionmatcher, self).__init__(m1._root, m1._cwd)
531 super(intersectionmatcher, self).__init__(m1._root, m1._cwd)
532 self._m1 = m1
532 self._m1 = m1
533 self._m2 = m2
533 self._m2 = m2
534 self.bad = m1.bad
534 self.bad = m1.bad
535 self.explicitdir = m1.explicitdir
535 self.explicitdir = m1.explicitdir
536 self.traversedir = m1.traversedir
536 self.traversedir = m1.traversedir
537
537
538 @propertycache
538 @propertycache
539 def _files(self):
539 def _files(self):
540 if self.isexact():
540 if self.isexact():
541 m1, m2 = self._m1, self._m2
541 m1, m2 = self._m1, self._m2
542 if not m1.isexact():
542 if not m1.isexact():
543 m1, m2 = m2, m1
543 m1, m2 = m2, m1
544 return [f for f in m1.files() if m2(f)]
544 return [f for f in m1.files() if m2(f)]
545 # It neither m1 nor m2 is an exact matcher, we can't easily intersect
545 # It neither m1 nor m2 is an exact matcher, we can't easily intersect
546 # the set of files, because their files() are not always files. For
546 # the set of files, because their files() are not always files. For
547 # example, if intersecting a matcher "-I glob:foo.txt" with matcher of
547 # example, if intersecting a matcher "-I glob:foo.txt" with matcher of
548 # "path:dir2", we don't want to remove "dir2" from the set.
548 # "path:dir2", we don't want to remove "dir2" from the set.
549 return self._m1.files() + self._m2.files()
549 return self._m1.files() + self._m2.files()
550
550
551 def matchfn(self, f):
551 def matchfn(self, f):
552 return self._m1(f) and self._m2(f)
552 return self._m1(f) and self._m2(f)
553
553
554 def visitdir(self, dir):
554 def visitdir(self, dir):
555 visit1 = self._m1.visitdir(dir)
555 visit1 = self._m1.visitdir(dir)
556 if visit1 == 'all':
556 if visit1 == 'all':
557 return self._m2.visitdir(dir)
557 return self._m2.visitdir(dir)
558 # bool() because visit1=True + visit2='all' should not be 'all'
558 # bool() because visit1=True + visit2='all' should not be 'all'
559 return bool(visit1 and self._m2.visitdir(dir))
559 return bool(visit1 and self._m2.visitdir(dir))
560
560
561 def always(self):
561 def always(self):
562 return self._m1.always() and self._m2.always()
562 return self._m1.always() and self._m2.always()
563
563
564 def isexact(self):
564 def isexact(self):
565 return self._m1.isexact() or self._m2.isexact()
565 return self._m1.isexact() or self._m2.isexact()
566
566
567 def __repr__(self):
567 def __repr__(self):
568 return ('<intersectionmatcher m1=%r, m2=%r>' % (self._m1, self._m2))
568 return ('<intersectionmatcher m1=%r, m2=%r>' % (self._m1, self._m2))
569
569
570 class subdirmatcher(basematcher):
570 class subdirmatcher(basematcher):
571 """Adapt a matcher to work on a subdirectory only.
571 """Adapt a matcher to work on a subdirectory only.
572
572
573 The paths are remapped to remove/insert the path as needed:
573 The paths are remapped to remove/insert the path as needed:
574
574
575 >>> m1 = match('root', '', ['a.txt', 'sub/b.txt'])
575 >>> m1 = match('root', '', ['a.txt', 'sub/b.txt'])
576 >>> m2 = subdirmatcher('sub', m1)
576 >>> m2 = subdirmatcher('sub', m1)
577 >>> bool(m2('a.txt'))
577 >>> bool(m2('a.txt'))
578 False
578 False
579 >>> bool(m2('b.txt'))
579 >>> bool(m2('b.txt'))
580 True
580 True
581 >>> bool(m2.matchfn('a.txt'))
581 >>> bool(m2.matchfn('a.txt'))
582 False
582 False
583 >>> bool(m2.matchfn('b.txt'))
583 >>> bool(m2.matchfn('b.txt'))
584 True
584 True
585 >>> m2.files()
585 >>> m2.files()
586 ['b.txt']
586 ['b.txt']
587 >>> m2.exact('b.txt')
587 >>> m2.exact('b.txt')
588 True
588 True
589 >>> util.pconvert(m2.rel('b.txt'))
589 >>> util.pconvert(m2.rel('b.txt'))
590 'sub/b.txt'
590 'sub/b.txt'
591 >>> def bad(f, msg):
591 >>> def bad(f, msg):
592 ... print "%s: %s" % (f, msg)
592 ... print "%s: %s" % (f, msg)
593 >>> m1.bad = bad
593 >>> m1.bad = bad
594 >>> m2.bad('x.txt', 'No such file')
594 >>> m2.bad('x.txt', 'No such file')
595 sub/x.txt: No such file
595 sub/x.txt: No such file
596 >>> m2.abs('c.txt')
596 >>> m2.abs('c.txt')
597 'sub/c.txt'
597 'sub/c.txt'
598 """
598 """
599
599
600 def __init__(self, path, matcher):
600 def __init__(self, path, matcher):
601 super(subdirmatcher, self).__init__(matcher._root, matcher._cwd)
601 super(subdirmatcher, self).__init__(matcher._root, matcher._cwd)
602 self._path = path
602 self._path = path
603 self._matcher = matcher
603 self._matcher = matcher
604 self._always = matcher.always()
604 self._always = matcher.always()
605
605
606 self._files = [f[len(path) + 1:] for f in matcher._files
606 self._files = [f[len(path) + 1:] for f in matcher._files
607 if f.startswith(path + "/")]
607 if f.startswith(path + "/")]
608
608
609 # If the parent repo had a path to this subrepo and the matcher is
609 # If the parent repo had a path to this subrepo and the matcher is
610 # a prefix matcher, this submatcher always matches.
610 # a prefix matcher, this submatcher always matches.
611 if matcher.prefix():
611 if matcher.prefix():
612 self._always = any(f == path for f in matcher._files)
612 self._always = any(f == path for f in matcher._files)
613
613
614 def bad(self, f, msg):
614 def bad(self, f, msg):
615 self._matcher.bad(self._path + "/" + f, msg)
615 self._matcher.bad(self._path + "/" + f, msg)
616
616
617 def abs(self, f):
617 def abs(self, f):
618 return self._matcher.abs(self._path + "/" + f)
618 return self._matcher.abs(self._path + "/" + f)
619
619
620 def rel(self, f):
620 def rel(self, f):
621 return self._matcher.rel(self._path + "/" + f)
621 return self._matcher.rel(self._path + "/" + f)
622
622
623 def uipath(self, f):
623 def uipath(self, f):
624 return self._matcher.uipath(self._path + "/" + f)
624 return self._matcher.uipath(self._path + "/" + f)
625
625
626 def matchfn(self, f):
626 def matchfn(self, f):
627 # Some information is lost in the superclass's constructor, so we
627 # Some information is lost in the superclass's constructor, so we
628 # can not accurately create the matching function for the subdirectory
628 # can not accurately create the matching function for the subdirectory
629 # from the inputs. Instead, we override matchfn() and visitdir() to
629 # from the inputs. Instead, we override matchfn() and visitdir() to
630 # call the original matcher with the subdirectory path prepended.
630 # call the original matcher with the subdirectory path prepended.
631 return self._matcher.matchfn(self._path + "/" + f)
631 return self._matcher.matchfn(self._path + "/" + f)
632
632
633 def visitdir(self, dir):
633 def visitdir(self, dir):
634 if dir == '.':
634 if dir == '.':
635 dir = self._path
635 dir = self._path
636 else:
636 else:
637 dir = self._path + "/" + dir
637 dir = self._path + "/" + dir
638 return self._matcher.visitdir(dir)
638 return self._matcher.visitdir(dir)
639
639
640 def always(self):
640 def always(self):
641 return self._always
641 return self._always
642
642
643 def prefix(self):
643 def prefix(self):
644 return self._matcher.prefix() and not self._always
644 return self._matcher.prefix() and not self._always
645
645
646 def __repr__(self):
646 def __repr__(self):
647 return ('<subdirmatcher path=%r, matcher=%r>' %
647 return ('<subdirmatcher path=%r, matcher=%r>' %
648 (self._path, self._matcher))
648 (self._path, self._matcher))
649
649
650 class forceincludematcher(basematcher):
651 """A matcher that returns true for any of the forced includes before testing
652 against the actual matcher."""
653 def __init__(self, matcher, includes):
654 self._matcher = matcher
655 self._includes = includes
656
657 def matchfn(self, f):
658 return f in self._includes or self._matcher(f)
659
660 def __repr__(self):
661 return ('<forceincludematcher matcher=%r, includes=%r>' %
662 (self._matcher, sorted(self._includes)))
663
664 class unionmatcher(basematcher):
650 class unionmatcher(basematcher):
665 """A matcher that is the union of several matchers."""
651 """A matcher that is the union of several matchers."""
666 def __init__(self, matchers):
652 def __init__(self, matchers):
667 self._matchers = matchers
653 self._matchers = matchers
668
654
669 def matchfn(self, f):
655 def matchfn(self, f):
670 for match in self._matchers:
656 for match in self._matchers:
671 if match(f):
657 if match(f):
672 return True
658 return True
673 return False
659 return False
674
660
675 def __repr__(self):
661 def __repr__(self):
676 return ('<unionmatcher matchers=%r>' % self._matchers)
662 return ('<unionmatcher matchers=%r>' % self._matchers)
677
663
678 class negatematcher(basematcher):
664 class negatematcher(basematcher):
679 def __init__(self, matcher):
665 def __init__(self, matcher):
680 self._matcher = matcher
666 self._matcher = matcher
681
667
682 def matchfn(self, f):
668 def matchfn(self, f):
683 return not self._matcher(f)
669 return not self._matcher(f)
684
670
685 def __repr__(self):
671 def __repr__(self):
686 return ('<negatematcher matcher=%r>' % self._matcher)
672 return ('<negatematcher matcher=%r>' % self._matcher)
687
673
688 def patkind(pattern, default=None):
674 def patkind(pattern, default=None):
689 '''If pattern is 'kind:pat' with a known kind, return kind.'''
675 '''If pattern is 'kind:pat' with a known kind, return kind.'''
690 return _patsplit(pattern, default)[0]
676 return _patsplit(pattern, default)[0]
691
677
692 def _patsplit(pattern, default):
678 def _patsplit(pattern, default):
693 """Split a string into the optional pattern kind prefix and the actual
679 """Split a string into the optional pattern kind prefix and the actual
694 pattern."""
680 pattern."""
695 if ':' in pattern:
681 if ':' in pattern:
696 kind, pat = pattern.split(':', 1)
682 kind, pat = pattern.split(':', 1)
697 if kind in ('re', 'glob', 'path', 'relglob', 'relpath', 'relre',
683 if kind in ('re', 'glob', 'path', 'relglob', 'relpath', 'relre',
698 'listfile', 'listfile0', 'set', 'include', 'subinclude',
684 'listfile', 'listfile0', 'set', 'include', 'subinclude',
699 'rootfilesin'):
685 'rootfilesin'):
700 return kind, pat
686 return kind, pat
701 return default, pattern
687 return default, pattern
702
688
703 def _globre(pat):
689 def _globre(pat):
704 r'''Convert an extended glob string to a regexp string.
690 r'''Convert an extended glob string to a regexp string.
705
691
706 >>> print _globre(r'?')
692 >>> print _globre(r'?')
707 .
693 .
708 >>> print _globre(r'*')
694 >>> print _globre(r'*')
709 [^/]*
695 [^/]*
710 >>> print _globre(r'**')
696 >>> print _globre(r'**')
711 .*
697 .*
712 >>> print _globre(r'**/a')
698 >>> print _globre(r'**/a')
713 (?:.*/)?a
699 (?:.*/)?a
714 >>> print _globre(r'a/**/b')
700 >>> print _globre(r'a/**/b')
715 a\/(?:.*/)?b
701 a\/(?:.*/)?b
716 >>> print _globre(r'[a*?!^][^b][!c]')
702 >>> print _globre(r'[a*?!^][^b][!c]')
717 [a*?!^][\^b][^c]
703 [a*?!^][\^b][^c]
718 >>> print _globre(r'{a,b}')
704 >>> print _globre(r'{a,b}')
719 (?:a|b)
705 (?:a|b)
720 >>> print _globre(r'.\*\?')
706 >>> print _globre(r'.\*\?')
721 \.\*\?
707 \.\*\?
722 '''
708 '''
723 i, n = 0, len(pat)
709 i, n = 0, len(pat)
724 res = ''
710 res = ''
725 group = 0
711 group = 0
726 escape = util.re.escape
712 escape = util.re.escape
727 def peek():
713 def peek():
728 return i < n and pat[i:i + 1]
714 return i < n and pat[i:i + 1]
729 while i < n:
715 while i < n:
730 c = pat[i:i + 1]
716 c = pat[i:i + 1]
731 i += 1
717 i += 1
732 if c not in '*?[{},\\':
718 if c not in '*?[{},\\':
733 res += escape(c)
719 res += escape(c)
734 elif c == '*':
720 elif c == '*':
735 if peek() == '*':
721 if peek() == '*':
736 i += 1
722 i += 1
737 if peek() == '/':
723 if peek() == '/':
738 i += 1
724 i += 1
739 res += '(?:.*/)?'
725 res += '(?:.*/)?'
740 else:
726 else:
741 res += '.*'
727 res += '.*'
742 else:
728 else:
743 res += '[^/]*'
729 res += '[^/]*'
744 elif c == '?':
730 elif c == '?':
745 res += '.'
731 res += '.'
746 elif c == '[':
732 elif c == '[':
747 j = i
733 j = i
748 if j < n and pat[j:j + 1] in '!]':
734 if j < n and pat[j:j + 1] in '!]':
749 j += 1
735 j += 1
750 while j < n and pat[j:j + 1] != ']':
736 while j < n and pat[j:j + 1] != ']':
751 j += 1
737 j += 1
752 if j >= n:
738 if j >= n:
753 res += '\\['
739 res += '\\['
754 else:
740 else:
755 stuff = pat[i:j].replace('\\','\\\\')
741 stuff = pat[i:j].replace('\\','\\\\')
756 i = j + 1
742 i = j + 1
757 if stuff[0:1] == '!':
743 if stuff[0:1] == '!':
758 stuff = '^' + stuff[1:]
744 stuff = '^' + stuff[1:]
759 elif stuff[0:1] == '^':
745 elif stuff[0:1] == '^':
760 stuff = '\\' + stuff
746 stuff = '\\' + stuff
761 res = '%s[%s]' % (res, stuff)
747 res = '%s[%s]' % (res, stuff)
762 elif c == '{':
748 elif c == '{':
763 group += 1
749 group += 1
764 res += '(?:'
750 res += '(?:'
765 elif c == '}' and group:
751 elif c == '}' and group:
766 res += ')'
752 res += ')'
767 group -= 1
753 group -= 1
768 elif c == ',' and group:
754 elif c == ',' and group:
769 res += '|'
755 res += '|'
770 elif c == '\\':
756 elif c == '\\':
771 p = peek()
757 p = peek()
772 if p:
758 if p:
773 i += 1
759 i += 1
774 res += escape(p)
760 res += escape(p)
775 else:
761 else:
776 res += escape(c)
762 res += escape(c)
777 else:
763 else:
778 res += escape(c)
764 res += escape(c)
779 return res
765 return res
780
766
781 def _regex(kind, pat, globsuffix):
767 def _regex(kind, pat, globsuffix):
782 '''Convert a (normalized) pattern of any kind into a regular expression.
768 '''Convert a (normalized) pattern of any kind into a regular expression.
783 globsuffix is appended to the regexp of globs.'''
769 globsuffix is appended to the regexp of globs.'''
784 if not pat:
770 if not pat:
785 return ''
771 return ''
786 if kind == 're':
772 if kind == 're':
787 return pat
773 return pat
788 if kind in ('path', 'relpath'):
774 if kind in ('path', 'relpath'):
789 if pat == '.':
775 if pat == '.':
790 return ''
776 return ''
791 return util.re.escape(pat) + '(?:/|$)'
777 return util.re.escape(pat) + '(?:/|$)'
792 if kind == 'rootfilesin':
778 if kind == 'rootfilesin':
793 if pat == '.':
779 if pat == '.':
794 escaped = ''
780 escaped = ''
795 else:
781 else:
796 # Pattern is a directory name.
782 # Pattern is a directory name.
797 escaped = util.re.escape(pat) + '/'
783 escaped = util.re.escape(pat) + '/'
798 # Anything after the pattern must be a non-directory.
784 # Anything after the pattern must be a non-directory.
799 return escaped + '[^/]+$'
785 return escaped + '[^/]+$'
800 if kind == 'relglob':
786 if kind == 'relglob':
801 return '(?:|.*/)' + _globre(pat) + globsuffix
787 return '(?:|.*/)' + _globre(pat) + globsuffix
802 if kind == 'relre':
788 if kind == 'relre':
803 if pat.startswith('^'):
789 if pat.startswith('^'):
804 return pat
790 return pat
805 return '.*' + pat
791 return '.*' + pat
806 return _globre(pat) + globsuffix
792 return _globre(pat) + globsuffix
807
793
808 def _buildmatch(ctx, kindpats, globsuffix, listsubrepos, root):
794 def _buildmatch(ctx, kindpats, globsuffix, listsubrepos, root):
809 '''Return regexp string and a matcher function for kindpats.
795 '''Return regexp string and a matcher function for kindpats.
810 globsuffix is appended to the regexp of globs.'''
796 globsuffix is appended to the regexp of globs.'''
811 matchfuncs = []
797 matchfuncs = []
812
798
813 subincludes, kindpats = _expandsubinclude(kindpats, root)
799 subincludes, kindpats = _expandsubinclude(kindpats, root)
814 if subincludes:
800 if subincludes:
815 submatchers = {}
801 submatchers = {}
816 def matchsubinclude(f):
802 def matchsubinclude(f):
817 for prefix, matcherargs in subincludes:
803 for prefix, matcherargs in subincludes:
818 if f.startswith(prefix):
804 if f.startswith(prefix):
819 mf = submatchers.get(prefix)
805 mf = submatchers.get(prefix)
820 if mf is None:
806 if mf is None:
821 mf = match(*matcherargs)
807 mf = match(*matcherargs)
822 submatchers[prefix] = mf
808 submatchers[prefix] = mf
823
809
824 if mf(f[len(prefix):]):
810 if mf(f[len(prefix):]):
825 return True
811 return True
826 return False
812 return False
827 matchfuncs.append(matchsubinclude)
813 matchfuncs.append(matchsubinclude)
828
814
829 fset, kindpats = _expandsets(kindpats, ctx, listsubrepos)
815 fset, kindpats = _expandsets(kindpats, ctx, listsubrepos)
830 if fset:
816 if fset:
831 matchfuncs.append(fset.__contains__)
817 matchfuncs.append(fset.__contains__)
832
818
833 regex = ''
819 regex = ''
834 if kindpats:
820 if kindpats:
835 regex, mf = _buildregexmatch(kindpats, globsuffix)
821 regex, mf = _buildregexmatch(kindpats, globsuffix)
836 matchfuncs.append(mf)
822 matchfuncs.append(mf)
837
823
838 if len(matchfuncs) == 1:
824 if len(matchfuncs) == 1:
839 return regex, matchfuncs[0]
825 return regex, matchfuncs[0]
840 else:
826 else:
841 return regex, lambda f: any(mf(f) for mf in matchfuncs)
827 return regex, lambda f: any(mf(f) for mf in matchfuncs)
842
828
843 def _buildregexmatch(kindpats, globsuffix):
829 def _buildregexmatch(kindpats, globsuffix):
844 """Build a match function from a list of kinds and kindpats,
830 """Build a match function from a list of kinds and kindpats,
845 return regexp string and a matcher function."""
831 return regexp string and a matcher function."""
846 try:
832 try:
847 regex = '(?:%s)' % '|'.join([_regex(k, p, globsuffix)
833 regex = '(?:%s)' % '|'.join([_regex(k, p, globsuffix)
848 for (k, p, s) in kindpats])
834 for (k, p, s) in kindpats])
849 if len(regex) > 20000:
835 if len(regex) > 20000:
850 raise OverflowError
836 raise OverflowError
851 return regex, _rematcher(regex)
837 return regex, _rematcher(regex)
852 except OverflowError:
838 except OverflowError:
853 # We're using a Python with a tiny regex engine and we
839 # We're using a Python with a tiny regex engine and we
854 # made it explode, so we'll divide the pattern list in two
840 # made it explode, so we'll divide the pattern list in two
855 # until it works
841 # until it works
856 l = len(kindpats)
842 l = len(kindpats)
857 if l < 2:
843 if l < 2:
858 raise
844 raise
859 regexa, a = _buildregexmatch(kindpats[:l//2], globsuffix)
845 regexa, a = _buildregexmatch(kindpats[:l//2], globsuffix)
860 regexb, b = _buildregexmatch(kindpats[l//2:], globsuffix)
846 regexb, b = _buildregexmatch(kindpats[l//2:], globsuffix)
861 return regex, lambda s: a(s) or b(s)
847 return regex, lambda s: a(s) or b(s)
862 except re.error:
848 except re.error:
863 for k, p, s in kindpats:
849 for k, p, s in kindpats:
864 try:
850 try:
865 _rematcher('(?:%s)' % _regex(k, p, globsuffix))
851 _rematcher('(?:%s)' % _regex(k, p, globsuffix))
866 except re.error:
852 except re.error:
867 if s:
853 if s:
868 raise error.Abort(_("%s: invalid pattern (%s): %s") %
854 raise error.Abort(_("%s: invalid pattern (%s): %s") %
869 (s, k, p))
855 (s, k, p))
870 else:
856 else:
871 raise error.Abort(_("invalid pattern (%s): %s") % (k, p))
857 raise error.Abort(_("invalid pattern (%s): %s") % (k, p))
872 raise error.Abort(_("invalid pattern"))
858 raise error.Abort(_("invalid pattern"))
873
859
874 def _patternrootsanddirs(kindpats):
860 def _patternrootsanddirs(kindpats):
875 '''Returns roots and directories corresponding to each pattern.
861 '''Returns roots and directories corresponding to each pattern.
876
862
877 This calculates the roots and directories exactly matching the patterns and
863 This calculates the roots and directories exactly matching the patterns and
878 returns a tuple of (roots, dirs) for each. It does not return other
864 returns a tuple of (roots, dirs) for each. It does not return other
879 directories which may also need to be considered, like the parent
865 directories which may also need to be considered, like the parent
880 directories.
866 directories.
881 '''
867 '''
882 r = []
868 r = []
883 d = []
869 d = []
884 for kind, pat, source in kindpats:
870 for kind, pat, source in kindpats:
885 if kind == 'glob': # find the non-glob prefix
871 if kind == 'glob': # find the non-glob prefix
886 root = []
872 root = []
887 for p in pat.split('/'):
873 for p in pat.split('/'):
888 if '[' in p or '{' in p or '*' in p or '?' in p:
874 if '[' in p or '{' in p or '*' in p or '?' in p:
889 break
875 break
890 root.append(p)
876 root.append(p)
891 r.append('/'.join(root) or '.')
877 r.append('/'.join(root) or '.')
892 elif kind in ('relpath', 'path'):
878 elif kind in ('relpath', 'path'):
893 r.append(pat or '.')
879 r.append(pat or '.')
894 elif kind in ('rootfilesin',):
880 elif kind in ('rootfilesin',):
895 d.append(pat or '.')
881 d.append(pat or '.')
896 else: # relglob, re, relre
882 else: # relglob, re, relre
897 r.append('.')
883 r.append('.')
898 return r, d
884 return r, d
899
885
900 def _roots(kindpats):
886 def _roots(kindpats):
901 '''Returns root directories to match recursively from the given patterns.'''
887 '''Returns root directories to match recursively from the given patterns.'''
902 roots, dirs = _patternrootsanddirs(kindpats)
888 roots, dirs = _patternrootsanddirs(kindpats)
903 return roots
889 return roots
904
890
905 def _rootsanddirs(kindpats):
891 def _rootsanddirs(kindpats):
906 '''Returns roots and exact directories from patterns.
892 '''Returns roots and exact directories from patterns.
907
893
908 roots are directories to match recursively, whereas exact directories should
894 roots are directories to match recursively, whereas exact directories should
909 be matched non-recursively. The returned (roots, dirs) tuple will also
895 be matched non-recursively. The returned (roots, dirs) tuple will also
910 include directories that need to be implicitly considered as either, such as
896 include directories that need to be implicitly considered as either, such as
911 parent directories.
897 parent directories.
912
898
913 >>> _rootsanddirs(\
899 >>> _rootsanddirs(\
914 [('glob', 'g/h/*', ''), ('glob', 'g/h', ''), ('glob', 'g*', '')])
900 [('glob', 'g/h/*', ''), ('glob', 'g/h', ''), ('glob', 'g*', '')])
915 (['g/h', 'g/h', '.'], ['g', '.'])
901 (['g/h', 'g/h', '.'], ['g', '.'])
916 >>> _rootsanddirs(\
902 >>> _rootsanddirs(\
917 [('rootfilesin', 'g/h', ''), ('rootfilesin', '', '')])
903 [('rootfilesin', 'g/h', ''), ('rootfilesin', '', '')])
918 ([], ['g/h', '.', 'g', '.'])
904 ([], ['g/h', '.', 'g', '.'])
919 >>> _rootsanddirs(\
905 >>> _rootsanddirs(\
920 [('relpath', 'r', ''), ('path', 'p/p', ''), ('path', '', '')])
906 [('relpath', 'r', ''), ('path', 'p/p', ''), ('path', '', '')])
921 (['r', 'p/p', '.'], ['p', '.'])
907 (['r', 'p/p', '.'], ['p', '.'])
922 >>> _rootsanddirs(\
908 >>> _rootsanddirs(\
923 [('relglob', 'rg*', ''), ('re', 're/', ''), ('relre', 'rr', '')])
909 [('relglob', 'rg*', ''), ('re', 're/', ''), ('relre', 'rr', '')])
924 (['.', '.', '.'], ['.'])
910 (['.', '.', '.'], ['.'])
925 '''
911 '''
926 r, d = _patternrootsanddirs(kindpats)
912 r, d = _patternrootsanddirs(kindpats)
927
913
928 # Append the parents as non-recursive/exact directories, since they must be
914 # Append the parents as non-recursive/exact directories, since they must be
929 # scanned to get to either the roots or the other exact directories.
915 # scanned to get to either the roots or the other exact directories.
930 d.extend(util.dirs(d))
916 d.extend(util.dirs(d))
931 d.extend(util.dirs(r))
917 d.extend(util.dirs(r))
932 # util.dirs() does not include the root directory, so add it manually
918 # util.dirs() does not include the root directory, so add it manually
933 d.append('.')
919 d.append('.')
934
920
935 return r, d
921 return r, d
936
922
937 def _explicitfiles(kindpats):
923 def _explicitfiles(kindpats):
938 '''Returns the potential explicit filenames from the patterns.
924 '''Returns the potential explicit filenames from the patterns.
939
925
940 >>> _explicitfiles([('path', 'foo/bar', '')])
926 >>> _explicitfiles([('path', 'foo/bar', '')])
941 ['foo/bar']
927 ['foo/bar']
942 >>> _explicitfiles([('rootfilesin', 'foo/bar', '')])
928 >>> _explicitfiles([('rootfilesin', 'foo/bar', '')])
943 []
929 []
944 '''
930 '''
945 # Keep only the pattern kinds where one can specify filenames (vs only
931 # Keep only the pattern kinds where one can specify filenames (vs only
946 # directory names).
932 # directory names).
947 filable = [kp for kp in kindpats if kp[0] not in ('rootfilesin',)]
933 filable = [kp for kp in kindpats if kp[0] not in ('rootfilesin',)]
948 return _roots(filable)
934 return _roots(filable)
949
935
950 def _prefix(kindpats):
936 def _prefix(kindpats):
951 '''Whether all the patterns match a prefix (i.e. recursively)'''
937 '''Whether all the patterns match a prefix (i.e. recursively)'''
952 for kind, pat, source in kindpats:
938 for kind, pat, source in kindpats:
953 if kind not in ('path', 'relpath'):
939 if kind not in ('path', 'relpath'):
954 return False
940 return False
955 return True
941 return True
956
942
957 _commentre = None
943 _commentre = None
958
944
959 def readpatternfile(filepath, warn, sourceinfo=False):
945 def readpatternfile(filepath, warn, sourceinfo=False):
960 '''parse a pattern file, returning a list of
946 '''parse a pattern file, returning a list of
961 patterns. These patterns should be given to compile()
947 patterns. These patterns should be given to compile()
962 to be validated and converted into a match function.
948 to be validated and converted into a match function.
963
949
964 trailing white space is dropped.
950 trailing white space is dropped.
965 the escape character is backslash.
951 the escape character is backslash.
966 comments start with #.
952 comments start with #.
967 empty lines are skipped.
953 empty lines are skipped.
968
954
969 lines can be of the following formats:
955 lines can be of the following formats:
970
956
971 syntax: regexp # defaults following lines to non-rooted regexps
957 syntax: regexp # defaults following lines to non-rooted regexps
972 syntax: glob # defaults following lines to non-rooted globs
958 syntax: glob # defaults following lines to non-rooted globs
973 re:pattern # non-rooted regular expression
959 re:pattern # non-rooted regular expression
974 glob:pattern # non-rooted glob
960 glob:pattern # non-rooted glob
975 pattern # pattern of the current default type
961 pattern # pattern of the current default type
976
962
977 if sourceinfo is set, returns a list of tuples:
963 if sourceinfo is set, returns a list of tuples:
978 (pattern, lineno, originalline). This is useful to debug ignore patterns.
964 (pattern, lineno, originalline). This is useful to debug ignore patterns.
979 '''
965 '''
980
966
981 syntaxes = {'re': 'relre:', 'regexp': 'relre:', 'glob': 'relglob:',
967 syntaxes = {'re': 'relre:', 'regexp': 'relre:', 'glob': 'relglob:',
982 'include': 'include', 'subinclude': 'subinclude'}
968 'include': 'include', 'subinclude': 'subinclude'}
983 syntax = 'relre:'
969 syntax = 'relre:'
984 patterns = []
970 patterns = []
985
971
986 fp = open(filepath, 'rb')
972 fp = open(filepath, 'rb')
987 for lineno, line in enumerate(util.iterfile(fp), start=1):
973 for lineno, line in enumerate(util.iterfile(fp), start=1):
988 if "#" in line:
974 if "#" in line:
989 global _commentre
975 global _commentre
990 if not _commentre:
976 if not _commentre:
991 _commentre = util.re.compile(br'((?:^|[^\\])(?:\\\\)*)#.*')
977 _commentre = util.re.compile(br'((?:^|[^\\])(?:\\\\)*)#.*')
992 # remove comments prefixed by an even number of escapes
978 # remove comments prefixed by an even number of escapes
993 m = _commentre.search(line)
979 m = _commentre.search(line)
994 if m:
980 if m:
995 line = line[:m.end(1)]
981 line = line[:m.end(1)]
996 # fixup properly escaped comments that survived the above
982 # fixup properly escaped comments that survived the above
997 line = line.replace("\\#", "#")
983 line = line.replace("\\#", "#")
998 line = line.rstrip()
984 line = line.rstrip()
999 if not line:
985 if not line:
1000 continue
986 continue
1001
987
1002 if line.startswith('syntax:'):
988 if line.startswith('syntax:'):
1003 s = line[7:].strip()
989 s = line[7:].strip()
1004 try:
990 try:
1005 syntax = syntaxes[s]
991 syntax = syntaxes[s]
1006 except KeyError:
992 except KeyError:
1007 if warn:
993 if warn:
1008 warn(_("%s: ignoring invalid syntax '%s'\n") %
994 warn(_("%s: ignoring invalid syntax '%s'\n") %
1009 (filepath, s))
995 (filepath, s))
1010 continue
996 continue
1011
997
1012 linesyntax = syntax
998 linesyntax = syntax
1013 for s, rels in syntaxes.iteritems():
999 for s, rels in syntaxes.iteritems():
1014 if line.startswith(rels):
1000 if line.startswith(rels):
1015 linesyntax = rels
1001 linesyntax = rels
1016 line = line[len(rels):]
1002 line = line[len(rels):]
1017 break
1003 break
1018 elif line.startswith(s+':'):
1004 elif line.startswith(s+':'):
1019 linesyntax = rels
1005 linesyntax = rels
1020 line = line[len(s) + 1:]
1006 line = line[len(s) + 1:]
1021 break
1007 break
1022 if sourceinfo:
1008 if sourceinfo:
1023 patterns.append((linesyntax + line, lineno, line))
1009 patterns.append((linesyntax + line, lineno, line))
1024 else:
1010 else:
1025 patterns.append(linesyntax + line)
1011 patterns.append(linesyntax + line)
1026 fp.close()
1012 fp.close()
1027 return patterns
1013 return patterns
@@ -1,671 +1,678
1 # sparse.py - functionality for sparse checkouts
1 # sparse.py - functionality for sparse checkouts
2 #
2 #
3 # Copyright 2014 Facebook, Inc.
3 # Copyright 2014 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 from __future__ import absolute_import
8 from __future__ import absolute_import
9
9
10 import collections
10 import collections
11 import hashlib
11 import hashlib
12 import os
12 import os
13
13
14 from .i18n import _
14 from .i18n import _
15 from .node import nullid
15 from .node import nullid
16 from . import (
16 from . import (
17 error,
17 error,
18 match as matchmod,
18 match as matchmod,
19 merge as mergemod,
19 merge as mergemod,
20 pycompat,
20 pycompat,
21 util,
21 util,
22 )
22 )
23
23
24 # Whether sparse features are enabled. This variable is intended to be
24 # Whether sparse features are enabled. This variable is intended to be
25 # temporary to facilitate porting sparse to core. It should eventually be
25 # temporary to facilitate porting sparse to core. It should eventually be
26 # a per-repo option, possibly a repo requirement.
26 # a per-repo option, possibly a repo requirement.
27 enabled = False
27 enabled = False
28
28
29 def parseconfig(ui, raw):
29 def parseconfig(ui, raw):
30 """Parse sparse config file content.
30 """Parse sparse config file content.
31
31
32 Returns a tuple of includes, excludes, and profiles.
32 Returns a tuple of includes, excludes, and profiles.
33 """
33 """
34 includes = set()
34 includes = set()
35 excludes = set()
35 excludes = set()
36 current = includes
36 current = includes
37 profiles = []
37 profiles = []
38 for line in raw.split('\n'):
38 for line in raw.split('\n'):
39 line = line.strip()
39 line = line.strip()
40 if not line or line.startswith('#'):
40 if not line or line.startswith('#'):
41 # empty or comment line, skip
41 # empty or comment line, skip
42 continue
42 continue
43 elif line.startswith('%include '):
43 elif line.startswith('%include '):
44 line = line[9:].strip()
44 line = line[9:].strip()
45 if line:
45 if line:
46 profiles.append(line)
46 profiles.append(line)
47 elif line == '[include]':
47 elif line == '[include]':
48 if current != includes:
48 if current != includes:
49 # TODO pass filename into this API so we can report it.
49 # TODO pass filename into this API so we can report it.
50 raise error.Abort(_('sparse config cannot have includes ' +
50 raise error.Abort(_('sparse config cannot have includes ' +
51 'after excludes'))
51 'after excludes'))
52 continue
52 continue
53 elif line == '[exclude]':
53 elif line == '[exclude]':
54 current = excludes
54 current = excludes
55 elif line:
55 elif line:
56 if line.strip().startswith('/'):
56 if line.strip().startswith('/'):
57 ui.warn(_('warning: sparse profile cannot use' +
57 ui.warn(_('warning: sparse profile cannot use' +
58 ' paths starting with /, ignoring %s\n') % line)
58 ' paths starting with /, ignoring %s\n') % line)
59 continue
59 continue
60 current.add(line)
60 current.add(line)
61
61
62 return includes, excludes, profiles
62 return includes, excludes, profiles
63
63
64 # Exists as separate function to facilitate monkeypatching.
64 # Exists as separate function to facilitate monkeypatching.
65 def readprofile(repo, profile, changeid):
65 def readprofile(repo, profile, changeid):
66 """Resolve the raw content of a sparse profile file."""
66 """Resolve the raw content of a sparse profile file."""
67 # TODO add some kind of cache here because this incurs a manifest
67 # TODO add some kind of cache here because this incurs a manifest
68 # resolve and can be slow.
68 # resolve and can be slow.
69 return repo.filectx(profile, changeid=changeid).data()
69 return repo.filectx(profile, changeid=changeid).data()
70
70
71 def patternsforrev(repo, rev):
71 def patternsforrev(repo, rev):
72 """Obtain sparse checkout patterns for the given rev.
72 """Obtain sparse checkout patterns for the given rev.
73
73
74 Returns a tuple of iterables representing includes, excludes, and
74 Returns a tuple of iterables representing includes, excludes, and
75 patterns.
75 patterns.
76 """
76 """
77 # Feature isn't enabled. No-op.
77 # Feature isn't enabled. No-op.
78 if not enabled:
78 if not enabled:
79 return set(), set(), []
79 return set(), set(), []
80
80
81 raw = repo.vfs.tryread('sparse')
81 raw = repo.vfs.tryread('sparse')
82 if not raw:
82 if not raw:
83 return set(), set(), []
83 return set(), set(), []
84
84
85 if rev is None:
85 if rev is None:
86 raise error.Abort(_('cannot parse sparse patterns from working '
86 raise error.Abort(_('cannot parse sparse patterns from working '
87 'directory'))
87 'directory'))
88
88
89 includes, excludes, profiles = parseconfig(repo.ui, raw)
89 includes, excludes, profiles = parseconfig(repo.ui, raw)
90 ctx = repo[rev]
90 ctx = repo[rev]
91
91
92 if profiles:
92 if profiles:
93 visited = set()
93 visited = set()
94 while profiles:
94 while profiles:
95 profile = profiles.pop()
95 profile = profiles.pop()
96 if profile in visited:
96 if profile in visited:
97 continue
97 continue
98
98
99 visited.add(profile)
99 visited.add(profile)
100
100
101 try:
101 try:
102 raw = readprofile(repo, profile, rev)
102 raw = readprofile(repo, profile, rev)
103 except error.ManifestLookupError:
103 except error.ManifestLookupError:
104 msg = (
104 msg = (
105 "warning: sparse profile '%s' not found "
105 "warning: sparse profile '%s' not found "
106 "in rev %s - ignoring it\n" % (profile, ctx))
106 "in rev %s - ignoring it\n" % (profile, ctx))
107 # experimental config: sparse.missingwarning
107 # experimental config: sparse.missingwarning
108 if repo.ui.configbool(
108 if repo.ui.configbool(
109 'sparse', 'missingwarning', True):
109 'sparse', 'missingwarning', True):
110 repo.ui.warn(msg)
110 repo.ui.warn(msg)
111 else:
111 else:
112 repo.ui.debug(msg)
112 repo.ui.debug(msg)
113 continue
113 continue
114
114
115 pincludes, pexcludes, subprofs = parseconfig(repo.ui, raw)
115 pincludes, pexcludes, subprofs = parseconfig(repo.ui, raw)
116 includes.update(pincludes)
116 includes.update(pincludes)
117 excludes.update(pexcludes)
117 excludes.update(pexcludes)
118 for subprofile in subprofs:
118 for subprofile in subprofs:
119 profiles.append(subprofile)
119 profiles.append(subprofile)
120
120
121 profiles = visited
121 profiles = visited
122
122
123 if includes:
123 if includes:
124 includes.add('.hg*')
124 includes.add('.hg*')
125
125
126 return includes, excludes, profiles
126 return includes, excludes, profiles
127
127
128 def activeconfig(repo):
128 def activeconfig(repo):
129 """Determine the active sparse config rules.
129 """Determine the active sparse config rules.
130
130
131 Rules are constructed by reading the current sparse config and bringing in
131 Rules are constructed by reading the current sparse config and bringing in
132 referenced profiles from parents of the working directory.
132 referenced profiles from parents of the working directory.
133 """
133 """
134 revs = [repo.changelog.rev(node) for node in
134 revs = [repo.changelog.rev(node) for node in
135 repo.dirstate.parents() if node != nullid]
135 repo.dirstate.parents() if node != nullid]
136
136
137 allincludes = set()
137 allincludes = set()
138 allexcludes = set()
138 allexcludes = set()
139 allprofiles = set()
139 allprofiles = set()
140
140
141 for rev in revs:
141 for rev in revs:
142 includes, excludes, profiles = patternsforrev(repo, rev)
142 includes, excludes, profiles = patternsforrev(repo, rev)
143 allincludes |= includes
143 allincludes |= includes
144 allexcludes |= excludes
144 allexcludes |= excludes
145 allprofiles |= set(profiles)
145 allprofiles |= set(profiles)
146
146
147 return allincludes, allexcludes, allprofiles
147 return allincludes, allexcludes, allprofiles
148
148
149 def configsignature(repo, includetemp=True):
149 def configsignature(repo, includetemp=True):
150 """Obtain the signature string for the current sparse configuration.
150 """Obtain the signature string for the current sparse configuration.
151
151
152 This is used to construct a cache key for matchers.
152 This is used to construct a cache key for matchers.
153 """
153 """
154 cache = repo._sparsesignaturecache
154 cache = repo._sparsesignaturecache
155
155
156 signature = cache.get('signature')
156 signature = cache.get('signature')
157
157
158 if includetemp:
158 if includetemp:
159 tempsignature = cache.get('tempsignature')
159 tempsignature = cache.get('tempsignature')
160 else:
160 else:
161 tempsignature = '0'
161 tempsignature = '0'
162
162
163 if signature is None or (includetemp and tempsignature is None):
163 if signature is None or (includetemp and tempsignature is None):
164 signature = hashlib.sha1(repo.vfs.tryread('sparse')).hexdigest()
164 signature = hashlib.sha1(repo.vfs.tryread('sparse')).hexdigest()
165 cache['signature'] = signature
165 cache['signature'] = signature
166
166
167 if includetemp:
167 if includetemp:
168 raw = repo.vfs.tryread('tempsparse')
168 raw = repo.vfs.tryread('tempsparse')
169 tempsignature = hashlib.sha1(raw).hexdigest()
169 tempsignature = hashlib.sha1(raw).hexdigest()
170 cache['tempsignature'] = tempsignature
170 cache['tempsignature'] = tempsignature
171
171
172 return '%s %s' % (signature, tempsignature)
172 return '%s %s' % (signature, tempsignature)
173
173
174 def writeconfig(repo, includes, excludes, profiles):
174 def writeconfig(repo, includes, excludes, profiles):
175 """Write the sparse config file given a sparse configuration."""
175 """Write the sparse config file given a sparse configuration."""
176 with repo.vfs('sparse', 'wb') as fh:
176 with repo.vfs('sparse', 'wb') as fh:
177 for p in sorted(profiles):
177 for p in sorted(profiles):
178 fh.write('%%include %s\n' % p)
178 fh.write('%%include %s\n' % p)
179
179
180 if includes:
180 if includes:
181 fh.write('[include]\n')
181 fh.write('[include]\n')
182 for i in sorted(includes):
182 for i in sorted(includes):
183 fh.write(i)
183 fh.write(i)
184 fh.write('\n')
184 fh.write('\n')
185
185
186 if excludes:
186 if excludes:
187 fh.write('[exclude]\n')
187 fh.write('[exclude]\n')
188 for e in sorted(excludes):
188 for e in sorted(excludes):
189 fh.write(e)
189 fh.write(e)
190 fh.write('\n')
190 fh.write('\n')
191
191
192 repo._sparsesignaturecache.clear()
192 repo._sparsesignaturecache.clear()
193
193
194 def readtemporaryincludes(repo):
194 def readtemporaryincludes(repo):
195 raw = repo.vfs.tryread('tempsparse')
195 raw = repo.vfs.tryread('tempsparse')
196 if not raw:
196 if not raw:
197 return set()
197 return set()
198
198
199 return set(raw.split('\n'))
199 return set(raw.split('\n'))
200
200
201 def writetemporaryincludes(repo, includes):
201 def writetemporaryincludes(repo, includes):
202 repo.vfs.write('tempsparse', '\n'.join(sorted(includes)))
202 repo.vfs.write('tempsparse', '\n'.join(sorted(includes)))
203 repo._sparsesignaturecache.clear()
203 repo._sparsesignaturecache.clear()
204
204
205 def addtemporaryincludes(repo, additional):
205 def addtemporaryincludes(repo, additional):
206 includes = readtemporaryincludes(repo)
206 includes = readtemporaryincludes(repo)
207 for i in additional:
207 for i in additional:
208 includes.add(i)
208 includes.add(i)
209 writetemporaryincludes(repo, includes)
209 writetemporaryincludes(repo, includes)
210
210
211 def prunetemporaryincludes(repo):
211 def prunetemporaryincludes(repo):
212 if not enabled or not repo.vfs.exists('tempsparse'):
212 if not enabled or not repo.vfs.exists('tempsparse'):
213 return
213 return
214
214
215 s = repo.status()
215 s = repo.status()
216 if s.modified or s.added or s.removed or s.deleted:
216 if s.modified or s.added or s.removed or s.deleted:
217 # Still have pending changes. Don't bother trying to prune.
217 # Still have pending changes. Don't bother trying to prune.
218 return
218 return
219
219
220 sparsematch = matcher(repo, includetemp=False)
220 sparsematch = matcher(repo, includetemp=False)
221 dirstate = repo.dirstate
221 dirstate = repo.dirstate
222 actions = []
222 actions = []
223 dropped = []
223 dropped = []
224 tempincludes = readtemporaryincludes(repo)
224 tempincludes = readtemporaryincludes(repo)
225 for file in tempincludes:
225 for file in tempincludes:
226 if file in dirstate and not sparsematch(file):
226 if file in dirstate and not sparsematch(file):
227 message = _('dropping temporarily included sparse files')
227 message = _('dropping temporarily included sparse files')
228 actions.append((file, None, message))
228 actions.append((file, None, message))
229 dropped.append(file)
229 dropped.append(file)
230
230
231 typeactions = collections.defaultdict(list)
231 typeactions = collections.defaultdict(list)
232 typeactions['r'] = actions
232 typeactions['r'] = actions
233 mergemod.applyupdates(repo, typeactions, repo[None], repo['.'], False)
233 mergemod.applyupdates(repo, typeactions, repo[None], repo['.'], False)
234
234
235 # Fix dirstate
235 # Fix dirstate
236 for file in dropped:
236 for file in dropped:
237 dirstate.drop(file)
237 dirstate.drop(file)
238
238
239 repo.vfs.unlink('tempsparse')
239 repo.vfs.unlink('tempsparse')
240 repo._sparsesignaturecache.clear()
240 repo._sparsesignaturecache.clear()
241 msg = _('cleaned up %d temporarily added file(s) from the '
241 msg = _('cleaned up %d temporarily added file(s) from the '
242 'sparse checkout\n')
242 'sparse checkout\n')
243 repo.ui.status(msg % len(tempincludes))
243 repo.ui.status(msg % len(tempincludes))
244
244
245 def forceincludematcher(matcher, includes):
246 """Returns a matcher that returns true for any of the forced includes
247 before testing against the actual matcher."""
248 kindpats = [('path', include, '') for include in includes]
249 includematcher = matchmod.includematcher('', '', kindpats)
250 return matchmod.unionmatcher([includematcher, matcher])
251
245 def matcher(repo, revs=None, includetemp=True):
252 def matcher(repo, revs=None, includetemp=True):
246 """Obtain a matcher for sparse working directories for the given revs.
253 """Obtain a matcher for sparse working directories for the given revs.
247
254
248 If multiple revisions are specified, the matcher is the union of all
255 If multiple revisions are specified, the matcher is the union of all
249 revs.
256 revs.
250
257
251 ``includetemp`` indicates whether to use the temporary sparse profile.
258 ``includetemp`` indicates whether to use the temporary sparse profile.
252 """
259 """
253 # If sparse isn't enabled, sparse matcher matches everything.
260 # If sparse isn't enabled, sparse matcher matches everything.
254 if not enabled:
261 if not enabled:
255 return matchmod.always(repo.root, '')
262 return matchmod.always(repo.root, '')
256
263
257 if not revs or revs == [None]:
264 if not revs or revs == [None]:
258 revs = [repo.changelog.rev(node)
265 revs = [repo.changelog.rev(node)
259 for node in repo.dirstate.parents() if node != nullid]
266 for node in repo.dirstate.parents() if node != nullid]
260
267
261 signature = configsignature(repo, includetemp=includetemp)
268 signature = configsignature(repo, includetemp=includetemp)
262
269
263 key = '%s %s' % (signature, ' '.join(map(pycompat.bytestr, revs)))
270 key = '%s %s' % (signature, ' '.join(map(pycompat.bytestr, revs)))
264
271
265 result = repo._sparsematchercache.get(key)
272 result = repo._sparsematchercache.get(key)
266 if result:
273 if result:
267 return result
274 return result
268
275
269 matchers = []
276 matchers = []
270 for rev in revs:
277 for rev in revs:
271 try:
278 try:
272 includes, excludes, profiles = patternsforrev(repo, rev)
279 includes, excludes, profiles = patternsforrev(repo, rev)
273
280
274 if includes or excludes:
281 if includes or excludes:
275 # Explicitly include subdirectories of includes so
282 # Explicitly include subdirectories of includes so
276 # status will walk them down to the actual include.
283 # status will walk them down to the actual include.
277 subdirs = set()
284 subdirs = set()
278 for include in includes:
285 for include in includes:
279 # TODO consider using posix path functions here so Windows
286 # TODO consider using posix path functions here so Windows
280 # \ directory separators don't come into play.
287 # \ directory separators don't come into play.
281 dirname = os.path.dirname(include)
288 dirname = os.path.dirname(include)
282 # basename is used to avoid issues with absolute
289 # basename is used to avoid issues with absolute
283 # paths (which on Windows can include the drive).
290 # paths (which on Windows can include the drive).
284 while os.path.basename(dirname):
291 while os.path.basename(dirname):
285 subdirs.add(dirname)
292 subdirs.add(dirname)
286 dirname = os.path.dirname(dirname)
293 dirname = os.path.dirname(dirname)
287
294
288 matcher = matchmod.match(repo.root, '', [],
295 matcher = matchmod.match(repo.root, '', [],
289 include=includes, exclude=excludes,
296 include=includes, exclude=excludes,
290 default='relpath')
297 default='relpath')
291 if subdirs:
298 if subdirs:
292 matcher = matchmod.forceincludematcher(matcher, subdirs)
299 matcher = forceincludematcher(matcher, subdirs)
293 matchers.append(matcher)
300 matchers.append(matcher)
294 except IOError:
301 except IOError:
295 pass
302 pass
296
303
297 if not matchers:
304 if not matchers:
298 result = matchmod.always(repo.root, '')
305 result = matchmod.always(repo.root, '')
299 elif len(matchers) == 1:
306 elif len(matchers) == 1:
300 result = matchers[0]
307 result = matchers[0]
301 else:
308 else:
302 result = matchmod.unionmatcher(matchers)
309 result = matchmod.unionmatcher(matchers)
303
310
304 if includetemp:
311 if includetemp:
305 tempincludes = readtemporaryincludes(repo)
312 tempincludes = readtemporaryincludes(repo)
306 result = matchmod.forceincludematcher(result, tempincludes)
313 result = forceincludematcher(result, tempincludes)
307
314
308 repo._sparsematchercache[key] = result
315 repo._sparsematchercache[key] = result
309
316
310 return result
317 return result
311
318
312 def filterupdatesactions(repo, wctx, mctx, branchmerge, actions):
319 def filterupdatesactions(repo, wctx, mctx, branchmerge, actions):
313 """Filter updates to only lay out files that match the sparse rules."""
320 """Filter updates to only lay out files that match the sparse rules."""
314 if not enabled:
321 if not enabled:
315 return actions
322 return actions
316
323
317 oldrevs = [pctx.rev() for pctx in wctx.parents()]
324 oldrevs = [pctx.rev() for pctx in wctx.parents()]
318 oldsparsematch = matcher(repo, oldrevs)
325 oldsparsematch = matcher(repo, oldrevs)
319
326
320 if oldsparsematch.always():
327 if oldsparsematch.always():
321 return actions
328 return actions
322
329
323 files = set()
330 files = set()
324 prunedactions = {}
331 prunedactions = {}
325
332
326 if branchmerge:
333 if branchmerge:
327 # If we're merging, use the wctx filter, since we're merging into
334 # If we're merging, use the wctx filter, since we're merging into
328 # the wctx.
335 # the wctx.
329 sparsematch = matcher(repo, [wctx.parents()[0].rev()])
336 sparsematch = matcher(repo, [wctx.parents()[0].rev()])
330 else:
337 else:
331 # If we're updating, use the target context's filter, since we're
338 # If we're updating, use the target context's filter, since we're
332 # moving to the target context.
339 # moving to the target context.
333 sparsematch = matcher(repo, [mctx.rev()])
340 sparsematch = matcher(repo, [mctx.rev()])
334
341
335 temporaryfiles = []
342 temporaryfiles = []
336 for file, action in actions.iteritems():
343 for file, action in actions.iteritems():
337 type, args, msg = action
344 type, args, msg = action
338 files.add(file)
345 files.add(file)
339 if sparsematch(file):
346 if sparsematch(file):
340 prunedactions[file] = action
347 prunedactions[file] = action
341 elif type == 'm':
348 elif type == 'm':
342 temporaryfiles.append(file)
349 temporaryfiles.append(file)
343 prunedactions[file] = action
350 prunedactions[file] = action
344 elif branchmerge:
351 elif branchmerge:
345 if type != 'k':
352 if type != 'k':
346 temporaryfiles.append(file)
353 temporaryfiles.append(file)
347 prunedactions[file] = action
354 prunedactions[file] = action
348 elif type == 'f':
355 elif type == 'f':
349 prunedactions[file] = action
356 prunedactions[file] = action
350 elif file in wctx:
357 elif file in wctx:
351 prunedactions[file] = ('r', args, msg)
358 prunedactions[file] = ('r', args, msg)
352
359
353 if len(temporaryfiles) > 0:
360 if len(temporaryfiles) > 0:
354 repo.ui.status(_('temporarily included %d file(s) in the sparse '
361 repo.ui.status(_('temporarily included %d file(s) in the sparse '
355 'checkout for merging\n') % len(temporaryfiles))
362 'checkout for merging\n') % len(temporaryfiles))
356 addtemporaryincludes(repo, temporaryfiles)
363 addtemporaryincludes(repo, temporaryfiles)
357
364
358 # Add the new files to the working copy so they can be merged, etc
365 # Add the new files to the working copy so they can be merged, etc
359 actions = []
366 actions = []
360 message = 'temporarily adding to sparse checkout'
367 message = 'temporarily adding to sparse checkout'
361 wctxmanifest = repo[None].manifest()
368 wctxmanifest = repo[None].manifest()
362 for file in temporaryfiles:
369 for file in temporaryfiles:
363 if file in wctxmanifest:
370 if file in wctxmanifest:
364 fctx = repo[None][file]
371 fctx = repo[None][file]
365 actions.append((file, (fctx.flags(), False), message))
372 actions.append((file, (fctx.flags(), False), message))
366
373
367 typeactions = collections.defaultdict(list)
374 typeactions = collections.defaultdict(list)
368 typeactions['g'] = actions
375 typeactions['g'] = actions
369 mergemod.applyupdates(repo, typeactions, repo[None], repo['.'],
376 mergemod.applyupdates(repo, typeactions, repo[None], repo['.'],
370 False)
377 False)
371
378
372 dirstate = repo.dirstate
379 dirstate = repo.dirstate
373 for file, flags, msg in actions:
380 for file, flags, msg in actions:
374 dirstate.normal(file)
381 dirstate.normal(file)
375
382
376 profiles = activeconfig(repo)[2]
383 profiles = activeconfig(repo)[2]
377 changedprofiles = profiles & files
384 changedprofiles = profiles & files
378 # If an active profile changed during the update, refresh the checkout.
385 # If an active profile changed during the update, refresh the checkout.
379 # Don't do this during a branch merge, since all incoming changes should
386 # Don't do this during a branch merge, since all incoming changes should
380 # have been handled by the temporary includes above.
387 # have been handled by the temporary includes above.
381 if changedprofiles and not branchmerge:
388 if changedprofiles and not branchmerge:
382 mf = mctx.manifest()
389 mf = mctx.manifest()
383 for file in mf:
390 for file in mf:
384 old = oldsparsematch(file)
391 old = oldsparsematch(file)
385 new = sparsematch(file)
392 new = sparsematch(file)
386 if not old and new:
393 if not old and new:
387 flags = mf.flags(file)
394 flags = mf.flags(file)
388 prunedactions[file] = ('g', (flags, False), '')
395 prunedactions[file] = ('g', (flags, False), '')
389 elif old and not new:
396 elif old and not new:
390 prunedactions[file] = ('r', [], '')
397 prunedactions[file] = ('r', [], '')
391
398
392 return prunedactions
399 return prunedactions
393
400
394 def refreshwdir(repo, origstatus, origsparsematch, force=False):
401 def refreshwdir(repo, origstatus, origsparsematch, force=False):
395 """Refreshes working directory by taking sparse config into account.
402 """Refreshes working directory by taking sparse config into account.
396
403
397 The old status and sparse matcher is compared against the current sparse
404 The old status and sparse matcher is compared against the current sparse
398 matcher.
405 matcher.
399
406
400 Will abort if a file with pending changes is being excluded or included
407 Will abort if a file with pending changes is being excluded or included
401 unless ``force`` is True.
408 unless ``force`` is True.
402 """
409 """
403 # Verify there are no pending changes
410 # Verify there are no pending changes
404 pending = set()
411 pending = set()
405 pending.update(origstatus.modified)
412 pending.update(origstatus.modified)
406 pending.update(origstatus.added)
413 pending.update(origstatus.added)
407 pending.update(origstatus.removed)
414 pending.update(origstatus.removed)
408 sparsematch = matcher(repo)
415 sparsematch = matcher(repo)
409 abort = False
416 abort = False
410
417
411 for f in pending:
418 for f in pending:
412 if not sparsematch(f):
419 if not sparsematch(f):
413 repo.ui.warn(_("pending changes to '%s'\n") % f)
420 repo.ui.warn(_("pending changes to '%s'\n") % f)
414 abort = not force
421 abort = not force
415
422
416 if abort:
423 if abort:
417 raise error.Abort(_('could not update sparseness due to pending '
424 raise error.Abort(_('could not update sparseness due to pending '
418 'changes'))
425 'changes'))
419
426
420 # Calculate actions
427 # Calculate actions
421 dirstate = repo.dirstate
428 dirstate = repo.dirstate
422 ctx = repo['.']
429 ctx = repo['.']
423 added = []
430 added = []
424 lookup = []
431 lookup = []
425 dropped = []
432 dropped = []
426 mf = ctx.manifest()
433 mf = ctx.manifest()
427 files = set(mf)
434 files = set(mf)
428
435
429 actions = {}
436 actions = {}
430
437
431 for file in files:
438 for file in files:
432 old = origsparsematch(file)
439 old = origsparsematch(file)
433 new = sparsematch(file)
440 new = sparsematch(file)
434 # Add files that are newly included, or that don't exist in
441 # Add files that are newly included, or that don't exist in
435 # the dirstate yet.
442 # the dirstate yet.
436 if (new and not old) or (old and new and not file in dirstate):
443 if (new and not old) or (old and new and not file in dirstate):
437 fl = mf.flags(file)
444 fl = mf.flags(file)
438 if repo.wvfs.exists(file):
445 if repo.wvfs.exists(file):
439 actions[file] = ('e', (fl,), '')
446 actions[file] = ('e', (fl,), '')
440 lookup.append(file)
447 lookup.append(file)
441 else:
448 else:
442 actions[file] = ('g', (fl, False), '')
449 actions[file] = ('g', (fl, False), '')
443 added.append(file)
450 added.append(file)
444 # Drop files that are newly excluded, or that still exist in
451 # Drop files that are newly excluded, or that still exist in
445 # the dirstate.
452 # the dirstate.
446 elif (old and not new) or (not old and not new and file in dirstate):
453 elif (old and not new) or (not old and not new and file in dirstate):
447 dropped.append(file)
454 dropped.append(file)
448 if file not in pending:
455 if file not in pending:
449 actions[file] = ('r', [], '')
456 actions[file] = ('r', [], '')
450
457
451 # Verify there are no pending changes in newly included files
458 # Verify there are no pending changes in newly included files
452 abort = False
459 abort = False
453 for file in lookup:
460 for file in lookup:
454 repo.ui.warn(_("pending changes to '%s'\n") % file)
461 repo.ui.warn(_("pending changes to '%s'\n") % file)
455 abort = not force
462 abort = not force
456 if abort:
463 if abort:
457 raise error.Abort(_('cannot change sparseness due to pending '
464 raise error.Abort(_('cannot change sparseness due to pending '
458 'changes (delete the files or use '
465 'changes (delete the files or use '
459 '--force to bring them back dirty)'))
466 '--force to bring them back dirty)'))
460
467
461 # Check for files that were only in the dirstate.
468 # Check for files that were only in the dirstate.
462 for file, state in dirstate.iteritems():
469 for file, state in dirstate.iteritems():
463 if not file in files:
470 if not file in files:
464 old = origsparsematch(file)
471 old = origsparsematch(file)
465 new = sparsematch(file)
472 new = sparsematch(file)
466 if old and not new:
473 if old and not new:
467 dropped.append(file)
474 dropped.append(file)
468
475
469 # Apply changes to disk
476 # Apply changes to disk
470 typeactions = dict((m, []) for m in 'a f g am cd dc r dm dg m e k'.split())
477 typeactions = dict((m, []) for m in 'a f g am cd dc r dm dg m e k'.split())
471 for f, (m, args, msg) in actions.iteritems():
478 for f, (m, args, msg) in actions.iteritems():
472 if m not in typeactions:
479 if m not in typeactions:
473 typeactions[m] = []
480 typeactions[m] = []
474 typeactions[m].append((f, args, msg))
481 typeactions[m].append((f, args, msg))
475
482
476 mergemod.applyupdates(repo, typeactions, repo[None], repo['.'], False)
483 mergemod.applyupdates(repo, typeactions, repo[None], repo['.'], False)
477
484
478 # Fix dirstate
485 # Fix dirstate
479 for file in added:
486 for file in added:
480 dirstate.normal(file)
487 dirstate.normal(file)
481
488
482 for file in dropped:
489 for file in dropped:
483 dirstate.drop(file)
490 dirstate.drop(file)
484
491
485 for file in lookup:
492 for file in lookup:
486 # File exists on disk, and we're bringing it back in an unknown state.
493 # File exists on disk, and we're bringing it back in an unknown state.
487 dirstate.normallookup(file)
494 dirstate.normallookup(file)
488
495
489 return added, dropped, lookup
496 return added, dropped, lookup
490
497
491 def aftercommit(repo, node):
498 def aftercommit(repo, node):
492 """Perform actions after a working directory commit."""
499 """Perform actions after a working directory commit."""
493 # This function is called unconditionally, even if sparse isn't
500 # This function is called unconditionally, even if sparse isn't
494 # enabled.
501 # enabled.
495 ctx = repo[node]
502 ctx = repo[node]
496
503
497 profiles = patternsforrev(repo, ctx.rev())[2]
504 profiles = patternsforrev(repo, ctx.rev())[2]
498
505
499 # profiles will only have data if sparse is enabled.
506 # profiles will only have data if sparse is enabled.
500 if set(profiles) & set(ctx.files()):
507 if set(profiles) & set(ctx.files()):
501 origstatus = repo.status()
508 origstatus = repo.status()
502 origsparsematch = matcher(repo)
509 origsparsematch = matcher(repo)
503 refreshwdir(repo, origstatus, origsparsematch, force=True)
510 refreshwdir(repo, origstatus, origsparsematch, force=True)
504
511
505 prunetemporaryincludes(repo)
512 prunetemporaryincludes(repo)
506
513
507 def clearrules(repo, force=False):
514 def clearrules(repo, force=False):
508 """Clears include/exclude rules from the sparse config.
515 """Clears include/exclude rules from the sparse config.
509
516
510 The remaining sparse config only has profiles, if defined. The working
517 The remaining sparse config only has profiles, if defined. The working
511 directory is refreshed, as needed.
518 directory is refreshed, as needed.
512 """
519 """
513 with repo.wlock():
520 with repo.wlock():
514 raw = repo.vfs.tryread('sparse')
521 raw = repo.vfs.tryread('sparse')
515 includes, excludes, profiles = parseconfig(repo.ui, raw)
522 includes, excludes, profiles = parseconfig(repo.ui, raw)
516
523
517 if not includes and not excludes:
524 if not includes and not excludes:
518 return
525 return
519
526
520 oldstatus = repo.status()
527 oldstatus = repo.status()
521 oldmatch = matcher(repo)
528 oldmatch = matcher(repo)
522 writeconfig(repo, set(), set(), profiles)
529 writeconfig(repo, set(), set(), profiles)
523 refreshwdir(repo, oldstatus, oldmatch, force=force)
530 refreshwdir(repo, oldstatus, oldmatch, force=force)
524
531
525 def importfromfiles(repo, opts, paths, force=False):
532 def importfromfiles(repo, opts, paths, force=False):
526 """Import sparse config rules from files.
533 """Import sparse config rules from files.
527
534
528 The updated sparse config is written out and the working directory
535 The updated sparse config is written out and the working directory
529 is refreshed, as needed.
536 is refreshed, as needed.
530 """
537 """
531 with repo.wlock():
538 with repo.wlock():
532 # read current configuration
539 # read current configuration
533 raw = repo.vfs.tryread('sparse')
540 raw = repo.vfs.tryread('sparse')
534 oincludes, oexcludes, oprofiles = parseconfig(repo.ui, raw)
541 oincludes, oexcludes, oprofiles = parseconfig(repo.ui, raw)
535 includes, excludes, profiles = map(
542 includes, excludes, profiles = map(
536 set, (oincludes, oexcludes, oprofiles))
543 set, (oincludes, oexcludes, oprofiles))
537
544
538 aincludes, aexcludes, aprofiles = activeconfig(repo)
545 aincludes, aexcludes, aprofiles = activeconfig(repo)
539
546
540 # Import rules on top; only take in rules that are not yet
547 # Import rules on top; only take in rules that are not yet
541 # part of the active rules.
548 # part of the active rules.
542 changed = False
549 changed = False
543 for p in paths:
550 for p in paths:
544 with util.posixfile(util.expandpath(p)) as fh:
551 with util.posixfile(util.expandpath(p)) as fh:
545 raw = fh.read()
552 raw = fh.read()
546
553
547 iincludes, iexcludes, iprofiles = parseconfig(repo.ui, raw)
554 iincludes, iexcludes, iprofiles = parseconfig(repo.ui, raw)
548 oldsize = len(includes) + len(excludes) + len(profiles)
555 oldsize = len(includes) + len(excludes) + len(profiles)
549 includes.update(iincludes - aincludes)
556 includes.update(iincludes - aincludes)
550 excludes.update(iexcludes - aexcludes)
557 excludes.update(iexcludes - aexcludes)
551 profiles.update(set(iprofiles) - aprofiles)
558 profiles.update(set(iprofiles) - aprofiles)
552 if len(includes) + len(excludes) + len(profiles) > oldsize:
559 if len(includes) + len(excludes) + len(profiles) > oldsize:
553 changed = True
560 changed = True
554
561
555 profilecount = includecount = excludecount = 0
562 profilecount = includecount = excludecount = 0
556 fcounts = (0, 0, 0)
563 fcounts = (0, 0, 0)
557
564
558 if changed:
565 if changed:
559 profilecount = len(profiles - aprofiles)
566 profilecount = len(profiles - aprofiles)
560 includecount = len(includes - aincludes)
567 includecount = len(includes - aincludes)
561 excludecount = len(excludes - aexcludes)
568 excludecount = len(excludes - aexcludes)
562
569
563 oldstatus = repo.status()
570 oldstatus = repo.status()
564 oldsparsematch = matcher(repo)
571 oldsparsematch = matcher(repo)
565
572
566 # TODO remove this try..except once the matcher integrates better
573 # TODO remove this try..except once the matcher integrates better
567 # with dirstate. We currently have to write the updated config
574 # with dirstate. We currently have to write the updated config
568 # because that will invalidate the matcher cache and force a
575 # because that will invalidate the matcher cache and force a
569 # re-read. We ideally want to update the cached matcher on the
576 # re-read. We ideally want to update the cached matcher on the
570 # repo instance then flush the new config to disk once wdir is
577 # repo instance then flush the new config to disk once wdir is
571 # updated. But this requires massive rework to matcher() and its
578 # updated. But this requires massive rework to matcher() and its
572 # consumers.
579 # consumers.
573 writeconfig(repo, includes, excludes, profiles)
580 writeconfig(repo, includes, excludes, profiles)
574
581
575 try:
582 try:
576 fcounts = map(
583 fcounts = map(
577 len,
584 len,
578 refreshwdir(repo, oldstatus, oldsparsematch, force=force))
585 refreshwdir(repo, oldstatus, oldsparsematch, force=force))
579 except Exception:
586 except Exception:
580 writeconfig(repo, oincludes, oexcludes, oprofiles)
587 writeconfig(repo, oincludes, oexcludes, oprofiles)
581 raise
588 raise
582
589
583 printchanges(repo.ui, opts, profilecount, includecount, excludecount,
590 printchanges(repo.ui, opts, profilecount, includecount, excludecount,
584 *fcounts)
591 *fcounts)
585
592
586 def updateconfig(repo, pats, opts, include=False, exclude=False, reset=False,
593 def updateconfig(repo, pats, opts, include=False, exclude=False, reset=False,
587 delete=False, enableprofile=False, disableprofile=False,
594 delete=False, enableprofile=False, disableprofile=False,
588 force=False):
595 force=False):
589 """Perform a sparse config update.
596 """Perform a sparse config update.
590
597
591 Only one of the actions may be performed.
598 Only one of the actions may be performed.
592
599
593 The new config is written out and a working directory refresh is performed.
600 The new config is written out and a working directory refresh is performed.
594 """
601 """
595 with repo.wlock():
602 with repo.wlock():
596 oldmatcher = matcher(repo)
603 oldmatcher = matcher(repo)
597
604
598 raw = repo.vfs.tryread('sparse')
605 raw = repo.vfs.tryread('sparse')
599 oldinclude, oldexclude, oldprofiles = parseconfig(repo.ui, raw)
606 oldinclude, oldexclude, oldprofiles = parseconfig(repo.ui, raw)
600 oldprofiles = set(oldprofiles)
607 oldprofiles = set(oldprofiles)
601
608
602 if reset:
609 if reset:
603 newinclude = set()
610 newinclude = set()
604 newexclude = set()
611 newexclude = set()
605 newprofiles = set()
612 newprofiles = set()
606 else:
613 else:
607 newinclude = set(oldinclude)
614 newinclude = set(oldinclude)
608 newexclude = set(oldexclude)
615 newexclude = set(oldexclude)
609 newprofiles = set(oldprofiles)
616 newprofiles = set(oldprofiles)
610
617
611 oldstatus = repo.status()
618 oldstatus = repo.status()
612
619
613 if any(pat.startswith('/') for pat in pats):
620 if any(pat.startswith('/') for pat in pats):
614 repo.ui.warn(_('warning: paths cannot start with /, ignoring: %s\n')
621 repo.ui.warn(_('warning: paths cannot start with /, ignoring: %s\n')
615 % ([pat for pat in pats if pat.startswith('/')]))
622 % ([pat for pat in pats if pat.startswith('/')]))
616 elif include:
623 elif include:
617 newinclude.update(pats)
624 newinclude.update(pats)
618 elif exclude:
625 elif exclude:
619 newexclude.update(pats)
626 newexclude.update(pats)
620 elif enableprofile:
627 elif enableprofile:
621 newprofiles.update(pats)
628 newprofiles.update(pats)
622 elif disableprofile:
629 elif disableprofile:
623 newprofiles.difference_update(pats)
630 newprofiles.difference_update(pats)
624 elif delete:
631 elif delete:
625 newinclude.difference_update(pats)
632 newinclude.difference_update(pats)
626 newexclude.difference_update(pats)
633 newexclude.difference_update(pats)
627
634
628 profilecount = (len(newprofiles - oldprofiles) -
635 profilecount = (len(newprofiles - oldprofiles) -
629 len(oldprofiles - newprofiles))
636 len(oldprofiles - newprofiles))
630 includecount = (len(newinclude - oldinclude) -
637 includecount = (len(newinclude - oldinclude) -
631 len(oldinclude - newinclude))
638 len(oldinclude - newinclude))
632 excludecount = (len(newexclude - oldexclude) -
639 excludecount = (len(newexclude - oldexclude) -
633 len(oldexclude - newexclude))
640 len(oldexclude - newexclude))
634
641
635 # TODO clean up this writeconfig() + try..except pattern once we can.
642 # TODO clean up this writeconfig() + try..except pattern once we can.
636 # See comment in importfromfiles() explaining it.
643 # See comment in importfromfiles() explaining it.
637 writeconfig(repo, newinclude, newexclude, newprofiles)
644 writeconfig(repo, newinclude, newexclude, newprofiles)
638
645
639 try:
646 try:
640 fcounts = map(
647 fcounts = map(
641 len,
648 len,
642 refreshwdir(repo, oldstatus, oldmatcher, force=force))
649 refreshwdir(repo, oldstatus, oldmatcher, force=force))
643
650
644 printchanges(repo.ui, opts, profilecount, includecount,
651 printchanges(repo.ui, opts, profilecount, includecount,
645 excludecount, *fcounts)
652 excludecount, *fcounts)
646 except Exception:
653 except Exception:
647 writeconfig(repo, oldinclude, oldexclude, oldprofiles)
654 writeconfig(repo, oldinclude, oldexclude, oldprofiles)
648 raise
655 raise
649
656
650 def printchanges(ui, opts, profilecount=0, includecount=0, excludecount=0,
657 def printchanges(ui, opts, profilecount=0, includecount=0, excludecount=0,
651 added=0, dropped=0, conflicting=0):
658 added=0, dropped=0, conflicting=0):
652 """Print output summarizing sparse config changes."""
659 """Print output summarizing sparse config changes."""
653 with ui.formatter('sparse', opts) as fm:
660 with ui.formatter('sparse', opts) as fm:
654 fm.startitem()
661 fm.startitem()
655 fm.condwrite(ui.verbose, 'profiles_added', _('Profiles changed: %d\n'),
662 fm.condwrite(ui.verbose, 'profiles_added', _('Profiles changed: %d\n'),
656 profilecount)
663 profilecount)
657 fm.condwrite(ui.verbose, 'include_rules_added',
664 fm.condwrite(ui.verbose, 'include_rules_added',
658 _('Include rules changed: %d\n'), includecount)
665 _('Include rules changed: %d\n'), includecount)
659 fm.condwrite(ui.verbose, 'exclude_rules_added',
666 fm.condwrite(ui.verbose, 'exclude_rules_added',
660 _('Exclude rules changed: %d\n'), excludecount)
667 _('Exclude rules changed: %d\n'), excludecount)
661
668
662 # In 'plain' verbose mode, mergemod.applyupdates already outputs what
669 # In 'plain' verbose mode, mergemod.applyupdates already outputs what
663 # files are added or removed outside of the templating formatter
670 # files are added or removed outside of the templating formatter
664 # framework. No point in repeating ourselves in that case.
671 # framework. No point in repeating ourselves in that case.
665 if not fm.isplain():
672 if not fm.isplain():
666 fm.condwrite(ui.verbose, 'files_added', _('Files added: %d\n'),
673 fm.condwrite(ui.verbose, 'files_added', _('Files added: %d\n'),
667 added)
674 added)
668 fm.condwrite(ui.verbose, 'files_dropped', _('Files dropped: %d\n'),
675 fm.condwrite(ui.verbose, 'files_dropped', _('Files dropped: %d\n'),
669 dropped)
676 dropped)
670 fm.condwrite(ui.verbose, 'files_conflicting',
677 fm.condwrite(ui.verbose, 'files_conflicting',
671 _('Files conflicting: %d\n'), conflicting)
678 _('Files conflicting: %d\n'), conflicting)
General Comments 0
You need to be logged in to leave comments. Login now