##// END OF EJS Templates
fileset: split the logic for matching a size expression to a separate method...
Matt Harbison -
r35633:0e369eca default
parent child Browse files
Show More
@@ -1,632 +1,634 b''
1 # fileset.py - file set queries for mercurial
1 # fileset.py - file set queries for mercurial
2 #
2 #
3 # Copyright 2010 Matt Mackall <mpm@selenic.com>
3 # Copyright 2010 Matt Mackall <mpm@selenic.com>
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 re
10 import re
11
11
12 from .i18n import _
12 from .i18n import _
13 from . import (
13 from . import (
14 error,
14 error,
15 merge,
15 merge,
16 parser,
16 parser,
17 pycompat,
17 pycompat,
18 registrar,
18 registrar,
19 scmutil,
19 scmutil,
20 util,
20 util,
21 )
21 )
22
22
23 elements = {
23 elements = {
24 # token-type: binding-strength, primary, prefix, infix, suffix
24 # token-type: binding-strength, primary, prefix, infix, suffix
25 "(": (20, None, ("group", 1, ")"), ("func", 1, ")"), None),
25 "(": (20, None, ("group", 1, ")"), ("func", 1, ")"), None),
26 "-": (5, None, ("negate", 19), ("minus", 5), None),
26 "-": (5, None, ("negate", 19), ("minus", 5), None),
27 "not": (10, None, ("not", 10), None, None),
27 "not": (10, None, ("not", 10), None, None),
28 "!": (10, None, ("not", 10), None, None),
28 "!": (10, None, ("not", 10), None, None),
29 "and": (5, None, None, ("and", 5), None),
29 "and": (5, None, None, ("and", 5), None),
30 "&": (5, None, None, ("and", 5), None),
30 "&": (5, None, None, ("and", 5), None),
31 "or": (4, None, None, ("or", 4), None),
31 "or": (4, None, None, ("or", 4), None),
32 "|": (4, None, None, ("or", 4), None),
32 "|": (4, None, None, ("or", 4), None),
33 "+": (4, None, None, ("or", 4), None),
33 "+": (4, None, None, ("or", 4), None),
34 ",": (2, None, None, ("list", 2), None),
34 ",": (2, None, None, ("list", 2), None),
35 ")": (0, None, None, None, None),
35 ")": (0, None, None, None, None),
36 "symbol": (0, "symbol", None, None, None),
36 "symbol": (0, "symbol", None, None, None),
37 "string": (0, "string", None, None, None),
37 "string": (0, "string", None, None, None),
38 "end": (0, None, None, None, None),
38 "end": (0, None, None, None, None),
39 }
39 }
40
40
41 keywords = {'and', 'or', 'not'}
41 keywords = {'and', 'or', 'not'}
42
42
43 globchars = ".*{}[]?/\\_"
43 globchars = ".*{}[]?/\\_"
44
44
45 def tokenize(program):
45 def tokenize(program):
46 pos, l = 0, len(program)
46 pos, l = 0, len(program)
47 program = pycompat.bytestr(program)
47 program = pycompat.bytestr(program)
48 while pos < l:
48 while pos < l:
49 c = program[pos]
49 c = program[pos]
50 if c.isspace(): # skip inter-token whitespace
50 if c.isspace(): # skip inter-token whitespace
51 pass
51 pass
52 elif c in "(),-|&+!": # handle simple operators
52 elif c in "(),-|&+!": # handle simple operators
53 yield (c, None, pos)
53 yield (c, None, pos)
54 elif (c in '"\'' or c == 'r' and
54 elif (c in '"\'' or c == 'r' and
55 program[pos:pos + 2] in ("r'", 'r"')): # handle quoted strings
55 program[pos:pos + 2] in ("r'", 'r"')): # handle quoted strings
56 if c == 'r':
56 if c == 'r':
57 pos += 1
57 pos += 1
58 c = program[pos]
58 c = program[pos]
59 decode = lambda x: x
59 decode = lambda x: x
60 else:
60 else:
61 decode = parser.unescapestr
61 decode = parser.unescapestr
62 pos += 1
62 pos += 1
63 s = pos
63 s = pos
64 while pos < l: # find closing quote
64 while pos < l: # find closing quote
65 d = program[pos]
65 d = program[pos]
66 if d == '\\': # skip over escaped characters
66 if d == '\\': # skip over escaped characters
67 pos += 2
67 pos += 2
68 continue
68 continue
69 if d == c:
69 if d == c:
70 yield ('string', decode(program[s:pos]), s)
70 yield ('string', decode(program[s:pos]), s)
71 break
71 break
72 pos += 1
72 pos += 1
73 else:
73 else:
74 raise error.ParseError(_("unterminated string"), s)
74 raise error.ParseError(_("unterminated string"), s)
75 elif c.isalnum() or c in globchars or ord(c) > 127:
75 elif c.isalnum() or c in globchars or ord(c) > 127:
76 # gather up a symbol/keyword
76 # gather up a symbol/keyword
77 s = pos
77 s = pos
78 pos += 1
78 pos += 1
79 while pos < l: # find end of symbol
79 while pos < l: # find end of symbol
80 d = program[pos]
80 d = program[pos]
81 if not (d.isalnum() or d in globchars or ord(d) > 127):
81 if not (d.isalnum() or d in globchars or ord(d) > 127):
82 break
82 break
83 pos += 1
83 pos += 1
84 sym = program[s:pos]
84 sym = program[s:pos]
85 if sym in keywords: # operator keywords
85 if sym in keywords: # operator keywords
86 yield (sym, None, s)
86 yield (sym, None, s)
87 else:
87 else:
88 yield ('symbol', sym, s)
88 yield ('symbol', sym, s)
89 pos -= 1
89 pos -= 1
90 else:
90 else:
91 raise error.ParseError(_("syntax error"), pos)
91 raise error.ParseError(_("syntax error"), pos)
92 pos += 1
92 pos += 1
93 yield ('end', None, pos)
93 yield ('end', None, pos)
94
94
95 def parse(expr):
95 def parse(expr):
96 p = parser.parser(elements)
96 p = parser.parser(elements)
97 tree, pos = p.parse(tokenize(expr))
97 tree, pos = p.parse(tokenize(expr))
98 if pos != len(expr):
98 if pos != len(expr):
99 raise error.ParseError(_("invalid token"), pos)
99 raise error.ParseError(_("invalid token"), pos)
100 return tree
100 return tree
101
101
102 def getstring(x, err):
102 def getstring(x, err):
103 if x and (x[0] == 'string' or x[0] == 'symbol'):
103 if x and (x[0] == 'string' or x[0] == 'symbol'):
104 return x[1]
104 return x[1]
105 raise error.ParseError(err)
105 raise error.ParseError(err)
106
106
107 def getset(mctx, x):
107 def getset(mctx, x):
108 if not x:
108 if not x:
109 raise error.ParseError(_("missing argument"))
109 raise error.ParseError(_("missing argument"))
110 return methods[x[0]](mctx, *x[1:])
110 return methods[x[0]](mctx, *x[1:])
111
111
112 def stringset(mctx, x):
112 def stringset(mctx, x):
113 m = mctx.matcher([x])
113 m = mctx.matcher([x])
114 return [f for f in mctx.subset if m(f)]
114 return [f for f in mctx.subset if m(f)]
115
115
116 def andset(mctx, x, y):
116 def andset(mctx, x, y):
117 return getset(mctx.narrow(getset(mctx, x)), y)
117 return getset(mctx.narrow(getset(mctx, x)), y)
118
118
119 def orset(mctx, x, y):
119 def orset(mctx, x, y):
120 # needs optimizing
120 # needs optimizing
121 xl = getset(mctx, x)
121 xl = getset(mctx, x)
122 yl = getset(mctx, y)
122 yl = getset(mctx, y)
123 return xl + [f for f in yl if f not in xl]
123 return xl + [f for f in yl if f not in xl]
124
124
125 def notset(mctx, x):
125 def notset(mctx, x):
126 s = set(getset(mctx, x))
126 s = set(getset(mctx, x))
127 return [r for r in mctx.subset if r not in s]
127 return [r for r in mctx.subset if r not in s]
128
128
129 def minusset(mctx, x, y):
129 def minusset(mctx, x, y):
130 xl = getset(mctx, x)
130 xl = getset(mctx, x)
131 yl = set(getset(mctx, y))
131 yl = set(getset(mctx, y))
132 return [f for f in xl if f not in yl]
132 return [f for f in xl if f not in yl]
133
133
134 def listset(mctx, a, b):
134 def listset(mctx, a, b):
135 raise error.ParseError(_("can't use a list in this context"),
135 raise error.ParseError(_("can't use a list in this context"),
136 hint=_('see hg help "filesets.x or y"'))
136 hint=_('see hg help "filesets.x or y"'))
137
137
138 # symbols are callable like:
138 # symbols are callable like:
139 # fun(mctx, x)
139 # fun(mctx, x)
140 # with:
140 # with:
141 # mctx - current matchctx instance
141 # mctx - current matchctx instance
142 # x - argument in tree form
142 # x - argument in tree form
143 symbols = {}
143 symbols = {}
144
144
145 # filesets using matchctx.status()
145 # filesets using matchctx.status()
146 _statuscallers = set()
146 _statuscallers = set()
147
147
148 # filesets using matchctx.existing()
148 # filesets using matchctx.existing()
149 _existingcallers = set()
149 _existingcallers = set()
150
150
151 predicate = registrar.filesetpredicate()
151 predicate = registrar.filesetpredicate()
152
152
153 @predicate('modified()', callstatus=True)
153 @predicate('modified()', callstatus=True)
154 def modified(mctx, x):
154 def modified(mctx, x):
155 """File that is modified according to :hg:`status`.
155 """File that is modified according to :hg:`status`.
156 """
156 """
157 # i18n: "modified" is a keyword
157 # i18n: "modified" is a keyword
158 getargs(x, 0, 0, _("modified takes no arguments"))
158 getargs(x, 0, 0, _("modified takes no arguments"))
159 s = set(mctx.status().modified)
159 s = set(mctx.status().modified)
160 return [f for f in mctx.subset if f in s]
160 return [f for f in mctx.subset if f in s]
161
161
162 @predicate('added()', callstatus=True)
162 @predicate('added()', callstatus=True)
163 def added(mctx, x):
163 def added(mctx, x):
164 """File that is added according to :hg:`status`.
164 """File that is added according to :hg:`status`.
165 """
165 """
166 # i18n: "added" is a keyword
166 # i18n: "added" is a keyword
167 getargs(x, 0, 0, _("added takes no arguments"))
167 getargs(x, 0, 0, _("added takes no arguments"))
168 s = set(mctx.status().added)
168 s = set(mctx.status().added)
169 return [f for f in mctx.subset if f in s]
169 return [f for f in mctx.subset if f in s]
170
170
171 @predicate('removed()', callstatus=True)
171 @predicate('removed()', callstatus=True)
172 def removed(mctx, x):
172 def removed(mctx, x):
173 """File that is removed according to :hg:`status`.
173 """File that is removed according to :hg:`status`.
174 """
174 """
175 # i18n: "removed" is a keyword
175 # i18n: "removed" is a keyword
176 getargs(x, 0, 0, _("removed takes no arguments"))
176 getargs(x, 0, 0, _("removed takes no arguments"))
177 s = set(mctx.status().removed)
177 s = set(mctx.status().removed)
178 return [f for f in mctx.subset if f in s]
178 return [f for f in mctx.subset if f in s]
179
179
180 @predicate('deleted()', callstatus=True)
180 @predicate('deleted()', callstatus=True)
181 def deleted(mctx, x):
181 def deleted(mctx, x):
182 """Alias for ``missing()``.
182 """Alias for ``missing()``.
183 """
183 """
184 # i18n: "deleted" is a keyword
184 # i18n: "deleted" is a keyword
185 getargs(x, 0, 0, _("deleted takes no arguments"))
185 getargs(x, 0, 0, _("deleted takes no arguments"))
186 s = set(mctx.status().deleted)
186 s = set(mctx.status().deleted)
187 return [f for f in mctx.subset if f in s]
187 return [f for f in mctx.subset if f in s]
188
188
189 @predicate('missing()', callstatus=True)
189 @predicate('missing()', callstatus=True)
190 def missing(mctx, x):
190 def missing(mctx, x):
191 """File that is missing according to :hg:`status`.
191 """File that is missing according to :hg:`status`.
192 """
192 """
193 # i18n: "missing" is a keyword
193 # i18n: "missing" is a keyword
194 getargs(x, 0, 0, _("missing takes no arguments"))
194 getargs(x, 0, 0, _("missing takes no arguments"))
195 s = set(mctx.status().deleted)
195 s = set(mctx.status().deleted)
196 return [f for f in mctx.subset if f in s]
196 return [f for f in mctx.subset if f in s]
197
197
198 @predicate('unknown()', callstatus=True)
198 @predicate('unknown()', callstatus=True)
199 def unknown(mctx, x):
199 def unknown(mctx, x):
200 """File that is unknown according to :hg:`status`. These files will only be
200 """File that is unknown according to :hg:`status`. These files will only be
201 considered if this predicate is used.
201 considered if this predicate is used.
202 """
202 """
203 # i18n: "unknown" is a keyword
203 # i18n: "unknown" is a keyword
204 getargs(x, 0, 0, _("unknown takes no arguments"))
204 getargs(x, 0, 0, _("unknown takes no arguments"))
205 s = set(mctx.status().unknown)
205 s = set(mctx.status().unknown)
206 return [f for f in mctx.subset if f in s]
206 return [f for f in mctx.subset if f in s]
207
207
208 @predicate('ignored()', callstatus=True)
208 @predicate('ignored()', callstatus=True)
209 def ignored(mctx, x):
209 def ignored(mctx, x):
210 """File that is ignored according to :hg:`status`. These files will only be
210 """File that is ignored according to :hg:`status`. These files will only be
211 considered if this predicate is used.
211 considered if this predicate is used.
212 """
212 """
213 # i18n: "ignored" is a keyword
213 # i18n: "ignored" is a keyword
214 getargs(x, 0, 0, _("ignored takes no arguments"))
214 getargs(x, 0, 0, _("ignored takes no arguments"))
215 s = set(mctx.status().ignored)
215 s = set(mctx.status().ignored)
216 return [f for f in mctx.subset if f in s]
216 return [f for f in mctx.subset if f in s]
217
217
218 @predicate('clean()', callstatus=True)
218 @predicate('clean()', callstatus=True)
219 def clean(mctx, x):
219 def clean(mctx, x):
220 """File that is clean according to :hg:`status`.
220 """File that is clean according to :hg:`status`.
221 """
221 """
222 # i18n: "clean" is a keyword
222 # i18n: "clean" is a keyword
223 getargs(x, 0, 0, _("clean takes no arguments"))
223 getargs(x, 0, 0, _("clean takes no arguments"))
224 s = set(mctx.status().clean)
224 s = set(mctx.status().clean)
225 return [f for f in mctx.subset if f in s]
225 return [f for f in mctx.subset if f in s]
226
226
227 def func(mctx, a, b):
227 def func(mctx, a, b):
228 if a[0] == 'symbol' and a[1] in symbols:
228 if a[0] == 'symbol' and a[1] in symbols:
229 funcname = a[1]
229 funcname = a[1]
230 enabled = mctx._existingenabled
230 enabled = mctx._existingenabled
231 mctx._existingenabled = funcname in _existingcallers
231 mctx._existingenabled = funcname in _existingcallers
232 try:
232 try:
233 return symbols[funcname](mctx, b)
233 return symbols[funcname](mctx, b)
234 finally:
234 finally:
235 mctx._existingenabled = enabled
235 mctx._existingenabled = enabled
236
236
237 keep = lambda fn: getattr(fn, '__doc__', None) is not None
237 keep = lambda fn: getattr(fn, '__doc__', None) is not None
238
238
239 syms = [s for (s, fn) in symbols.items() if keep(fn)]
239 syms = [s for (s, fn) in symbols.items() if keep(fn)]
240 raise error.UnknownIdentifier(a[1], syms)
240 raise error.UnknownIdentifier(a[1], syms)
241
241
242 def getlist(x):
242 def getlist(x):
243 if not x:
243 if not x:
244 return []
244 return []
245 if x[0] == 'list':
245 if x[0] == 'list':
246 return getlist(x[1]) + [x[2]]
246 return getlist(x[1]) + [x[2]]
247 return [x]
247 return [x]
248
248
249 def getargs(x, min, max, err):
249 def getargs(x, min, max, err):
250 l = getlist(x)
250 l = getlist(x)
251 if len(l) < min or len(l) > max:
251 if len(l) < min or len(l) > max:
252 raise error.ParseError(err)
252 raise error.ParseError(err)
253 return l
253 return l
254
254
255 @predicate('binary()', callexisting=True)
255 @predicate('binary()', callexisting=True)
256 def binary(mctx, x):
256 def binary(mctx, x):
257 """File that appears to be binary (contains NUL bytes).
257 """File that appears to be binary (contains NUL bytes).
258 """
258 """
259 # i18n: "binary" is a keyword
259 # i18n: "binary" is a keyword
260 getargs(x, 0, 0, _("binary takes no arguments"))
260 getargs(x, 0, 0, _("binary takes no arguments"))
261 return [f for f in mctx.existing() if mctx.ctx[f].isbinary()]
261 return [f for f in mctx.existing() if mctx.ctx[f].isbinary()]
262
262
263 @predicate('exec()', callexisting=True)
263 @predicate('exec()', callexisting=True)
264 def exec_(mctx, x):
264 def exec_(mctx, x):
265 """File that is marked as executable.
265 """File that is marked as executable.
266 """
266 """
267 # i18n: "exec" is a keyword
267 # i18n: "exec" is a keyword
268 getargs(x, 0, 0, _("exec takes no arguments"))
268 getargs(x, 0, 0, _("exec takes no arguments"))
269 return [f for f in mctx.existing() if mctx.ctx.flags(f) == 'x']
269 return [f for f in mctx.existing() if mctx.ctx.flags(f) == 'x']
270
270
271 @predicate('symlink()', callexisting=True)
271 @predicate('symlink()', callexisting=True)
272 def symlink(mctx, x):
272 def symlink(mctx, x):
273 """File that is marked as a symlink.
273 """File that is marked as a symlink.
274 """
274 """
275 # i18n: "symlink" is a keyword
275 # i18n: "symlink" is a keyword
276 getargs(x, 0, 0, _("symlink takes no arguments"))
276 getargs(x, 0, 0, _("symlink takes no arguments"))
277 return [f for f in mctx.existing() if mctx.ctx.flags(f) == 'l']
277 return [f for f in mctx.existing() if mctx.ctx.flags(f) == 'l']
278
278
279 @predicate('resolved()')
279 @predicate('resolved()')
280 def resolved(mctx, x):
280 def resolved(mctx, x):
281 """File that is marked resolved according to :hg:`resolve -l`.
281 """File that is marked resolved according to :hg:`resolve -l`.
282 """
282 """
283 # i18n: "resolved" is a keyword
283 # i18n: "resolved" is a keyword
284 getargs(x, 0, 0, _("resolved takes no arguments"))
284 getargs(x, 0, 0, _("resolved takes no arguments"))
285 if mctx.ctx.rev() is not None:
285 if mctx.ctx.rev() is not None:
286 return []
286 return []
287 ms = merge.mergestate.read(mctx.ctx.repo())
287 ms = merge.mergestate.read(mctx.ctx.repo())
288 return [f for f in mctx.subset if f in ms and ms[f] == 'r']
288 return [f for f in mctx.subset if f in ms and ms[f] == 'r']
289
289
290 @predicate('unresolved()')
290 @predicate('unresolved()')
291 def unresolved(mctx, x):
291 def unresolved(mctx, x):
292 """File that is marked unresolved according to :hg:`resolve -l`.
292 """File that is marked unresolved according to :hg:`resolve -l`.
293 """
293 """
294 # i18n: "unresolved" is a keyword
294 # i18n: "unresolved" is a keyword
295 getargs(x, 0, 0, _("unresolved takes no arguments"))
295 getargs(x, 0, 0, _("unresolved takes no arguments"))
296 if mctx.ctx.rev() is not None:
296 if mctx.ctx.rev() is not None:
297 return []
297 return []
298 ms = merge.mergestate.read(mctx.ctx.repo())
298 ms = merge.mergestate.read(mctx.ctx.repo())
299 return [f for f in mctx.subset if f in ms and ms[f] == 'u']
299 return [f for f in mctx.subset if f in ms and ms[f] == 'u']
300
300
301 @predicate('hgignore()')
301 @predicate('hgignore()')
302 def hgignore(mctx, x):
302 def hgignore(mctx, x):
303 """File that matches the active .hgignore pattern.
303 """File that matches the active .hgignore pattern.
304 """
304 """
305 # i18n: "hgignore" is a keyword
305 # i18n: "hgignore" is a keyword
306 getargs(x, 0, 0, _("hgignore takes no arguments"))
306 getargs(x, 0, 0, _("hgignore takes no arguments"))
307 ignore = mctx.ctx.repo().dirstate._ignore
307 ignore = mctx.ctx.repo().dirstate._ignore
308 return [f for f in mctx.subset if ignore(f)]
308 return [f for f in mctx.subset if ignore(f)]
309
309
310 @predicate('portable()')
310 @predicate('portable()')
311 def portable(mctx, x):
311 def portable(mctx, x):
312 """File that has a portable name. (This doesn't include filenames with case
312 """File that has a portable name. (This doesn't include filenames with case
313 collisions.)
313 collisions.)
314 """
314 """
315 # i18n: "portable" is a keyword
315 # i18n: "portable" is a keyword
316 getargs(x, 0, 0, _("portable takes no arguments"))
316 getargs(x, 0, 0, _("portable takes no arguments"))
317 checkwinfilename = util.checkwinfilename
317 checkwinfilename = util.checkwinfilename
318 return [f for f in mctx.subset if checkwinfilename(f) is None]
318 return [f for f in mctx.subset if checkwinfilename(f) is None]
319
319
320 @predicate('grep(regex)', callexisting=True)
320 @predicate('grep(regex)', callexisting=True)
321 def grep(mctx, x):
321 def grep(mctx, x):
322 """File contains the given regular expression.
322 """File contains the given regular expression.
323 """
323 """
324 try:
324 try:
325 # i18n: "grep" is a keyword
325 # i18n: "grep" is a keyword
326 r = re.compile(getstring(x, _("grep requires a pattern")))
326 r = re.compile(getstring(x, _("grep requires a pattern")))
327 except re.error as e:
327 except re.error as e:
328 raise error.ParseError(_('invalid match pattern: %s') % e)
328 raise error.ParseError(_('invalid match pattern: %s') % e)
329 return [f for f in mctx.existing() if r.search(mctx.ctx[f].data())]
329 return [f for f in mctx.existing() if r.search(mctx.ctx[f].data())]
330
330
331 def _sizetomax(s):
331 def _sizetomax(s):
332 try:
332 try:
333 s = s.strip().lower()
333 s = s.strip().lower()
334 for k, v in util._sizeunits:
334 for k, v in util._sizeunits:
335 if s.endswith(k):
335 if s.endswith(k):
336 # max(4k) = 5k - 1, max(4.5k) = 4.6k - 1
336 # max(4k) = 5k - 1, max(4.5k) = 4.6k - 1
337 n = s[:-len(k)]
337 n = s[:-len(k)]
338 inc = 1.0
338 inc = 1.0
339 if "." in n:
339 if "." in n:
340 inc /= 10 ** len(n.split(".")[1])
340 inc /= 10 ** len(n.split(".")[1])
341 return int((float(n) + inc) * v) - 1
341 return int((float(n) + inc) * v) - 1
342 # no extension, this is a precise value
342 # no extension, this is a precise value
343 return int(s)
343 return int(s)
344 except ValueError:
344 except ValueError:
345 raise error.ParseError(_("couldn't parse size: %s") % s)
345 raise error.ParseError(_("couldn't parse size: %s") % s)
346
346
347 def sizematcher(x):
348 """Return a function(size) -> bool from the ``size()`` expression"""
349
350 # i18n: "size" is a keyword
351 expr = getstring(x, _("size requires an expression")).strip()
352 if '-' in expr: # do we have a range?
353 a, b = expr.split('-', 1)
354 a = util.sizetoint(a)
355 b = util.sizetoint(b)
356 return lambda x: x >= a and x <= b
357 elif expr.startswith("<="):
358 a = util.sizetoint(expr[2:])
359 return lambda x: x <= a
360 elif expr.startswith("<"):
361 a = util.sizetoint(expr[1:])
362 return lambda x: x < a
363 elif expr.startswith(">="):
364 a = util.sizetoint(expr[2:])
365 return lambda x: x >= a
366 elif expr.startswith(">"):
367 a = util.sizetoint(expr[1:])
368 return lambda x: x > a
369 elif expr[0].isdigit or expr[0] == '.':
370 a = util.sizetoint(expr)
371 b = _sizetomax(expr)
372 return lambda x: x >= a and x <= b
373 raise error.ParseError(_("couldn't parse size: %s") % expr)
374
347 @predicate('size(expression)', callexisting=True)
375 @predicate('size(expression)', callexisting=True)
348 def size(mctx, x):
376 def size(mctx, x):
349 """File size matches the given expression. Examples:
377 """File size matches the given expression. Examples:
350
378
351 - size('1k') - files from 1024 to 2047 bytes
379 - size('1k') - files from 1024 to 2047 bytes
352 - size('< 20k') - files less than 20480 bytes
380 - size('< 20k') - files less than 20480 bytes
353 - size('>= .5MB') - files at least 524288 bytes
381 - size('>= .5MB') - files at least 524288 bytes
354 - size('4k - 1MB') - files from 4096 bytes to 1048576 bytes
382 - size('4k - 1MB') - files from 4096 bytes to 1048576 bytes
355 """
383 """
356
384 m = sizematcher(x)
357 # i18n: "size" is a keyword
358 expr = getstring(x, _("size requires an expression")).strip()
359 if '-' in expr: # do we have a range?
360 a, b = expr.split('-', 1)
361 a = util.sizetoint(a)
362 b = util.sizetoint(b)
363 m = lambda x: x >= a and x <= b
364 elif expr.startswith("<="):
365 a = util.sizetoint(expr[2:])
366 m = lambda x: x <= a
367 elif expr.startswith("<"):
368 a = util.sizetoint(expr[1:])
369 m = lambda x: x < a
370 elif expr.startswith(">="):
371 a = util.sizetoint(expr[2:])
372 m = lambda x: x >= a
373 elif expr.startswith(">"):
374 a = util.sizetoint(expr[1:])
375 m = lambda x: x > a
376 elif expr[0].isdigit or expr[0] == '.':
377 a = util.sizetoint(expr)
378 b = _sizetomax(expr)
379 m = lambda x: x >= a and x <= b
380 else:
381 raise error.ParseError(_("couldn't parse size: %s") % expr)
382
383 return [f for f in mctx.existing() if m(mctx.ctx[f].size())]
385 return [f for f in mctx.existing() if m(mctx.ctx[f].size())]
384
386
385 @predicate('encoding(name)', callexisting=True)
387 @predicate('encoding(name)', callexisting=True)
386 def encoding(mctx, x):
388 def encoding(mctx, x):
387 """File can be successfully decoded with the given character
389 """File can be successfully decoded with the given character
388 encoding. May not be useful for encodings other than ASCII and
390 encoding. May not be useful for encodings other than ASCII and
389 UTF-8.
391 UTF-8.
390 """
392 """
391
393
392 # i18n: "encoding" is a keyword
394 # i18n: "encoding" is a keyword
393 enc = getstring(x, _("encoding requires an encoding name"))
395 enc = getstring(x, _("encoding requires an encoding name"))
394
396
395 s = []
397 s = []
396 for f in mctx.existing():
398 for f in mctx.existing():
397 d = mctx.ctx[f].data()
399 d = mctx.ctx[f].data()
398 try:
400 try:
399 d.decode(enc)
401 d.decode(enc)
400 except LookupError:
402 except LookupError:
401 raise error.Abort(_("unknown encoding '%s'") % enc)
403 raise error.Abort(_("unknown encoding '%s'") % enc)
402 except UnicodeDecodeError:
404 except UnicodeDecodeError:
403 continue
405 continue
404 s.append(f)
406 s.append(f)
405
407
406 return s
408 return s
407
409
408 @predicate('eol(style)', callexisting=True)
410 @predicate('eol(style)', callexisting=True)
409 def eol(mctx, x):
411 def eol(mctx, x):
410 """File contains newlines of the given style (dos, unix, mac). Binary
412 """File contains newlines of the given style (dos, unix, mac). Binary
411 files are excluded, files with mixed line endings match multiple
413 files are excluded, files with mixed line endings match multiple
412 styles.
414 styles.
413 """
415 """
414
416
415 # i18n: "eol" is a keyword
417 # i18n: "eol" is a keyword
416 enc = getstring(x, _("eol requires a style name"))
418 enc = getstring(x, _("eol requires a style name"))
417
419
418 s = []
420 s = []
419 for f in mctx.existing():
421 for f in mctx.existing():
420 d = mctx.ctx[f].data()
422 d = mctx.ctx[f].data()
421 if util.binary(d):
423 if util.binary(d):
422 continue
424 continue
423 if (enc == 'dos' or enc == 'win') and '\r\n' in d:
425 if (enc == 'dos' or enc == 'win') and '\r\n' in d:
424 s.append(f)
426 s.append(f)
425 elif enc == 'unix' and re.search('(?<!\r)\n', d):
427 elif enc == 'unix' and re.search('(?<!\r)\n', d):
426 s.append(f)
428 s.append(f)
427 elif enc == 'mac' and re.search('\r(?!\n)', d):
429 elif enc == 'mac' and re.search('\r(?!\n)', d):
428 s.append(f)
430 s.append(f)
429 return s
431 return s
430
432
431 @predicate('copied()')
433 @predicate('copied()')
432 def copied(mctx, x):
434 def copied(mctx, x):
433 """File that is recorded as being copied.
435 """File that is recorded as being copied.
434 """
436 """
435 # i18n: "copied" is a keyword
437 # i18n: "copied" is a keyword
436 getargs(x, 0, 0, _("copied takes no arguments"))
438 getargs(x, 0, 0, _("copied takes no arguments"))
437 s = []
439 s = []
438 for f in mctx.subset:
440 for f in mctx.subset:
439 p = mctx.ctx[f].parents()
441 p = mctx.ctx[f].parents()
440 if p and p[0].path() != f:
442 if p and p[0].path() != f:
441 s.append(f)
443 s.append(f)
442 return s
444 return s
443
445
444 @predicate('revs(revs, pattern)')
446 @predicate('revs(revs, pattern)')
445 def revs(mctx, x):
447 def revs(mctx, x):
446 """Evaluate set in the specified revisions. If the revset match multiple
448 """Evaluate set in the specified revisions. If the revset match multiple
447 revs, this will return file matching pattern in any of the revision.
449 revs, this will return file matching pattern in any of the revision.
448 """
450 """
449 # i18n: "revs" is a keyword
451 # i18n: "revs" is a keyword
450 r, x = getargs(x, 2, 2, _("revs takes two arguments"))
452 r, x = getargs(x, 2, 2, _("revs takes two arguments"))
451 # i18n: "revs" is a keyword
453 # i18n: "revs" is a keyword
452 revspec = getstring(r, _("first argument to revs must be a revision"))
454 revspec = getstring(r, _("first argument to revs must be a revision"))
453 repo = mctx.ctx.repo()
455 repo = mctx.ctx.repo()
454 revs = scmutil.revrange(repo, [revspec])
456 revs = scmutil.revrange(repo, [revspec])
455
457
456 found = set()
458 found = set()
457 result = []
459 result = []
458 for r in revs:
460 for r in revs:
459 ctx = repo[r]
461 ctx = repo[r]
460 for f in getset(mctx.switch(ctx, _buildstatus(ctx, x)), x):
462 for f in getset(mctx.switch(ctx, _buildstatus(ctx, x)), x):
461 if f not in found:
463 if f not in found:
462 found.add(f)
464 found.add(f)
463 result.append(f)
465 result.append(f)
464 return result
466 return result
465
467
466 @predicate('status(base, rev, pattern)')
468 @predicate('status(base, rev, pattern)')
467 def status(mctx, x):
469 def status(mctx, x):
468 """Evaluate predicate using status change between ``base`` and
470 """Evaluate predicate using status change between ``base`` and
469 ``rev``. Examples:
471 ``rev``. Examples:
470
472
471 - ``status(3, 7, added())`` - matches files added from "3" to "7"
473 - ``status(3, 7, added())`` - matches files added from "3" to "7"
472 """
474 """
473 repo = mctx.ctx.repo()
475 repo = mctx.ctx.repo()
474 # i18n: "status" is a keyword
476 # i18n: "status" is a keyword
475 b, r, x = getargs(x, 3, 3, _("status takes three arguments"))
477 b, r, x = getargs(x, 3, 3, _("status takes three arguments"))
476 # i18n: "status" is a keyword
478 # i18n: "status" is a keyword
477 baseerr = _("first argument to status must be a revision")
479 baseerr = _("first argument to status must be a revision")
478 baserevspec = getstring(b, baseerr)
480 baserevspec = getstring(b, baseerr)
479 if not baserevspec:
481 if not baserevspec:
480 raise error.ParseError(baseerr)
482 raise error.ParseError(baseerr)
481 reverr = _("second argument to status must be a revision")
483 reverr = _("second argument to status must be a revision")
482 revspec = getstring(r, reverr)
484 revspec = getstring(r, reverr)
483 if not revspec:
485 if not revspec:
484 raise error.ParseError(reverr)
486 raise error.ParseError(reverr)
485 basenode, node = scmutil.revpair(repo, [baserevspec, revspec])
487 basenode, node = scmutil.revpair(repo, [baserevspec, revspec])
486 basectx = repo[basenode]
488 basectx = repo[basenode]
487 ctx = repo[node]
489 ctx = repo[node]
488 return getset(mctx.switch(ctx, _buildstatus(ctx, x, basectx=basectx)), x)
490 return getset(mctx.switch(ctx, _buildstatus(ctx, x, basectx=basectx)), x)
489
491
490 @predicate('subrepo([pattern])')
492 @predicate('subrepo([pattern])')
491 def subrepo(mctx, x):
493 def subrepo(mctx, x):
492 """Subrepositories whose paths match the given pattern.
494 """Subrepositories whose paths match the given pattern.
493 """
495 """
494 # i18n: "subrepo" is a keyword
496 # i18n: "subrepo" is a keyword
495 getargs(x, 0, 1, _("subrepo takes at most one argument"))
497 getargs(x, 0, 1, _("subrepo takes at most one argument"))
496 ctx = mctx.ctx
498 ctx = mctx.ctx
497 sstate = sorted(ctx.substate)
499 sstate = sorted(ctx.substate)
498 if x:
500 if x:
499 # i18n: "subrepo" is a keyword
501 # i18n: "subrepo" is a keyword
500 pat = getstring(x, _("subrepo requires a pattern or no arguments"))
502 pat = getstring(x, _("subrepo requires a pattern or no arguments"))
501
503
502 from . import match as matchmod # avoid circular import issues
504 from . import match as matchmod # avoid circular import issues
503 fast = not matchmod.patkind(pat)
505 fast = not matchmod.patkind(pat)
504 if fast:
506 if fast:
505 def m(s):
507 def m(s):
506 return (s == pat)
508 return (s == pat)
507 else:
509 else:
508 m = matchmod.match(ctx.repo().root, '', [pat], ctx=ctx)
510 m = matchmod.match(ctx.repo().root, '', [pat], ctx=ctx)
509 return [sub for sub in sstate if m(sub)]
511 return [sub for sub in sstate if m(sub)]
510 else:
512 else:
511 return [sub for sub in sstate]
513 return [sub for sub in sstate]
512
514
513 methods = {
515 methods = {
514 'string': stringset,
516 'string': stringset,
515 'symbol': stringset,
517 'symbol': stringset,
516 'and': andset,
518 'and': andset,
517 'or': orset,
519 'or': orset,
518 'minus': minusset,
520 'minus': minusset,
519 'list': listset,
521 'list': listset,
520 'group': getset,
522 'group': getset,
521 'not': notset,
523 'not': notset,
522 'func': func,
524 'func': func,
523 }
525 }
524
526
525 class matchctx(object):
527 class matchctx(object):
526 def __init__(self, ctx, subset, status=None):
528 def __init__(self, ctx, subset, status=None):
527 self.ctx = ctx
529 self.ctx = ctx
528 self.subset = subset
530 self.subset = subset
529 self._status = status
531 self._status = status
530 self._existingenabled = False
532 self._existingenabled = False
531 def status(self):
533 def status(self):
532 return self._status
534 return self._status
533 def matcher(self, patterns):
535 def matcher(self, patterns):
534 return self.ctx.match(patterns)
536 return self.ctx.match(patterns)
535 def filter(self, files):
537 def filter(self, files):
536 return [f for f in files if f in self.subset]
538 return [f for f in files if f in self.subset]
537 def existing(self):
539 def existing(self):
538 assert self._existingenabled, 'unexpected existing() invocation'
540 assert self._existingenabled, 'unexpected existing() invocation'
539 if self._status is not None:
541 if self._status is not None:
540 removed = set(self._status[3])
542 removed = set(self._status[3])
541 unknown = set(self._status[4] + self._status[5])
543 unknown = set(self._status[4] + self._status[5])
542 else:
544 else:
543 removed = set()
545 removed = set()
544 unknown = set()
546 unknown = set()
545 return (f for f in self.subset
547 return (f for f in self.subset
546 if (f in self.ctx and f not in removed) or f in unknown)
548 if (f in self.ctx and f not in removed) or f in unknown)
547 def narrow(self, files):
549 def narrow(self, files):
548 return matchctx(self.ctx, self.filter(files), self._status)
550 return matchctx(self.ctx, self.filter(files), self._status)
549 def switch(self, ctx, status=None):
551 def switch(self, ctx, status=None):
550 subset = self.filter(_buildsubset(ctx, status))
552 subset = self.filter(_buildsubset(ctx, status))
551 return matchctx(ctx, subset, status)
553 return matchctx(ctx, subset, status)
552
554
553 class fullmatchctx(matchctx):
555 class fullmatchctx(matchctx):
554 """A match context where any files in any revisions should be valid"""
556 """A match context where any files in any revisions should be valid"""
555
557
556 def __init__(self, ctx, status=None):
558 def __init__(self, ctx, status=None):
557 subset = _buildsubset(ctx, status)
559 subset = _buildsubset(ctx, status)
558 super(fullmatchctx, self).__init__(ctx, subset, status)
560 super(fullmatchctx, self).__init__(ctx, subset, status)
559 def switch(self, ctx, status=None):
561 def switch(self, ctx, status=None):
560 return fullmatchctx(ctx, status)
562 return fullmatchctx(ctx, status)
561
563
562 # filesets using matchctx.switch()
564 # filesets using matchctx.switch()
563 _switchcallers = [
565 _switchcallers = [
564 'revs',
566 'revs',
565 'status',
567 'status',
566 ]
568 ]
567
569
568 def _intree(funcs, tree):
570 def _intree(funcs, tree):
569 if isinstance(tree, tuple):
571 if isinstance(tree, tuple):
570 if tree[0] == 'func' and tree[1][0] == 'symbol':
572 if tree[0] == 'func' and tree[1][0] == 'symbol':
571 if tree[1][1] in funcs:
573 if tree[1][1] in funcs:
572 return True
574 return True
573 if tree[1][1] in _switchcallers:
575 if tree[1][1] in _switchcallers:
574 # arguments won't be evaluated in the current context
576 # arguments won't be evaluated in the current context
575 return False
577 return False
576 for s in tree[1:]:
578 for s in tree[1:]:
577 if _intree(funcs, s):
579 if _intree(funcs, s):
578 return True
580 return True
579 return False
581 return False
580
582
581 def _buildsubset(ctx, status):
583 def _buildsubset(ctx, status):
582 if status:
584 if status:
583 subset = []
585 subset = []
584 for c in status:
586 for c in status:
585 subset.extend(c)
587 subset.extend(c)
586 return subset
588 return subset
587 else:
589 else:
588 return list(ctx.walk(ctx.match([])))
590 return list(ctx.walk(ctx.match([])))
589
591
590 def getfileset(ctx, expr):
592 def getfileset(ctx, expr):
591 tree = parse(expr)
593 tree = parse(expr)
592 return getset(fullmatchctx(ctx, _buildstatus(ctx, tree)), tree)
594 return getset(fullmatchctx(ctx, _buildstatus(ctx, tree)), tree)
593
595
594 def _buildstatus(ctx, tree, basectx=None):
596 def _buildstatus(ctx, tree, basectx=None):
595 # do we need status info?
597 # do we need status info?
596
598
597 # temporaty boolean to simplify the next conditional
599 # temporaty boolean to simplify the next conditional
598 purewdir = ctx.rev() is None and basectx is None
600 purewdir = ctx.rev() is None and basectx is None
599
601
600 if (_intree(_statuscallers, tree) or
602 if (_intree(_statuscallers, tree) or
601 # Using matchctx.existing() on a workingctx requires us to check
603 # Using matchctx.existing() on a workingctx requires us to check
602 # for deleted files.
604 # for deleted files.
603 (purewdir and _intree(_existingcallers, tree))):
605 (purewdir and _intree(_existingcallers, tree))):
604 unknown = _intree(['unknown'], tree)
606 unknown = _intree(['unknown'], tree)
605 ignored = _intree(['ignored'], tree)
607 ignored = _intree(['ignored'], tree)
606
608
607 r = ctx.repo()
609 r = ctx.repo()
608 if basectx is None:
610 if basectx is None:
609 basectx = ctx.p1()
611 basectx = ctx.p1()
610 return r.status(basectx, ctx,
612 return r.status(basectx, ctx,
611 unknown=unknown, ignored=ignored, clean=True)
613 unknown=unknown, ignored=ignored, clean=True)
612 else:
614 else:
613 return None
615 return None
614
616
615 def prettyformat(tree):
617 def prettyformat(tree):
616 return parser.prettyformat(tree, ('string', 'symbol'))
618 return parser.prettyformat(tree, ('string', 'symbol'))
617
619
618 def loadpredicate(ui, extname, registrarobj):
620 def loadpredicate(ui, extname, registrarobj):
619 """Load fileset predicates from specified registrarobj
621 """Load fileset predicates from specified registrarobj
620 """
622 """
621 for name, func in registrarobj._table.iteritems():
623 for name, func in registrarobj._table.iteritems():
622 symbols[name] = func
624 symbols[name] = func
623 if func._callstatus:
625 if func._callstatus:
624 _statuscallers.add(name)
626 _statuscallers.add(name)
625 if func._callexisting:
627 if func._callexisting:
626 _existingcallers.add(name)
628 _existingcallers.add(name)
627
629
628 # load built-in predicates explicitly to setup _statuscallers/_existingcallers
630 # load built-in predicates explicitly to setup _statuscallers/_existingcallers
629 loadpredicate(None, None, predicate)
631 loadpredicate(None, None, predicate)
630
632
631 # tell hggettext to extract docstrings from these functions:
633 # tell hggettext to extract docstrings from these functions:
632 i18nfunctions = symbols.values()
634 i18nfunctions = symbols.values()
General Comments 0
You need to be logged in to leave comments. Login now