##// END OF EJS Templates
fileset: don't suggest private or undocumented queries...
Matt Harbison -
r25633:0f44d357 default
parent child Browse files
Show More
@@ -1,524 +1,528 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 import re
9 9 import parser, error, util, merge
10 10 from i18n import _
11 11
12 12 elements = {
13 13 "(": (20, ("group", 1, ")"), ("func", 1, ")")),
14 14 "-": (5, ("negate", 19), ("minus", 5)),
15 15 "not": (10, ("not", 10)),
16 16 "!": (10, ("not", 10)),
17 17 "and": (5, None, ("and", 5)),
18 18 "&": (5, None, ("and", 5)),
19 19 "or": (4, None, ("or", 4)),
20 20 "|": (4, None, ("or", 4)),
21 21 "+": (4, None, ("or", 4)),
22 22 ",": (2, None, ("list", 2)),
23 23 ")": (0, None, None),
24 24 "symbol": (0, ("symbol",), None),
25 25 "string": (0, ("string",), None),
26 26 "end": (0, None, None),
27 27 }
28 28
29 29 keywords = set(['and', 'or', 'not'])
30 30
31 31 globchars = ".*{}[]?/\\_"
32 32
33 33 def tokenize(program):
34 34 pos, l = 0, len(program)
35 35 while pos < l:
36 36 c = program[pos]
37 37 if c.isspace(): # skip inter-token whitespace
38 38 pass
39 39 elif c in "(),-|&+!": # handle simple operators
40 40 yield (c, None, pos)
41 41 elif (c in '"\'' or c == 'r' and
42 42 program[pos:pos + 2] in ("r'", 'r"')): # handle quoted strings
43 43 if c == 'r':
44 44 pos += 1
45 45 c = program[pos]
46 46 decode = lambda x: x
47 47 else:
48 48 decode = lambda x: x.decode('string-escape')
49 49 pos += 1
50 50 s = pos
51 51 while pos < l: # find closing quote
52 52 d = program[pos]
53 53 if d == '\\': # skip over escaped characters
54 54 pos += 2
55 55 continue
56 56 if d == c:
57 57 yield ('string', decode(program[s:pos]), s)
58 58 break
59 59 pos += 1
60 60 else:
61 61 raise error.ParseError(_("unterminated string"), s)
62 62 elif c.isalnum() or c in globchars or ord(c) > 127:
63 63 # gather up a symbol/keyword
64 64 s = pos
65 65 pos += 1
66 66 while pos < l: # find end of symbol
67 67 d = program[pos]
68 68 if not (d.isalnum() or d in globchars or ord(d) > 127):
69 69 break
70 70 pos += 1
71 71 sym = program[s:pos]
72 72 if sym in keywords: # operator keywords
73 73 yield (sym, None, s)
74 74 else:
75 75 yield ('symbol', sym, s)
76 76 pos -= 1
77 77 else:
78 78 raise error.ParseError(_("syntax error"), pos)
79 79 pos += 1
80 80 yield ('end', None, pos)
81 81
82 82 def parse(expr):
83 83 p = parser.parser(tokenize, elements)
84 84 tree, pos = p.parse(expr)
85 85 if pos != len(expr):
86 86 raise error.ParseError(_("invalid token"), pos)
87 87 return tree
88 88
89 89 def getstring(x, err):
90 90 if x and (x[0] == 'string' or x[0] == 'symbol'):
91 91 return x[1]
92 92 raise error.ParseError(err)
93 93
94 94 def getset(mctx, x):
95 95 if not x:
96 96 raise error.ParseError(_("missing argument"))
97 97 return methods[x[0]](mctx, *x[1:])
98 98
99 99 def stringset(mctx, x):
100 100 m = mctx.matcher([x])
101 101 return [f for f in mctx.subset if m(f)]
102 102
103 103 def andset(mctx, x, y):
104 104 return getset(mctx.narrow(getset(mctx, x)), y)
105 105
106 106 def orset(mctx, x, y):
107 107 # needs optimizing
108 108 xl = getset(mctx, x)
109 109 yl = getset(mctx, y)
110 110 return xl + [f for f in yl if f not in xl]
111 111
112 112 def notset(mctx, x):
113 113 s = set(getset(mctx, x))
114 114 return [r for r in mctx.subset if r not in s]
115 115
116 116 def minusset(mctx, x, y):
117 117 xl = getset(mctx, x)
118 118 yl = set(getset(mctx, y))
119 119 return [f for f in xl if f not in yl]
120 120
121 121 def listset(mctx, a, b):
122 122 raise error.ParseError(_("can't use a list in this context"))
123 123
124 124 def modified(mctx, x):
125 125 """``modified()``
126 126 File that is modified according to status.
127 127 """
128 128 # i18n: "modified" is a keyword
129 129 getargs(x, 0, 0, _("modified takes no arguments"))
130 130 s = mctx.status().modified
131 131 return [f for f in mctx.subset if f in s]
132 132
133 133 def added(mctx, x):
134 134 """``added()``
135 135 File that is added according to status.
136 136 """
137 137 # i18n: "added" is a keyword
138 138 getargs(x, 0, 0, _("added takes no arguments"))
139 139 s = mctx.status().added
140 140 return [f for f in mctx.subset if f in s]
141 141
142 142 def removed(mctx, x):
143 143 """``removed()``
144 144 File that is removed according to status.
145 145 """
146 146 # i18n: "removed" is a keyword
147 147 getargs(x, 0, 0, _("removed takes no arguments"))
148 148 s = mctx.status().removed
149 149 return [f for f in mctx.subset if f in s]
150 150
151 151 def deleted(mctx, x):
152 152 """``deleted()``
153 153 File that is deleted according to status.
154 154 """
155 155 # i18n: "deleted" is a keyword
156 156 getargs(x, 0, 0, _("deleted takes no arguments"))
157 157 s = mctx.status().deleted
158 158 return [f for f in mctx.subset if f in s]
159 159
160 160 def unknown(mctx, x):
161 161 """``unknown()``
162 162 File that is unknown according to status. These files will only be
163 163 considered if this predicate is used.
164 164 """
165 165 # i18n: "unknown" is a keyword
166 166 getargs(x, 0, 0, _("unknown takes no arguments"))
167 167 s = mctx.status().unknown
168 168 return [f for f in mctx.subset if f in s]
169 169
170 170 def ignored(mctx, x):
171 171 """``ignored()``
172 172 File that is ignored according to status. These files will only be
173 173 considered if this predicate is used.
174 174 """
175 175 # i18n: "ignored" is a keyword
176 176 getargs(x, 0, 0, _("ignored takes no arguments"))
177 177 s = mctx.status().ignored
178 178 return [f for f in mctx.subset if f in s]
179 179
180 180 def clean(mctx, x):
181 181 """``clean()``
182 182 File that is clean according to status.
183 183 """
184 184 # i18n: "clean" is a keyword
185 185 getargs(x, 0, 0, _("clean takes no arguments"))
186 186 s = mctx.status().clean
187 187 return [f for f in mctx.subset if f in s]
188 188
189 189 def func(mctx, a, b):
190 190 if a[0] == 'symbol' and a[1] in symbols:
191 191 return symbols[a[1]](mctx, b)
192 raise error.UnknownIdentifier(a[1], symbols.keys())
192
193 keep = lambda fn: getattr(fn, '__doc__', None) is not None
194
195 syms = [s for (s, fn) in symbols.items() if keep(fn)]
196 raise error.UnknownIdentifier(a[1], syms)
193 197
194 198 def getlist(x):
195 199 if not x:
196 200 return []
197 201 if x[0] == 'list':
198 202 return getlist(x[1]) + [x[2]]
199 203 return [x]
200 204
201 205 def getargs(x, min, max, err):
202 206 l = getlist(x)
203 207 if len(l) < min or len(l) > max:
204 208 raise error.ParseError(err)
205 209 return l
206 210
207 211 def binary(mctx, x):
208 212 """``binary()``
209 213 File that appears to be binary (contains NUL bytes).
210 214 """
211 215 # i18n: "binary" is a keyword
212 216 getargs(x, 0, 0, _("binary takes no arguments"))
213 217 return [f for f in mctx.existing() if util.binary(mctx.ctx[f].data())]
214 218
215 219 def exec_(mctx, x):
216 220 """``exec()``
217 221 File that is marked as executable.
218 222 """
219 223 # i18n: "exec" is a keyword
220 224 getargs(x, 0, 0, _("exec takes no arguments"))
221 225 return [f for f in mctx.existing() if mctx.ctx.flags(f) == 'x']
222 226
223 227 def symlink(mctx, x):
224 228 """``symlink()``
225 229 File that is marked as a symlink.
226 230 """
227 231 # i18n: "symlink" is a keyword
228 232 getargs(x, 0, 0, _("symlink takes no arguments"))
229 233 return [f for f in mctx.existing() if mctx.ctx.flags(f) == 'l']
230 234
231 235 def resolved(mctx, x):
232 236 """``resolved()``
233 237 File that is marked resolved according to the resolve state.
234 238 """
235 239 # i18n: "resolved" is a keyword
236 240 getargs(x, 0, 0, _("resolved takes no arguments"))
237 241 if mctx.ctx.rev() is not None:
238 242 return []
239 243 ms = merge.mergestate(mctx.ctx.repo())
240 244 return [f for f in mctx.subset if f in ms and ms[f] == 'r']
241 245
242 246 def unresolved(mctx, x):
243 247 """``unresolved()``
244 248 File that is marked unresolved according to the resolve state.
245 249 """
246 250 # i18n: "unresolved" is a keyword
247 251 getargs(x, 0, 0, _("unresolved takes no arguments"))
248 252 if mctx.ctx.rev() is not None:
249 253 return []
250 254 ms = merge.mergestate(mctx.ctx.repo())
251 255 return [f for f in mctx.subset if f in ms and ms[f] == 'u']
252 256
253 257 def hgignore(mctx, x):
254 258 """``hgignore()``
255 259 File that matches the active .hgignore pattern.
256 260 """
257 261 # i18n: "hgignore" is a keyword
258 262 getargs(x, 0, 0, _("hgignore takes no arguments"))
259 263 ignore = mctx.ctx.repo().dirstate._ignore
260 264 return [f for f in mctx.subset if ignore(f)]
261 265
262 266 def portable(mctx, x):
263 267 """``portable()``
264 268 File that has a portable name. (This doesn't include filenames with case
265 269 collisions.)
266 270 """
267 271 # i18n: "portable" is a keyword
268 272 getargs(x, 0, 0, _("portable takes no arguments"))
269 273 checkwinfilename = util.checkwinfilename
270 274 return [f for f in mctx.subset if checkwinfilename(f) is None]
271 275
272 276 def grep(mctx, x):
273 277 """``grep(regex)``
274 278 File contains the given regular expression.
275 279 """
276 280 try:
277 281 # i18n: "grep" is a keyword
278 282 r = re.compile(getstring(x, _("grep requires a pattern")))
279 283 except re.error, e:
280 284 raise error.ParseError(_('invalid match pattern: %s') % e)
281 285 return [f for f in mctx.existing() if r.search(mctx.ctx[f].data())]
282 286
283 287 def _sizetomax(s):
284 288 try:
285 289 s = s.strip()
286 290 for k, v in util._sizeunits:
287 291 if s.endswith(k):
288 292 # max(4k) = 5k - 1, max(4.5k) = 4.6k - 1
289 293 n = s[:-len(k)]
290 294 inc = 1.0
291 295 if "." in n:
292 296 inc /= 10 ** len(n.split(".")[1])
293 297 return int((float(n) + inc) * v) - 1
294 298 # no extension, this is a precise value
295 299 return int(s)
296 300 except ValueError:
297 301 raise error.ParseError(_("couldn't parse size: %s") % s)
298 302
299 303 def size(mctx, x):
300 304 """``size(expression)``
301 305 File size matches the given expression. Examples:
302 306
303 307 - 1k (files from 1024 to 2047 bytes)
304 308 - < 20k (files less than 20480 bytes)
305 309 - >= .5MB (files at least 524288 bytes)
306 310 - 4k - 1MB (files from 4096 bytes to 1048576 bytes)
307 311 """
308 312
309 313 # i18n: "size" is a keyword
310 314 expr = getstring(x, _("size requires an expression")).strip()
311 315 if '-' in expr: # do we have a range?
312 316 a, b = expr.split('-', 1)
313 317 a = util.sizetoint(a)
314 318 b = util.sizetoint(b)
315 319 m = lambda x: x >= a and x <= b
316 320 elif expr.startswith("<="):
317 321 a = util.sizetoint(expr[2:])
318 322 m = lambda x: x <= a
319 323 elif expr.startswith("<"):
320 324 a = util.sizetoint(expr[1:])
321 325 m = lambda x: x < a
322 326 elif expr.startswith(">="):
323 327 a = util.sizetoint(expr[2:])
324 328 m = lambda x: x >= a
325 329 elif expr.startswith(">"):
326 330 a = util.sizetoint(expr[1:])
327 331 m = lambda x: x > a
328 332 elif expr[0].isdigit or expr[0] == '.':
329 333 a = util.sizetoint(expr)
330 334 b = _sizetomax(expr)
331 335 m = lambda x: x >= a and x <= b
332 336 else:
333 337 raise error.ParseError(_("couldn't parse size: %s") % expr)
334 338
335 339 return [f for f in mctx.existing() if m(mctx.ctx[f].size())]
336 340
337 341 def encoding(mctx, x):
338 342 """``encoding(name)``
339 343 File can be successfully decoded with the given character
340 344 encoding. May not be useful for encodings other than ASCII and
341 345 UTF-8.
342 346 """
343 347
344 348 # i18n: "encoding" is a keyword
345 349 enc = getstring(x, _("encoding requires an encoding name"))
346 350
347 351 s = []
348 352 for f in mctx.existing():
349 353 d = mctx.ctx[f].data()
350 354 try:
351 355 d.decode(enc)
352 356 except LookupError:
353 357 raise util.Abort(_("unknown encoding '%s'") % enc)
354 358 except UnicodeDecodeError:
355 359 continue
356 360 s.append(f)
357 361
358 362 return s
359 363
360 364 def eol(mctx, x):
361 365 """``eol(style)``
362 366 File contains newlines of the given style (dos, unix, mac). Binary
363 367 files are excluded, files with mixed line endings match multiple
364 368 styles.
365 369 """
366 370
367 371 # i18n: "encoding" is a keyword
368 372 enc = getstring(x, _("encoding requires an encoding name"))
369 373
370 374 s = []
371 375 for f in mctx.existing():
372 376 d = mctx.ctx[f].data()
373 377 if util.binary(d):
374 378 continue
375 379 if (enc == 'dos' or enc == 'win') and '\r\n' in d:
376 380 s.append(f)
377 381 elif enc == 'unix' and re.search('(?<!\r)\n', d):
378 382 s.append(f)
379 383 elif enc == 'mac' and re.search('\r(?!\n)', d):
380 384 s.append(f)
381 385 return s
382 386
383 387 def copied(mctx, x):
384 388 """``copied()``
385 389 File that is recorded as being copied.
386 390 """
387 391 # i18n: "copied" is a keyword
388 392 getargs(x, 0, 0, _("copied takes no arguments"))
389 393 s = []
390 394 for f in mctx.subset:
391 395 p = mctx.ctx[f].parents()
392 396 if p and p[0].path() != f:
393 397 s.append(f)
394 398 return s
395 399
396 400 def subrepo(mctx, x):
397 401 """``subrepo([pattern])``
398 402 Subrepositories whose paths match the given pattern.
399 403 """
400 404 # i18n: "subrepo" is a keyword
401 405 getargs(x, 0, 1, _("subrepo takes at most one argument"))
402 406 ctx = mctx.ctx
403 407 sstate = sorted(ctx.substate)
404 408 if x:
405 409 # i18n: "subrepo" is a keyword
406 410 pat = getstring(x, _("subrepo requires a pattern or no arguments"))
407 411
408 412 import match as matchmod # avoid circular import issues
409 413 fast = not matchmod.patkind(pat)
410 414 if fast:
411 415 def m(s):
412 416 return (s == pat)
413 417 else:
414 418 m = matchmod.match(ctx.repo().root, '', [pat], ctx=ctx)
415 419 return [sub for sub in sstate if m(sub)]
416 420 else:
417 421 return [sub for sub in sstate]
418 422
419 423 symbols = {
420 424 'added': added,
421 425 'binary': binary,
422 426 'clean': clean,
423 427 'copied': copied,
424 428 'deleted': deleted,
425 429 'encoding': encoding,
426 430 'eol': eol,
427 431 'exec': exec_,
428 432 'grep': grep,
429 433 'ignored': ignored,
430 434 'hgignore': hgignore,
431 435 'modified': modified,
432 436 'portable': portable,
433 437 'removed': removed,
434 438 'resolved': resolved,
435 439 'size': size,
436 440 'symlink': symlink,
437 441 'unknown': unknown,
438 442 'unresolved': unresolved,
439 443 'subrepo': subrepo,
440 444 }
441 445
442 446 methods = {
443 447 'string': stringset,
444 448 'symbol': stringset,
445 449 'and': andset,
446 450 'or': orset,
447 451 'minus': minusset,
448 452 'list': listset,
449 453 'group': getset,
450 454 'not': notset,
451 455 'func': func,
452 456 }
453 457
454 458 class matchctx(object):
455 459 def __init__(self, ctx, subset=None, status=None):
456 460 self.ctx = ctx
457 461 self.subset = subset
458 462 self._status = status
459 463 def status(self):
460 464 return self._status
461 465 def matcher(self, patterns):
462 466 return self.ctx.match(patterns)
463 467 def filter(self, files):
464 468 return [f for f in files if f in self.subset]
465 469 def existing(self):
466 470 if self._status is not None:
467 471 removed = set(self._status[3])
468 472 unknown = set(self._status[4] + self._status[5])
469 473 else:
470 474 removed = set()
471 475 unknown = set()
472 476 return (f for f in self.subset
473 477 if (f in self.ctx and f not in removed) or f in unknown)
474 478 def narrow(self, files):
475 479 return matchctx(self.ctx, self.filter(files), self._status)
476 480
477 481 def _intree(funcs, tree):
478 482 if isinstance(tree, tuple):
479 483 if tree[0] == 'func' and tree[1][0] == 'symbol':
480 484 if tree[1][1] in funcs:
481 485 return True
482 486 for s in tree[1:]:
483 487 if _intree(funcs, s):
484 488 return True
485 489 return False
486 490
487 491 # filesets using matchctx.existing()
488 492 _existingcallers = [
489 493 'binary',
490 494 'exec',
491 495 'grep',
492 496 'size',
493 497 'symlink',
494 498 ]
495 499
496 500 def getfileset(ctx, expr):
497 501 tree = parse(expr)
498 502
499 503 # do we need status info?
500 504 if (_intree(['modified', 'added', 'removed', 'deleted',
501 505 'unknown', 'ignored', 'clean'], tree) or
502 506 # Using matchctx.existing() on a workingctx requires us to check
503 507 # for deleted files.
504 508 (ctx.rev() is None and _intree(_existingcallers, tree))):
505 509 unknown = _intree(['unknown'], tree)
506 510 ignored = _intree(['ignored'], tree)
507 511
508 512 r = ctx.repo()
509 513 status = r.status(ctx.p1(), ctx,
510 514 unknown=unknown, ignored=ignored, clean=True)
511 515 subset = []
512 516 for c in status:
513 517 subset.extend(c)
514 518 else:
515 519 status = None
516 520 subset = list(ctx.walk(ctx.match([])))
517 521
518 522 return getset(matchctx(ctx, subset, status), tree)
519 523
520 524 def prettyformat(tree):
521 525 return parser.prettyformat(tree, ('string', 'symbol'))
522 526
523 527 # tell hggettext to extract docstrings from these functions:
524 528 i18nfunctions = symbols.values()
General Comments 0
You need to be logged in to leave comments. Login now