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