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