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