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