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