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