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