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