##// END OF EJS Templates
revset: use appropriate predicate name in error messages...
FUJIWARA Katsunori -
r17258:5822345e stable
parent child Browse files
Show More
@@ -1,1834 +1,1834 b''
1 1 # revset.py - revision 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, util, error, discovery, hbisect, phases
10 10 import node
11 11 import bookmarks as bookmarksmod
12 12 import match as matchmod
13 13 from i18n import _
14 14 import encoding
15 15
16 16 def _revancestors(repo, revs, followfirst):
17 17 """Like revlog.ancestors(), but supports followfirst."""
18 18 cut = followfirst and 1 or None
19 19 cl = repo.changelog
20 20 visit = util.deque(revs)
21 21 seen = set([node.nullrev])
22 22 while visit:
23 23 for parent in cl.parentrevs(visit.popleft())[:cut]:
24 24 if parent not in seen:
25 25 visit.append(parent)
26 26 seen.add(parent)
27 27 yield parent
28 28
29 29 def _revdescendants(repo, revs, followfirst):
30 30 """Like revlog.descendants() but supports followfirst."""
31 31 cut = followfirst and 1 or None
32 32 cl = repo.changelog
33 33 first = min(revs)
34 34 nullrev = node.nullrev
35 35 if first == nullrev:
36 36 # Are there nodes with a null first parent and a non-null
37 37 # second one? Maybe. Do we care? Probably not.
38 38 for i in cl:
39 39 yield i
40 40 return
41 41
42 42 seen = set(revs)
43 43 for i in xrange(first + 1, len(cl)):
44 44 for x in cl.parentrevs(i)[:cut]:
45 45 if x != nullrev and x in seen:
46 46 seen.add(i)
47 47 yield i
48 48 break
49 49
50 50 def _revsbetween(repo, roots, heads):
51 51 """Return all paths between roots and heads, inclusive of both endpoint
52 52 sets."""
53 53 if not roots:
54 54 return []
55 55 parentrevs = repo.changelog.parentrevs
56 56 visit = heads[:]
57 57 reachable = set()
58 58 seen = {}
59 59 minroot = min(roots)
60 60 roots = set(roots)
61 61 # open-code the post-order traversal due to the tiny size of
62 62 # sys.getrecursionlimit()
63 63 while visit:
64 64 rev = visit.pop()
65 65 if rev in roots:
66 66 reachable.add(rev)
67 67 parents = parentrevs(rev)
68 68 seen[rev] = parents
69 69 for parent in parents:
70 70 if parent >= minroot and parent not in seen:
71 71 visit.append(parent)
72 72 if not reachable:
73 73 return []
74 74 for rev in sorted(seen):
75 75 for parent in seen[rev]:
76 76 if parent in reachable:
77 77 reachable.add(rev)
78 78 return sorted(reachable)
79 79
80 80 elements = {
81 81 "(": (20, ("group", 1, ")"), ("func", 1, ")")),
82 82 "~": (18, None, ("ancestor", 18)),
83 83 "^": (18, None, ("parent", 18), ("parentpost", 18)),
84 84 "-": (5, ("negate", 19), ("minus", 5)),
85 85 "::": (17, ("dagrangepre", 17), ("dagrange", 17),
86 86 ("dagrangepost", 17)),
87 87 "..": (17, ("dagrangepre", 17), ("dagrange", 17),
88 88 ("dagrangepost", 17)),
89 89 ":": (15, ("rangepre", 15), ("range", 15), ("rangepost", 15)),
90 90 "not": (10, ("not", 10)),
91 91 "!": (10, ("not", 10)),
92 92 "and": (5, None, ("and", 5)),
93 93 "&": (5, None, ("and", 5)),
94 94 "or": (4, None, ("or", 4)),
95 95 "|": (4, None, ("or", 4)),
96 96 "+": (4, None, ("or", 4)),
97 97 ",": (2, None, ("list", 2)),
98 98 ")": (0, None, None),
99 99 "symbol": (0, ("symbol",), None),
100 100 "string": (0, ("string",), None),
101 101 "end": (0, None, None),
102 102 }
103 103
104 104 keywords = set(['and', 'or', 'not'])
105 105
106 106 def tokenize(program):
107 107 pos, l = 0, len(program)
108 108 while pos < l:
109 109 c = program[pos]
110 110 if c.isspace(): # skip inter-token whitespace
111 111 pass
112 112 elif c == ':' and program[pos:pos + 2] == '::': # look ahead carefully
113 113 yield ('::', None, pos)
114 114 pos += 1 # skip ahead
115 115 elif c == '.' and program[pos:pos + 2] == '..': # look ahead carefully
116 116 yield ('..', None, pos)
117 117 pos += 1 # skip ahead
118 118 elif c in "():,-|&+!~^": # handle simple operators
119 119 yield (c, None, pos)
120 120 elif (c in '"\'' or c == 'r' and
121 121 program[pos:pos + 2] in ("r'", 'r"')): # handle quoted strings
122 122 if c == 'r':
123 123 pos += 1
124 124 c = program[pos]
125 125 decode = lambda x: x
126 126 else:
127 127 decode = lambda x: x.decode('string-escape')
128 128 pos += 1
129 129 s = pos
130 130 while pos < l: # find closing quote
131 131 d = program[pos]
132 132 if d == '\\': # skip over escaped characters
133 133 pos += 2
134 134 continue
135 135 if d == c:
136 136 yield ('string', decode(program[s:pos]), s)
137 137 break
138 138 pos += 1
139 139 else:
140 140 raise error.ParseError(_("unterminated string"), s)
141 141 # gather up a symbol/keyword
142 142 elif c.isalnum() or c in '._' or ord(c) > 127:
143 143 s = pos
144 144 pos += 1
145 145 while pos < l: # find end of symbol
146 146 d = program[pos]
147 147 if not (d.isalnum() or d in "._/" or ord(d) > 127):
148 148 break
149 149 if d == '.' and program[pos - 1] == '.': # special case for ..
150 150 pos -= 1
151 151 break
152 152 pos += 1
153 153 sym = program[s:pos]
154 154 if sym in keywords: # operator keywords
155 155 yield (sym, None, s)
156 156 else:
157 157 yield ('symbol', sym, s)
158 158 pos -= 1
159 159 else:
160 160 raise error.ParseError(_("syntax error"), pos)
161 161 pos += 1
162 162 yield ('end', None, pos)
163 163
164 164 # helpers
165 165
166 166 def getstring(x, err):
167 167 if x and (x[0] == 'string' or x[0] == 'symbol'):
168 168 return x[1]
169 169 raise error.ParseError(err)
170 170
171 171 def getlist(x):
172 172 if not x:
173 173 return []
174 174 if x[0] == 'list':
175 175 return getlist(x[1]) + [x[2]]
176 176 return [x]
177 177
178 178 def getargs(x, min, max, err):
179 179 l = getlist(x)
180 180 if len(l) < min or (max >= 0 and len(l) > max):
181 181 raise error.ParseError(err)
182 182 return l
183 183
184 184 def getset(repo, subset, x):
185 185 if not x:
186 186 raise error.ParseError(_("missing argument"))
187 187 return methods[x[0]](repo, subset, *x[1:])
188 188
189 189 def _getrevsource(repo, r):
190 190 extra = repo[r].extra()
191 191 for label in ('source', 'transplant_source', 'rebase_source'):
192 192 if label in extra:
193 193 try:
194 194 return repo[extra[label]].rev()
195 195 except error.RepoLookupError:
196 196 pass
197 197 return None
198 198
199 199 # operator methods
200 200
201 201 def stringset(repo, subset, x):
202 202 x = repo[x].rev()
203 203 if x == -1 and len(subset) == len(repo):
204 204 return [-1]
205 205 if len(subset) == len(repo) or x in subset:
206 206 return [x]
207 207 return []
208 208
209 209 def symbolset(repo, subset, x):
210 210 if x in symbols:
211 211 raise error.ParseError(_("can't use %s here") % x)
212 212 return stringset(repo, subset, x)
213 213
214 214 def rangeset(repo, subset, x, y):
215 215 m = getset(repo, subset, x)
216 216 if not m:
217 217 m = getset(repo, range(len(repo)), x)
218 218
219 219 n = getset(repo, subset, y)
220 220 if not n:
221 221 n = getset(repo, range(len(repo)), y)
222 222
223 223 if not m or not n:
224 224 return []
225 225 m, n = m[0], n[-1]
226 226
227 227 if m < n:
228 228 r = range(m, n + 1)
229 229 else:
230 230 r = range(m, n - 1, -1)
231 231 s = set(subset)
232 232 return [x for x in r if x in s]
233 233
234 234 def dagrange(repo, subset, x, y):
235 235 if subset:
236 236 r = range(len(repo))
237 237 xs = _revsbetween(repo, getset(repo, r, x), getset(repo, r, y))
238 238 s = set(subset)
239 239 return [r for r in xs if r in s]
240 240 return []
241 241
242 242 def andset(repo, subset, x, y):
243 243 return getset(repo, getset(repo, subset, x), y)
244 244
245 245 def orset(repo, subset, x, y):
246 246 xl = getset(repo, subset, x)
247 247 s = set(xl)
248 248 yl = getset(repo, [r for r in subset if r not in s], y)
249 249 return xl + yl
250 250
251 251 def notset(repo, subset, x):
252 252 s = set(getset(repo, subset, x))
253 253 return [r for r in subset if r not in s]
254 254
255 255 def listset(repo, subset, a, b):
256 256 raise error.ParseError(_("can't use a list in this context"))
257 257
258 258 def func(repo, subset, a, b):
259 259 if a[0] == 'symbol' and a[1] in symbols:
260 260 return symbols[a[1]](repo, subset, b)
261 261 raise error.ParseError(_("not a function: %s") % a[1])
262 262
263 263 # functions
264 264
265 265 def adds(repo, subset, x):
266 266 """``adds(pattern)``
267 267 Changesets that add a file matching pattern.
268 268 """
269 269 # i18n: "adds" is a keyword
270 270 pat = getstring(x, _("adds requires a pattern"))
271 271 return checkstatus(repo, subset, pat, 1)
272 272
273 273 def ancestor(repo, subset, x):
274 274 """``ancestor(single, single)``
275 275 Greatest common ancestor of the two changesets.
276 276 """
277 277 # i18n: "ancestor" is a keyword
278 278 l = getargs(x, 2, 2, _("ancestor requires two arguments"))
279 279 r = range(len(repo))
280 280 a = getset(repo, r, l[0])
281 281 b = getset(repo, r, l[1])
282 282 if len(a) != 1 or len(b) != 1:
283 283 # i18n: "ancestor" is a keyword
284 284 raise error.ParseError(_("ancestor arguments must be single revisions"))
285 285 an = [repo[a[0]].ancestor(repo[b[0]]).rev()]
286 286
287 287 return [r for r in an if r in subset]
288 288
289 289 def _ancestors(repo, subset, x, followfirst=False):
290 290 args = getset(repo, range(len(repo)), x)
291 291 if not args:
292 292 return []
293 293 s = set(_revancestors(repo, args, followfirst)) | set(args)
294 294 return [r for r in subset if r in s]
295 295
296 296 def ancestors(repo, subset, x):
297 297 """``ancestors(set)``
298 298 Changesets that are ancestors of a changeset in set.
299 299 """
300 300 return _ancestors(repo, subset, x)
301 301
302 302 def _firstancestors(repo, subset, x):
303 303 # ``_firstancestors(set)``
304 304 # Like ``ancestors(set)`` but follows only the first parents.
305 305 return _ancestors(repo, subset, x, followfirst=True)
306 306
307 307 def ancestorspec(repo, subset, x, n):
308 308 """``set~n``
309 309 Changesets that are the Nth ancestor (first parents only) of a changeset
310 310 in set.
311 311 """
312 312 try:
313 313 n = int(n[1])
314 314 except (TypeError, ValueError):
315 315 raise error.ParseError(_("~ expects a number"))
316 316 ps = set()
317 317 cl = repo.changelog
318 318 for r in getset(repo, subset, x):
319 319 for i in range(n):
320 320 r = cl.parentrevs(r)[0]
321 321 ps.add(r)
322 322 return [r for r in subset if r in ps]
323 323
324 324 def author(repo, subset, x):
325 325 """``author(string)``
326 326 Alias for ``user(string)``.
327 327 """
328 328 # i18n: "author" is a keyword
329 329 n = encoding.lower(getstring(x, _("author requires a string")))
330 330 kind, pattern, matcher = _substringmatcher(n)
331 331 return [r for r in subset if matcher(encoding.lower(repo[r].user()))]
332 332
333 333 def bisect(repo, subset, x):
334 334 """``bisect(string)``
335 335 Changesets marked in the specified bisect status:
336 336
337 337 - ``good``, ``bad``, ``skip``: csets explicitly marked as good/bad/skip
338 338 - ``goods``, ``bads`` : csets topologicaly good/bad
339 339 - ``range`` : csets taking part in the bisection
340 340 - ``pruned`` : csets that are goods, bads or skipped
341 341 - ``untested`` : csets whose fate is yet unknown
342 342 - ``ignored`` : csets ignored due to DAG topology
343 343 - ``current`` : the cset currently being bisected
344 344 """
345 345 status = getstring(x, _("bisect requires a string")).lower()
346 346 state = set(hbisect.get(repo, status))
347 347 return [r for r in subset if r in state]
348 348
349 349 # Backward-compatibility
350 350 # - no help entry so that we do not advertise it any more
351 351 def bisected(repo, subset, x):
352 352 return bisect(repo, subset, x)
353 353
354 354 def bookmark(repo, subset, x):
355 355 """``bookmark([name])``
356 356 The named bookmark or all bookmarks.
357 357
358 358 If `name` starts with `re:`, the remainder of the name is treated as
359 359 a regular expression. To match a bookmark that actually starts with `re:`,
360 360 use the prefix `literal:`.
361 361 """
362 362 # i18n: "bookmark" is a keyword
363 363 args = getargs(x, 0, 1, _('bookmark takes one or no arguments'))
364 364 if args:
365 365 bm = getstring(args[0],
366 366 # i18n: "bookmark" is a keyword
367 367 _('the argument to bookmark must be a string'))
368 368 kind, pattern, matcher = _stringmatcher(bm)
369 369 if kind == 'literal':
370 370 bmrev = bookmarksmod.listbookmarks(repo).get(bm, None)
371 371 if not bmrev:
372 372 raise util.Abort(_("bookmark '%s' does not exist") % bm)
373 373 bmrev = repo[bmrev].rev()
374 374 return [r for r in subset if r == bmrev]
375 375 else:
376 376 matchrevs = set()
377 377 for name, bmrev in bookmarksmod.listbookmarks(repo).iteritems():
378 378 if matcher(name):
379 379 matchrevs.add(bmrev)
380 380 if not matchrevs:
381 381 raise util.Abort(_("no bookmarks exist that match '%s'")
382 382 % pattern)
383 383 bmrevs = set()
384 384 for bmrev in matchrevs:
385 385 bmrevs.add(repo[bmrev].rev())
386 386 return [r for r in subset if r in bmrevs]
387 387
388 388 bms = set([repo[r].rev()
389 389 for r in bookmarksmod.listbookmarks(repo).values()])
390 390 return [r for r in subset if r in bms]
391 391
392 392 def branch(repo, subset, x):
393 393 """``branch(string or set)``
394 394 All changesets belonging to the given branch or the branches of the given
395 395 changesets.
396 396
397 397 If `string` starts with `re:`, the remainder of the name is treated as
398 398 a regular expression. To match a branch that actually starts with `re:`,
399 399 use the prefix `literal:`.
400 400 """
401 401 try:
402 402 b = getstring(x, '')
403 403 except error.ParseError:
404 404 # not a string, but another revspec, e.g. tip()
405 405 pass
406 406 else:
407 407 kind, pattern, matcher = _stringmatcher(b)
408 408 if kind == 'literal':
409 409 # note: falls through to the revspec case if no branch with
410 410 # this name exists
411 411 if pattern in repo.branchmap():
412 412 return [r for r in subset if matcher(repo[r].branch())]
413 413 else:
414 414 return [r for r in subset if matcher(repo[r].branch())]
415 415
416 416 s = getset(repo, range(len(repo)), x)
417 417 b = set()
418 418 for r in s:
419 419 b.add(repo[r].branch())
420 420 s = set(s)
421 421 return [r for r in subset if r in s or repo[r].branch() in b]
422 422
423 423 def checkstatus(repo, subset, pat, field):
424 424 m = None
425 425 s = []
426 426 hasset = matchmod.patkind(pat) == 'set'
427 427 fname = None
428 428 for r in subset:
429 429 c = repo[r]
430 430 if not m or hasset:
431 431 m = matchmod.match(repo.root, repo.getcwd(), [pat], ctx=c)
432 432 if not m.anypats() and len(m.files()) == 1:
433 433 fname = m.files()[0]
434 434 if fname is not None:
435 435 if fname not in c.files():
436 436 continue
437 437 else:
438 438 for f in c.files():
439 439 if m(f):
440 440 break
441 441 else:
442 442 continue
443 443 files = repo.status(c.p1().node(), c.node())[field]
444 444 if fname is not None:
445 445 if fname in files:
446 446 s.append(r)
447 447 else:
448 448 for f in files:
449 449 if m(f):
450 450 s.append(r)
451 451 break
452 452 return s
453 453
454 454 def _children(repo, narrow, parentset):
455 455 cs = set()
456 456 pr = repo.changelog.parentrevs
457 457 for r in narrow:
458 458 for p in pr(r):
459 459 if p in parentset:
460 460 cs.add(r)
461 461 return cs
462 462
463 463 def children(repo, subset, x):
464 464 """``children(set)``
465 465 Child changesets of changesets in set.
466 466 """
467 467 s = set(getset(repo, range(len(repo)), x))
468 468 cs = _children(repo, subset, s)
469 469 return [r for r in subset if r in cs]
470 470
471 471 def closed(repo, subset, x):
472 472 """``closed()``
473 473 Changeset is closed.
474 474 """
475 475 # i18n: "closed" is a keyword
476 476 getargs(x, 0, 0, _("closed takes no arguments"))
477 477 return [r for r in subset if repo[r].closesbranch()]
478 478
479 479 def contains(repo, subset, x):
480 480 """``contains(pattern)``
481 481 Revision contains a file matching pattern. See :hg:`help patterns`
482 482 for information about file patterns.
483 483 """
484 484 # i18n: "contains" is a keyword
485 485 pat = getstring(x, _("contains requires a pattern"))
486 486 m = None
487 487 s = []
488 488 if not matchmod.patkind(pat):
489 489 for r in subset:
490 490 if pat in repo[r]:
491 491 s.append(r)
492 492 else:
493 493 for r in subset:
494 494 c = repo[r]
495 495 if not m or matchmod.patkind(pat) == 'set':
496 496 m = matchmod.match(repo.root, repo.getcwd(), [pat], ctx=c)
497 497 for f in c.manifest():
498 498 if m(f):
499 499 s.append(r)
500 500 break
501 501 return s
502 502
503 503 def converted(repo, subset, x):
504 504 """``converted([id])``
505 505 Changesets converted from the given identifier in the old repository if
506 506 present, or all converted changesets if no identifier is specified.
507 507 """
508 508
509 509 # There is exactly no chance of resolving the revision, so do a simple
510 510 # string compare and hope for the best
511 511
512 512 # i18n: "converted" is a keyword
513 513 rev = None
514 514 l = getargs(x, 0, 1, _('converted takes one or no arguments'))
515 515 if l:
516 516 rev = getstring(l[0], _('converted requires a revision'))
517 517
518 518 def _matchvalue(r):
519 519 source = repo[r].extra().get('convert_revision', None)
520 520 return source is not None and (rev is None or source.startswith(rev))
521 521
522 522 return [r for r in subset if _matchvalue(r)]
523 523
524 524 def date(repo, subset, x):
525 525 """``date(interval)``
526 526 Changesets within the interval, see :hg:`help dates`.
527 527 """
528 528 # i18n: "date" is a keyword
529 529 ds = getstring(x, _("date requires a string"))
530 530 dm = util.matchdate(ds)
531 531 return [r for r in subset if dm(repo[r].date()[0])]
532 532
533 533 def desc(repo, subset, x):
534 534 """``desc(string)``
535 535 Search commit message for string. The match is case-insensitive.
536 536 """
537 537 # i18n: "desc" is a keyword
538 538 ds = encoding.lower(getstring(x, _("desc requires a string")))
539 539 l = []
540 540 for r in subset:
541 541 c = repo[r]
542 542 if ds in encoding.lower(c.description()):
543 543 l.append(r)
544 544 return l
545 545
546 546 def _descendants(repo, subset, x, followfirst=False):
547 547 args = getset(repo, range(len(repo)), x)
548 548 if not args:
549 549 return []
550 550 s = set(_revdescendants(repo, args, followfirst)) | set(args)
551 551 return [r for r in subset if r in s]
552 552
553 553 def descendants(repo, subset, x):
554 554 """``descendants(set)``
555 555 Changesets which are descendants of changesets in set.
556 556 """
557 557 return _descendants(repo, subset, x)
558 558
559 559 def _firstdescendants(repo, subset, x):
560 560 # ``_firstdescendants(set)``
561 561 # Like ``descendants(set)`` but follows only the first parents.
562 562 return _descendants(repo, subset, x, followfirst=True)
563 563
564 564 def destination(repo, subset, x):
565 565 """``destination([set])``
566 566 Changesets that were created by a graft, transplant or rebase operation,
567 567 with the given revisions specified as the source. Omitting the optional set
568 568 is the same as passing all().
569 569 """
570 570 if x is not None:
571 571 args = set(getset(repo, range(len(repo)), x))
572 572 else:
573 573 args = set(getall(repo, range(len(repo)), x))
574 574
575 575 dests = set()
576 576
577 577 # subset contains all of the possible destinations that can be returned, so
578 578 # iterate over them and see if their source(s) were provided in the args.
579 579 # Even if the immediate src of r is not in the args, src's source (or
580 580 # further back) may be. Scanning back further than the immediate src allows
581 581 # transitive transplants and rebases to yield the same results as transitive
582 582 # grafts.
583 583 for r in subset:
584 584 src = _getrevsource(repo, r)
585 585 lineage = None
586 586
587 587 while src is not None:
588 588 if lineage is None:
589 589 lineage = list()
590 590
591 591 lineage.append(r)
592 592
593 593 # The visited lineage is a match if the current source is in the arg
594 594 # set. Since every candidate dest is visited by way of iterating
595 595 # subset, any dests futher back in the lineage will be tested by a
596 596 # different iteration over subset. Likewise, if the src was already
597 597 # selected, the current lineage can be selected without going back
598 598 # further.
599 599 if src in args or src in dests:
600 600 dests.update(lineage)
601 601 break
602 602
603 603 r = src
604 604 src = _getrevsource(repo, r)
605 605
606 606 return [r for r in subset if r in dests]
607 607
608 608 def draft(repo, subset, x):
609 609 """``draft()``
610 610 Changeset in draft phase."""
611 611 getargs(x, 0, 0, _("draft takes no arguments"))
612 612 pc = repo._phasecache
613 613 return [r for r in subset if pc.phase(repo, r) == phases.draft]
614 614
615 615 def extinct(repo, subset, x):
616 616 """``extinct()``
617 617 obsolete changeset with obsolete descendant only."""
618 getargs(x, 0, 0, _("obsolete takes no arguments"))
618 getargs(x, 0, 0, _("extinct takes no arguments"))
619 619 extinctset = set(repo.revs('(obsolete()::) - (::(not obsolete()))'))
620 620 return [r for r in subset if r in extinctset]
621 621
622 622 def extra(repo, subset, x):
623 623 """``extra(label, [value])``
624 624 Changesets with the given label in the extra metadata, with the given
625 625 optional value.
626 626
627 627 If `value` starts with `re:`, the remainder of the value is treated as
628 628 a regular expression. To match a value that actually starts with `re:`,
629 629 use the prefix `literal:`.
630 630 """
631 631
632 632 l = getargs(x, 1, 2, _('extra takes at least 1 and at most 2 arguments'))
633 633 label = getstring(l[0], _('first argument to extra must be a string'))
634 634 value = None
635 635
636 636 if len(l) > 1:
637 637 value = getstring(l[1], _('second argument to extra must be a string'))
638 638 kind, value, matcher = _stringmatcher(value)
639 639
640 640 def _matchvalue(r):
641 641 extra = repo[r].extra()
642 642 return label in extra and (value is None or matcher(extra[label]))
643 643
644 644 return [r for r in subset if _matchvalue(r)]
645 645
646 646 def filelog(repo, subset, x):
647 647 """``filelog(pattern)``
648 648 Changesets connected to the specified filelog.
649 649
650 650 If you want to get all changesets affecting matched files exactly,
651 651 use ``file()`` predicate, because ``filelog()`` may omit some changesets
652 652 for performance reasons: see :hg:`help log` for detail.
653 653 """
654 654
655 655 pat = getstring(x, _("filelog requires a pattern"))
656 656 m = matchmod.match(repo.root, repo.getcwd(), [pat], default='relpath',
657 657 ctx=repo[None])
658 658 s = set()
659 659
660 660 if not matchmod.patkind(pat):
661 661 for f in m.files():
662 662 fl = repo.file(f)
663 663 for fr in fl:
664 664 s.add(fl.linkrev(fr))
665 665 else:
666 666 for f in repo[None]:
667 667 if m(f):
668 668 fl = repo.file(f)
669 669 for fr in fl:
670 670 s.add(fl.linkrev(fr))
671 671
672 672 return [r for r in subset if r in s]
673 673
674 674 def first(repo, subset, x):
675 675 """``first(set, [n])``
676 676 An alias for limit().
677 677 """
678 678 return limit(repo, subset, x)
679 679
680 680 def _follow(repo, subset, x, name, followfirst=False):
681 681 l = getargs(x, 0, 1, _("%s takes no arguments or a filename") % name)
682 682 c = repo['.']
683 683 if l:
684 684 x = getstring(l[0], _("%s expected a filename") % name)
685 685 if x in c:
686 686 cx = c[x]
687 687 s = set(ctx.rev() for ctx in cx.ancestors(followfirst=followfirst))
688 688 # include the revision responsible for the most recent version
689 689 s.add(cx.linkrev())
690 690 else:
691 691 return []
692 692 else:
693 693 s = set(_revancestors(repo, [c.rev()], followfirst)) | set([c.rev()])
694 694
695 695 return [r for r in subset if r in s]
696 696
697 697 def follow(repo, subset, x):
698 698 """``follow([file])``
699 699 An alias for ``::.`` (ancestors of the working copy's first parent).
700 700 If a filename is specified, the history of the given file is followed,
701 701 including copies.
702 702 """
703 703 return _follow(repo, subset, x, 'follow')
704 704
705 705 def _followfirst(repo, subset, x):
706 706 # ``followfirst([file])``
707 707 # Like ``follow([file])`` but follows only the first parent of
708 708 # every revision or file revision.
709 709 return _follow(repo, subset, x, '_followfirst', followfirst=True)
710 710
711 711 def getall(repo, subset, x):
712 712 """``all()``
713 713 All changesets, the same as ``0:tip``.
714 714 """
715 715 # i18n: "all" is a keyword
716 716 getargs(x, 0, 0, _("all takes no arguments"))
717 717 return subset
718 718
719 719 def grep(repo, subset, x):
720 720 """``grep(regex)``
721 721 Like ``keyword(string)`` but accepts a regex. Use ``grep(r'...')``
722 722 to ensure special escape characters are handled correctly. Unlike
723 723 ``keyword(string)``, the match is case-sensitive.
724 724 """
725 725 try:
726 726 # i18n: "grep" is a keyword
727 727 gr = re.compile(getstring(x, _("grep requires a string")))
728 728 except re.error, e:
729 729 raise error.ParseError(_('invalid match pattern: %s') % e)
730 730 l = []
731 731 for r in subset:
732 732 c = repo[r]
733 733 for e in c.files() + [c.user(), c.description()]:
734 734 if gr.search(e):
735 735 l.append(r)
736 736 break
737 737 return l
738 738
739 739 def _matchfiles(repo, subset, x):
740 740 # _matchfiles takes a revset list of prefixed arguments:
741 741 #
742 742 # [p:foo, i:bar, x:baz]
743 743 #
744 744 # builds a match object from them and filters subset. Allowed
745 745 # prefixes are 'p:' for regular patterns, 'i:' for include
746 746 # patterns and 'x:' for exclude patterns. Use 'r:' prefix to pass
747 747 # a revision identifier, or the empty string to reference the
748 748 # working directory, from which the match object is
749 749 # initialized. Use 'd:' to set the default matching mode, default
750 750 # to 'glob'. At most one 'r:' and 'd:' argument can be passed.
751 751
752 752 # i18n: "_matchfiles" is a keyword
753 753 l = getargs(x, 1, -1, _("_matchfiles requires at least one argument"))
754 754 pats, inc, exc = [], [], []
755 755 hasset = False
756 756 rev, default = None, None
757 757 for arg in l:
758 758 s = getstring(arg, _("_matchfiles requires string arguments"))
759 759 prefix, value = s[:2], s[2:]
760 760 if prefix == 'p:':
761 761 pats.append(value)
762 762 elif prefix == 'i:':
763 763 inc.append(value)
764 764 elif prefix == 'x:':
765 765 exc.append(value)
766 766 elif prefix == 'r:':
767 767 if rev is not None:
768 768 raise error.ParseError(_('_matchfiles expected at most one '
769 769 'revision'))
770 770 rev = value
771 771 elif prefix == 'd:':
772 772 if default is not None:
773 773 raise error.ParseError(_('_matchfiles expected at most one '
774 774 'default mode'))
775 775 default = value
776 776 else:
777 777 raise error.ParseError(_('invalid _matchfiles prefix: %s') % prefix)
778 778 if not hasset and matchmod.patkind(value) == 'set':
779 779 hasset = True
780 780 if not default:
781 781 default = 'glob'
782 782 m = None
783 783 s = []
784 784 for r in subset:
785 785 c = repo[r]
786 786 if not m or (hasset and rev is None):
787 787 ctx = c
788 788 if rev is not None:
789 789 ctx = repo[rev or None]
790 790 m = matchmod.match(repo.root, repo.getcwd(), pats, include=inc,
791 791 exclude=exc, ctx=ctx, default=default)
792 792 for f in c.files():
793 793 if m(f):
794 794 s.append(r)
795 795 break
796 796 return s
797 797
798 798 def hasfile(repo, subset, x):
799 799 """``file(pattern)``
800 800 Changesets affecting files matched by pattern.
801 801
802 802 If you want to pick changesets up fast, consider to
803 803 use ``filelog()`` predicate, too.
804 804 """
805 805 # i18n: "file" is a keyword
806 806 pat = getstring(x, _("file requires a pattern"))
807 807 return _matchfiles(repo, subset, ('string', 'p:' + pat))
808 808
809 809 def head(repo, subset, x):
810 810 """``head()``
811 811 Changeset is a named branch head.
812 812 """
813 813 # i18n: "head" is a keyword
814 814 getargs(x, 0, 0, _("head takes no arguments"))
815 815 hs = set()
816 816 for b, ls in repo.branchmap().iteritems():
817 817 hs.update(repo[h].rev() for h in ls)
818 818 return [r for r in subset if r in hs]
819 819
820 820 def heads(repo, subset, x):
821 821 """``heads(set)``
822 822 Members of set with no children in set.
823 823 """
824 824 s = getset(repo, subset, x)
825 825 ps = set(parents(repo, subset, x))
826 826 return [r for r in s if r not in ps]
827 827
828 828 def keyword(repo, subset, x):
829 829 """``keyword(string)``
830 830 Search commit message, user name, and names of changed files for
831 831 string. The match is case-insensitive.
832 832 """
833 833 # i18n: "keyword" is a keyword
834 834 kw = encoding.lower(getstring(x, _("keyword requires a string")))
835 835 l = []
836 836 for r in subset:
837 837 c = repo[r]
838 838 t = " ".join(c.files() + [c.user(), c.description()])
839 839 if kw in encoding.lower(t):
840 840 l.append(r)
841 841 return l
842 842
843 843 def limit(repo, subset, x):
844 844 """``limit(set, [n])``
845 845 First n members of set, defaulting to 1.
846 846 """
847 847 # i18n: "limit" is a keyword
848 848 l = getargs(x, 1, 2, _("limit requires one or two arguments"))
849 849 try:
850 850 lim = 1
851 851 if len(l) == 2:
852 852 # i18n: "limit" is a keyword
853 853 lim = int(getstring(l[1], _("limit requires a number")))
854 854 except (TypeError, ValueError):
855 855 # i18n: "limit" is a keyword
856 856 raise error.ParseError(_("limit expects a number"))
857 857 ss = set(subset)
858 858 os = getset(repo, range(len(repo)), l[0])[:lim]
859 859 return [r for r in os if r in ss]
860 860
861 861 def last(repo, subset, x):
862 862 """``last(set, [n])``
863 863 Last n members of set, defaulting to 1.
864 864 """
865 865 # i18n: "last" is a keyword
866 866 l = getargs(x, 1, 2, _("last requires one or two arguments"))
867 867 try:
868 868 lim = 1
869 869 if len(l) == 2:
870 870 # i18n: "last" is a keyword
871 871 lim = int(getstring(l[1], _("last requires a number")))
872 872 except (TypeError, ValueError):
873 873 # i18n: "last" is a keyword
874 874 raise error.ParseError(_("last expects a number"))
875 875 ss = set(subset)
876 876 os = getset(repo, range(len(repo)), l[0])[-lim:]
877 877 return [r for r in os if r in ss]
878 878
879 879 def maxrev(repo, subset, x):
880 880 """``max(set)``
881 881 Changeset with highest revision number in set.
882 882 """
883 883 os = getset(repo, range(len(repo)), x)
884 884 if os:
885 885 m = max(os)
886 886 if m in subset:
887 887 return [m]
888 888 return []
889 889
890 890 def merge(repo, subset, x):
891 891 """``merge()``
892 892 Changeset is a merge changeset.
893 893 """
894 894 # i18n: "merge" is a keyword
895 895 getargs(x, 0, 0, _("merge takes no arguments"))
896 896 cl = repo.changelog
897 897 return [r for r in subset if cl.parentrevs(r)[1] != -1]
898 898
899 899 def minrev(repo, subset, x):
900 900 """``min(set)``
901 901 Changeset with lowest revision number in set.
902 902 """
903 903 os = getset(repo, range(len(repo)), x)
904 904 if os:
905 905 m = min(os)
906 906 if m in subset:
907 907 return [m]
908 908 return []
909 909
910 910 def modifies(repo, subset, x):
911 911 """``modifies(pattern)``
912 912 Changesets modifying files matched by pattern.
913 913 """
914 914 # i18n: "modifies" is a keyword
915 915 pat = getstring(x, _("modifies requires a pattern"))
916 916 return checkstatus(repo, subset, pat, 0)
917 917
918 918 def node_(repo, subset, x):
919 919 """``id(string)``
920 920 Revision non-ambiguously specified by the given hex string prefix.
921 921 """
922 922 # i18n: "id" is a keyword
923 923 l = getargs(x, 1, 1, _("id requires one argument"))
924 924 # i18n: "id" is a keyword
925 925 n = getstring(l[0], _("id requires a string"))
926 926 if len(n) == 40:
927 927 rn = repo[n].rev()
928 928 else:
929 929 rn = None
930 930 pm = repo.changelog._partialmatch(n)
931 931 if pm is not None:
932 932 rn = repo.changelog.rev(pm)
933 933
934 934 return [r for r in subset if r == rn]
935 935
936 936 def obsolete(repo, subset, x):
937 937 """``obsolete()``
938 938 Mutable changeset with a newer version."""
939 939 getargs(x, 0, 0, _("obsolete takes no arguments"))
940 940 return [r for r in subset if repo[r].obsolete()]
941 941
942 942 def origin(repo, subset, x):
943 943 """``origin([set])``
944 944 Changesets that were specified as a source for the grafts, transplants or
945 945 rebases that created the given revisions. Omitting the optional set is the
946 946 same as passing all(). If a changeset created by these operations is itself
947 947 specified as a source for one of these operations, only the source changeset
948 948 for the first operation is selected.
949 949 """
950 950 if x is not None:
951 951 args = set(getset(repo, range(len(repo)), x))
952 952 else:
953 953 args = set(getall(repo, range(len(repo)), x))
954 954
955 955 def _firstsrc(rev):
956 956 src = _getrevsource(repo, rev)
957 957 if src is None:
958 958 return None
959 959
960 960 while True:
961 961 prev = _getrevsource(repo, src)
962 962
963 963 if prev is None:
964 964 return src
965 965 src = prev
966 966
967 967 o = set([_firstsrc(r) for r in args])
968 968 return [r for r in subset if r in o]
969 969
970 970 def outgoing(repo, subset, x):
971 971 """``outgoing([path])``
972 972 Changesets not found in the specified destination repository, or the
973 973 default push location.
974 974 """
975 975 import hg # avoid start-up nasties
976 976 # i18n: "outgoing" is a keyword
977 977 l = getargs(x, 0, 1, _("outgoing takes one or no arguments"))
978 978 # i18n: "outgoing" is a keyword
979 979 dest = l and getstring(l[0], _("outgoing requires a repository path")) or ''
980 980 dest = repo.ui.expandpath(dest or 'default-push', dest or 'default')
981 981 dest, branches = hg.parseurl(dest)
982 982 revs, checkout = hg.addbranchrevs(repo, repo, branches, [])
983 983 if revs:
984 984 revs = [repo.lookup(rev) for rev in revs]
985 985 other = hg.peer(repo, {}, dest)
986 986 repo.ui.pushbuffer()
987 987 outgoing = discovery.findcommonoutgoing(repo, other, onlyheads=revs)
988 988 repo.ui.popbuffer()
989 989 cl = repo.changelog
990 990 o = set([cl.rev(r) for r in outgoing.missing])
991 991 return [r for r in subset if r in o]
992 992
993 993 def p1(repo, subset, x):
994 994 """``p1([set])``
995 995 First parent of changesets in set, or the working directory.
996 996 """
997 997 if x is None:
998 998 p = repo[x].p1().rev()
999 999 return [r for r in subset if r == p]
1000 1000
1001 1001 ps = set()
1002 1002 cl = repo.changelog
1003 1003 for r in getset(repo, range(len(repo)), x):
1004 1004 ps.add(cl.parentrevs(r)[0])
1005 1005 return [r for r in subset if r in ps]
1006 1006
1007 1007 def p2(repo, subset, x):
1008 1008 """``p2([set])``
1009 1009 Second parent of changesets in set, or the working directory.
1010 1010 """
1011 1011 if x is None:
1012 1012 ps = repo[x].parents()
1013 1013 try:
1014 1014 p = ps[1].rev()
1015 1015 return [r for r in subset if r == p]
1016 1016 except IndexError:
1017 1017 return []
1018 1018
1019 1019 ps = set()
1020 1020 cl = repo.changelog
1021 1021 for r in getset(repo, range(len(repo)), x):
1022 1022 ps.add(cl.parentrevs(r)[1])
1023 1023 return [r for r in subset if r in ps]
1024 1024
1025 1025 def parents(repo, subset, x):
1026 1026 """``parents([set])``
1027 1027 The set of all parents for all changesets in set, or the working directory.
1028 1028 """
1029 1029 if x is None:
1030 1030 ps = tuple(p.rev() for p in repo[x].parents())
1031 1031 return [r for r in subset if r in ps]
1032 1032
1033 1033 ps = set()
1034 1034 cl = repo.changelog
1035 1035 for r in getset(repo, range(len(repo)), x):
1036 1036 ps.update(cl.parentrevs(r))
1037 1037 return [r for r in subset if r in ps]
1038 1038
1039 1039 def parentspec(repo, subset, x, n):
1040 1040 """``set^0``
1041 1041 The set.
1042 1042 ``set^1`` (or ``set^``), ``set^2``
1043 1043 First or second parent, respectively, of all changesets in set.
1044 1044 """
1045 1045 try:
1046 1046 n = int(n[1])
1047 1047 if n not in (0, 1, 2):
1048 1048 raise ValueError
1049 1049 except (TypeError, ValueError):
1050 1050 raise error.ParseError(_("^ expects a number 0, 1, or 2"))
1051 1051 ps = set()
1052 1052 cl = repo.changelog
1053 1053 for r in getset(repo, subset, x):
1054 1054 if n == 0:
1055 1055 ps.add(r)
1056 1056 elif n == 1:
1057 1057 ps.add(cl.parentrevs(r)[0])
1058 1058 elif n == 2:
1059 1059 parents = cl.parentrevs(r)
1060 1060 if len(parents) > 1:
1061 1061 ps.add(parents[1])
1062 1062 return [r for r in subset if r in ps]
1063 1063
1064 1064 def present(repo, subset, x):
1065 1065 """``present(set)``
1066 1066 An empty set, if any revision in set isn't found; otherwise,
1067 1067 all revisions in set.
1068 1068
1069 1069 If any of specified revisions is not present in the local repository,
1070 1070 the query is normally aborted. But this predicate allows the query
1071 1071 to continue even in such cases.
1072 1072 """
1073 1073 try:
1074 1074 return getset(repo, subset, x)
1075 1075 except error.RepoLookupError:
1076 1076 return []
1077 1077
1078 1078 def public(repo, subset, x):
1079 1079 """``public()``
1080 1080 Changeset in public phase."""
1081 1081 getargs(x, 0, 0, _("public takes no arguments"))
1082 1082 pc = repo._phasecache
1083 1083 return [r for r in subset if pc.phase(repo, r) == phases.public]
1084 1084
1085 1085 def remote(repo, subset, x):
1086 1086 """``remote([id [,path]])``
1087 1087 Local revision that corresponds to the given identifier in a
1088 1088 remote repository, if present. Here, the '.' identifier is a
1089 1089 synonym for the current local branch.
1090 1090 """
1091 1091
1092 1092 import hg # avoid start-up nasties
1093 1093 # i18n: "remote" is a keyword
1094 1094 l = getargs(x, 0, 2, _("remote takes one, two or no arguments"))
1095 1095
1096 1096 q = '.'
1097 1097 if len(l) > 0:
1098 1098 # i18n: "remote" is a keyword
1099 1099 q = getstring(l[0], _("remote requires a string id"))
1100 1100 if q == '.':
1101 1101 q = repo['.'].branch()
1102 1102
1103 1103 dest = ''
1104 1104 if len(l) > 1:
1105 1105 # i18n: "remote" is a keyword
1106 1106 dest = getstring(l[1], _("remote requires a repository path"))
1107 1107 dest = repo.ui.expandpath(dest or 'default')
1108 1108 dest, branches = hg.parseurl(dest)
1109 1109 revs, checkout = hg.addbranchrevs(repo, repo, branches, [])
1110 1110 if revs:
1111 1111 revs = [repo.lookup(rev) for rev in revs]
1112 1112 other = hg.peer(repo, {}, dest)
1113 1113 n = other.lookup(q)
1114 1114 if n in repo:
1115 1115 r = repo[n].rev()
1116 1116 if r in subset:
1117 1117 return [r]
1118 1118 return []
1119 1119
1120 1120 def removes(repo, subset, x):
1121 1121 """``removes(pattern)``
1122 1122 Changesets which remove files matching pattern.
1123 1123 """
1124 1124 # i18n: "removes" is a keyword
1125 1125 pat = getstring(x, _("removes requires a pattern"))
1126 1126 return checkstatus(repo, subset, pat, 2)
1127 1127
1128 1128 def rev(repo, subset, x):
1129 1129 """``rev(number)``
1130 1130 Revision with the given numeric identifier.
1131 1131 """
1132 1132 # i18n: "rev" is a keyword
1133 1133 l = getargs(x, 1, 1, _("rev requires one argument"))
1134 1134 try:
1135 1135 # i18n: "rev" is a keyword
1136 1136 l = int(getstring(l[0], _("rev requires a number")))
1137 1137 except (TypeError, ValueError):
1138 1138 # i18n: "rev" is a keyword
1139 1139 raise error.ParseError(_("rev expects a number"))
1140 1140 return [r for r in subset if r == l]
1141 1141
1142 1142 def matching(repo, subset, x):
1143 1143 """``matching(revision [, field])``
1144 1144 Changesets in which a given set of fields match the set of fields in the
1145 1145 selected revision or set.
1146 1146
1147 1147 To match more than one field pass the list of fields to match separated
1148 1148 by spaces (e.g. ``author description``).
1149 1149
1150 1150 Valid fields are most regular revision fields and some special fields.
1151 1151
1152 1152 Regular revision fields are ``description``, ``author``, ``branch``,
1153 1153 ``date``, ``files``, ``phase``, ``parents``, ``substate``, ``user``
1154 1154 and ``diff``.
1155 1155 Note that ``author`` and ``user`` are synonyms. ``diff`` refers to the
1156 1156 contents of the revision. Two revisions matching their ``diff`` will
1157 1157 also match their ``files``.
1158 1158
1159 1159 Special fields are ``summary`` and ``metadata``:
1160 1160 ``summary`` matches the first line of the description.
1161 1161 ``metadata`` is equivalent to matching ``description user date``
1162 1162 (i.e. it matches the main metadata fields).
1163 1163
1164 1164 ``metadata`` is the default field which is used when no fields are
1165 1165 specified. You can match more than one field at a time.
1166 1166 """
1167 1167 l = getargs(x, 1, 2, _("matching takes 1 or 2 arguments"))
1168 1168
1169 1169 revs = getset(repo, xrange(len(repo)), l[0])
1170 1170
1171 1171 fieldlist = ['metadata']
1172 1172 if len(l) > 1:
1173 1173 fieldlist = getstring(l[1],
1174 1174 _("matching requires a string "
1175 1175 "as its second argument")).split()
1176 1176
1177 1177 # Make sure that there are no repeated fields,
1178 1178 # expand the 'special' 'metadata' field type
1179 1179 # and check the 'files' whenever we check the 'diff'
1180 1180 fields = []
1181 1181 for field in fieldlist:
1182 1182 if field == 'metadata':
1183 1183 fields += ['user', 'description', 'date']
1184 1184 elif field == 'diff':
1185 1185 # a revision matching the diff must also match the files
1186 1186 # since matching the diff is very costly, make sure to
1187 1187 # also match the files first
1188 1188 fields += ['files', 'diff']
1189 1189 else:
1190 1190 if field == 'author':
1191 1191 field = 'user'
1192 1192 fields.append(field)
1193 1193 fields = set(fields)
1194 1194 if 'summary' in fields and 'description' in fields:
1195 1195 # If a revision matches its description it also matches its summary
1196 1196 fields.discard('summary')
1197 1197
1198 1198 # We may want to match more than one field
1199 1199 # Not all fields take the same amount of time to be matched
1200 1200 # Sort the selected fields in order of increasing matching cost
1201 1201 fieldorder = ['phase', 'parents', 'user', 'date', 'branch', 'summary',
1202 1202 'files', 'description', 'substate', 'diff']
1203 1203 def fieldkeyfunc(f):
1204 1204 try:
1205 1205 return fieldorder.index(f)
1206 1206 except ValueError:
1207 1207 # assume an unknown field is very costly
1208 1208 return len(fieldorder)
1209 1209 fields = list(fields)
1210 1210 fields.sort(key=fieldkeyfunc)
1211 1211
1212 1212 # Each field will be matched with its own "getfield" function
1213 1213 # which will be added to the getfieldfuncs array of functions
1214 1214 getfieldfuncs = []
1215 1215 _funcs = {
1216 1216 'user': lambda r: repo[r].user(),
1217 1217 'branch': lambda r: repo[r].branch(),
1218 1218 'date': lambda r: repo[r].date(),
1219 1219 'description': lambda r: repo[r].description(),
1220 1220 'files': lambda r: repo[r].files(),
1221 1221 'parents': lambda r: repo[r].parents(),
1222 1222 'phase': lambda r: repo[r].phase(),
1223 1223 'substate': lambda r: repo[r].substate,
1224 1224 'summary': lambda r: repo[r].description().splitlines()[0],
1225 1225 'diff': lambda r: list(repo[r].diff(git=True),)
1226 1226 }
1227 1227 for info in fields:
1228 1228 getfield = _funcs.get(info, None)
1229 1229 if getfield is None:
1230 1230 raise error.ParseError(
1231 1231 _("unexpected field name passed to matching: %s") % info)
1232 1232 getfieldfuncs.append(getfield)
1233 1233 # convert the getfield array of functions into a "getinfo" function
1234 1234 # which returns an array of field values (or a single value if there
1235 1235 # is only one field to match)
1236 1236 getinfo = lambda r: [f(r) for f in getfieldfuncs]
1237 1237
1238 1238 matches = set()
1239 1239 for rev in revs:
1240 1240 target = getinfo(rev)
1241 1241 for r in subset:
1242 1242 match = True
1243 1243 for n, f in enumerate(getfieldfuncs):
1244 1244 if target[n] != f(r):
1245 1245 match = False
1246 1246 break
1247 1247 if match:
1248 1248 matches.add(r)
1249 1249 return [r for r in subset if r in matches]
1250 1250
1251 1251 def reverse(repo, subset, x):
1252 1252 """``reverse(set)``
1253 1253 Reverse order of set.
1254 1254 """
1255 1255 l = getset(repo, subset, x)
1256 1256 if not isinstance(l, list):
1257 1257 l = list(l)
1258 1258 l.reverse()
1259 1259 return l
1260 1260
1261 1261 def roots(repo, subset, x):
1262 1262 """``roots(set)``
1263 1263 Changesets in set with no parent changeset in set.
1264 1264 """
1265 1265 s = set(getset(repo, xrange(len(repo)), x))
1266 1266 subset = [r for r in subset if r in s]
1267 1267 cs = _children(repo, subset, s)
1268 1268 return [r for r in subset if r not in cs]
1269 1269
1270 1270 def secret(repo, subset, x):
1271 1271 """``secret()``
1272 1272 Changeset in secret phase."""
1273 1273 getargs(x, 0, 0, _("secret takes no arguments"))
1274 1274 pc = repo._phasecache
1275 1275 return [r for r in subset if pc.phase(repo, r) == phases.secret]
1276 1276
1277 1277 def sort(repo, subset, x):
1278 1278 """``sort(set[, [-]key...])``
1279 1279 Sort set by keys. The default sort order is ascending, specify a key
1280 1280 as ``-key`` to sort in descending order.
1281 1281
1282 1282 The keys can be:
1283 1283
1284 1284 - ``rev`` for the revision number,
1285 1285 - ``branch`` for the branch name,
1286 1286 - ``desc`` for the commit message (description),
1287 1287 - ``user`` for user name (``author`` can be used as an alias),
1288 1288 - ``date`` for the commit date
1289 1289 """
1290 1290 # i18n: "sort" is a keyword
1291 1291 l = getargs(x, 1, 2, _("sort requires one or two arguments"))
1292 1292 keys = "rev"
1293 1293 if len(l) == 2:
1294 1294 keys = getstring(l[1], _("sort spec must be a string"))
1295 1295
1296 1296 s = l[0]
1297 1297 keys = keys.split()
1298 1298 l = []
1299 1299 def invert(s):
1300 1300 return "".join(chr(255 - ord(c)) for c in s)
1301 1301 for r in getset(repo, subset, s):
1302 1302 c = repo[r]
1303 1303 e = []
1304 1304 for k in keys:
1305 1305 if k == 'rev':
1306 1306 e.append(r)
1307 1307 elif k == '-rev':
1308 1308 e.append(-r)
1309 1309 elif k == 'branch':
1310 1310 e.append(c.branch())
1311 1311 elif k == '-branch':
1312 1312 e.append(invert(c.branch()))
1313 1313 elif k == 'desc':
1314 1314 e.append(c.description())
1315 1315 elif k == '-desc':
1316 1316 e.append(invert(c.description()))
1317 1317 elif k in 'user author':
1318 1318 e.append(c.user())
1319 1319 elif k in '-user -author':
1320 1320 e.append(invert(c.user()))
1321 1321 elif k == 'date':
1322 1322 e.append(c.date()[0])
1323 1323 elif k == '-date':
1324 1324 e.append(-c.date()[0])
1325 1325 else:
1326 1326 raise error.ParseError(_("unknown sort key %r") % k)
1327 1327 e.append(r)
1328 1328 l.append(e)
1329 1329 l.sort()
1330 1330 return [e[-1] for e in l]
1331 1331
1332 1332 def _stringmatcher(pattern):
1333 1333 """
1334 1334 accepts a string, possibly starting with 're:' or 'literal:' prefix.
1335 1335 returns the matcher name, pattern, and matcher function.
1336 1336 missing or unknown prefixes are treated as literal matches.
1337 1337
1338 1338 helper for tests:
1339 1339 >>> def test(pattern, *tests):
1340 1340 ... kind, pattern, matcher = _stringmatcher(pattern)
1341 1341 ... return (kind, pattern, [bool(matcher(t)) for t in tests])
1342 1342
1343 1343 exact matching (no prefix):
1344 1344 >>> test('abcdefg', 'abc', 'def', 'abcdefg')
1345 1345 ('literal', 'abcdefg', [False, False, True])
1346 1346
1347 1347 regex matching ('re:' prefix)
1348 1348 >>> test('re:a.+b', 'nomatch', 'fooadef', 'fooadefbar')
1349 1349 ('re', 'a.+b', [False, False, True])
1350 1350
1351 1351 force exact matches ('literal:' prefix)
1352 1352 >>> test('literal:re:foobar', 'foobar', 're:foobar')
1353 1353 ('literal', 're:foobar', [False, True])
1354 1354
1355 1355 unknown prefixes are ignored and treated as literals
1356 1356 >>> test('foo:bar', 'foo', 'bar', 'foo:bar')
1357 1357 ('literal', 'foo:bar', [False, False, True])
1358 1358 """
1359 1359 if pattern.startswith('re:'):
1360 1360 pattern = pattern[3:]
1361 1361 try:
1362 1362 regex = re.compile(pattern)
1363 1363 except re.error, e:
1364 1364 raise error.ParseError(_('invalid regular expression: %s')
1365 1365 % e)
1366 1366 return 're', pattern, regex.search
1367 1367 elif pattern.startswith('literal:'):
1368 1368 pattern = pattern[8:]
1369 1369 return 'literal', pattern, pattern.__eq__
1370 1370
1371 1371 def _substringmatcher(pattern):
1372 1372 kind, pattern, matcher = _stringmatcher(pattern)
1373 1373 if kind == 'literal':
1374 1374 matcher = lambda s: pattern in s
1375 1375 return kind, pattern, matcher
1376 1376
1377 1377 def tag(repo, subset, x):
1378 1378 """``tag([name])``
1379 1379 The specified tag by name, or all tagged revisions if no name is given.
1380 1380 """
1381 1381 # i18n: "tag" is a keyword
1382 1382 args = getargs(x, 0, 1, _("tag takes one or no arguments"))
1383 1383 cl = repo.changelog
1384 1384 if args:
1385 1385 pattern = getstring(args[0],
1386 1386 # i18n: "tag" is a keyword
1387 1387 _('the argument to tag must be a string'))
1388 1388 kind, pattern, matcher = _stringmatcher(pattern)
1389 1389 if kind == 'literal':
1390 1390 # avoid resolving all tags
1391 1391 tn = repo._tagscache.tags.get(pattern, None)
1392 1392 if tn is None:
1393 1393 raise util.Abort(_("tag '%s' does not exist") % pattern)
1394 1394 s = set([repo[tn].rev()])
1395 1395 else:
1396 1396 s = set([cl.rev(n) for t, n in repo.tagslist() if matcher(t)])
1397 1397 if not s:
1398 1398 raise util.Abort(_("no tags exist that match '%s'") % pattern)
1399 1399 else:
1400 1400 s = set([cl.rev(n) for t, n in repo.tagslist() if t != 'tip'])
1401 1401 return [r for r in subset if r in s]
1402 1402
1403 1403 def tagged(repo, subset, x):
1404 1404 return tag(repo, subset, x)
1405 1405
1406 1406 def unstable(repo, subset, x):
1407 1407 """``unstable()``
1408 1408 Unstable changesets are non-obsolete with obsolete descendants."""
1409 getargs(x, 0, 0, _("obsolete takes no arguments"))
1409 getargs(x, 0, 0, _("unstable takes no arguments"))
1410 1410 unstableset = set(repo.revs('(obsolete()::) - obsolete()'))
1411 1411 return [r for r in subset if r in unstableset]
1412 1412
1413 1413
1414 1414 def user(repo, subset, x):
1415 1415 """``user(string)``
1416 1416 User name contains string. The match is case-insensitive.
1417 1417
1418 1418 If `string` starts with `re:`, the remainder of the string is treated as
1419 1419 a regular expression. To match a user that actually contains `re:`, use
1420 1420 the prefix `literal:`.
1421 1421 """
1422 1422 return author(repo, subset, x)
1423 1423
1424 1424 # for internal use
1425 1425 def _list(repo, subset, x):
1426 1426 s = getstring(x, "internal error")
1427 1427 if not s:
1428 1428 return []
1429 1429 if not isinstance(subset, set):
1430 1430 subset = set(subset)
1431 1431 ls = [repo[r].rev() for r in s.split('\0')]
1432 1432 return [r for r in ls if r in subset]
1433 1433
1434 1434 symbols = {
1435 1435 "adds": adds,
1436 1436 "all": getall,
1437 1437 "ancestor": ancestor,
1438 1438 "ancestors": ancestors,
1439 1439 "_firstancestors": _firstancestors,
1440 1440 "author": author,
1441 1441 "bisect": bisect,
1442 1442 "bisected": bisected,
1443 1443 "bookmark": bookmark,
1444 1444 "branch": branch,
1445 1445 "children": children,
1446 1446 "closed": closed,
1447 1447 "contains": contains,
1448 1448 "converted": converted,
1449 1449 "date": date,
1450 1450 "desc": desc,
1451 1451 "descendants": descendants,
1452 1452 "_firstdescendants": _firstdescendants,
1453 1453 "destination": destination,
1454 1454 "draft": draft,
1455 1455 "extinct": extinct,
1456 1456 "extra": extra,
1457 1457 "file": hasfile,
1458 1458 "filelog": filelog,
1459 1459 "first": first,
1460 1460 "follow": follow,
1461 1461 "_followfirst": _followfirst,
1462 1462 "grep": grep,
1463 1463 "head": head,
1464 1464 "heads": heads,
1465 1465 "id": node_,
1466 1466 "keyword": keyword,
1467 1467 "last": last,
1468 1468 "limit": limit,
1469 1469 "_matchfiles": _matchfiles,
1470 1470 "max": maxrev,
1471 1471 "merge": merge,
1472 1472 "min": minrev,
1473 1473 "modifies": modifies,
1474 1474 "obsolete": obsolete,
1475 1475 "origin": origin,
1476 1476 "outgoing": outgoing,
1477 1477 "p1": p1,
1478 1478 "p2": p2,
1479 1479 "parents": parents,
1480 1480 "present": present,
1481 1481 "public": public,
1482 1482 "remote": remote,
1483 1483 "removes": removes,
1484 1484 "rev": rev,
1485 1485 "reverse": reverse,
1486 1486 "roots": roots,
1487 1487 "sort": sort,
1488 1488 "secret": secret,
1489 1489 "matching": matching,
1490 1490 "tag": tag,
1491 1491 "tagged": tagged,
1492 1492 "user": user,
1493 1493 "unstable": unstable,
1494 1494 "_list": _list,
1495 1495 }
1496 1496
1497 1497 methods = {
1498 1498 "range": rangeset,
1499 1499 "dagrange": dagrange,
1500 1500 "string": stringset,
1501 1501 "symbol": symbolset,
1502 1502 "and": andset,
1503 1503 "or": orset,
1504 1504 "not": notset,
1505 1505 "list": listset,
1506 1506 "func": func,
1507 1507 "ancestor": ancestorspec,
1508 1508 "parent": parentspec,
1509 1509 "parentpost": p1,
1510 1510 }
1511 1511
1512 1512 def optimize(x, small):
1513 1513 if x is None:
1514 1514 return 0, x
1515 1515
1516 1516 smallbonus = 1
1517 1517 if small:
1518 1518 smallbonus = .5
1519 1519
1520 1520 op = x[0]
1521 1521 if op == 'minus':
1522 1522 return optimize(('and', x[1], ('not', x[2])), small)
1523 1523 elif op == 'dagrangepre':
1524 1524 return optimize(('func', ('symbol', 'ancestors'), x[1]), small)
1525 1525 elif op == 'dagrangepost':
1526 1526 return optimize(('func', ('symbol', 'descendants'), x[1]), small)
1527 1527 elif op == 'rangepre':
1528 1528 return optimize(('range', ('string', '0'), x[1]), small)
1529 1529 elif op == 'rangepost':
1530 1530 return optimize(('range', x[1], ('string', 'tip')), small)
1531 1531 elif op == 'negate':
1532 1532 return optimize(('string',
1533 1533 '-' + getstring(x[1], _("can't negate that"))), small)
1534 1534 elif op in 'string symbol negate':
1535 1535 return smallbonus, x # single revisions are small
1536 1536 elif op == 'and':
1537 1537 wa, ta = optimize(x[1], True)
1538 1538 wb, tb = optimize(x[2], True)
1539 1539 w = min(wa, wb)
1540 1540 if wa > wb:
1541 1541 return w, (op, tb, ta)
1542 1542 return w, (op, ta, tb)
1543 1543 elif op == 'or':
1544 1544 wa, ta = optimize(x[1], False)
1545 1545 wb, tb = optimize(x[2], False)
1546 1546 if wb < wa:
1547 1547 wb, wa = wa, wb
1548 1548 return max(wa, wb), (op, ta, tb)
1549 1549 elif op == 'not':
1550 1550 o = optimize(x[1], not small)
1551 1551 return o[0], (op, o[1])
1552 1552 elif op == 'parentpost':
1553 1553 o = optimize(x[1], small)
1554 1554 return o[0], (op, o[1])
1555 1555 elif op == 'group':
1556 1556 return optimize(x[1], small)
1557 1557 elif op in 'dagrange range list parent ancestorspec':
1558 1558 if op == 'parent':
1559 1559 # x^:y means (x^) : y, not x ^ (:y)
1560 1560 post = ('parentpost', x[1])
1561 1561 if x[2][0] == 'dagrangepre':
1562 1562 return optimize(('dagrange', post, x[2][1]), small)
1563 1563 elif x[2][0] == 'rangepre':
1564 1564 return optimize(('range', post, x[2][1]), small)
1565 1565
1566 1566 wa, ta = optimize(x[1], small)
1567 1567 wb, tb = optimize(x[2], small)
1568 1568 return wa + wb, (op, ta, tb)
1569 1569 elif op == 'func':
1570 1570 f = getstring(x[1], _("not a symbol"))
1571 1571 wa, ta = optimize(x[2], small)
1572 1572 if f in ("author branch closed date desc file grep keyword "
1573 1573 "outgoing user"):
1574 1574 w = 10 # slow
1575 1575 elif f in "modifies adds removes":
1576 1576 w = 30 # slower
1577 1577 elif f == "contains":
1578 1578 w = 100 # very slow
1579 1579 elif f == "ancestor":
1580 1580 w = 1 * smallbonus
1581 1581 elif f in "reverse limit first":
1582 1582 w = 0
1583 1583 elif f in "sort":
1584 1584 w = 10 # assume most sorts look at changelog
1585 1585 else:
1586 1586 w = 1
1587 1587 return w + wa, (op, x[1], ta)
1588 1588 return 1, x
1589 1589
1590 1590 _aliasarg = ('func', ('symbol', '_aliasarg'))
1591 1591 def _getaliasarg(tree):
1592 1592 """If tree matches ('func', ('symbol', '_aliasarg'), ('string', X))
1593 1593 return X, None otherwise.
1594 1594 """
1595 1595 if (len(tree) == 3 and tree[:2] == _aliasarg
1596 1596 and tree[2][0] == 'string'):
1597 1597 return tree[2][1]
1598 1598 return None
1599 1599
1600 1600 def _checkaliasarg(tree, known=None):
1601 1601 """Check tree contains no _aliasarg construct or only ones which
1602 1602 value is in known. Used to avoid alias placeholders injection.
1603 1603 """
1604 1604 if isinstance(tree, tuple):
1605 1605 arg = _getaliasarg(tree)
1606 1606 if arg is not None and (not known or arg not in known):
1607 1607 raise error.ParseError(_("not a function: %s") % '_aliasarg')
1608 1608 for t in tree:
1609 1609 _checkaliasarg(t, known)
1610 1610
1611 1611 class revsetalias(object):
1612 1612 funcre = re.compile('^([^(]+)\(([^)]+)\)$')
1613 1613 args = None
1614 1614
1615 1615 def __init__(self, name, value):
1616 1616 '''Aliases like:
1617 1617
1618 1618 h = heads(default)
1619 1619 b($1) = ancestors($1) - ancestors(default)
1620 1620 '''
1621 1621 m = self.funcre.search(name)
1622 1622 if m:
1623 1623 self.name = m.group(1)
1624 1624 self.tree = ('func', ('symbol', m.group(1)))
1625 1625 self.args = [x.strip() for x in m.group(2).split(',')]
1626 1626 for arg in self.args:
1627 1627 # _aliasarg() is an unknown symbol only used separate
1628 1628 # alias argument placeholders from regular strings.
1629 1629 value = value.replace(arg, '_aliasarg(%r)' % (arg,))
1630 1630 else:
1631 1631 self.name = name
1632 1632 self.tree = ('symbol', name)
1633 1633
1634 1634 self.replacement, pos = parse(value)
1635 1635 if pos != len(value):
1636 1636 raise error.ParseError(_('invalid token'), pos)
1637 1637 # Check for placeholder injection
1638 1638 _checkaliasarg(self.replacement, self.args)
1639 1639
1640 1640 def _getalias(aliases, tree):
1641 1641 """If tree looks like an unexpanded alias, return it. Return None
1642 1642 otherwise.
1643 1643 """
1644 1644 if isinstance(tree, tuple) and tree:
1645 1645 if tree[0] == 'symbol' and len(tree) == 2:
1646 1646 name = tree[1]
1647 1647 alias = aliases.get(name)
1648 1648 if alias and alias.args is None and alias.tree == tree:
1649 1649 return alias
1650 1650 if tree[0] == 'func' and len(tree) > 1:
1651 1651 if tree[1][0] == 'symbol' and len(tree[1]) == 2:
1652 1652 name = tree[1][1]
1653 1653 alias = aliases.get(name)
1654 1654 if alias and alias.args is not None and alias.tree == tree[:2]:
1655 1655 return alias
1656 1656 return None
1657 1657
1658 1658 def _expandargs(tree, args):
1659 1659 """Replace _aliasarg instances with the substitution value of the
1660 1660 same name in args, recursively.
1661 1661 """
1662 1662 if not tree or not isinstance(tree, tuple):
1663 1663 return tree
1664 1664 arg = _getaliasarg(tree)
1665 1665 if arg is not None:
1666 1666 return args[arg]
1667 1667 return tuple(_expandargs(t, args) for t in tree)
1668 1668
1669 1669 def _expandaliases(aliases, tree, expanding, cache):
1670 1670 """Expand aliases in tree, recursively.
1671 1671
1672 1672 'aliases' is a dictionary mapping user defined aliases to
1673 1673 revsetalias objects.
1674 1674 """
1675 1675 if not isinstance(tree, tuple):
1676 1676 # Do not expand raw strings
1677 1677 return tree
1678 1678 alias = _getalias(aliases, tree)
1679 1679 if alias is not None:
1680 1680 if alias in expanding:
1681 1681 raise error.ParseError(_('infinite expansion of revset alias "%s" '
1682 1682 'detected') % alias.name)
1683 1683 expanding.append(alias)
1684 1684 if alias.name not in cache:
1685 1685 cache[alias.name] = _expandaliases(aliases, alias.replacement,
1686 1686 expanding, cache)
1687 1687 result = cache[alias.name]
1688 1688 expanding.pop()
1689 1689 if alias.args is not None:
1690 1690 l = getlist(tree[2])
1691 1691 if len(l) != len(alias.args):
1692 1692 raise error.ParseError(
1693 1693 _('invalid number of arguments: %s') % len(l))
1694 1694 l = [_expandaliases(aliases, a, [], cache) for a in l]
1695 1695 result = _expandargs(result, dict(zip(alias.args, l)))
1696 1696 else:
1697 1697 result = tuple(_expandaliases(aliases, t, expanding, cache)
1698 1698 for t in tree)
1699 1699 return result
1700 1700
1701 1701 def findaliases(ui, tree):
1702 1702 _checkaliasarg(tree)
1703 1703 aliases = {}
1704 1704 for k, v in ui.configitems('revsetalias'):
1705 1705 alias = revsetalias(k, v)
1706 1706 aliases[alias.name] = alias
1707 1707 return _expandaliases(aliases, tree, [], {})
1708 1708
1709 1709 parse = parser.parser(tokenize, elements).parse
1710 1710
1711 1711 def match(ui, spec):
1712 1712 if not spec:
1713 1713 raise error.ParseError(_("empty query"))
1714 1714 tree, pos = parse(spec)
1715 1715 if (pos != len(spec)):
1716 1716 raise error.ParseError(_("invalid token"), pos)
1717 1717 if ui:
1718 1718 tree = findaliases(ui, tree)
1719 1719 weight, tree = optimize(tree, True)
1720 1720 def mfunc(repo, subset):
1721 1721 return getset(repo, subset, tree)
1722 1722 return mfunc
1723 1723
1724 1724 def formatspec(expr, *args):
1725 1725 '''
1726 1726 This is a convenience function for using revsets internally, and
1727 1727 escapes arguments appropriately. Aliases are intentionally ignored
1728 1728 so that intended expression behavior isn't accidentally subverted.
1729 1729
1730 1730 Supported arguments:
1731 1731
1732 1732 %r = revset expression, parenthesized
1733 1733 %d = int(arg), no quoting
1734 1734 %s = string(arg), escaped and single-quoted
1735 1735 %b = arg.branch(), escaped and single-quoted
1736 1736 %n = hex(arg), single-quoted
1737 1737 %% = a literal '%'
1738 1738
1739 1739 Prefixing the type with 'l' specifies a parenthesized list of that type.
1740 1740
1741 1741 >>> formatspec('%r:: and %lr', '10 or 11', ("this()", "that()"))
1742 1742 '(10 or 11):: and ((this()) or (that()))'
1743 1743 >>> formatspec('%d:: and not %d::', 10, 20)
1744 1744 '10:: and not 20::'
1745 1745 >>> formatspec('%ld or %ld', [], [1])
1746 1746 "_list('') or 1"
1747 1747 >>> formatspec('keyword(%s)', 'foo\\xe9')
1748 1748 "keyword('foo\\\\xe9')"
1749 1749 >>> b = lambda: 'default'
1750 1750 >>> b.branch = b
1751 1751 >>> formatspec('branch(%b)', b)
1752 1752 "branch('default')"
1753 1753 >>> formatspec('root(%ls)', ['a', 'b', 'c', 'd'])
1754 1754 "root(_list('a\\x00b\\x00c\\x00d'))"
1755 1755 '''
1756 1756
1757 1757 def quote(s):
1758 1758 return repr(str(s))
1759 1759
1760 1760 def argtype(c, arg):
1761 1761 if c == 'd':
1762 1762 return str(int(arg))
1763 1763 elif c == 's':
1764 1764 return quote(arg)
1765 1765 elif c == 'r':
1766 1766 parse(arg) # make sure syntax errors are confined
1767 1767 return '(%s)' % arg
1768 1768 elif c == 'n':
1769 1769 return quote(node.hex(arg))
1770 1770 elif c == 'b':
1771 1771 return quote(arg.branch())
1772 1772
1773 1773 def listexp(s, t):
1774 1774 l = len(s)
1775 1775 if l == 0:
1776 1776 return "_list('')"
1777 1777 elif l == 1:
1778 1778 return argtype(t, s[0])
1779 1779 elif t == 'd':
1780 1780 return "_list('%s')" % "\0".join(str(int(a)) for a in s)
1781 1781 elif t == 's':
1782 1782 return "_list('%s')" % "\0".join(s)
1783 1783 elif t == 'n':
1784 1784 return "_list('%s')" % "\0".join(node.hex(a) for a in s)
1785 1785 elif t == 'b':
1786 1786 return "_list('%s')" % "\0".join(a.branch() for a in s)
1787 1787
1788 1788 m = l // 2
1789 1789 return '(%s or %s)' % (listexp(s[:m], t), listexp(s[m:], t))
1790 1790
1791 1791 ret = ''
1792 1792 pos = 0
1793 1793 arg = 0
1794 1794 while pos < len(expr):
1795 1795 c = expr[pos]
1796 1796 if c == '%':
1797 1797 pos += 1
1798 1798 d = expr[pos]
1799 1799 if d == '%':
1800 1800 ret += d
1801 1801 elif d in 'dsnbr':
1802 1802 ret += argtype(d, args[arg])
1803 1803 arg += 1
1804 1804 elif d == 'l':
1805 1805 # a list of some type
1806 1806 pos += 1
1807 1807 d = expr[pos]
1808 1808 ret += listexp(list(args[arg]), d)
1809 1809 arg += 1
1810 1810 else:
1811 1811 raise util.Abort('unexpected revspec format character %s' % d)
1812 1812 else:
1813 1813 ret += c
1814 1814 pos += 1
1815 1815
1816 1816 return ret
1817 1817
1818 1818 def prettyformat(tree):
1819 1819 def _prettyformat(tree, level, lines):
1820 1820 if not isinstance(tree, tuple) or tree[0] in ('string', 'symbol'):
1821 1821 lines.append((level, str(tree)))
1822 1822 else:
1823 1823 lines.append((level, '(%s' % tree[0]))
1824 1824 for s in tree[1:]:
1825 1825 _prettyformat(s, level + 1, lines)
1826 1826 lines[-1:] = [(lines[-1][0], lines[-1][1] + ')')]
1827 1827
1828 1828 lines = []
1829 1829 _prettyformat(tree, 0, lines)
1830 1830 output = '\n'.join((' '*l + s) for l, s in lines)
1831 1831 return output
1832 1832
1833 1833 # tell hggettext to extract docstrings from these functions:
1834 1834 i18nfunctions = symbols.values()
General Comments 0
You need to be logged in to leave comments. Login now