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