##// END OF EJS Templates
revset: balance %l or-expressions (issue3129)
Matt Mackall -
r15595:a585d78e stable
parent child Browse files
Show More
@@ -1,1135 +1,1141
1 # revset.py - revision set queries for mercurial
1 # revset.py - revision set queries for mercurial
2 #
2 #
3 # Copyright 2010 Matt Mackall <mpm@selenic.com>
3 # Copyright 2010 Matt Mackall <mpm@selenic.com>
4 #
4 #
5 # This software may be used and distributed according to the terms of the
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2 or any later version.
6 # GNU General Public License version 2 or any later version.
7
7
8 import re
8 import re
9 import parser, util, error, discovery, hbisect
9 import parser, util, error, discovery, hbisect
10 import node as nodemod
10 import node as nodemod
11 import bookmarks as bookmarksmod
11 import bookmarks as bookmarksmod
12 import match as matchmod
12 import match as matchmod
13 from i18n import _
13 from i18n import _
14
14
15 elements = {
15 elements = {
16 "(": (20, ("group", 1, ")"), ("func", 1, ")")),
16 "(": (20, ("group", 1, ")"), ("func", 1, ")")),
17 "~": (18, None, ("ancestor", 18)),
17 "~": (18, None, ("ancestor", 18)),
18 "^": (18, None, ("parent", 18), ("parentpost", 18)),
18 "^": (18, None, ("parent", 18), ("parentpost", 18)),
19 "-": (5, ("negate", 19), ("minus", 5)),
19 "-": (5, ("negate", 19), ("minus", 5)),
20 "::": (17, ("dagrangepre", 17), ("dagrange", 17),
20 "::": (17, ("dagrangepre", 17), ("dagrange", 17),
21 ("dagrangepost", 17)),
21 ("dagrangepost", 17)),
22 "..": (17, ("dagrangepre", 17), ("dagrange", 17),
22 "..": (17, ("dagrangepre", 17), ("dagrange", 17),
23 ("dagrangepost", 17)),
23 ("dagrangepost", 17)),
24 ":": (15, ("rangepre", 15), ("range", 15), ("rangepost", 15)),
24 ":": (15, ("rangepre", 15), ("range", 15), ("rangepost", 15)),
25 "not": (10, ("not", 10)),
25 "not": (10, ("not", 10)),
26 "!": (10, ("not", 10)),
26 "!": (10, ("not", 10)),
27 "and": (5, None, ("and", 5)),
27 "and": (5, None, ("and", 5)),
28 "&": (5, None, ("and", 5)),
28 "&": (5, None, ("and", 5)),
29 "or": (4, None, ("or", 4)),
29 "or": (4, None, ("or", 4)),
30 "|": (4, None, ("or", 4)),
30 "|": (4, None, ("or", 4)),
31 "+": (4, None, ("or", 4)),
31 "+": (4, None, ("or", 4)),
32 ",": (2, None, ("list", 2)),
32 ",": (2, None, ("list", 2)),
33 ")": (0, None, None),
33 ")": (0, None, None),
34 "symbol": (0, ("symbol",), None),
34 "symbol": (0, ("symbol",), None),
35 "string": (0, ("string",), None),
35 "string": (0, ("string",), None),
36 "end": (0, None, None),
36 "end": (0, None, None),
37 }
37 }
38
38
39 keywords = set(['and', 'or', 'not'])
39 keywords = set(['and', 'or', 'not'])
40
40
41 def tokenize(program):
41 def tokenize(program):
42 pos, l = 0, len(program)
42 pos, l = 0, len(program)
43 while pos < l:
43 while pos < l:
44 c = program[pos]
44 c = program[pos]
45 if c.isspace(): # skip inter-token whitespace
45 if c.isspace(): # skip inter-token whitespace
46 pass
46 pass
47 elif c == ':' and program[pos:pos + 2] == '::': # look ahead carefully
47 elif c == ':' and program[pos:pos + 2] == '::': # look ahead carefully
48 yield ('::', None, pos)
48 yield ('::', None, pos)
49 pos += 1 # skip ahead
49 pos += 1 # skip ahead
50 elif c == '.' and program[pos:pos + 2] == '..': # look ahead carefully
50 elif c == '.' and program[pos:pos + 2] == '..': # look ahead carefully
51 yield ('..', None, pos)
51 yield ('..', None, pos)
52 pos += 1 # skip ahead
52 pos += 1 # skip ahead
53 elif c in "():,-|&+!~^": # handle simple operators
53 elif c in "():,-|&+!~^": # handle simple operators
54 yield (c, None, pos)
54 yield (c, None, pos)
55 elif (c in '"\'' or c == 'r' and
55 elif (c in '"\'' or c == 'r' and
56 program[pos:pos + 2] in ("r'", 'r"')): # handle quoted strings
56 program[pos:pos + 2] in ("r'", 'r"')): # handle quoted strings
57 if c == 'r':
57 if c == 'r':
58 pos += 1
58 pos += 1
59 c = program[pos]
59 c = program[pos]
60 decode = lambda x: x
60 decode = lambda x: x
61 else:
61 else:
62 decode = lambda x: x.decode('string-escape')
62 decode = lambda x: x.decode('string-escape')
63 pos += 1
63 pos += 1
64 s = pos
64 s = pos
65 while pos < l: # find closing quote
65 while pos < l: # find closing quote
66 d = program[pos]
66 d = program[pos]
67 if d == '\\': # skip over escaped characters
67 if d == '\\': # skip over escaped characters
68 pos += 2
68 pos += 2
69 continue
69 continue
70 if d == c:
70 if d == c:
71 yield ('string', decode(program[s:pos]), s)
71 yield ('string', decode(program[s:pos]), s)
72 break
72 break
73 pos += 1
73 pos += 1
74 else:
74 else:
75 raise error.ParseError(_("unterminated string"), s)
75 raise error.ParseError(_("unterminated string"), s)
76 elif c.isalnum() or c in '._' or ord(c) > 127: # gather up a symbol/keyword
76 elif c.isalnum() or c in '._' or ord(c) > 127: # gather up a symbol/keyword
77 s = pos
77 s = pos
78 pos += 1
78 pos += 1
79 while pos < l: # find end of symbol
79 while pos < l: # find end of symbol
80 d = program[pos]
80 d = program[pos]
81 if not (d.isalnum() or d in "._" or ord(d) > 127):
81 if not (d.isalnum() or d in "._" or ord(d) > 127):
82 break
82 break
83 if d == '.' and program[pos - 1] == '.': # special case for ..
83 if d == '.' and program[pos - 1] == '.': # special case for ..
84 pos -= 1
84 pos -= 1
85 break
85 break
86 pos += 1
86 pos += 1
87 sym = program[s:pos]
87 sym = program[s:pos]
88 if sym in keywords: # operator keywords
88 if sym in keywords: # operator keywords
89 yield (sym, None, s)
89 yield (sym, None, s)
90 else:
90 else:
91 yield ('symbol', sym, s)
91 yield ('symbol', sym, s)
92 pos -= 1
92 pos -= 1
93 else:
93 else:
94 raise error.ParseError(_("syntax error"), pos)
94 raise error.ParseError(_("syntax error"), pos)
95 pos += 1
95 pos += 1
96 yield ('end', None, pos)
96 yield ('end', None, pos)
97
97
98 # helpers
98 # helpers
99
99
100 def getstring(x, err):
100 def getstring(x, err):
101 if x and (x[0] == 'string' or x[0] == 'symbol'):
101 if x and (x[0] == 'string' or x[0] == 'symbol'):
102 return x[1]
102 return x[1]
103 raise error.ParseError(err)
103 raise error.ParseError(err)
104
104
105 def getlist(x):
105 def getlist(x):
106 if not x:
106 if not x:
107 return []
107 return []
108 if x[0] == 'list':
108 if x[0] == 'list':
109 return getlist(x[1]) + [x[2]]
109 return getlist(x[1]) + [x[2]]
110 return [x]
110 return [x]
111
111
112 def getargs(x, min, max, err):
112 def getargs(x, min, max, err):
113 l = getlist(x)
113 l = getlist(x)
114 if len(l) < min or len(l) > max:
114 if len(l) < min or len(l) > max:
115 raise error.ParseError(err)
115 raise error.ParseError(err)
116 return l
116 return l
117
117
118 def getset(repo, subset, x):
118 def getset(repo, subset, x):
119 if not x:
119 if not x:
120 raise error.ParseError(_("missing argument"))
120 raise error.ParseError(_("missing argument"))
121 return methods[x[0]](repo, subset, *x[1:])
121 return methods[x[0]](repo, subset, *x[1:])
122
122
123 # operator methods
123 # operator methods
124
124
125 def stringset(repo, subset, x):
125 def stringset(repo, subset, x):
126 x = repo[x].rev()
126 x = repo[x].rev()
127 if x == -1 and len(subset) == len(repo):
127 if x == -1 and len(subset) == len(repo):
128 return [-1]
128 return [-1]
129 if len(subset) == len(repo) or x in subset:
129 if len(subset) == len(repo) or x in subset:
130 return [x]
130 return [x]
131 return []
131 return []
132
132
133 def symbolset(repo, subset, x):
133 def symbolset(repo, subset, x):
134 if x in symbols:
134 if x in symbols:
135 raise error.ParseError(_("can't use %s here") % x)
135 raise error.ParseError(_("can't use %s here") % x)
136 return stringset(repo, subset, x)
136 return stringset(repo, subset, x)
137
137
138 def rangeset(repo, subset, x, y):
138 def rangeset(repo, subset, x, y):
139 m = getset(repo, subset, x)
139 m = getset(repo, subset, x)
140 if not m:
140 if not m:
141 m = getset(repo, range(len(repo)), x)
141 m = getset(repo, range(len(repo)), x)
142
142
143 n = getset(repo, subset, y)
143 n = getset(repo, subset, y)
144 if not n:
144 if not n:
145 n = getset(repo, range(len(repo)), y)
145 n = getset(repo, range(len(repo)), y)
146
146
147 if not m or not n:
147 if not m or not n:
148 return []
148 return []
149 m, n = m[0], n[-1]
149 m, n = m[0], n[-1]
150
150
151 if m < n:
151 if m < n:
152 r = range(m, n + 1)
152 r = range(m, n + 1)
153 else:
153 else:
154 r = range(m, n - 1, -1)
154 r = range(m, n - 1, -1)
155 s = set(subset)
155 s = set(subset)
156 return [x for x in r if x in s]
156 return [x for x in r if x in s]
157
157
158 def andset(repo, subset, x, y):
158 def andset(repo, subset, x, y):
159 return getset(repo, getset(repo, subset, x), y)
159 return getset(repo, getset(repo, subset, x), y)
160
160
161 def orset(repo, subset, x, y):
161 def orset(repo, subset, x, y):
162 xl = getset(repo, subset, x)
162 xl = getset(repo, subset, x)
163 s = set(xl)
163 s = set(xl)
164 yl = getset(repo, [r for r in subset if r not in s], y)
164 yl = getset(repo, [r for r in subset if r not in s], y)
165 return xl + yl
165 return xl + yl
166
166
167 def notset(repo, subset, x):
167 def notset(repo, subset, x):
168 s = set(getset(repo, subset, x))
168 s = set(getset(repo, subset, x))
169 return [r for r in subset if r not in s]
169 return [r for r in subset if r not in s]
170
170
171 def listset(repo, subset, a, b):
171 def listset(repo, subset, a, b):
172 raise error.ParseError(_("can't use a list in this context"))
172 raise error.ParseError(_("can't use a list in this context"))
173
173
174 def func(repo, subset, a, b):
174 def func(repo, subset, a, b):
175 if a[0] == 'symbol' and a[1] in symbols:
175 if a[0] == 'symbol' and a[1] in symbols:
176 return symbols[a[1]](repo, subset, b)
176 return symbols[a[1]](repo, subset, b)
177 raise error.ParseError(_("not a function: %s") % a[1])
177 raise error.ParseError(_("not a function: %s") % a[1])
178
178
179 # functions
179 # functions
180
180
181 def adds(repo, subset, x):
181 def adds(repo, subset, x):
182 """``adds(pattern)``
182 """``adds(pattern)``
183 Changesets that add a file matching pattern.
183 Changesets that add a file matching pattern.
184 """
184 """
185 # i18n: "adds" is a keyword
185 # i18n: "adds" is a keyword
186 pat = getstring(x, _("adds requires a pattern"))
186 pat = getstring(x, _("adds requires a pattern"))
187 return checkstatus(repo, subset, pat, 1)
187 return checkstatus(repo, subset, pat, 1)
188
188
189 def ancestor(repo, subset, x):
189 def ancestor(repo, subset, x):
190 """``ancestor(single, single)``
190 """``ancestor(single, single)``
191 Greatest common ancestor of the two changesets.
191 Greatest common ancestor of the two changesets.
192 """
192 """
193 # i18n: "ancestor" is a keyword
193 # i18n: "ancestor" is a keyword
194 l = getargs(x, 2, 2, _("ancestor requires two arguments"))
194 l = getargs(x, 2, 2, _("ancestor requires two arguments"))
195 r = range(len(repo))
195 r = range(len(repo))
196 a = getset(repo, r, l[0])
196 a = getset(repo, r, l[0])
197 b = getset(repo, r, l[1])
197 b = getset(repo, r, l[1])
198 if len(a) != 1 or len(b) != 1:
198 if len(a) != 1 or len(b) != 1:
199 # i18n: "ancestor" is a keyword
199 # i18n: "ancestor" is a keyword
200 raise error.ParseError(_("ancestor arguments must be single revisions"))
200 raise error.ParseError(_("ancestor arguments must be single revisions"))
201 an = [repo[a[0]].ancestor(repo[b[0]]).rev()]
201 an = [repo[a[0]].ancestor(repo[b[0]]).rev()]
202
202
203 return [r for r in an if r in subset]
203 return [r for r in an if r in subset]
204
204
205 def ancestors(repo, subset, x):
205 def ancestors(repo, subset, x):
206 """``ancestors(set)``
206 """``ancestors(set)``
207 Changesets that are ancestors of a changeset in set.
207 Changesets that are ancestors of a changeset in set.
208 """
208 """
209 args = getset(repo, range(len(repo)), x)
209 args = getset(repo, range(len(repo)), x)
210 if not args:
210 if not args:
211 return []
211 return []
212 s = set(repo.changelog.ancestors(*args)) | set(args)
212 s = set(repo.changelog.ancestors(*args)) | set(args)
213 return [r for r in subset if r in s]
213 return [r for r in subset if r in s]
214
214
215 def ancestorspec(repo, subset, x, n):
215 def ancestorspec(repo, subset, x, n):
216 """``set~n``
216 """``set~n``
217 Changesets that are the Nth ancestor (first parents only) of a changeset in set.
217 Changesets that are the Nth ancestor (first parents only) of a changeset in set.
218 """
218 """
219 try:
219 try:
220 n = int(n[1])
220 n = int(n[1])
221 except (TypeError, ValueError):
221 except (TypeError, ValueError):
222 raise error.ParseError(_("~ expects a number"))
222 raise error.ParseError(_("~ expects a number"))
223 ps = set()
223 ps = set()
224 cl = repo.changelog
224 cl = repo.changelog
225 for r in getset(repo, subset, x):
225 for r in getset(repo, subset, x):
226 for i in range(n):
226 for i in range(n):
227 r = cl.parentrevs(r)[0]
227 r = cl.parentrevs(r)[0]
228 ps.add(r)
228 ps.add(r)
229 return [r for r in subset if r in ps]
229 return [r for r in subset if r in ps]
230
230
231 def author(repo, subset, x):
231 def author(repo, subset, x):
232 """``author(string)``
232 """``author(string)``
233 Alias for ``user(string)``.
233 Alias for ``user(string)``.
234 """
234 """
235 # i18n: "author" is a keyword
235 # i18n: "author" is a keyword
236 n = getstring(x, _("author requires a string")).lower()
236 n = getstring(x, _("author requires a string")).lower()
237 return [r for r in subset if n in repo[r].user().lower()]
237 return [r for r in subset if n in repo[r].user().lower()]
238
238
239 def bisect(repo, subset, x):
239 def bisect(repo, subset, x):
240 """``bisect(string)``
240 """``bisect(string)``
241 Changesets marked in the specified bisect status:
241 Changesets marked in the specified bisect status:
242
242
243 - ``good``, ``bad``, ``skip``: csets explicitly marked as good/bad/skip
243 - ``good``, ``bad``, ``skip``: csets explicitly marked as good/bad/skip
244 - ``goods``, ``bads`` : csets topologicaly good/bad
244 - ``goods``, ``bads`` : csets topologicaly good/bad
245 - ``range`` : csets taking part in the bisection
245 - ``range`` : csets taking part in the bisection
246 - ``pruned`` : csets that are goods, bads or skipped
246 - ``pruned`` : csets that are goods, bads or skipped
247 - ``untested`` : csets whose fate is yet unknown
247 - ``untested`` : csets whose fate is yet unknown
248 - ``ignored`` : csets ignored due to DAG topology
248 - ``ignored`` : csets ignored due to DAG topology
249 """
249 """
250 status = getstring(x, _("bisect requires a string")).lower()
250 status = getstring(x, _("bisect requires a string")).lower()
251 return [r for r in subset if r in hbisect.get(repo, status)]
251 return [r for r in subset if r in hbisect.get(repo, status)]
252
252
253 # Backward-compatibility
253 # Backward-compatibility
254 # - no help entry so that we do not advertise it any more
254 # - no help entry so that we do not advertise it any more
255 def bisected(repo, subset, x):
255 def bisected(repo, subset, x):
256 return bisect(repo, subset, x)
256 return bisect(repo, subset, x)
257
257
258 def bookmark(repo, subset, x):
258 def bookmark(repo, subset, x):
259 """``bookmark([name])``
259 """``bookmark([name])``
260 The named bookmark or all bookmarks.
260 The named bookmark or all bookmarks.
261 """
261 """
262 # i18n: "bookmark" is a keyword
262 # i18n: "bookmark" is a keyword
263 args = getargs(x, 0, 1, _('bookmark takes one or no arguments'))
263 args = getargs(x, 0, 1, _('bookmark takes one or no arguments'))
264 if args:
264 if args:
265 bm = getstring(args[0],
265 bm = getstring(args[0],
266 # i18n: "bookmark" is a keyword
266 # i18n: "bookmark" is a keyword
267 _('the argument to bookmark must be a string'))
267 _('the argument to bookmark must be a string'))
268 bmrev = bookmarksmod.listbookmarks(repo).get(bm, None)
268 bmrev = bookmarksmod.listbookmarks(repo).get(bm, None)
269 if not bmrev:
269 if not bmrev:
270 raise util.Abort(_("bookmark '%s' does not exist") % bm)
270 raise util.Abort(_("bookmark '%s' does not exist") % bm)
271 bmrev = repo[bmrev].rev()
271 bmrev = repo[bmrev].rev()
272 return [r for r in subset if r == bmrev]
272 return [r for r in subset if r == bmrev]
273 bms = set([repo[r].rev()
273 bms = set([repo[r].rev()
274 for r in bookmarksmod.listbookmarks(repo).values()])
274 for r in bookmarksmod.listbookmarks(repo).values()])
275 return [r for r in subset if r in bms]
275 return [r for r in subset if r in bms]
276
276
277 def branch(repo, subset, x):
277 def branch(repo, subset, x):
278 """``branch(string or set)``
278 """``branch(string or set)``
279 All changesets belonging to the given branch or the branches of the given
279 All changesets belonging to the given branch or the branches of the given
280 changesets.
280 changesets.
281 """
281 """
282 try:
282 try:
283 b = getstring(x, '')
283 b = getstring(x, '')
284 if b in repo.branchmap():
284 if b in repo.branchmap():
285 return [r for r in subset if repo[r].branch() == b]
285 return [r for r in subset if repo[r].branch() == b]
286 except error.ParseError:
286 except error.ParseError:
287 # not a string, but another revspec, e.g. tip()
287 # not a string, but another revspec, e.g. tip()
288 pass
288 pass
289
289
290 s = getset(repo, range(len(repo)), x)
290 s = getset(repo, range(len(repo)), x)
291 b = set()
291 b = set()
292 for r in s:
292 for r in s:
293 b.add(repo[r].branch())
293 b.add(repo[r].branch())
294 s = set(s)
294 s = set(s)
295 return [r for r in subset if r in s or repo[r].branch() in b]
295 return [r for r in subset if r in s or repo[r].branch() in b]
296
296
297 def checkstatus(repo, subset, pat, field):
297 def checkstatus(repo, subset, pat, field):
298 m = matchmod.match(repo.root, repo.getcwd(), [pat])
298 m = matchmod.match(repo.root, repo.getcwd(), [pat])
299 s = []
299 s = []
300 fast = (m.files() == [pat])
300 fast = (m.files() == [pat])
301 for r in subset:
301 for r in subset:
302 c = repo[r]
302 c = repo[r]
303 if fast:
303 if fast:
304 if pat not in c.files():
304 if pat not in c.files():
305 continue
305 continue
306 else:
306 else:
307 for f in c.files():
307 for f in c.files():
308 if m(f):
308 if m(f):
309 break
309 break
310 else:
310 else:
311 continue
311 continue
312 files = repo.status(c.p1().node(), c.node())[field]
312 files = repo.status(c.p1().node(), c.node())[field]
313 if fast:
313 if fast:
314 if pat in files:
314 if pat in files:
315 s.append(r)
315 s.append(r)
316 else:
316 else:
317 for f in files:
317 for f in files:
318 if m(f):
318 if m(f):
319 s.append(r)
319 s.append(r)
320 break
320 break
321 return s
321 return s
322
322
323 def children(repo, subset, x):
323 def children(repo, subset, x):
324 """``children(set)``
324 """``children(set)``
325 Child changesets of changesets in set.
325 Child changesets of changesets in set.
326 """
326 """
327 cs = set()
327 cs = set()
328 cl = repo.changelog
328 cl = repo.changelog
329 s = set(getset(repo, range(len(repo)), x))
329 s = set(getset(repo, range(len(repo)), x))
330 for r in xrange(0, len(repo)):
330 for r in xrange(0, len(repo)):
331 for p in cl.parentrevs(r):
331 for p in cl.parentrevs(r):
332 if p in s:
332 if p in s:
333 cs.add(r)
333 cs.add(r)
334 return [r for r in subset if r in cs]
334 return [r for r in subset if r in cs]
335
335
336 def closed(repo, subset, x):
336 def closed(repo, subset, x):
337 """``closed()``
337 """``closed()``
338 Changeset is closed.
338 Changeset is closed.
339 """
339 """
340 # i18n: "closed" is a keyword
340 # i18n: "closed" is a keyword
341 getargs(x, 0, 0, _("closed takes no arguments"))
341 getargs(x, 0, 0, _("closed takes no arguments"))
342 return [r for r in subset if repo[r].extra().get('close')]
342 return [r for r in subset if repo[r].extra().get('close')]
343
343
344 def contains(repo, subset, x):
344 def contains(repo, subset, x):
345 """``contains(pattern)``
345 """``contains(pattern)``
346 Revision contains a file matching pattern. See :hg:`help patterns`
346 Revision contains a file matching pattern. See :hg:`help patterns`
347 for information about file patterns.
347 for information about file patterns.
348 """
348 """
349 # i18n: "contains" is a keyword
349 # i18n: "contains" is a keyword
350 pat = getstring(x, _("contains requires a pattern"))
350 pat = getstring(x, _("contains requires a pattern"))
351 m = matchmod.match(repo.root, repo.getcwd(), [pat])
351 m = matchmod.match(repo.root, repo.getcwd(), [pat])
352 s = []
352 s = []
353 if m.files() == [pat]:
353 if m.files() == [pat]:
354 for r in subset:
354 for r in subset:
355 if pat in repo[r]:
355 if pat in repo[r]:
356 s.append(r)
356 s.append(r)
357 else:
357 else:
358 for r in subset:
358 for r in subset:
359 for f in repo[r].manifest():
359 for f in repo[r].manifest():
360 if m(f):
360 if m(f):
361 s.append(r)
361 s.append(r)
362 break
362 break
363 return s
363 return s
364
364
365 def date(repo, subset, x):
365 def date(repo, subset, x):
366 """``date(interval)``
366 """``date(interval)``
367 Changesets within the interval, see :hg:`help dates`.
367 Changesets within the interval, see :hg:`help dates`.
368 """
368 """
369 # i18n: "date" is a keyword
369 # i18n: "date" is a keyword
370 ds = getstring(x, _("date requires a string"))
370 ds = getstring(x, _("date requires a string"))
371 dm = util.matchdate(ds)
371 dm = util.matchdate(ds)
372 return [r for r in subset if dm(repo[r].date()[0])]
372 return [r for r in subset if dm(repo[r].date()[0])]
373
373
374 def desc(repo, subset, x):
374 def desc(repo, subset, x):
375 """``desc(string)``
375 """``desc(string)``
376 Search commit message for string. The match is case-insensitive.
376 Search commit message for string. The match is case-insensitive.
377 """
377 """
378 # i18n: "desc" is a keyword
378 # i18n: "desc" is a keyword
379 ds = getstring(x, _("desc requires a string")).lower()
379 ds = getstring(x, _("desc requires a string")).lower()
380 l = []
380 l = []
381 for r in subset:
381 for r in subset:
382 c = repo[r]
382 c = repo[r]
383 if ds in c.description().lower():
383 if ds in c.description().lower():
384 l.append(r)
384 l.append(r)
385 return l
385 return l
386
386
387 def descendants(repo, subset, x):
387 def descendants(repo, subset, x):
388 """``descendants(set)``
388 """``descendants(set)``
389 Changesets which are descendants of changesets in set.
389 Changesets which are descendants of changesets in set.
390 """
390 """
391 args = getset(repo, range(len(repo)), x)
391 args = getset(repo, range(len(repo)), x)
392 if not args:
392 if not args:
393 return []
393 return []
394 s = set(repo.changelog.descendants(*args)) | set(args)
394 s = set(repo.changelog.descendants(*args)) | set(args)
395 return [r for r in subset if r in s]
395 return [r for r in subset if r in s]
396
396
397 def filelog(repo, subset, x):
397 def filelog(repo, subset, x):
398 """``filelog(pattern)``
398 """``filelog(pattern)``
399 Changesets connected to the specified filelog.
399 Changesets connected to the specified filelog.
400 """
400 """
401
401
402 pat = getstring(x, _("filelog requires a pattern"))
402 pat = getstring(x, _("filelog requires a pattern"))
403 m = matchmod.match(repo.root, repo.getcwd(), [pat], default='relpath')
403 m = matchmod.match(repo.root, repo.getcwd(), [pat], default='relpath')
404 s = set()
404 s = set()
405
405
406 if not m.anypats():
406 if not m.anypats():
407 for f in m.files():
407 for f in m.files():
408 fl = repo.file(f)
408 fl = repo.file(f)
409 for fr in fl:
409 for fr in fl:
410 s.add(fl.linkrev(fr))
410 s.add(fl.linkrev(fr))
411 else:
411 else:
412 for f in repo[None]:
412 for f in repo[None]:
413 if m(f):
413 if m(f):
414 fl = repo.file(f)
414 fl = repo.file(f)
415 for fr in fl:
415 for fr in fl:
416 s.add(fl.linkrev(fr))
416 s.add(fl.linkrev(fr))
417
417
418 return [r for r in subset if r in s]
418 return [r for r in subset if r in s]
419
419
420 def first(repo, subset, x):
420 def first(repo, subset, x):
421 """``first(set, [n])``
421 """``first(set, [n])``
422 An alias for limit().
422 An alias for limit().
423 """
423 """
424 return limit(repo, subset, x)
424 return limit(repo, subset, x)
425
425
426 def follow(repo, subset, x):
426 def follow(repo, subset, x):
427 """``follow([file])``
427 """``follow([file])``
428 An alias for ``::.`` (ancestors of the working copy's first parent).
428 An alias for ``::.`` (ancestors of the working copy's first parent).
429 If a filename is specified, the history of the given file is followed,
429 If a filename is specified, the history of the given file is followed,
430 including copies.
430 including copies.
431 """
431 """
432 # i18n: "follow" is a keyword
432 # i18n: "follow" is a keyword
433 l = getargs(x, 0, 1, _("follow takes no arguments or a filename"))
433 l = getargs(x, 0, 1, _("follow takes no arguments or a filename"))
434 p = repo['.'].rev()
434 p = repo['.'].rev()
435 if l:
435 if l:
436 x = getstring(l[0], _("follow expected a filename"))
436 x = getstring(l[0], _("follow expected a filename"))
437 if x in repo['.']:
437 if x in repo['.']:
438 s = set(ctx.rev() for ctx in repo['.'][x].ancestors())
438 s = set(ctx.rev() for ctx in repo['.'][x].ancestors())
439 else:
439 else:
440 return []
440 return []
441 else:
441 else:
442 s = set(repo.changelog.ancestors(p))
442 s = set(repo.changelog.ancestors(p))
443
443
444 s |= set([p])
444 s |= set([p])
445 return [r for r in subset if r in s]
445 return [r for r in subset if r in s]
446
446
447 def followfile(repo, subset, x):
447 def followfile(repo, subset, x):
448 """``follow()``
448 """``follow()``
449 An alias for ``::.`` (ancestors of the working copy's first parent).
449 An alias for ``::.`` (ancestors of the working copy's first parent).
450 """
450 """
451 # i18n: "follow" is a keyword
451 # i18n: "follow" is a keyword
452 getargs(x, 0, 0, _("follow takes no arguments"))
452 getargs(x, 0, 0, _("follow takes no arguments"))
453 p = repo['.'].rev()
453 p = repo['.'].rev()
454 s = set(repo.changelog.ancestors(p)) | set([p])
454 s = set(repo.changelog.ancestors(p)) | set([p])
455 return [r for r in subset if r in s]
455 return [r for r in subset if r in s]
456
456
457 def getall(repo, subset, x):
457 def getall(repo, subset, x):
458 """``all()``
458 """``all()``
459 All changesets, the same as ``0:tip``.
459 All changesets, the same as ``0:tip``.
460 """
460 """
461 # i18n: "all" is a keyword
461 # i18n: "all" is a keyword
462 getargs(x, 0, 0, _("all takes no arguments"))
462 getargs(x, 0, 0, _("all takes no arguments"))
463 return subset
463 return subset
464
464
465 def grep(repo, subset, x):
465 def grep(repo, subset, x):
466 """``grep(regex)``
466 """``grep(regex)``
467 Like ``keyword(string)`` but accepts a regex. Use ``grep(r'...')``
467 Like ``keyword(string)`` but accepts a regex. Use ``grep(r'...')``
468 to ensure special escape characters are handled correctly. Unlike
468 to ensure special escape characters are handled correctly. Unlike
469 ``keyword(string)``, the match is case-sensitive.
469 ``keyword(string)``, the match is case-sensitive.
470 """
470 """
471 try:
471 try:
472 # i18n: "grep" is a keyword
472 # i18n: "grep" is a keyword
473 gr = re.compile(getstring(x, _("grep requires a string")))
473 gr = re.compile(getstring(x, _("grep requires a string")))
474 except re.error, e:
474 except re.error, e:
475 raise error.ParseError(_('invalid match pattern: %s') % e)
475 raise error.ParseError(_('invalid match pattern: %s') % e)
476 l = []
476 l = []
477 for r in subset:
477 for r in subset:
478 c = repo[r]
478 c = repo[r]
479 for e in c.files() + [c.user(), c.description()]:
479 for e in c.files() + [c.user(), c.description()]:
480 if gr.search(e):
480 if gr.search(e):
481 l.append(r)
481 l.append(r)
482 break
482 break
483 return l
483 return l
484
484
485 def hasfile(repo, subset, x):
485 def hasfile(repo, subset, x):
486 """``file(pattern)``
486 """``file(pattern)``
487 Changesets affecting files matched by pattern.
487 Changesets affecting files matched by pattern.
488 """
488 """
489 # i18n: "file" is a keyword
489 # i18n: "file" is a keyword
490 pat = getstring(x, _("file requires a pattern"))
490 pat = getstring(x, _("file requires a pattern"))
491 m = matchmod.match(repo.root, repo.getcwd(), [pat])
491 m = matchmod.match(repo.root, repo.getcwd(), [pat])
492 s = []
492 s = []
493 for r in subset:
493 for r in subset:
494 for f in repo[r].files():
494 for f in repo[r].files():
495 if m(f):
495 if m(f):
496 s.append(r)
496 s.append(r)
497 break
497 break
498 return s
498 return s
499
499
500 def head(repo, subset, x):
500 def head(repo, subset, x):
501 """``head()``
501 """``head()``
502 Changeset is a named branch head.
502 Changeset is a named branch head.
503 """
503 """
504 # i18n: "head" is a keyword
504 # i18n: "head" is a keyword
505 getargs(x, 0, 0, _("head takes no arguments"))
505 getargs(x, 0, 0, _("head takes no arguments"))
506 hs = set()
506 hs = set()
507 for b, ls in repo.branchmap().iteritems():
507 for b, ls in repo.branchmap().iteritems():
508 hs.update(repo[h].rev() for h in ls)
508 hs.update(repo[h].rev() for h in ls)
509 return [r for r in subset if r in hs]
509 return [r for r in subset if r in hs]
510
510
511 def heads(repo, subset, x):
511 def heads(repo, subset, x):
512 """``heads(set)``
512 """``heads(set)``
513 Members of set with no children in set.
513 Members of set with no children in set.
514 """
514 """
515 s = getset(repo, subset, x)
515 s = getset(repo, subset, x)
516 ps = set(parents(repo, subset, x))
516 ps = set(parents(repo, subset, x))
517 return [r for r in s if r not in ps]
517 return [r for r in s if r not in ps]
518
518
519 def keyword(repo, subset, x):
519 def keyword(repo, subset, x):
520 """``keyword(string)``
520 """``keyword(string)``
521 Search commit message, user name, and names of changed files for
521 Search commit message, user name, and names of changed files for
522 string. The match is case-insensitive.
522 string. The match is case-insensitive.
523 """
523 """
524 # i18n: "keyword" is a keyword
524 # i18n: "keyword" is a keyword
525 kw = getstring(x, _("keyword requires a string")).lower()
525 kw = getstring(x, _("keyword requires a string")).lower()
526 l = []
526 l = []
527 for r in subset:
527 for r in subset:
528 c = repo[r]
528 c = repo[r]
529 t = " ".join(c.files() + [c.user(), c.description()])
529 t = " ".join(c.files() + [c.user(), c.description()])
530 if kw in t.lower():
530 if kw in t.lower():
531 l.append(r)
531 l.append(r)
532 return l
532 return l
533
533
534 def limit(repo, subset, x):
534 def limit(repo, subset, x):
535 """``limit(set, [n])``
535 """``limit(set, [n])``
536 First n members of set, defaulting to 1.
536 First n members of set, defaulting to 1.
537 """
537 """
538 # i18n: "limit" is a keyword
538 # i18n: "limit" is a keyword
539 l = getargs(x, 1, 2, _("limit requires one or two arguments"))
539 l = getargs(x, 1, 2, _("limit requires one or two arguments"))
540 try:
540 try:
541 lim = 1
541 lim = 1
542 if len(l) == 2:
542 if len(l) == 2:
543 # i18n: "limit" is a keyword
543 # i18n: "limit" is a keyword
544 lim = int(getstring(l[1], _("limit requires a number")))
544 lim = int(getstring(l[1], _("limit requires a number")))
545 except (TypeError, ValueError):
545 except (TypeError, ValueError):
546 # i18n: "limit" is a keyword
546 # i18n: "limit" is a keyword
547 raise error.ParseError(_("limit expects a number"))
547 raise error.ParseError(_("limit expects a number"))
548 ss = set(subset)
548 ss = set(subset)
549 os = getset(repo, range(len(repo)), l[0])[:lim]
549 os = getset(repo, range(len(repo)), l[0])[:lim]
550 return [r for r in os if r in ss]
550 return [r for r in os if r in ss]
551
551
552 def last(repo, subset, x):
552 def last(repo, subset, x):
553 """``last(set, [n])``
553 """``last(set, [n])``
554 Last n members of set, defaulting to 1.
554 Last n members of set, defaulting to 1.
555 """
555 """
556 # i18n: "last" is a keyword
556 # i18n: "last" is a keyword
557 l = getargs(x, 1, 2, _("last requires one or two arguments"))
557 l = getargs(x, 1, 2, _("last requires one or two arguments"))
558 try:
558 try:
559 lim = 1
559 lim = 1
560 if len(l) == 2:
560 if len(l) == 2:
561 # i18n: "last" is a keyword
561 # i18n: "last" is a keyword
562 lim = int(getstring(l[1], _("last requires a number")))
562 lim = int(getstring(l[1], _("last requires a number")))
563 except (TypeError, ValueError):
563 except (TypeError, ValueError):
564 # i18n: "last" is a keyword
564 # i18n: "last" is a keyword
565 raise error.ParseError(_("last expects a number"))
565 raise error.ParseError(_("last expects a number"))
566 ss = set(subset)
566 ss = set(subset)
567 os = getset(repo, range(len(repo)), l[0])[-lim:]
567 os = getset(repo, range(len(repo)), l[0])[-lim:]
568 return [r for r in os if r in ss]
568 return [r for r in os if r in ss]
569
569
570 def maxrev(repo, subset, x):
570 def maxrev(repo, subset, x):
571 """``max(set)``
571 """``max(set)``
572 Changeset with highest revision number in set.
572 Changeset with highest revision number in set.
573 """
573 """
574 os = getset(repo, range(len(repo)), x)
574 os = getset(repo, range(len(repo)), x)
575 if os:
575 if os:
576 m = max(os)
576 m = max(os)
577 if m in subset:
577 if m in subset:
578 return [m]
578 return [m]
579 return []
579 return []
580
580
581 def merge(repo, subset, x):
581 def merge(repo, subset, x):
582 """``merge()``
582 """``merge()``
583 Changeset is a merge changeset.
583 Changeset is a merge changeset.
584 """
584 """
585 # i18n: "merge" is a keyword
585 # i18n: "merge" is a keyword
586 getargs(x, 0, 0, _("merge takes no arguments"))
586 getargs(x, 0, 0, _("merge takes no arguments"))
587 cl = repo.changelog
587 cl = repo.changelog
588 return [r for r in subset if cl.parentrevs(r)[1] != -1]
588 return [r for r in subset if cl.parentrevs(r)[1] != -1]
589
589
590 def minrev(repo, subset, x):
590 def minrev(repo, subset, x):
591 """``min(set)``
591 """``min(set)``
592 Changeset with lowest revision number in set.
592 Changeset with lowest revision number in set.
593 """
593 """
594 os = getset(repo, range(len(repo)), x)
594 os = getset(repo, range(len(repo)), x)
595 if os:
595 if os:
596 m = min(os)
596 m = min(os)
597 if m in subset:
597 if m in subset:
598 return [m]
598 return [m]
599 return []
599 return []
600
600
601 def modifies(repo, subset, x):
601 def modifies(repo, subset, x):
602 """``modifies(pattern)``
602 """``modifies(pattern)``
603 Changesets modifying files matched by pattern.
603 Changesets modifying files matched by pattern.
604 """
604 """
605 # i18n: "modifies" is a keyword
605 # i18n: "modifies" is a keyword
606 pat = getstring(x, _("modifies requires a pattern"))
606 pat = getstring(x, _("modifies requires a pattern"))
607 return checkstatus(repo, subset, pat, 0)
607 return checkstatus(repo, subset, pat, 0)
608
608
609 def node(repo, subset, x):
609 def node(repo, subset, x):
610 """``id(string)``
610 """``id(string)``
611 Revision non-ambiguously specified by the given hex string prefix.
611 Revision non-ambiguously specified by the given hex string prefix.
612 """
612 """
613 # i18n: "id" is a keyword
613 # i18n: "id" is a keyword
614 l = getargs(x, 1, 1, _("id requires one argument"))
614 l = getargs(x, 1, 1, _("id requires one argument"))
615 # i18n: "id" is a keyword
615 # i18n: "id" is a keyword
616 n = getstring(l[0], _("id requires a string"))
616 n = getstring(l[0], _("id requires a string"))
617 if len(n) == 40:
617 if len(n) == 40:
618 rn = repo[n].rev()
618 rn = repo[n].rev()
619 else:
619 else:
620 rn = repo.changelog.rev(repo.changelog._partialmatch(n))
620 rn = repo.changelog.rev(repo.changelog._partialmatch(n))
621 return [r for r in subset if r == rn]
621 return [r for r in subset if r == rn]
622
622
623 def outgoing(repo, subset, x):
623 def outgoing(repo, subset, x):
624 """``outgoing([path])``
624 """``outgoing([path])``
625 Changesets not found in the specified destination repository, or the
625 Changesets not found in the specified destination repository, or the
626 default push location.
626 default push location.
627 """
627 """
628 import hg # avoid start-up nasties
628 import hg # avoid start-up nasties
629 # i18n: "outgoing" is a keyword
629 # i18n: "outgoing" is a keyword
630 l = getargs(x, 0, 1, _("outgoing takes one or no arguments"))
630 l = getargs(x, 0, 1, _("outgoing takes one or no arguments"))
631 # i18n: "outgoing" is a keyword
631 # i18n: "outgoing" is a keyword
632 dest = l and getstring(l[0], _("outgoing requires a repository path")) or ''
632 dest = l and getstring(l[0], _("outgoing requires a repository path")) or ''
633 dest = repo.ui.expandpath(dest or 'default-push', dest or 'default')
633 dest = repo.ui.expandpath(dest or 'default-push', dest or 'default')
634 dest, branches = hg.parseurl(dest)
634 dest, branches = hg.parseurl(dest)
635 revs, checkout = hg.addbranchrevs(repo, repo, branches, [])
635 revs, checkout = hg.addbranchrevs(repo, repo, branches, [])
636 if revs:
636 if revs:
637 revs = [repo.lookup(rev) for rev in revs]
637 revs = [repo.lookup(rev) for rev in revs]
638 other = hg.peer(repo, {}, dest)
638 other = hg.peer(repo, {}, dest)
639 repo.ui.pushbuffer()
639 repo.ui.pushbuffer()
640 common, outheads = discovery.findcommonoutgoing(repo, other, onlyheads=revs)
640 common, outheads = discovery.findcommonoutgoing(repo, other, onlyheads=revs)
641 repo.ui.popbuffer()
641 repo.ui.popbuffer()
642 cl = repo.changelog
642 cl = repo.changelog
643 o = set([cl.rev(r) for r in repo.changelog.findmissing(common, outheads)])
643 o = set([cl.rev(r) for r in repo.changelog.findmissing(common, outheads)])
644 return [r for r in subset if r in o]
644 return [r for r in subset if r in o]
645
645
646 def p1(repo, subset, x):
646 def p1(repo, subset, x):
647 """``p1([set])``
647 """``p1([set])``
648 First parent of changesets in set, or the working directory.
648 First parent of changesets in set, or the working directory.
649 """
649 """
650 if x is None:
650 if x is None:
651 p = repo[x].p1().rev()
651 p = repo[x].p1().rev()
652 return [r for r in subset if r == p]
652 return [r for r in subset if r == p]
653
653
654 ps = set()
654 ps = set()
655 cl = repo.changelog
655 cl = repo.changelog
656 for r in getset(repo, range(len(repo)), x):
656 for r in getset(repo, range(len(repo)), x):
657 ps.add(cl.parentrevs(r)[0])
657 ps.add(cl.parentrevs(r)[0])
658 return [r for r in subset if r in ps]
658 return [r for r in subset if r in ps]
659
659
660 def p2(repo, subset, x):
660 def p2(repo, subset, x):
661 """``p2([set])``
661 """``p2([set])``
662 Second parent of changesets in set, or the working directory.
662 Second parent of changesets in set, or the working directory.
663 """
663 """
664 if x is None:
664 if x is None:
665 ps = repo[x].parents()
665 ps = repo[x].parents()
666 try:
666 try:
667 p = ps[1].rev()
667 p = ps[1].rev()
668 return [r for r in subset if r == p]
668 return [r for r in subset if r == p]
669 except IndexError:
669 except IndexError:
670 return []
670 return []
671
671
672 ps = set()
672 ps = set()
673 cl = repo.changelog
673 cl = repo.changelog
674 for r in getset(repo, range(len(repo)), x):
674 for r in getset(repo, range(len(repo)), x):
675 ps.add(cl.parentrevs(r)[1])
675 ps.add(cl.parentrevs(r)[1])
676 return [r for r in subset if r in ps]
676 return [r for r in subset if r in ps]
677
677
678 def parents(repo, subset, x):
678 def parents(repo, subset, x):
679 """``parents([set])``
679 """``parents([set])``
680 The set of all parents for all changesets in set, or the working directory.
680 The set of all parents for all changesets in set, or the working directory.
681 """
681 """
682 if x is None:
682 if x is None:
683 ps = tuple(p.rev() for p in repo[x].parents())
683 ps = tuple(p.rev() for p in repo[x].parents())
684 return [r for r in subset if r in ps]
684 return [r for r in subset if r in ps]
685
685
686 ps = set()
686 ps = set()
687 cl = repo.changelog
687 cl = repo.changelog
688 for r in getset(repo, range(len(repo)), x):
688 for r in getset(repo, range(len(repo)), x):
689 ps.update(cl.parentrevs(r))
689 ps.update(cl.parentrevs(r))
690 return [r for r in subset if r in ps]
690 return [r for r in subset if r in ps]
691
691
692 def parentspec(repo, subset, x, n):
692 def parentspec(repo, subset, x, n):
693 """``set^0``
693 """``set^0``
694 The set.
694 The set.
695 ``set^1`` (or ``set^``), ``set^2``
695 ``set^1`` (or ``set^``), ``set^2``
696 First or second parent, respectively, of all changesets in set.
696 First or second parent, respectively, of all changesets in set.
697 """
697 """
698 try:
698 try:
699 n = int(n[1])
699 n = int(n[1])
700 if n not in (0, 1, 2):
700 if n not in (0, 1, 2):
701 raise ValueError
701 raise ValueError
702 except (TypeError, ValueError):
702 except (TypeError, ValueError):
703 raise error.ParseError(_("^ expects a number 0, 1, or 2"))
703 raise error.ParseError(_("^ expects a number 0, 1, or 2"))
704 ps = set()
704 ps = set()
705 cl = repo.changelog
705 cl = repo.changelog
706 for r in getset(repo, subset, x):
706 for r in getset(repo, subset, x):
707 if n == 0:
707 if n == 0:
708 ps.add(r)
708 ps.add(r)
709 elif n == 1:
709 elif n == 1:
710 ps.add(cl.parentrevs(r)[0])
710 ps.add(cl.parentrevs(r)[0])
711 elif n == 2:
711 elif n == 2:
712 parents = cl.parentrevs(r)
712 parents = cl.parentrevs(r)
713 if len(parents) > 1:
713 if len(parents) > 1:
714 ps.add(parents[1])
714 ps.add(parents[1])
715 return [r for r in subset if r in ps]
715 return [r for r in subset if r in ps]
716
716
717 def present(repo, subset, x):
717 def present(repo, subset, x):
718 """``present(set)``
718 """``present(set)``
719 An empty set, if any revision in set isn't found; otherwise,
719 An empty set, if any revision in set isn't found; otherwise,
720 all revisions in set.
720 all revisions in set.
721 """
721 """
722 try:
722 try:
723 return getset(repo, subset, x)
723 return getset(repo, subset, x)
724 except error.RepoLookupError:
724 except error.RepoLookupError:
725 return []
725 return []
726
726
727 def removes(repo, subset, x):
727 def removes(repo, subset, x):
728 """``removes(pattern)``
728 """``removes(pattern)``
729 Changesets which remove files matching pattern.
729 Changesets which remove files matching pattern.
730 """
730 """
731 # i18n: "removes" is a keyword
731 # i18n: "removes" is a keyword
732 pat = getstring(x, _("removes requires a pattern"))
732 pat = getstring(x, _("removes requires a pattern"))
733 return checkstatus(repo, subset, pat, 2)
733 return checkstatus(repo, subset, pat, 2)
734
734
735 def rev(repo, subset, x):
735 def rev(repo, subset, x):
736 """``rev(number)``
736 """``rev(number)``
737 Revision with the given numeric identifier.
737 Revision with the given numeric identifier.
738 """
738 """
739 # i18n: "rev" is a keyword
739 # i18n: "rev" is a keyword
740 l = getargs(x, 1, 1, _("rev requires one argument"))
740 l = getargs(x, 1, 1, _("rev requires one argument"))
741 try:
741 try:
742 # i18n: "rev" is a keyword
742 # i18n: "rev" is a keyword
743 l = int(getstring(l[0], _("rev requires a number")))
743 l = int(getstring(l[0], _("rev requires a number")))
744 except (TypeError, ValueError):
744 except (TypeError, ValueError):
745 # i18n: "rev" is a keyword
745 # i18n: "rev" is a keyword
746 raise error.ParseError(_("rev expects a number"))
746 raise error.ParseError(_("rev expects a number"))
747 return [r for r in subset if r == l]
747 return [r for r in subset if r == l]
748
748
749 def reverse(repo, subset, x):
749 def reverse(repo, subset, x):
750 """``reverse(set)``
750 """``reverse(set)``
751 Reverse order of set.
751 Reverse order of set.
752 """
752 """
753 l = getset(repo, subset, x)
753 l = getset(repo, subset, x)
754 l.reverse()
754 l.reverse()
755 return l
755 return l
756
756
757 def roots(repo, subset, x):
757 def roots(repo, subset, x):
758 """``roots(set)``
758 """``roots(set)``
759 Changesets with no parent changeset in set.
759 Changesets with no parent changeset in set.
760 """
760 """
761 s = getset(repo, subset, x)
761 s = getset(repo, subset, x)
762 cs = set(children(repo, subset, x))
762 cs = set(children(repo, subset, x))
763 return [r for r in s if r not in cs]
763 return [r for r in s if r not in cs]
764
764
765 def sort(repo, subset, x):
765 def sort(repo, subset, x):
766 """``sort(set[, [-]key...])``
766 """``sort(set[, [-]key...])``
767 Sort set by keys. The default sort order is ascending, specify a key
767 Sort set by keys. The default sort order is ascending, specify a key
768 as ``-key`` to sort in descending order.
768 as ``-key`` to sort in descending order.
769
769
770 The keys can be:
770 The keys can be:
771
771
772 - ``rev`` for the revision number,
772 - ``rev`` for the revision number,
773 - ``branch`` for the branch name,
773 - ``branch`` for the branch name,
774 - ``desc`` for the commit message (description),
774 - ``desc`` for the commit message (description),
775 - ``user`` for user name (``author`` can be used as an alias),
775 - ``user`` for user name (``author`` can be used as an alias),
776 - ``date`` for the commit date
776 - ``date`` for the commit date
777 """
777 """
778 # i18n: "sort" is a keyword
778 # i18n: "sort" is a keyword
779 l = getargs(x, 1, 2, _("sort requires one or two arguments"))
779 l = getargs(x, 1, 2, _("sort requires one or two arguments"))
780 keys = "rev"
780 keys = "rev"
781 if len(l) == 2:
781 if len(l) == 2:
782 keys = getstring(l[1], _("sort spec must be a string"))
782 keys = getstring(l[1], _("sort spec must be a string"))
783
783
784 s = l[0]
784 s = l[0]
785 keys = keys.split()
785 keys = keys.split()
786 l = []
786 l = []
787 def invert(s):
787 def invert(s):
788 return "".join(chr(255 - ord(c)) for c in s)
788 return "".join(chr(255 - ord(c)) for c in s)
789 for r in getset(repo, subset, s):
789 for r in getset(repo, subset, s):
790 c = repo[r]
790 c = repo[r]
791 e = []
791 e = []
792 for k in keys:
792 for k in keys:
793 if k == 'rev':
793 if k == 'rev':
794 e.append(r)
794 e.append(r)
795 elif k == '-rev':
795 elif k == '-rev':
796 e.append(-r)
796 e.append(-r)
797 elif k == 'branch':
797 elif k == 'branch':
798 e.append(c.branch())
798 e.append(c.branch())
799 elif k == '-branch':
799 elif k == '-branch':
800 e.append(invert(c.branch()))
800 e.append(invert(c.branch()))
801 elif k == 'desc':
801 elif k == 'desc':
802 e.append(c.description())
802 e.append(c.description())
803 elif k == '-desc':
803 elif k == '-desc':
804 e.append(invert(c.description()))
804 e.append(invert(c.description()))
805 elif k in 'user author':
805 elif k in 'user author':
806 e.append(c.user())
806 e.append(c.user())
807 elif k in '-user -author':
807 elif k in '-user -author':
808 e.append(invert(c.user()))
808 e.append(invert(c.user()))
809 elif k == 'date':
809 elif k == 'date':
810 e.append(c.date()[0])
810 e.append(c.date()[0])
811 elif k == '-date':
811 elif k == '-date':
812 e.append(-c.date()[0])
812 e.append(-c.date()[0])
813 else:
813 else:
814 raise error.ParseError(_("unknown sort key %r") % k)
814 raise error.ParseError(_("unknown sort key %r") % k)
815 e.append(r)
815 e.append(r)
816 l.append(e)
816 l.append(e)
817 l.sort()
817 l.sort()
818 return [e[-1] for e in l]
818 return [e[-1] for e in l]
819
819
820 def tag(repo, subset, x):
820 def tag(repo, subset, x):
821 """``tag([name])``
821 """``tag([name])``
822 The specified tag by name, or all tagged revisions if no name is given.
822 The specified tag by name, or all tagged revisions if no name is given.
823 """
823 """
824 # i18n: "tag" is a keyword
824 # i18n: "tag" is a keyword
825 args = getargs(x, 0, 1, _("tag takes one or no arguments"))
825 args = getargs(x, 0, 1, _("tag takes one or no arguments"))
826 cl = repo.changelog
826 cl = repo.changelog
827 if args:
827 if args:
828 tn = getstring(args[0],
828 tn = getstring(args[0],
829 # i18n: "tag" is a keyword
829 # i18n: "tag" is a keyword
830 _('the argument to tag must be a string'))
830 _('the argument to tag must be a string'))
831 if not repo.tags().get(tn, None):
831 if not repo.tags().get(tn, None):
832 raise util.Abort(_("tag '%s' does not exist") % tn)
832 raise util.Abort(_("tag '%s' does not exist") % tn)
833 s = set([cl.rev(n) for t, n in repo.tagslist() if t == tn])
833 s = set([cl.rev(n) for t, n in repo.tagslist() if t == tn])
834 else:
834 else:
835 s = set([cl.rev(n) for t, n in repo.tagslist() if t != 'tip'])
835 s = set([cl.rev(n) for t, n in repo.tagslist() if t != 'tip'])
836 return [r for r in subset if r in s]
836 return [r for r in subset if r in s]
837
837
838 def tagged(repo, subset, x):
838 def tagged(repo, subset, x):
839 return tag(repo, subset, x)
839 return tag(repo, subset, x)
840
840
841 def user(repo, subset, x):
841 def user(repo, subset, x):
842 """``user(string)``
842 """``user(string)``
843 User name contains string. The match is case-insensitive.
843 User name contains string. The match is case-insensitive.
844 """
844 """
845 return author(repo, subset, x)
845 return author(repo, subset, x)
846
846
847 symbols = {
847 symbols = {
848 "adds": adds,
848 "adds": adds,
849 "all": getall,
849 "all": getall,
850 "ancestor": ancestor,
850 "ancestor": ancestor,
851 "ancestors": ancestors,
851 "ancestors": ancestors,
852 "author": author,
852 "author": author,
853 "bisect": bisect,
853 "bisect": bisect,
854 "bisected": bisected,
854 "bisected": bisected,
855 "bookmark": bookmark,
855 "bookmark": bookmark,
856 "branch": branch,
856 "branch": branch,
857 "children": children,
857 "children": children,
858 "closed": closed,
858 "closed": closed,
859 "contains": contains,
859 "contains": contains,
860 "date": date,
860 "date": date,
861 "desc": desc,
861 "desc": desc,
862 "descendants": descendants,
862 "descendants": descendants,
863 "file": hasfile,
863 "file": hasfile,
864 "filelog": filelog,
864 "filelog": filelog,
865 "first": first,
865 "first": first,
866 "follow": follow,
866 "follow": follow,
867 "grep": grep,
867 "grep": grep,
868 "head": head,
868 "head": head,
869 "heads": heads,
869 "heads": heads,
870 "id": node,
870 "id": node,
871 "keyword": keyword,
871 "keyword": keyword,
872 "last": last,
872 "last": last,
873 "limit": limit,
873 "limit": limit,
874 "max": maxrev,
874 "max": maxrev,
875 "merge": merge,
875 "merge": merge,
876 "min": minrev,
876 "min": minrev,
877 "modifies": modifies,
877 "modifies": modifies,
878 "outgoing": outgoing,
878 "outgoing": outgoing,
879 "p1": p1,
879 "p1": p1,
880 "p2": p2,
880 "p2": p2,
881 "parents": parents,
881 "parents": parents,
882 "present": present,
882 "present": present,
883 "removes": removes,
883 "removes": removes,
884 "rev": rev,
884 "rev": rev,
885 "reverse": reverse,
885 "reverse": reverse,
886 "roots": roots,
886 "roots": roots,
887 "sort": sort,
887 "sort": sort,
888 "tag": tag,
888 "tag": tag,
889 "tagged": tagged,
889 "tagged": tagged,
890 "user": user,
890 "user": user,
891 }
891 }
892
892
893 methods = {
893 methods = {
894 "range": rangeset,
894 "range": rangeset,
895 "string": stringset,
895 "string": stringset,
896 "symbol": symbolset,
896 "symbol": symbolset,
897 "and": andset,
897 "and": andset,
898 "or": orset,
898 "or": orset,
899 "not": notset,
899 "not": notset,
900 "list": listset,
900 "list": listset,
901 "func": func,
901 "func": func,
902 "ancestor": ancestorspec,
902 "ancestor": ancestorspec,
903 "parent": parentspec,
903 "parent": parentspec,
904 "parentpost": p1,
904 "parentpost": p1,
905 }
905 }
906
906
907 def optimize(x, small):
907 def optimize(x, small):
908 if x is None:
908 if x is None:
909 return 0, x
909 return 0, x
910
910
911 smallbonus = 1
911 smallbonus = 1
912 if small:
912 if small:
913 smallbonus = .5
913 smallbonus = .5
914
914
915 op = x[0]
915 op = x[0]
916 if op == 'minus':
916 if op == 'minus':
917 return optimize(('and', x[1], ('not', x[2])), small)
917 return optimize(('and', x[1], ('not', x[2])), small)
918 elif op == 'dagrange':
918 elif op == 'dagrange':
919 return optimize(('and', ('func', ('symbol', 'descendants'), x[1]),
919 return optimize(('and', ('func', ('symbol', 'descendants'), x[1]),
920 ('func', ('symbol', 'ancestors'), x[2])), small)
920 ('func', ('symbol', 'ancestors'), x[2])), small)
921 elif op == 'dagrangepre':
921 elif op == 'dagrangepre':
922 return optimize(('func', ('symbol', 'ancestors'), x[1]), small)
922 return optimize(('func', ('symbol', 'ancestors'), x[1]), small)
923 elif op == 'dagrangepost':
923 elif op == 'dagrangepost':
924 return optimize(('func', ('symbol', 'descendants'), x[1]), small)
924 return optimize(('func', ('symbol', 'descendants'), x[1]), small)
925 elif op == 'rangepre':
925 elif op == 'rangepre':
926 return optimize(('range', ('string', '0'), x[1]), small)
926 return optimize(('range', ('string', '0'), x[1]), small)
927 elif op == 'rangepost':
927 elif op == 'rangepost':
928 return optimize(('range', x[1], ('string', 'tip')), small)
928 return optimize(('range', x[1], ('string', 'tip')), small)
929 elif op == 'negate':
929 elif op == 'negate':
930 return optimize(('string',
930 return optimize(('string',
931 '-' + getstring(x[1], _("can't negate that"))), small)
931 '-' + getstring(x[1], _("can't negate that"))), small)
932 elif op in 'string symbol negate':
932 elif op in 'string symbol negate':
933 return smallbonus, x # single revisions are small
933 return smallbonus, x # single revisions are small
934 elif op == 'and' or op == 'dagrange':
934 elif op == 'and' or op == 'dagrange':
935 wa, ta = optimize(x[1], True)
935 wa, ta = optimize(x[1], True)
936 wb, tb = optimize(x[2], True)
936 wb, tb = optimize(x[2], True)
937 w = min(wa, wb)
937 w = min(wa, wb)
938 if wa > wb:
938 if wa > wb:
939 return w, (op, tb, ta)
939 return w, (op, tb, ta)
940 return w, (op, ta, tb)
940 return w, (op, ta, tb)
941 elif op == 'or':
941 elif op == 'or':
942 wa, ta = optimize(x[1], False)
942 wa, ta = optimize(x[1], False)
943 wb, tb = optimize(x[2], False)
943 wb, tb = optimize(x[2], False)
944 if wb < wa:
944 if wb < wa:
945 wb, wa = wa, wb
945 wb, wa = wa, wb
946 return max(wa, wb), (op, ta, tb)
946 return max(wa, wb), (op, ta, tb)
947 elif op == 'not':
947 elif op == 'not':
948 o = optimize(x[1], not small)
948 o = optimize(x[1], not small)
949 return o[0], (op, o[1])
949 return o[0], (op, o[1])
950 elif op == 'parentpost':
950 elif op == 'parentpost':
951 o = optimize(x[1], small)
951 o = optimize(x[1], small)
952 return o[0], (op, o[1])
952 return o[0], (op, o[1])
953 elif op == 'group':
953 elif op == 'group':
954 return optimize(x[1], small)
954 return optimize(x[1], small)
955 elif op in 'range list parent ancestorspec':
955 elif op in 'range list parent ancestorspec':
956 if op == 'parent':
956 if op == 'parent':
957 # x^:y means (x^) : y, not x ^ (:y)
957 # x^:y means (x^) : y, not x ^ (:y)
958 post = ('parentpost', x[1])
958 post = ('parentpost', x[1])
959 if x[2][0] == 'dagrangepre':
959 if x[2][0] == 'dagrangepre':
960 return optimize(('dagrange', post, x[2][1]), small)
960 return optimize(('dagrange', post, x[2][1]), small)
961 elif x[2][0] == 'rangepre':
961 elif x[2][0] == 'rangepre':
962 return optimize(('range', post, x[2][1]), small)
962 return optimize(('range', post, x[2][1]), small)
963
963
964 wa, ta = optimize(x[1], small)
964 wa, ta = optimize(x[1], small)
965 wb, tb = optimize(x[2], small)
965 wb, tb = optimize(x[2], small)
966 return wa + wb, (op, ta, tb)
966 return wa + wb, (op, ta, tb)
967 elif op == 'func':
967 elif op == 'func':
968 f = getstring(x[1], _("not a symbol"))
968 f = getstring(x[1], _("not a symbol"))
969 wa, ta = optimize(x[2], small)
969 wa, ta = optimize(x[2], small)
970 if f in ("author branch closed date desc file grep keyword "
970 if f in ("author branch closed date desc file grep keyword "
971 "outgoing user"):
971 "outgoing user"):
972 w = 10 # slow
972 w = 10 # slow
973 elif f in "modifies adds removes":
973 elif f in "modifies adds removes":
974 w = 30 # slower
974 w = 30 # slower
975 elif f == "contains":
975 elif f == "contains":
976 w = 100 # very slow
976 w = 100 # very slow
977 elif f == "ancestor":
977 elif f == "ancestor":
978 w = 1 * smallbonus
978 w = 1 * smallbonus
979 elif f in "reverse limit first":
979 elif f in "reverse limit first":
980 w = 0
980 w = 0
981 elif f in "sort":
981 elif f in "sort":
982 w = 10 # assume most sorts look at changelog
982 w = 10 # assume most sorts look at changelog
983 else:
983 else:
984 w = 1
984 w = 1
985 return w + wa, (op, x[1], ta)
985 return w + wa, (op, x[1], ta)
986 return 1, x
986 return 1, x
987
987
988 class revsetalias(object):
988 class revsetalias(object):
989 funcre = re.compile('^([^(]+)\(([^)]+)\)$')
989 funcre = re.compile('^([^(]+)\(([^)]+)\)$')
990 args = None
990 args = None
991
991
992 def __init__(self, name, value):
992 def __init__(self, name, value):
993 '''Aliases like:
993 '''Aliases like:
994
994
995 h = heads(default)
995 h = heads(default)
996 b($1) = ancestors($1) - ancestors(default)
996 b($1) = ancestors($1) - ancestors(default)
997 '''
997 '''
998 if isinstance(name, tuple): # parameter substitution
998 if isinstance(name, tuple): # parameter substitution
999 self.tree = name
999 self.tree = name
1000 self.replacement = value
1000 self.replacement = value
1001 else: # alias definition
1001 else: # alias definition
1002 m = self.funcre.search(name)
1002 m = self.funcre.search(name)
1003 if m:
1003 if m:
1004 self.tree = ('func', ('symbol', m.group(1)))
1004 self.tree = ('func', ('symbol', m.group(1)))
1005 self.args = [x.strip() for x in m.group(2).split(',')]
1005 self.args = [x.strip() for x in m.group(2).split(',')]
1006 for arg in self.args:
1006 for arg in self.args:
1007 value = value.replace(arg, repr(arg))
1007 value = value.replace(arg, repr(arg))
1008 else:
1008 else:
1009 self.tree = ('symbol', name)
1009 self.tree = ('symbol', name)
1010
1010
1011 self.replacement, pos = parse(value)
1011 self.replacement, pos = parse(value)
1012 if pos != len(value):
1012 if pos != len(value):
1013 raise error.ParseError(_('invalid token'), pos)
1013 raise error.ParseError(_('invalid token'), pos)
1014
1014
1015 def process(self, tree):
1015 def process(self, tree):
1016 if isinstance(tree, tuple):
1016 if isinstance(tree, tuple):
1017 if self.args is None:
1017 if self.args is None:
1018 if tree == self.tree:
1018 if tree == self.tree:
1019 return self.replacement
1019 return self.replacement
1020 elif tree[:2] == self.tree:
1020 elif tree[:2] == self.tree:
1021 l = getlist(tree[2])
1021 l = getlist(tree[2])
1022 if len(l) != len(self.args):
1022 if len(l) != len(self.args):
1023 raise error.ParseError(
1023 raise error.ParseError(
1024 _('invalid number of arguments: %s') % len(l))
1024 _('invalid number of arguments: %s') % len(l))
1025 result = self.replacement
1025 result = self.replacement
1026 for a, v in zip(self.args, l):
1026 for a, v in zip(self.args, l):
1027 valalias = revsetalias(('string', a), v)
1027 valalias = revsetalias(('string', a), v)
1028 result = valalias.process(result)
1028 result = valalias.process(result)
1029 return result
1029 return result
1030 return tuple(map(self.process, tree))
1030 return tuple(map(self.process, tree))
1031 return tree
1031 return tree
1032
1032
1033 def findaliases(ui, tree):
1033 def findaliases(ui, tree):
1034 for k, v in ui.configitems('revsetalias'):
1034 for k, v in ui.configitems('revsetalias'):
1035 alias = revsetalias(k, v)
1035 alias = revsetalias(k, v)
1036 tree = alias.process(tree)
1036 tree = alias.process(tree)
1037 return tree
1037 return tree
1038
1038
1039 parse = parser.parser(tokenize, elements).parse
1039 parse = parser.parser(tokenize, elements).parse
1040
1040
1041 def match(ui, spec):
1041 def match(ui, spec):
1042 if not spec:
1042 if not spec:
1043 raise error.ParseError(_("empty query"))
1043 raise error.ParseError(_("empty query"))
1044 tree, pos = parse(spec)
1044 tree, pos = parse(spec)
1045 if (pos != len(spec)):
1045 if (pos != len(spec)):
1046 raise error.ParseError(_("invalid token"), pos)
1046 raise error.ParseError(_("invalid token"), pos)
1047 if ui:
1047 if ui:
1048 tree = findaliases(ui, tree)
1048 tree = findaliases(ui, tree)
1049 weight, tree = optimize(tree, True)
1049 weight, tree = optimize(tree, True)
1050 def mfunc(repo, subset):
1050 def mfunc(repo, subset):
1051 return getset(repo, subset, tree)
1051 return getset(repo, subset, tree)
1052 return mfunc
1052 return mfunc
1053
1053
1054 def formatspec(expr, *args):
1054 def formatspec(expr, *args):
1055 '''
1055 '''
1056 This is a convenience function for using revsets internally, and
1056 This is a convenience function for using revsets internally, and
1057 escapes arguments appropriately. Aliases are intentionally ignored
1057 escapes arguments appropriately. Aliases are intentionally ignored
1058 so that intended expression behavior isn't accidentally subverted.
1058 so that intended expression behavior isn't accidentally subverted.
1059
1059
1060 Supported arguments:
1060 Supported arguments:
1061
1061
1062 %r = revset expression, parenthesized
1062 %r = revset expression, parenthesized
1063 %d = int(arg), no quoting
1063 %d = int(arg), no quoting
1064 %s = string(arg), escaped and single-quoted
1064 %s = string(arg), escaped and single-quoted
1065 %b = arg.branch(), escaped and single-quoted
1065 %b = arg.branch(), escaped and single-quoted
1066 %n = hex(arg), single-quoted
1066 %n = hex(arg), single-quoted
1067 %% = a literal '%'
1067 %% = a literal '%'
1068
1068
1069 Prefixing the type with 'l' specifies a parenthesized list of that type.
1069 Prefixing the type with 'l' specifies a parenthesized list of that type.
1070
1070
1071 >>> formatspec('%r:: and %lr', '10 or 11', ("this()", "that()"))
1071 >>> formatspec('%r:: and %lr', '10 or 11', ("this()", "that()"))
1072 '(10 or 11):: and ((this()) or (that()))'
1072 '(10 or 11):: and ((this()) or (that()))'
1073 >>> formatspec('%d:: and not %d::', 10, 20)
1073 >>> formatspec('%d:: and not %d::', 10, 20)
1074 '10:: and not 20::'
1074 '10:: and not 20::'
1075 >>> formatspec('%ld or %ld', [], [1])
1075 >>> formatspec('%ld or %ld', [], [1])
1076 '(0-0) or (1)'
1076 '(0-0) or 1'
1077 >>> formatspec('keyword(%s)', 'foo\\xe9')
1077 >>> formatspec('keyword(%s)', 'foo\\xe9')
1078 "keyword('foo\\\\xe9')"
1078 "keyword('foo\\\\xe9')"
1079 >>> b = lambda: 'default'
1079 >>> b = lambda: 'default'
1080 >>> b.branch = b
1080 >>> b.branch = b
1081 >>> formatspec('branch(%b)', b)
1081 >>> formatspec('branch(%b)', b)
1082 "branch('default')"
1082 "branch('default')"
1083 >>> formatspec('root(%ls)', ['a', 'b', 'c', 'd'])
1083 >>> formatspec('root(%ls)', ['a', 'b', 'c', 'd'])
1084 "root(('a' or 'b' or 'c' or 'd'))"
1084 "root((('a' or 'b') or ('c' or 'd')))"
1085 '''
1085 '''
1086
1086
1087 def quote(s):
1087 def quote(s):
1088 return repr(str(s))
1088 return repr(str(s))
1089
1089
1090 def argtype(c, arg):
1090 def argtype(c, arg):
1091 if c == 'd':
1091 if c == 'd':
1092 return str(int(arg))
1092 return str(int(arg))
1093 elif c == 's':
1093 elif c == 's':
1094 return quote(arg)
1094 return quote(arg)
1095 elif c == 'r':
1095 elif c == 'r':
1096 parse(arg) # make sure syntax errors are confined
1096 parse(arg) # make sure syntax errors are confined
1097 return '(%s)' % arg
1097 return '(%s)' % arg
1098 elif c == 'n':
1098 elif c == 'n':
1099 return quote(nodemod.hex(arg))
1099 return quote(nodemod.hex(arg))
1100 elif c == 'b':
1100 elif c == 'b':
1101 return quote(arg.branch())
1101 return quote(arg.branch())
1102
1102
1103 def listexp(s, t):
1104 "balance a list s of type t to limit parse tree depth"
1105 l = len(s)
1106 if l == 0:
1107 return '(0-0)' # a minimal way to represent an empty set
1108 if l == 1:
1109 return argtype(t, s[0])
1110 m = l / 2
1111 return '(%s or %s)' % (listexp(s[:m], t), listexp(s[m:], t))
1112
1103 ret = ''
1113 ret = ''
1104 pos = 0
1114 pos = 0
1105 arg = 0
1115 arg = 0
1106 while pos < len(expr):
1116 while pos < len(expr):
1107 c = expr[pos]
1117 c = expr[pos]
1108 if c == '%':
1118 if c == '%':
1109 pos += 1
1119 pos += 1
1110 d = expr[pos]
1120 d = expr[pos]
1111 if d == '%':
1121 if d == '%':
1112 ret += d
1122 ret += d
1113 elif d in 'dsnbr':
1123 elif d in 'dsnbr':
1114 ret += argtype(d, args[arg])
1124 ret += argtype(d, args[arg])
1115 arg += 1
1125 arg += 1
1116 elif d == 'l':
1126 elif d == 'l':
1117 # a list of some type
1127 # a list of some type
1118 pos += 1
1128 pos += 1
1119 d = expr[pos]
1129 d = expr[pos]
1120 if args[arg]:
1130 ret += listexp(args[arg], d)
1121 lv = ' or '.join(argtype(d, e) for e in args[arg])
1122 else:
1123 lv = '0-0' # a minimal way to represent an empty set
1124 ret += '(%s)' % lv
1125 arg += 1
1131 arg += 1
1126 else:
1132 else:
1127 raise util.Abort('unexpected revspec format character %s' % d)
1133 raise util.Abort('unexpected revspec format character %s' % d)
1128 else:
1134 else:
1129 ret += c
1135 ret += c
1130 pos += 1
1136 pos += 1
1131
1137
1132 return ret
1138 return ret
1133
1139
1134 # tell hggettext to extract docstrings from these functions:
1140 # tell hggettext to extract docstrings from these functions:
1135 i18nfunctions = symbols.values()
1141 i18nfunctions = symbols.values()
General Comments 0
You need to be logged in to leave comments. Login now