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