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