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