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