##// END OF EJS Templates
fileset: insert hints where status should be computed...
Yuya Nishihara -
r38915:e79a69af default
parent child Browse files
Show More
@@ -1,573 +1,577 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 errno
11 11 import re
12 12
13 13 from .i18n import _
14 14 from . import (
15 15 error,
16 16 filesetlang,
17 17 match as matchmod,
18 18 merge,
19 19 pycompat,
20 20 registrar,
21 21 scmutil,
22 22 util,
23 23 )
24 24 from .utils import (
25 25 stringutil,
26 26 )
27 27
28 28 # common weight constants
29 29 _WEIGHT_CHECK_FILENAME = filesetlang.WEIGHT_CHECK_FILENAME
30 30 _WEIGHT_READ_CONTENTS = filesetlang.WEIGHT_READ_CONTENTS
31 31 _WEIGHT_STATUS = filesetlang.WEIGHT_STATUS
32 32 _WEIGHT_STATUS_THOROUGH = filesetlang.WEIGHT_STATUS_THOROUGH
33 33
34 34 # helpers for processing parsed tree
35 35 getsymbol = filesetlang.getsymbol
36 36 getstring = filesetlang.getstring
37 37 _getkindpat = filesetlang.getkindpat
38 38 getpattern = filesetlang.getpattern
39 39 getargs = filesetlang.getargs
40 40
41 41 def getmatch(mctx, x):
42 42 if not x:
43 43 raise error.ParseError(_("missing argument"))
44 44 return methods[x[0]](mctx, *x[1:])
45 45
46 def getmatchwithstatus(mctx, x, hint):
47 return getmatch(mctx, x)
48
46 49 def stringmatch(mctx, x):
47 50 return mctx.matcher([x])
48 51
49 52 def kindpatmatch(mctx, x, y):
50 53 return stringmatch(mctx, _getkindpat(x, y, matchmod.allpatternkinds,
51 54 _("pattern must be a string")))
52 55
53 56 def patternsmatch(mctx, *xs):
54 57 allkinds = matchmod.allpatternkinds
55 58 patterns = [getpattern(x, allkinds, _("pattern must be a string"))
56 59 for x in xs]
57 60 return mctx.matcher(patterns)
58 61
59 62 def andmatch(mctx, x, y):
60 63 xm = getmatch(mctx, x)
61 64 ym = getmatch(mctx, y)
62 65 return matchmod.intersectmatchers(xm, ym)
63 66
64 67 def ormatch(mctx, *xs):
65 68 ms = [getmatch(mctx, x) for x in xs]
66 69 return matchmod.unionmatcher(ms)
67 70
68 71 def notmatch(mctx, x):
69 72 m = getmatch(mctx, x)
70 73 return mctx.predicate(lambda f: not m(f), predrepr=('<not %r>', m))
71 74
72 75 def minusmatch(mctx, x, y):
73 76 xm = getmatch(mctx, x)
74 77 ym = getmatch(mctx, y)
75 78 return matchmod.differencematcher(xm, ym)
76 79
77 80 def listmatch(mctx, *xs):
78 81 raise error.ParseError(_("can't use a list in this context"),
79 82 hint=_('see \'hg help "filesets.x or y"\''))
80 83
81 84 def func(mctx, a, b):
82 85 funcname = getsymbol(a)
83 86 if funcname in symbols:
84 87 return symbols[funcname](mctx, b)
85 88
86 89 keep = lambda fn: getattr(fn, '__doc__', None) is not None
87 90
88 91 syms = [s for (s, fn) in symbols.items() if keep(fn)]
89 92 raise error.UnknownIdentifier(funcname, syms)
90 93
91 94 # symbols are callable like:
92 95 # fun(mctx, x)
93 96 # with:
94 97 # mctx - current matchctx instance
95 98 # x - argument in tree form
96 99 symbols = filesetlang.symbols
97 100
98 101 # filesets using matchctx.status()
99 102 _statuscallers = set()
100 103
101 104 predicate = registrar.filesetpredicate()
102 105
103 106 @predicate('modified()', callstatus=True, weight=_WEIGHT_STATUS)
104 107 def modified(mctx, x):
105 108 """File that is modified according to :hg:`status`.
106 109 """
107 110 # i18n: "modified" is a keyword
108 111 getargs(x, 0, 0, _("modified takes no arguments"))
109 112 s = set(mctx.status().modified)
110 113 return mctx.predicate(s.__contains__, predrepr='modified')
111 114
112 115 @predicate('added()', callstatus=True, weight=_WEIGHT_STATUS)
113 116 def added(mctx, x):
114 117 """File that is added according to :hg:`status`.
115 118 """
116 119 # i18n: "added" is a keyword
117 120 getargs(x, 0, 0, _("added takes no arguments"))
118 121 s = set(mctx.status().added)
119 122 return mctx.predicate(s.__contains__, predrepr='added')
120 123
121 124 @predicate('removed()', callstatus=True, weight=_WEIGHT_STATUS)
122 125 def removed(mctx, x):
123 126 """File that is removed according to :hg:`status`.
124 127 """
125 128 # i18n: "removed" is a keyword
126 129 getargs(x, 0, 0, _("removed takes no arguments"))
127 130 s = set(mctx.status().removed)
128 131 return mctx.predicate(s.__contains__, predrepr='removed')
129 132
130 133 @predicate('deleted()', callstatus=True, weight=_WEIGHT_STATUS)
131 134 def deleted(mctx, x):
132 135 """Alias for ``missing()``.
133 136 """
134 137 # i18n: "deleted" is a keyword
135 138 getargs(x, 0, 0, _("deleted takes no arguments"))
136 139 s = set(mctx.status().deleted)
137 140 return mctx.predicate(s.__contains__, predrepr='deleted')
138 141
139 142 @predicate('missing()', callstatus=True, weight=_WEIGHT_STATUS)
140 143 def missing(mctx, x):
141 144 """File that is missing according to :hg:`status`.
142 145 """
143 146 # i18n: "missing" is a keyword
144 147 getargs(x, 0, 0, _("missing takes no arguments"))
145 148 s = set(mctx.status().deleted)
146 149 return mctx.predicate(s.__contains__, predrepr='deleted')
147 150
148 151 @predicate('unknown()', callstatus=True, weight=_WEIGHT_STATUS_THOROUGH)
149 152 def unknown(mctx, x):
150 153 """File that is unknown according to :hg:`status`."""
151 154 # i18n: "unknown" is a keyword
152 155 getargs(x, 0, 0, _("unknown takes no arguments"))
153 156 s = set(mctx.status().unknown)
154 157 return mctx.predicate(s.__contains__, predrepr='unknown')
155 158
156 159 @predicate('ignored()', callstatus=True, weight=_WEIGHT_STATUS_THOROUGH)
157 160 def ignored(mctx, x):
158 161 """File that is ignored according to :hg:`status`."""
159 162 # i18n: "ignored" is a keyword
160 163 getargs(x, 0, 0, _("ignored takes no arguments"))
161 164 s = set(mctx.status().ignored)
162 165 return mctx.predicate(s.__contains__, predrepr='ignored')
163 166
164 167 @predicate('clean()', callstatus=True, weight=_WEIGHT_STATUS)
165 168 def clean(mctx, x):
166 169 """File that is clean according to :hg:`status`.
167 170 """
168 171 # i18n: "clean" is a keyword
169 172 getargs(x, 0, 0, _("clean takes no arguments"))
170 173 s = set(mctx.status().clean)
171 174 return mctx.predicate(s.__contains__, predrepr='clean')
172 175
173 176 @predicate('tracked()')
174 177 def tracked(mctx, x):
175 178 """File that is under Mercurial control."""
176 179 # i18n: "tracked" is a keyword
177 180 getargs(x, 0, 0, _("tracked takes no arguments"))
178 181 return mctx.predicate(mctx.ctx.__contains__, predrepr='tracked')
179 182
180 183 @predicate('binary()', weight=_WEIGHT_READ_CONTENTS)
181 184 def binary(mctx, x):
182 185 """File that appears to be binary (contains NUL bytes).
183 186 """
184 187 # i18n: "binary" is a keyword
185 188 getargs(x, 0, 0, _("binary takes no arguments"))
186 189 return mctx.fpredicate(lambda fctx: fctx.isbinary(),
187 190 predrepr='binary', cache=True)
188 191
189 192 @predicate('exec()')
190 193 def exec_(mctx, x):
191 194 """File that is marked as executable.
192 195 """
193 196 # i18n: "exec" is a keyword
194 197 getargs(x, 0, 0, _("exec takes no arguments"))
195 198 ctx = mctx.ctx
196 199 return mctx.predicate(lambda f: ctx.flags(f) == 'x', predrepr='exec')
197 200
198 201 @predicate('symlink()')
199 202 def symlink(mctx, x):
200 203 """File that is marked as a symlink.
201 204 """
202 205 # i18n: "symlink" is a keyword
203 206 getargs(x, 0, 0, _("symlink takes no arguments"))
204 207 ctx = mctx.ctx
205 208 return mctx.predicate(lambda f: ctx.flags(f) == 'l', predrepr='symlink')
206 209
207 210 @predicate('resolved()', weight=_WEIGHT_STATUS)
208 211 def resolved(mctx, x):
209 212 """File that is marked resolved according to :hg:`resolve -l`.
210 213 """
211 214 # i18n: "resolved" is a keyword
212 215 getargs(x, 0, 0, _("resolved takes no arguments"))
213 216 if mctx.ctx.rev() is not None:
214 217 return mctx.never()
215 218 ms = merge.mergestate.read(mctx.ctx.repo())
216 219 return mctx.predicate(lambda f: f in ms and ms[f] == 'r',
217 220 predrepr='resolved')
218 221
219 222 @predicate('unresolved()', weight=_WEIGHT_STATUS)
220 223 def unresolved(mctx, x):
221 224 """File that is marked unresolved according to :hg:`resolve -l`.
222 225 """
223 226 # i18n: "unresolved" is a keyword
224 227 getargs(x, 0, 0, _("unresolved takes no arguments"))
225 228 if mctx.ctx.rev() is not None:
226 229 return mctx.never()
227 230 ms = merge.mergestate.read(mctx.ctx.repo())
228 231 return mctx.predicate(lambda f: f in ms and ms[f] == 'u',
229 232 predrepr='unresolved')
230 233
231 234 @predicate('hgignore()', weight=_WEIGHT_STATUS)
232 235 def hgignore(mctx, x):
233 236 """File that matches the active .hgignore pattern.
234 237 """
235 238 # i18n: "hgignore" is a keyword
236 239 getargs(x, 0, 0, _("hgignore takes no arguments"))
237 240 return mctx.ctx.repo().dirstate._ignore
238 241
239 242 @predicate('portable()', weight=_WEIGHT_CHECK_FILENAME)
240 243 def portable(mctx, x):
241 244 """File that has a portable name. (This doesn't include filenames with case
242 245 collisions.)
243 246 """
244 247 # i18n: "portable" is a keyword
245 248 getargs(x, 0, 0, _("portable takes no arguments"))
246 249 return mctx.predicate(lambda f: util.checkwinfilename(f) is None,
247 250 predrepr='portable')
248 251
249 252 @predicate('grep(regex)', weight=_WEIGHT_READ_CONTENTS)
250 253 def grep(mctx, x):
251 254 """File contains the given regular expression.
252 255 """
253 256 try:
254 257 # i18n: "grep" is a keyword
255 258 r = re.compile(getstring(x, _("grep requires a pattern")))
256 259 except re.error as e:
257 260 raise error.ParseError(_('invalid match pattern: %s') %
258 261 stringutil.forcebytestr(e))
259 262 return mctx.fpredicate(lambda fctx: r.search(fctx.data()),
260 263 predrepr=('grep(%r)', r.pattern), cache=True)
261 264
262 265 def _sizetomax(s):
263 266 try:
264 267 s = s.strip().lower()
265 268 for k, v in util._sizeunits:
266 269 if s.endswith(k):
267 270 # max(4k) = 5k - 1, max(4.5k) = 4.6k - 1
268 271 n = s[:-len(k)]
269 272 inc = 1.0
270 273 if "." in n:
271 274 inc /= 10 ** len(n.split(".")[1])
272 275 return int((float(n) + inc) * v) - 1
273 276 # no extension, this is a precise value
274 277 return int(s)
275 278 except ValueError:
276 279 raise error.ParseError(_("couldn't parse size: %s") % s)
277 280
278 281 def sizematcher(expr):
279 282 """Return a function(size) -> bool from the ``size()`` expression"""
280 283 expr = expr.strip()
281 284 if '-' in expr: # do we have a range?
282 285 a, b = expr.split('-', 1)
283 286 a = util.sizetoint(a)
284 287 b = util.sizetoint(b)
285 288 return lambda x: x >= a and x <= b
286 289 elif expr.startswith("<="):
287 290 a = util.sizetoint(expr[2:])
288 291 return lambda x: x <= a
289 292 elif expr.startswith("<"):
290 293 a = util.sizetoint(expr[1:])
291 294 return lambda x: x < a
292 295 elif expr.startswith(">="):
293 296 a = util.sizetoint(expr[2:])
294 297 return lambda x: x >= a
295 298 elif expr.startswith(">"):
296 299 a = util.sizetoint(expr[1:])
297 300 return lambda x: x > a
298 301 else:
299 302 a = util.sizetoint(expr)
300 303 b = _sizetomax(expr)
301 304 return lambda x: x >= a and x <= b
302 305
303 306 @predicate('size(expression)', weight=_WEIGHT_STATUS)
304 307 def size(mctx, x):
305 308 """File size matches the given expression. Examples:
306 309
307 310 - size('1k') - files from 1024 to 2047 bytes
308 311 - size('< 20k') - files less than 20480 bytes
309 312 - size('>= .5MB') - files at least 524288 bytes
310 313 - size('4k - 1MB') - files from 4096 bytes to 1048576 bytes
311 314 """
312 315 # i18n: "size" is a keyword
313 316 expr = getstring(x, _("size requires an expression"))
314 317 m = sizematcher(expr)
315 318 return mctx.fpredicate(lambda fctx: m(fctx.size()),
316 319 predrepr=('size(%r)', expr), cache=True)
317 320
318 321 @predicate('encoding(name)', weight=_WEIGHT_READ_CONTENTS)
319 322 def encoding(mctx, x):
320 323 """File can be successfully decoded with the given character
321 324 encoding. May not be useful for encodings other than ASCII and
322 325 UTF-8.
323 326 """
324 327
325 328 # i18n: "encoding" is a keyword
326 329 enc = getstring(x, _("encoding requires an encoding name"))
327 330
328 331 def encp(fctx):
329 332 d = fctx.data()
330 333 try:
331 334 d.decode(pycompat.sysstr(enc))
332 335 return True
333 336 except LookupError:
334 337 raise error.Abort(_("unknown encoding '%s'") % enc)
335 338 except UnicodeDecodeError:
336 339 return False
337 340
338 341 return mctx.fpredicate(encp, predrepr=('encoding(%r)', enc), cache=True)
339 342
340 343 @predicate('eol(style)', weight=_WEIGHT_READ_CONTENTS)
341 344 def eol(mctx, x):
342 345 """File contains newlines of the given style (dos, unix, mac). Binary
343 346 files are excluded, files with mixed line endings match multiple
344 347 styles.
345 348 """
346 349
347 350 # i18n: "eol" is a keyword
348 351 enc = getstring(x, _("eol requires a style name"))
349 352
350 353 def eolp(fctx):
351 354 if fctx.isbinary():
352 355 return False
353 356 d = fctx.data()
354 357 if (enc == 'dos' or enc == 'win') and '\r\n' in d:
355 358 return True
356 359 elif enc == 'unix' and re.search('(?<!\r)\n', d):
357 360 return True
358 361 elif enc == 'mac' and re.search('\r(?!\n)', d):
359 362 return True
360 363 return False
361 364 return mctx.fpredicate(eolp, predrepr=('eol(%r)', enc), cache=True)
362 365
363 366 @predicate('copied()')
364 367 def copied(mctx, x):
365 368 """File that is recorded as being copied.
366 369 """
367 370 # i18n: "copied" is a keyword
368 371 getargs(x, 0, 0, _("copied takes no arguments"))
369 372 def copiedp(fctx):
370 373 p = fctx.parents()
371 374 return p and p[0].path() != fctx.path()
372 375 return mctx.fpredicate(copiedp, predrepr='copied', cache=True)
373 376
374 377 @predicate('revs(revs, pattern)', weight=_WEIGHT_STATUS)
375 378 def revs(mctx, x):
376 379 """Evaluate set in the specified revisions. If the revset match multiple
377 380 revs, this will return file matching pattern in any of the revision.
378 381 """
379 382 # i18n: "revs" is a keyword
380 383 r, x = getargs(x, 2, 2, _("revs takes two arguments"))
381 384 # i18n: "revs" is a keyword
382 385 revspec = getstring(r, _("first argument to revs must be a revision"))
383 386 repo = mctx.ctx.repo()
384 387 revs = scmutil.revrange(repo, [revspec])
385 388
386 389 matchers = []
387 390 for r in revs:
388 391 ctx = repo[r]
389 392 mc = mctx.switch(ctx.p1(), ctx)
390 393 mc.buildstatus(x)
391 394 matchers.append(getmatch(mc, x))
392 395 if not matchers:
393 396 return mctx.never()
394 397 if len(matchers) == 1:
395 398 return matchers[0]
396 399 return matchmod.unionmatcher(matchers)
397 400
398 401 @predicate('status(base, rev, pattern)', weight=_WEIGHT_STATUS)
399 402 def status(mctx, x):
400 403 """Evaluate predicate using status change between ``base`` and
401 404 ``rev``. Examples:
402 405
403 406 - ``status(3, 7, added())`` - matches files added from "3" to "7"
404 407 """
405 408 repo = mctx.ctx.repo()
406 409 # i18n: "status" is a keyword
407 410 b, r, x = getargs(x, 3, 3, _("status takes three arguments"))
408 411 # i18n: "status" is a keyword
409 412 baseerr = _("first argument to status must be a revision")
410 413 baserevspec = getstring(b, baseerr)
411 414 if not baserevspec:
412 415 raise error.ParseError(baseerr)
413 416 reverr = _("second argument to status must be a revision")
414 417 revspec = getstring(r, reverr)
415 418 if not revspec:
416 419 raise error.ParseError(reverr)
417 420 basectx, ctx = scmutil.revpair(repo, [baserevspec, revspec])
418 421 mc = mctx.switch(basectx, ctx)
419 422 mc.buildstatus(x)
420 423 return getmatch(mc, x)
421 424
422 425 @predicate('subrepo([pattern])')
423 426 def subrepo(mctx, x):
424 427 """Subrepositories whose paths match the given pattern.
425 428 """
426 429 # i18n: "subrepo" is a keyword
427 430 getargs(x, 0, 1, _("subrepo takes at most one argument"))
428 431 ctx = mctx.ctx
429 432 sstate = ctx.substate
430 433 if x:
431 434 pat = getpattern(x, matchmod.allpatternkinds,
432 435 # i18n: "subrepo" is a keyword
433 436 _("subrepo requires a pattern or no arguments"))
434 437 fast = not matchmod.patkind(pat)
435 438 if fast:
436 439 def m(s):
437 440 return (s == pat)
438 441 else:
439 442 m = matchmod.match(ctx.repo().root, '', [pat], ctx=ctx)
440 443 return mctx.predicate(lambda f: f in sstate and m(f),
441 444 predrepr=('subrepo(%r)', pat))
442 445 else:
443 446 return mctx.predicate(sstate.__contains__, predrepr='subrepo')
444 447
445 448 methods = {
449 'withstatus': getmatchwithstatus,
446 450 'string': stringmatch,
447 451 'symbol': stringmatch,
448 452 'kindpat': kindpatmatch,
449 453 'patterns': patternsmatch,
450 454 'and': andmatch,
451 455 'or': ormatch,
452 456 'minus': minusmatch,
453 457 'list': listmatch,
454 458 'not': notmatch,
455 459 'func': func,
456 460 }
457 461
458 462 class matchctx(object):
459 463 def __init__(self, basectx, ctx, badfn=None):
460 464 self._basectx = basectx
461 465 self.ctx = ctx
462 466 self._badfn = badfn
463 467 self._status = None
464 468
465 469 def buildstatus(self, tree):
466 470 if not _intree(_statuscallers, tree):
467 471 return
468 472 unknown = _intree(['unknown'], tree)
469 473 ignored = _intree(['ignored'], tree)
470 474 self._status = self._basectx.status(self.ctx,
471 475 listignored=ignored,
472 476 listclean=True,
473 477 listunknown=unknown)
474 478
475 479 def status(self):
476 480 return self._status
477 481
478 482 def matcher(self, patterns):
479 483 return self.ctx.match(patterns, badfn=self._badfn)
480 484
481 485 def predicate(self, predfn, predrepr=None, cache=False):
482 486 """Create a matcher to select files by predfn(filename)"""
483 487 if cache:
484 488 predfn = util.cachefunc(predfn)
485 489 repo = self.ctx.repo()
486 490 return matchmod.predicatematcher(repo.root, repo.getcwd(), predfn,
487 491 predrepr=predrepr, badfn=self._badfn)
488 492
489 493 def fpredicate(self, predfn, predrepr=None, cache=False):
490 494 """Create a matcher to select files by predfn(fctx) at the current
491 495 revision
492 496
493 497 Missing files are ignored.
494 498 """
495 499 ctx = self.ctx
496 500 if ctx.rev() is None:
497 501 def fctxpredfn(f):
498 502 try:
499 503 fctx = ctx[f]
500 504 except error.LookupError:
501 505 return False
502 506 try:
503 507 fctx.audit()
504 508 except error.Abort:
505 509 return False
506 510 try:
507 511 return predfn(fctx)
508 512 except (IOError, OSError) as e:
509 513 # open()-ing a directory fails with EACCES on Windows
510 514 if e.errno in (errno.ENOENT, errno.EACCES, errno.ENOTDIR,
511 515 errno.EISDIR):
512 516 return False
513 517 raise
514 518 else:
515 519 def fctxpredfn(f):
516 520 try:
517 521 fctx = ctx[f]
518 522 except error.LookupError:
519 523 return False
520 524 return predfn(fctx)
521 525 return self.predicate(fctxpredfn, predrepr=predrepr, cache=cache)
522 526
523 527 def never(self):
524 528 """Create a matcher to select nothing"""
525 529 repo = self.ctx.repo()
526 530 return matchmod.nevermatcher(repo.root, repo.getcwd(),
527 531 badfn=self._badfn)
528 532
529 533 def switch(self, basectx, ctx):
530 534 return matchctx(basectx, ctx, self._badfn)
531 535
532 536 # filesets using matchctx.switch()
533 537 _switchcallers = [
534 538 'revs',
535 539 'status',
536 540 ]
537 541
538 542 def _intree(funcs, tree):
539 543 if isinstance(tree, tuple):
540 544 if tree[0] == 'func' and tree[1][0] == 'symbol':
541 545 if tree[1][1] in funcs:
542 546 return True
543 547 if tree[1][1] in _switchcallers:
544 548 # arguments won't be evaluated in the current context
545 549 return False
546 550 for s in tree[1:]:
547 551 if _intree(funcs, s):
548 552 return True
549 553 return False
550 554
551 555 def match(ctx, expr, badfn=None):
552 556 """Create a matcher for a single fileset expression"""
553 557 tree = filesetlang.parse(expr)
554 558 tree = filesetlang.analyze(tree)
555 559 tree = filesetlang.optimize(tree)
556 560 mctx = matchctx(ctx.p1(), ctx, badfn=badfn)
557 561 mctx.buildstatus(tree)
558 562 return getmatch(mctx, tree)
559 563
560 564
561 565 def loadpredicate(ui, extname, registrarobj):
562 566 """Load fileset predicates from specified registrarobj
563 567 """
564 568 for name, func in registrarobj._table.iteritems():
565 569 symbols[name] = func
566 570 if func._callstatus:
567 571 _statuscallers.add(name)
568 572
569 573 # load built-in predicates explicitly to setup _statuscallers
570 574 loadpredicate(None, None, predicate)
571 575
572 576 # tell hggettext to extract docstrings from these functions:
573 577 i18nfunctions = symbols.values()
@@ -1,249 +1,330 b''
1 1 # filesetlang.py - parser, tokenizer and utility for file set language
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 from .i18n import _
11 11 from . import (
12 12 error,
13 13 parser,
14 14 pycompat,
15 15 )
16 16
17 17 # common weight constants for static optimization
18 18 # (see registrar.filesetpredicate for details)
19 19 WEIGHT_CHECK_FILENAME = 0.5
20 20 WEIGHT_READ_CONTENTS = 30
21 21 WEIGHT_STATUS = 10
22 22 WEIGHT_STATUS_THOROUGH = 50
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 symbols = {}
46 46
47 47 globchars = ".*{}[]?/\\_"
48 48
49 49 def tokenize(program):
50 50 pos, l = 0, len(program)
51 51 program = pycompat.bytestr(program)
52 52 while pos < l:
53 53 c = program[pos]
54 54 if c.isspace(): # skip inter-token whitespace
55 55 pass
56 56 elif c in "(),-:|&+!": # handle simple operators
57 57 yield (c, None, pos)
58 58 elif (c in '"\'' or c == 'r' and
59 59 program[pos:pos + 2] in ("r'", 'r"')): # handle quoted strings
60 60 if c == 'r':
61 61 pos += 1
62 62 c = program[pos]
63 63 decode = lambda x: x
64 64 else:
65 65 decode = parser.unescapestr
66 66 pos += 1
67 67 s = pos
68 68 while pos < l: # find closing quote
69 69 d = program[pos]
70 70 if d == '\\': # skip over escaped characters
71 71 pos += 2
72 72 continue
73 73 if d == c:
74 74 yield ('string', decode(program[s:pos]), s)
75 75 break
76 76 pos += 1
77 77 else:
78 78 raise error.ParseError(_("unterminated string"), s)
79 79 elif c.isalnum() or c in globchars or ord(c) > 127:
80 80 # gather up a symbol/keyword
81 81 s = pos
82 82 pos += 1
83 83 while pos < l: # find end of symbol
84 84 d = program[pos]
85 85 if not (d.isalnum() or d in globchars or ord(d) > 127):
86 86 break
87 87 pos += 1
88 88 sym = program[s:pos]
89 89 if sym in keywords: # operator keywords
90 90 yield (sym, None, s)
91 91 else:
92 92 yield ('symbol', sym, s)
93 93 pos -= 1
94 94 else:
95 95 raise error.ParseError(_("syntax error"), pos)
96 96 pos += 1
97 97 yield ('end', None, pos)
98 98
99 99 def parse(expr):
100 100 p = parser.parser(elements)
101 101 tree, pos = p.parse(tokenize(expr))
102 102 if pos != len(expr):
103 103 raise error.ParseError(_("invalid token"), pos)
104 104 return parser.simplifyinfixops(tree, {'list', 'or'})
105 105
106 106 def getsymbol(x):
107 107 if x and x[0] == 'symbol':
108 108 return x[1]
109 109 raise error.ParseError(_('not a symbol'))
110 110
111 111 def getstring(x, err):
112 112 if x and (x[0] == 'string' or x[0] == 'symbol'):
113 113 return x[1]
114 114 raise error.ParseError(err)
115 115
116 116 def getkindpat(x, y, allkinds, err):
117 117 kind = getsymbol(x)
118 118 pat = getstring(y, err)
119 119 if kind not in allkinds:
120 120 raise error.ParseError(_("invalid pattern kind: %s") % kind)
121 121 return '%s:%s' % (kind, pat)
122 122
123 123 def getpattern(x, allkinds, err):
124 124 if x and x[0] == 'kindpat':
125 125 return getkindpat(x[1], x[2], allkinds, err)
126 126 return getstring(x, err)
127 127
128 128 def getlist(x):
129 129 if not x:
130 130 return []
131 131 if x[0] == 'list':
132 132 return list(x[1:])
133 133 return [x]
134 134
135 135 def getargs(x, min, max, err):
136 136 l = getlist(x)
137 137 if len(l) < min or len(l) > max:
138 138 raise error.ParseError(err)
139 139 return l
140 140
141 141 def _analyze(x):
142 142 if x is None:
143 143 return x
144 144
145 145 op = x[0]
146 146 if op in {'string', 'symbol'}:
147 147 return x
148 148 if op == 'kindpat':
149 149 getsymbol(x[1]) # kind must be a symbol
150 150 t = _analyze(x[2])
151 151 return (op, x[1], t)
152 152 if op == 'group':
153 153 return _analyze(x[1])
154 154 if op == 'negate':
155 155 raise error.ParseError(_("can't use negate operator in this context"))
156 156 if op == 'not':
157 157 t = _analyze(x[1])
158 158 return (op, t)
159 159 if op == 'and':
160 160 ta = _analyze(x[1])
161 161 tb = _analyze(x[2])
162 162 return (op, ta, tb)
163 163 if op == 'minus':
164 164 return _analyze(('and', x[1], ('not', x[2])))
165 165 if op in {'list', 'or'}:
166 166 ts = tuple(_analyze(y) for y in x[1:])
167 167 return (op,) + ts
168 168 if op == 'func':
169 169 getsymbol(x[1]) # function name must be a symbol
170 170 ta = _analyze(x[2])
171 171 return (op, x[1], ta)
172 172 raise error.ProgrammingError('invalid operator %r' % op)
173 173
174 def _insertstatushints(x):
175 """Insert hint nodes where status should be calculated (first path)
176
177 This works in bottom-up way, summing up status names and inserting hint
178 nodes at 'and' and 'or' as needed. Thus redundant hint nodes may be left.
179
180 Returns (status-names, new-tree) at the given subtree, where status-names
181 is a sum of status names referenced in the given subtree.
182 """
183 if x is None:
184 return (), x
185
186 op = x[0]
187 if op in {'string', 'symbol', 'kindpat'}:
188 return (), x
189 if op == 'not':
190 h, t = _insertstatushints(x[1])
191 return h, (op, t)
192 if op == 'and':
193 ha, ta = _insertstatushints(x[1])
194 hb, tb = _insertstatushints(x[2])
195 hr = ha + hb
196 if ha and hb:
197 return hr, ('withstatus', (op, ta, tb), ('string', ' '.join(hr)))
198 return hr, (op, ta, tb)
199 if op == 'or':
200 hs, ts = zip(*(_insertstatushints(y) for y in x[1:]))
201 hr = sum(hs, ())
202 if sum(bool(h) for h in hs) > 1:
203 return hr, ('withstatus', (op,) + ts, ('string', ' '.join(hr)))
204 return hr, (op,) + ts
205 if op == 'list':
206 hs, ts = zip(*(_insertstatushints(y) for y in x[1:]))
207 return sum(hs, ()), (op,) + ts
208 if op == 'func':
209 f = getsymbol(x[1])
210 # don't propagate 'ha' crossing a function boundary
211 ha, ta = _insertstatushints(x[2])
212 if getattr(symbols.get(f), '_callstatus', False):
213 return (f,), ('withstatus', (op, x[1], ta), ('string', f))
214 return (), (op, x[1], ta)
215 raise error.ProgrammingError('invalid operator %r' % op)
216
217 def _mergestatushints(x, instatus):
218 """Remove redundant status hint nodes (second path)
219
220 This is the top-down path to eliminate inner hint nodes.
221 """
222 if x is None:
223 return x
224
225 op = x[0]
226 if op == 'withstatus':
227 if instatus:
228 # drop redundant hint node
229 return _mergestatushints(x[1], instatus)
230 t = _mergestatushints(x[1], instatus=True)
231 return (op, t, x[2])
232 if op in {'string', 'symbol', 'kindpat'}:
233 return x
234 if op == 'not':
235 t = _mergestatushints(x[1], instatus)
236 return (op, t)
237 if op == 'and':
238 ta = _mergestatushints(x[1], instatus)
239 tb = _mergestatushints(x[2], instatus)
240 return (op, ta, tb)
241 if op in {'list', 'or'}:
242 ts = tuple(_mergestatushints(y, instatus) for y in x[1:])
243 return (op,) + ts
244 if op == 'func':
245 # don't propagate 'instatus' crossing a function boundary
246 ta = _mergestatushints(x[2], instatus=False)
247 return (op, x[1], ta)
248 raise error.ProgrammingError('invalid operator %r' % op)
249
174 250 def analyze(x):
175 251 """Transform raw parsed tree to evaluatable tree which can be fed to
176 252 optimize() or getmatch()
177 253
178 254 All pseudo operations should be mapped to real operations or functions
179 255 defined in methods or symbols table respectively.
180 256 """
181 return _analyze(x)
257 t = _analyze(x)
258 _h, t = _insertstatushints(t)
259 return _mergestatushints(t, instatus=False)
182 260
183 261 def _optimizeandops(op, ta, tb):
184 262 if tb is not None and tb[0] == 'not':
185 263 return ('minus', ta, tb[1])
186 264 return (op, ta, tb)
187 265
188 266 def _optimizeunion(xs):
189 267 # collect string patterns so they can be compiled into a single regexp
190 268 ws, ts, ss = [], [], []
191 269 for x in xs:
192 270 w, t = _optimize(x)
193 271 if t is not None and t[0] in {'string', 'symbol', 'kindpat'}:
194 272 ss.append(t)
195 273 continue
196 274 ws.append(w)
197 275 ts.append(t)
198 276 if ss:
199 277 ws.append(WEIGHT_CHECK_FILENAME)
200 278 ts.append(('patterns',) + tuple(ss))
201 279 return ws, ts
202 280
203 281 def _optimize(x):
204 282 if x is None:
205 283 return 0, x
206 284
207 285 op = x[0]
286 if op == 'withstatus':
287 w, t = _optimize(x[1])
288 return w, (op, t, x[2])
208 289 if op in {'string', 'symbol'}:
209 290 return WEIGHT_CHECK_FILENAME, x
210 291 if op == 'kindpat':
211 292 w, t = _optimize(x[2])
212 293 return w, (op, x[1], t)
213 294 if op == 'not':
214 295 w, t = _optimize(x[1])
215 296 return w, (op, t)
216 297 if op == 'and':
217 298 wa, ta = _optimize(x[1])
218 299 wb, tb = _optimize(x[2])
219 300 if wa <= wb:
220 301 return wa, _optimizeandops(op, ta, tb)
221 302 else:
222 303 return wb, _optimizeandops(op, tb, ta)
223 304 if op == 'or':
224 305 ws, ts = _optimizeunion(x[1:])
225 306 if len(ts) == 1:
226 307 return ws[0], ts[0] # 'or' operation is fully optimized out
227 308 ts = tuple(it[1] for it in sorted(enumerate(ts),
228 309 key=lambda it: ws[it[0]]))
229 310 return max(ws), (op,) + ts
230 311 if op == 'list':
231 312 ws, ts = zip(*(_optimize(y) for y in x[1:]))
232 313 return sum(ws), (op,) + ts
233 314 if op == 'func':
234 315 f = getsymbol(x[1])
235 316 w = getattr(symbols.get(f), '_weight', 1)
236 317 wa, ta = _optimize(x[2])
237 318 return w + wa, (op, x[1], ta)
238 319 raise error.ProgrammingError('invalid operator %r' % op)
239 320
240 321 def optimize(x):
241 322 """Reorder/rewrite evaluatable tree for optimization
242 323
243 324 All pseudo operations should be transformed beforehand.
244 325 """
245 326 _w, t = _optimize(x)
246 327 return t
247 328
248 329 def prettyformat(tree):
249 330 return parser.prettyformat(tree, ('string', 'symbol'))
@@ -1,90 +1,92 b''
1 1 # minifileset.py - a simple language to select files
2 2 #
3 3 # Copyright 2017 Facebook, Inc.
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 from .i18n import _
11 11 from . import (
12 12 error,
13 13 fileset,
14 14 filesetlang,
15 15 pycompat,
16 16 )
17 17
18 18 def _sizep(x):
19 19 # i18n: "size" is a keyword
20 20 expr = filesetlang.getstring(x, _("size requires an expression"))
21 21 return fileset.sizematcher(expr)
22 22
23 23 def _compile(tree):
24 24 if not tree:
25 25 raise error.ParseError(_("missing argument"))
26 26 op = tree[0]
27 if op in {'symbol', 'string', 'kindpat'}:
27 if op == 'withstatus':
28 return _compile(tree[1])
29 elif op in {'symbol', 'string', 'kindpat'}:
28 30 name = filesetlang.getpattern(tree, {'path'}, _('invalid file pattern'))
29 31 if name.startswith('**'): # file extension test, ex. "**.tar.gz"
30 32 ext = name[2:]
31 33 for c in pycompat.bytestr(ext):
32 34 if c in '*{}[]?/\\':
33 35 raise error.ParseError(_('reserved character: %s') % c)
34 36 return lambda n, s: n.endswith(ext)
35 37 elif name.startswith('path:'): # directory or full path test
36 38 p = name[5:] # prefix
37 39 pl = len(p)
38 40 f = lambda n, s: n.startswith(p) and (len(n) == pl
39 41 or n[pl:pl + 1] == '/')
40 42 return f
41 43 raise error.ParseError(_("unsupported file pattern: %s") % name,
42 44 hint=_('paths must be prefixed with "path:"'))
43 45 elif op in {'or', 'patterns'}:
44 46 funcs = [_compile(x) for x in tree[1:]]
45 47 return lambda n, s: any(f(n, s) for f in funcs)
46 48 elif op == 'and':
47 49 func1 = _compile(tree[1])
48 50 func2 = _compile(tree[2])
49 51 return lambda n, s: func1(n, s) and func2(n, s)
50 52 elif op == 'not':
51 53 return lambda n, s: not _compile(tree[1])(n, s)
52 54 elif op == 'func':
53 55 symbols = {
54 56 'all': lambda n, s: True,
55 57 'none': lambda n, s: False,
56 58 'size': lambda n, s: _sizep(tree[2])(s),
57 59 }
58 60
59 61 name = filesetlang.getsymbol(tree[1])
60 62 if name in symbols:
61 63 return symbols[name]
62 64
63 65 raise error.UnknownIdentifier(name, symbols.keys())
64 66 elif op == 'minus': # equivalent to 'x and not y'
65 67 func1 = _compile(tree[1])
66 68 func2 = _compile(tree[2])
67 69 return lambda n, s: func1(n, s) and not func2(n, s)
68 70 elif op == 'list':
69 71 raise error.ParseError(_("can't use a list in this context"),
70 72 hint=_('see \'hg help "filesets.x or y"\''))
71 73 raise error.ProgrammingError('illegal tree: %r' % (tree,))
72 74
73 75 def compile(text):
74 76 """generate a function (path, size) -> bool from filter specification.
75 77
76 78 "text" could contain the operators defined by the fileset language for
77 79 common logic operations, and parenthesis for grouping. The supported path
78 80 tests are '**.extname' for file extension test, and '"path:dir/subdir"'
79 81 for prefix test. The ``size()`` predicate is borrowed from filesets to test
80 82 file size. The predicates ``all()`` and ``none()`` are also supported.
81 83
82 84 '(**.php & size(">10MB")) | **.zip | (path:bin & !path:bin/README)' for
83 85 example, will catch all php files whose size is greater than 10 MB, all
84 86 files whose name ends with ".zip", and all files under "bin" in the repo
85 87 root except for "bin/README".
86 88 """
87 89 tree = filesetlang.parse(text)
88 90 tree = filesetlang.analyze(tree)
89 91 tree = filesetlang.optimize(tree)
90 92 return _compile(tree)
@@ -1,883 +1,1037 b''
1 1 $ fileset() {
2 2 > hg debugfileset --all-files "$@"
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 * matcher:
22 22 <patternmatcher patterns='(?:a1$)'>
23 23 a1
24 24 $ fileset -v 'a*'
25 25 (symbol 'a*')
26 26 * matcher:
27 27 <patternmatcher patterns='(?:a[^/]*$)'>
28 28 a1
29 29 a2
30 30 $ fileset -v '"re:a\d"'
31 31 (string 're:a\\d')
32 32 * matcher:
33 33 <patternmatcher patterns='(?:a\\d)'>
34 34 a1
35 35 a2
36 36 $ fileset -v '!re:"a\d"'
37 37 (not
38 38 (kindpat
39 39 (symbol 're')
40 40 (string 'a\\d')))
41 41 * matcher:
42 42 <predicatenmatcher
43 43 pred=<not
44 44 <patternmatcher patterns='(?:a\\d)'>>>
45 45 b1
46 46 b2
47 47 $ fileset -v 'path:a1 or glob:b?'
48 48 (or
49 49 (kindpat
50 50 (symbol 'path')
51 51 (symbol 'a1'))
52 52 (kindpat
53 53 (symbol 'glob')
54 54 (symbol 'b?')))
55 55 * matcher:
56 56 <patternmatcher patterns='(?:a1(?:/|$)|b.$)'>
57 57 a1
58 58 b1
59 59 b2
60 60 $ fileset -v --no-show-matcher 'a1 or a2'
61 61 (or
62 62 (symbol 'a1')
63 63 (symbol 'a2'))
64 64 a1
65 65 a2
66 66 $ fileset 'a1 | a2'
67 67 a1
68 68 a2
69 69 $ fileset 'a* and "*1"'
70 70 a1
71 71 $ fileset 'a* & "*1"'
72 72 a1
73 73 $ fileset 'not (r"a*")'
74 74 b1
75 75 b2
76 76 $ fileset '! ("a*")'
77 77 b1
78 78 b2
79 79 $ fileset 'a* - a1'
80 80 a2
81 81 $ fileset 'a_b'
82 82 $ fileset '"\xy"'
83 83 hg: parse error: invalid \x escape* (glob)
84 84 [255]
85 85
86 86 Test invalid syntax
87 87
88 88 $ fileset -v '"added"()'
89 89 (func
90 90 (string 'added')
91 91 None)
92 92 hg: parse error: not a symbol
93 93 [255]
94 94 $ fileset -v '()()'
95 95 (func
96 96 (group
97 97 None)
98 98 None)
99 99 hg: parse error: not a symbol
100 100 [255]
101 101 $ fileset -v -- '-x'
102 102 (negate
103 103 (symbol 'x'))
104 104 hg: parse error: can't use negate operator in this context
105 105 [255]
106 106 $ fileset -v -- '-()'
107 107 (negate
108 108 (group
109 109 None))
110 110 hg: parse error: can't use negate operator in this context
111 111 [255]
112 112 $ fileset -p parsed 'a, b, c'
113 113 * parsed:
114 114 (list
115 115 (symbol 'a')
116 116 (symbol 'b')
117 117 (symbol 'c'))
118 118 hg: parse error: can't use a list in this context
119 119 (see 'hg help "filesets.x or y"')
120 120 [255]
121 121
122 122 $ fileset '"path":.'
123 123 hg: parse error: not a symbol
124 124 [255]
125 125 $ fileset 'path:foo bar'
126 126 hg: parse error at 9: invalid token
127 127 [255]
128 128 $ fileset 'foo:bar:baz'
129 129 hg: parse error: not a symbol
130 130 [255]
131 131 $ fileset 'foo:bar()'
132 132 hg: parse error: pattern must be a string
133 133 [255]
134 134 $ fileset 'foo:bar'
135 135 hg: parse error: invalid pattern kind: foo
136 136 [255]
137 137
138 138 Show parsed tree at stages:
139 139
140 140 $ fileset -p unknown a
141 141 abort: invalid stage name: unknown
142 142 [255]
143 143
144 144 $ fileset -p parsed 'path:a1 or glob:b?'
145 145 * parsed:
146 146 (or
147 147 (kindpat
148 148 (symbol 'path')
149 149 (symbol 'a1'))
150 150 (kindpat
151 151 (symbol 'glob')
152 152 (symbol 'b?')))
153 153 a1
154 154 b1
155 155 b2
156 156
157 157 $ fileset -p all -s 'a1 or a2 or (grep("b") & clean())'
158 158 * parsed:
159 159 (or
160 160 (symbol 'a1')
161 161 (symbol 'a2')
162 162 (group
163 163 (and
164 164 (func
165 165 (symbol 'grep')
166 166 (string 'b'))
167 167 (func
168 168 (symbol 'clean')
169 169 None))))
170 170 * analyzed:
171 171 (or
172 172 (symbol 'a1')
173 173 (symbol 'a2')
174 174 (and
175 175 (func
176 176 (symbol 'grep')
177 177 (string 'b'))
178 (withstatus
178 179 (func
179 180 (symbol 'clean')
180 None)))
181 None)
182 (string 'clean'))))
181 183 * optimized:
182 184 (or
183 185 (patterns
184 186 (symbol 'a1')
185 187 (symbol 'a2'))
186 188 (and
189 (withstatus
187 190 (func
188 191 (symbol 'clean')
189 192 None)
193 (string 'clean'))
190 194 (func
191 195 (symbol 'grep')
192 196 (string 'b'))))
193 197 * matcher:
194 198 <unionmatcher matchers=[
195 199 <patternmatcher patterns='(?:a1$|a2$)'>,
196 200 <intersectionmatcher
197 201 m1=<predicatenmatcher pred=clean>,
198 202 m2=<predicatenmatcher pred=grep('b')>>]>
199 203 a1
200 204 a2
201 205 b1
202 206 b2
203 207
204 208 Union of basic patterns:
205 209
206 210 $ fileset -p optimized -s -r. 'a1 or a2 or path:b1'
207 211 * optimized:
208 212 (patterns
209 213 (symbol 'a1')
210 214 (symbol 'a2')
211 215 (kindpat
212 216 (symbol 'path')
213 217 (symbol 'b1')))
214 218 * matcher:
215 219 <patternmatcher patterns='(?:a1$|a2$|b1(?:/|$))'>
216 220 a1
217 221 a2
218 222 b1
219 223
220 224 OR expression should be reordered by weight:
221 225
222 226 $ fileset -p optimized -s -r. 'grep("a") or a1 or grep("b") or b2'
223 227 * optimized:
224 228 (or
225 229 (patterns
226 230 (symbol 'a1')
227 231 (symbol 'b2'))
228 232 (func
229 233 (symbol 'grep')
230 234 (string 'a'))
231 235 (func
232 236 (symbol 'grep')
233 237 (string 'b')))
234 238 * matcher:
235 239 <unionmatcher matchers=[
236 240 <patternmatcher patterns='(?:a1$|b2$)'>,
237 241 <predicatenmatcher pred=grep('a')>,
238 242 <predicatenmatcher pred=grep('b')>]>
239 243 a1
240 244 a2
241 245 b1
242 246 b2
243 247
244 248 Use differencematcher for 'x and not y':
245 249
246 250 $ fileset -p optimized -s 'a* and not a1'
247 251 * optimized:
248 252 (minus
249 253 (symbol 'a*')
250 254 (symbol 'a1'))
251 255 * matcher:
252 256 <differencematcher
253 257 m1=<patternmatcher patterns='(?:a[^/]*$)'>,
254 258 m2=<patternmatcher patterns='(?:a1$)'>>
255 259 a2
256 260
257 261 $ fileset -p optimized -s '!binary() and a*'
258 262 * optimized:
259 263 (minus
260 264 (symbol 'a*')
261 265 (func
262 266 (symbol 'binary')
263 267 None))
264 268 * matcher:
265 269 <differencematcher
266 270 m1=<patternmatcher patterns='(?:a[^/]*$)'>,
267 271 m2=<predicatenmatcher pred=binary>>
268 272 a1
269 273 a2
270 274
271 275 'x - y' is rewritten to 'x and not y' first so the operands can be reordered:
272 276
273 277 $ fileset -p analyzed -p optimized -s 'a* - a1'
274 278 * analyzed:
275 279 (and
276 280 (symbol 'a*')
277 281 (not
278 282 (symbol 'a1')))
279 283 * optimized:
280 284 (minus
281 285 (symbol 'a*')
282 286 (symbol 'a1'))
283 287 * matcher:
284 288 <differencematcher
285 289 m1=<patternmatcher patterns='(?:a[^/]*$)'>,
286 290 m2=<patternmatcher patterns='(?:a1$)'>>
287 291 a2
288 292
289 293 $ fileset -p analyzed -p optimized -s 'binary() - a*'
290 294 * analyzed:
291 295 (and
292 296 (func
293 297 (symbol 'binary')
294 298 None)
295 299 (not
296 300 (symbol 'a*')))
297 301 * optimized:
298 302 (and
299 303 (not
300 304 (symbol 'a*'))
301 305 (func
302 306 (symbol 'binary')
303 307 None))
304 308 * matcher:
305 309 <intersectionmatcher
306 310 m1=<predicatenmatcher
307 311 pred=<not
308 312 <patternmatcher patterns='(?:a[^/]*$)'>>>,
309 313 m2=<predicatenmatcher pred=binary>>
310 314
311 315 Test files status
312 316
313 317 $ rm a1
314 318 $ hg rm a2
315 319 $ echo b >> b2
316 320 $ hg cp b1 c1
317 321 $ echo c > c2
318 322 $ echo c > c3
319 323 $ cat > .hgignore <<EOF
320 324 > \.hgignore
321 325 > 2$
322 326 > EOF
323 327 $ fileset 'modified()'
324 328 b2
325 329 $ fileset 'added()'
326 330 c1
327 331 $ fileset 'removed()'
328 332 a2
329 333 $ fileset 'deleted()'
330 334 a1
331 335 $ fileset 'missing()'
332 336 a1
333 337 $ fileset 'unknown()'
334 338 c3
335 339 $ fileset 'ignored()'
336 340 .hgignore
337 341 c2
338 342 $ fileset 'hgignore()'
339 343 .hgignore
340 344 a2
341 345 b2
342 346 c2
343 347 $ fileset 'clean()'
344 348 b1
345 349 $ fileset 'copied()'
346 350 c1
347 351
348 352 Test files status in different revisions
349 353
350 354 $ hg status -m
351 355 M b2
352 356 $ fileset -r0 'revs("wdir()", modified())' --traceback
353 357 b2
354 358 $ hg status -a
355 359 A c1
356 360 $ fileset -r0 'revs("wdir()", added())'
357 361 c1
358 362 $ hg status --change 0 -a
359 363 A a1
360 364 A a2
361 365 A b1
362 366 A b2
363 367 $ hg status -mru
364 368 M b2
365 369 R a2
366 370 ? c3
367 371 $ fileset -r0 'added() and revs("wdir()", modified() or removed() or unknown())'
368 372 a2
369 373 b2
370 374 $ fileset -r0 'added() or revs("wdir()", added())'
371 375 a1
372 376 a2
373 377 b1
374 378 b2
375 379 c1
376 380
381 Test insertion of status hints
382
383 $ fileset -p optimized 'added()'
384 * optimized:
385 (withstatus
386 (func
387 (symbol 'added')
388 None)
389 (string 'added'))
390 c1
391
392 $ fileset -p optimized 'a* & removed()'
393 * optimized:
394 (and
395 (symbol 'a*')
396 (withstatus
397 (func
398 (symbol 'removed')
399 None)
400 (string 'removed')))
401 a2
402
403 $ fileset -p optimized 'a* - removed()'
404 * optimized:
405 (minus
406 (symbol 'a*')
407 (withstatus
408 (func
409 (symbol 'removed')
410 None)
411 (string 'removed')))
412 a1
413
414 $ fileset -p analyzed -p optimized '(added() + removed()) - a*'
415 * analyzed:
416 (and
417 (withstatus
418 (or
419 (func
420 (symbol 'added')
421 None)
422 (func
423 (symbol 'removed')
424 None))
425 (string 'added removed'))
426 (not
427 (symbol 'a*')))
428 * optimized:
429 (and
430 (not
431 (symbol 'a*'))
432 (withstatus
433 (or
434 (func
435 (symbol 'added')
436 None)
437 (func
438 (symbol 'removed')
439 None))
440 (string 'added removed')))
441 c1
442
443 $ fileset -p optimized 'a* + b* + added() + unknown()'
444 * optimized:
445 (withstatus
446 (or
447 (patterns
448 (symbol 'a*')
449 (symbol 'b*'))
450 (func
451 (symbol 'added')
452 None)
453 (func
454 (symbol 'unknown')
455 None))
456 (string 'added unknown'))
457 a1
458 a2
459 b1
460 b2
461 c1
462 c3
463
464 $ fileset -p analyzed -p optimized 'removed() & missing() & a*'
465 * analyzed:
466 (and
467 (withstatus
468 (and
469 (func
470 (symbol 'removed')
471 None)
472 (func
473 (symbol 'missing')
474 None))
475 (string 'removed missing'))
476 (symbol 'a*'))
477 * optimized:
478 (and
479 (symbol 'a*')
480 (withstatus
481 (and
482 (func
483 (symbol 'removed')
484 None)
485 (func
486 (symbol 'missing')
487 None))
488 (string 'removed missing')))
489
490 $ fileset -p optimized 'clean() & revs(0, added())'
491 * optimized:
492 (and
493 (withstatus
494 (func
495 (symbol 'clean')
496 None)
497 (string 'clean'))
498 (func
499 (symbol 'revs')
500 (list
501 (symbol '0')
502 (withstatus
503 (func
504 (symbol 'added')
505 None)
506 (string 'added')))))
507 b1
508
509 $ fileset -p optimized 'clean() & status(null, 0, b* & added())'
510 * optimized:
511 (and
512 (withstatus
513 (func
514 (symbol 'clean')
515 None)
516 (string 'clean'))
517 (func
518 (symbol 'status')
519 (list
520 (symbol 'null')
521 (symbol '0')
522 (and
523 (symbol 'b*')
524 (withstatus
525 (func
526 (symbol 'added')
527 None)
528 (string 'added'))))))
529 b1
530
377 531 Test files properties
378 532
379 533 >>> open('bin', 'wb').write(b'\0a') and None
380 534 $ fileset 'binary()'
381 535 bin
382 536 $ fileset 'binary() and unknown()'
383 537 bin
384 538 $ echo '^bin$' >> .hgignore
385 539 $ fileset 'binary() and ignored()'
386 540 bin
387 541 $ hg add bin
388 542 $ fileset 'binary()'
389 543 bin
390 544
391 545 $ fileset -p optimized -s 'binary() and b*'
392 546 * optimized:
393 547 (and
394 548 (symbol 'b*')
395 549 (func
396 550 (symbol 'binary')
397 551 None))
398 552 * matcher:
399 553 <intersectionmatcher
400 554 m1=<patternmatcher patterns='(?:b[^/]*$)'>,
401 555 m2=<predicatenmatcher pred=binary>>
402 556 bin
403 557
404 558 $ fileset 'grep("b{1}")'
405 559 .hgignore
406 560 b1
407 561 b2
408 562 c1
409 563 $ fileset 'grep("missingparens(")'
410 564 hg: parse error: invalid match pattern: (unbalanced parenthesis|missing \)).* (re)
411 565 [255]
412 566
413 567 #if execbit
414 568 $ chmod +x b2
415 569 $ fileset 'exec()'
416 570 b2
417 571 #endif
418 572
419 573 #if symlink
420 574 $ ln -s b2 b2link
421 575 $ fileset 'symlink() and unknown()'
422 576 b2link
423 577 $ hg add b2link
424 578 #endif
425 579
426 580 #if no-windows
427 581 $ echo foo > con.xml
428 582 $ fileset 'not portable()'
429 583 con.xml
430 584 $ hg --config ui.portablefilenames=ignore add con.xml
431 585 #endif
432 586
433 587 >>> open('1k', 'wb').write(b' '*1024) and None
434 588 >>> open('2k', 'wb').write(b' '*2048) and None
435 589 $ hg add 1k 2k
436 590 $ fileset 'size("bar")'
437 591 hg: parse error: couldn't parse size: bar
438 592 [255]
439 593 $ fileset '(1k, 2k)'
440 594 hg: parse error: can't use a list in this context
441 595 (see 'hg help "filesets.x or y"')
442 596 [255]
443 597 $ fileset 'size(1k)'
444 598 1k
445 599 $ fileset '(1k or 2k) and size("< 2k")'
446 600 1k
447 601 $ fileset '(1k or 2k) and size("<=2k")'
448 602 1k
449 603 2k
450 604 $ fileset '(1k or 2k) and size("> 1k")'
451 605 2k
452 606 $ fileset '(1k or 2k) and size(">=1K")'
453 607 1k
454 608 2k
455 609 $ fileset '(1k or 2k) and size(".5KB - 1.5kB")'
456 610 1k
457 611 $ fileset 'size("1M")'
458 612 $ fileset 'size("1 GB")'
459 613
460 614 Test merge states
461 615
462 616 $ hg ci -m manychanges
463 617 $ hg file -r . 'set:copied() & modified()'
464 618 [1]
465 619 $ hg up -C 0
466 620 * files updated, 0 files merged, * files removed, 0 files unresolved (glob)
467 621 $ echo c >> b2
468 622 $ hg ci -m diverging b2
469 623 created new head
470 624 $ fileset 'resolved()'
471 625 $ fileset 'unresolved()'
472 626 $ hg merge
473 627 merging b2
474 628 warning: conflicts while merging b2! (edit, then use 'hg resolve --mark')
475 629 * files updated, 0 files merged, 1 files removed, 1 files unresolved (glob)
476 630 use 'hg resolve' to retry unresolved file merges or 'hg merge --abort' to abandon
477 631 [1]
478 632 $ fileset 'resolved()'
479 633 $ fileset 'unresolved()'
480 634 b2
481 635 $ echo e > b2
482 636 $ hg resolve -m b2
483 637 (no more unresolved files)
484 638 $ fileset 'resolved()'
485 639 b2
486 640 $ fileset 'unresolved()'
487 641 $ hg ci -m merge
488 642
489 643 Test subrepo predicate
490 644
491 645 $ hg init sub
492 646 $ echo a > sub/suba
493 647 $ hg -R sub add sub/suba
494 648 $ hg -R sub ci -m sub
495 649 $ echo 'sub = sub' > .hgsub
496 650 $ hg init sub2
497 651 $ echo b > sub2/b
498 652 $ hg -R sub2 ci -Am sub2
499 653 adding b
500 654 $ echo 'sub2 = sub2' >> .hgsub
501 655 $ fileset 'subrepo()'
502 656 $ hg add .hgsub
503 657 $ fileset 'subrepo()'
504 658 sub
505 659 sub2
506 660 $ fileset 'subrepo("sub")'
507 661 sub
508 662 $ fileset 'subrepo("glob:*")'
509 663 sub
510 664 sub2
511 665 $ hg ci -m subrepo
512 666
513 667 Test that .hgsubstate is updated as appropriate during a conversion. The
514 668 saverev property is enough to alter the hashes of the subrepo.
515 669
516 670 $ hg init ../converted
517 671 $ hg --config extensions.convert= convert --config convert.hg.saverev=True \
518 672 > sub ../converted/sub
519 673 initializing destination ../converted/sub repository
520 674 scanning source...
521 675 sorting...
522 676 converting...
523 677 0 sub
524 678 $ hg clone -U sub2 ../converted/sub2
525 679 $ hg --config extensions.convert= convert --config convert.hg.saverev=True \
526 680 > . ../converted
527 681 scanning source...
528 682 sorting...
529 683 converting...
530 684 4 addfiles
531 685 3 manychanges
532 686 2 diverging
533 687 1 merge
534 688 0 subrepo
535 689 no ".hgsubstate" updates will be made for "sub2"
536 690 $ hg up -q -R ../converted -r tip
537 691 $ hg --cwd ../converted cat sub/suba sub2/b -r tip
538 692 a
539 693 b
540 694 $ oldnode=`hg log -r tip -T "{node}\n"`
541 695 $ newnode=`hg log -R ../converted -r tip -T "{node}\n"`
542 696 $ [ "$oldnode" != "$newnode" ] || echo "nothing changed"
543 697
544 698 Test with a revision
545 699
546 700 $ hg log -G --template '{rev} {desc}\n'
547 701 @ 4 subrepo
548 702 |
549 703 o 3 merge
550 704 |\
551 705 | o 2 diverging
552 706 | |
553 707 o | 1 manychanges
554 708 |/
555 709 o 0 addfiles
556 710
557 711 $ echo unknown > unknown
558 712 $ fileset -r1 'modified()'
559 713 b2
560 714 $ fileset -r1 'added() and c1'
561 715 c1
562 716 $ fileset -r1 'removed()'
563 717 a2
564 718 $ fileset -r1 'deleted()'
565 719 $ fileset -r1 'unknown()'
566 720 $ fileset -r1 'ignored()'
567 721 $ fileset -r1 'hgignore()'
568 722 .hgignore
569 723 a2
570 724 b2
571 725 bin
572 726 c2
573 727 sub2
574 728 $ fileset -r1 'binary()'
575 729 bin
576 730 $ fileset -r1 'size(1k)'
577 731 1k
578 732 $ fileset -r3 'resolved()'
579 733 $ fileset -r3 'unresolved()'
580 734
581 735 #if execbit
582 736 $ fileset -r1 'exec()'
583 737 b2
584 738 #endif
585 739
586 740 #if symlink
587 741 $ fileset -r1 'symlink()'
588 742 b2link
589 743 #endif
590 744
591 745 #if no-windows
592 746 $ fileset -r1 'not portable()'
593 747 con.xml
594 748 $ hg forget 'con.xml'
595 749 #endif
596 750
597 751 $ fileset -r4 'subrepo("re:su.*")'
598 752 sub
599 753 sub2
600 754 $ fileset -r4 'subrepo(re:su.*)'
601 755 sub
602 756 sub2
603 757 $ fileset -r4 'subrepo("sub")'
604 758 sub
605 759 $ fileset -r4 'b2 or c1'
606 760 b2
607 761 c1
608 762
609 763 >>> open('dos', 'wb').write(b"dos\r\n") and None
610 764 >>> open('mixed', 'wb').write(b"dos\r\nunix\n") and None
611 765 >>> open('mac', 'wb').write(b"mac\r") and None
612 766 $ hg add dos mixed mac
613 767
614 768 (remove a1, to examine safety of 'eol' on removed files)
615 769 $ rm a1
616 770
617 771 $ fileset 'eol(dos)'
618 772 dos
619 773 mixed
620 774 $ fileset 'eol(unix)'
621 775 .hgignore
622 776 .hgsub
623 777 .hgsubstate
624 778 b1
625 779 b2
626 780 b2.orig
627 781 c1
628 782 c2
629 783 c3
630 784 con.xml (no-windows !)
631 785 mixed
632 786 unknown
633 787 $ fileset 'eol(mac)'
634 788 mac
635 789
636 790 Test safety of 'encoding' on removed files
637 791
638 792 $ fileset 'encoding("ascii")'
639 793 .hgignore
640 794 .hgsub
641 795 .hgsubstate
642 796 1k
643 797 2k
644 798 b1
645 799 b2
646 800 b2.orig
647 801 b2link (symlink !)
648 802 bin
649 803 c1
650 804 c2
651 805 c3
652 806 con.xml (no-windows !)
653 807 dos
654 808 mac
655 809 mixed
656 810 unknown
657 811
658 812 Test 'revs(...)'
659 813 ================
660 814
661 815 small reminder of the repository state
662 816
663 817 $ hg log -G
664 818 @ changeset: 4:* (glob)
665 819 | tag: tip
666 820 | user: test
667 821 | date: Thu Jan 01 00:00:00 1970 +0000
668 822 | summary: subrepo
669 823 |
670 824 o changeset: 3:* (glob)
671 825 |\ parent: 2:55b05bdebf36
672 826 | | parent: 1:* (glob)
673 827 | | user: test
674 828 | | date: Thu Jan 01 00:00:00 1970 +0000
675 829 | | summary: merge
676 830 | |
677 831 | o changeset: 2:55b05bdebf36
678 832 | | parent: 0:8a9576c51c1f
679 833 | | user: test
680 834 | | date: Thu Jan 01 00:00:00 1970 +0000
681 835 | | summary: diverging
682 836 | |
683 837 o | changeset: 1:* (glob)
684 838 |/ user: test
685 839 | date: Thu Jan 01 00:00:00 1970 +0000
686 840 | summary: manychanges
687 841 |
688 842 o changeset: 0:8a9576c51c1f
689 843 user: test
690 844 date: Thu Jan 01 00:00:00 1970 +0000
691 845 summary: addfiles
692 846
693 847 $ hg status --change 0
694 848 A a1
695 849 A a2
696 850 A b1
697 851 A b2
698 852 $ hg status --change 1
699 853 M b2
700 854 A 1k
701 855 A 2k
702 856 A b2link (no-windows !)
703 857 A bin
704 858 A c1
705 859 A con.xml (no-windows !)
706 860 R a2
707 861 $ hg status --change 2
708 862 M b2
709 863 $ hg status --change 3
710 864 M b2
711 865 A 1k
712 866 A 2k
713 867 A b2link (no-windows !)
714 868 A bin
715 869 A c1
716 870 A con.xml (no-windows !)
717 871 R a2
718 872 $ hg status --change 4
719 873 A .hgsub
720 874 A .hgsubstate
721 875 $ hg status
722 876 A dos
723 877 A mac
724 878 A mixed
725 879 R con.xml (no-windows !)
726 880 ! a1
727 881 ? b2.orig
728 882 ? c3
729 883 ? unknown
730 884
731 885 Test files at -r0 should be filtered by files at wdir
732 886 -----------------------------------------------------
733 887
734 888 $ fileset -r0 'tracked() and revs("wdir()", tracked())'
735 889 a1
736 890 b1
737 891 b2
738 892
739 893 Test that "revs()" work at all
740 894 ------------------------------
741 895
742 896 $ fileset "revs('2', modified())"
743 897 b2
744 898
745 899 Test that "revs()" work for file missing in the working copy/current context
746 900 ----------------------------------------------------------------------------
747 901
748 902 (a2 not in working copy)
749 903
750 904 $ fileset "revs('0', added())"
751 905 a1
752 906 a2
753 907 b1
754 908 b2
755 909
756 910 (none of the file exist in "0")
757 911
758 912 $ fileset -r 0 "revs('4', added())"
759 913 .hgsub
760 914 .hgsubstate
761 915
762 916 Call with empty revset
763 917 --------------------------
764 918
765 919 $ fileset "revs('2-2', modified())"
766 920
767 921 Call with revset matching multiple revs
768 922 ---------------------------------------
769 923
770 924 $ fileset "revs('0+4', added())"
771 925 .hgsub
772 926 .hgsubstate
773 927 a1
774 928 a2
775 929 b1
776 930 b2
777 931
778 932 overlapping set
779 933
780 934 $ fileset "revs('1+2', modified())"
781 935 b2
782 936
783 937 test 'status(...)'
784 938 =================
785 939
786 940 Simple case
787 941 -----------
788 942
789 943 $ fileset "status(3, 4, added())"
790 944 .hgsub
791 945 .hgsubstate
792 946
793 947 use rev to restrict matched file
794 948 -----------------------------------------
795 949
796 950 $ hg status --removed --rev 0 --rev 1
797 951 R a2
798 952 $ fileset "status(0, 1, removed())"
799 953 a2
800 954 $ fileset "tracked() and status(0, 1, removed())"
801 955 $ fileset -r 4 "status(0, 1, removed())"
802 956 a2
803 957 $ fileset -r 4 "tracked() and status(0, 1, removed())"
804 958 $ fileset "revs('4', tracked() and status(0, 1, removed()))"
805 959 $ fileset "revs('0', tracked() and status(0, 1, removed()))"
806 960 a2
807 961
808 962 check wdir()
809 963 ------------
810 964
811 965 $ hg status --removed --rev 4
812 966 R con.xml (no-windows !)
813 967 $ fileset "status(4, 'wdir()', removed())"
814 968 con.xml (no-windows !)
815 969
816 970 $ hg status --removed --rev 2
817 971 R a2
818 972 $ fileset "status('2', 'wdir()', removed())"
819 973 a2
820 974
821 975 test backward status
822 976 --------------------
823 977
824 978 $ hg status --removed --rev 0 --rev 4
825 979 R a2
826 980 $ hg status --added --rev 4 --rev 0
827 981 A a2
828 982 $ fileset "status(4, 0, added())"
829 983 a2
830 984
831 985 test cross branch status
832 986 ------------------------
833 987
834 988 $ hg status --added --rev 1 --rev 2
835 989 A a2
836 990 $ fileset "status(1, 2, added())"
837 991 a2
838 992
839 993 test with multi revs revset
840 994 ---------------------------
841 995 $ hg status --added --rev 0:1 --rev 3:4
842 996 A .hgsub
843 997 A .hgsubstate
844 998 A 1k
845 999 A 2k
846 1000 A b2link (no-windows !)
847 1001 A bin
848 1002 A c1
849 1003 A con.xml (no-windows !)
850 1004 $ fileset "status('0:1', '3:4', added())"
851 1005 .hgsub
852 1006 .hgsubstate
853 1007 1k
854 1008 2k
855 1009 b2link (no-windows !)
856 1010 bin
857 1011 c1
858 1012 con.xml (no-windows !)
859 1013
860 1014 tests with empty value
861 1015 ----------------------
862 1016
863 1017 Fully empty revset
864 1018
865 1019 $ fileset "status('', '4', added())"
866 1020 hg: parse error: first argument to status must be a revision
867 1021 [255]
868 1022 $ fileset "status('2', '', added())"
869 1023 hg: parse error: second argument to status must be a revision
870 1024 [255]
871 1025
872 1026 Empty revset will error at the revset layer
873 1027
874 1028 $ fileset "status(' ', '4', added())"
875 1029 hg: parse error at 1: not a prefix: end
876 1030 (
877 1031 ^ here)
878 1032 [255]
879 1033 $ fileset "status('2', ' ', added())"
880 1034 hg: parse error at 1: not a prefix: end
881 1035 (
882 1036 ^ here)
883 1037 [255]
General Comments 0
You need to be logged in to leave comments. Login now