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