##// END OF EJS Templates
revsets: preserve ordering with the or operator...
Augie Fackler -
r13932:34f57700 default
parent child Browse files
Show More
@@ -1,844 +1,845
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, help, hbisect
9 import parser, util, error, discovery, help, hbisect
10 import bookmarks as bookmarksmod
10 import bookmarks as bookmarksmod
11 import match as matchmod
11 import match as matchmod
12 from i18n import _
12 from i18n import _
13
13
14 elements = {
14 elements = {
15 "(": (20, ("group", 1, ")"), ("func", 1, ")")),
15 "(": (20, ("group", 1, ")"), ("func", 1, ")")),
16 "-": (5, ("negate", 19), ("minus", 5)),
16 "-": (5, ("negate", 19), ("minus", 5)),
17 "::": (17, ("dagrangepre", 17), ("dagrange", 17),
17 "::": (17, ("dagrangepre", 17), ("dagrange", 17),
18 ("dagrangepost", 17)),
18 ("dagrangepost", 17)),
19 "..": (17, ("dagrangepre", 17), ("dagrange", 17),
19 "..": (17, ("dagrangepre", 17), ("dagrange", 17),
20 ("dagrangepost", 17)),
20 ("dagrangepost", 17)),
21 ":": (15, ("rangepre", 15), ("range", 15), ("rangepost", 15)),
21 ":": (15, ("rangepre", 15), ("range", 15), ("rangepost", 15)),
22 "not": (10, ("not", 10)),
22 "not": (10, ("not", 10)),
23 "!": (10, ("not", 10)),
23 "!": (10, ("not", 10)),
24 "and": (5, None, ("and", 5)),
24 "and": (5, None, ("and", 5)),
25 "&": (5, None, ("and", 5)),
25 "&": (5, None, ("and", 5)),
26 "or": (4, None, ("or", 4)),
26 "or": (4, None, ("or", 4)),
27 "|": (4, None, ("or", 4)),
27 "|": (4, None, ("or", 4)),
28 "+": (4, None, ("or", 4)),
28 "+": (4, None, ("or", 4)),
29 ",": (2, None, ("list", 2)),
29 ",": (2, None, ("list", 2)),
30 ")": (0, None, None),
30 ")": (0, None, None),
31 "symbol": (0, ("symbol",), None),
31 "symbol": (0, ("symbol",), None),
32 "string": (0, ("string",), None),
32 "string": (0, ("string",), None),
33 "end": (0, None, None),
33 "end": (0, None, None),
34 }
34 }
35
35
36 keywords = set(['and', 'or', 'not'])
36 keywords = set(['and', 'or', 'not'])
37
37
38 def tokenize(program):
38 def tokenize(program):
39 pos, l = 0, len(program)
39 pos, l = 0, len(program)
40 while pos < l:
40 while pos < l:
41 c = program[pos]
41 c = program[pos]
42 if c.isspace(): # skip inter-token whitespace
42 if c.isspace(): # skip inter-token whitespace
43 pass
43 pass
44 elif c == ':' and program[pos:pos + 2] == '::': # look ahead carefully
44 elif c == ':' and program[pos:pos + 2] == '::': # look ahead carefully
45 yield ('::', None, pos)
45 yield ('::', None, pos)
46 pos += 1 # skip ahead
46 pos += 1 # skip ahead
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 in "():,-|&+!": # handle simple operators
50 elif c in "():,-|&+!": # handle simple operators
51 yield (c, None, pos)
51 yield (c, None, pos)
52 elif (c in '"\'' or c == 'r' and
52 elif (c in '"\'' or c == 'r' and
53 program[pos:pos + 2] in ("r'", 'r"')): # handle quoted strings
53 program[pos:pos + 2] in ("r'", 'r"')): # handle quoted strings
54 if c == 'r':
54 if c == 'r':
55 pos += 1
55 pos += 1
56 c = program[pos]
56 c = program[pos]
57 decode = lambda x: x
57 decode = lambda x: x
58 else:
58 else:
59 decode = lambda x: x.decode('string-escape')
59 decode = lambda x: x.decode('string-escape')
60 pos += 1
60 pos += 1
61 s = pos
61 s = pos
62 while pos < l: # find closing quote
62 while pos < l: # find closing quote
63 d = program[pos]
63 d = program[pos]
64 if d == '\\': # skip over escaped characters
64 if d == '\\': # skip over escaped characters
65 pos += 2
65 pos += 2
66 continue
66 continue
67 if d == c:
67 if d == c:
68 yield ('string', decode(program[s:pos]), s)
68 yield ('string', decode(program[s:pos]), s)
69 break
69 break
70 pos += 1
70 pos += 1
71 else:
71 else:
72 raise error.ParseError(_("unterminated string"), s)
72 raise error.ParseError(_("unterminated string"), s)
73 elif c.isalnum() or c in '._' or ord(c) > 127: # gather up a symbol/keyword
73 elif c.isalnum() or c in '._' or ord(c) > 127: # gather up a symbol/keyword
74 s = pos
74 s = pos
75 pos += 1
75 pos += 1
76 while pos < l: # find end of symbol
76 while pos < l: # find end of symbol
77 d = program[pos]
77 d = program[pos]
78 if not (d.isalnum() or d in "._" or ord(d) > 127):
78 if not (d.isalnum() or d in "._" or ord(d) > 127):
79 break
79 break
80 if d == '.' and program[pos - 1] == '.': # special case for ..
80 if d == '.' and program[pos - 1] == '.': # special case for ..
81 pos -= 1
81 pos -= 1
82 break
82 break
83 pos += 1
83 pos += 1
84 sym = program[s:pos]
84 sym = program[s:pos]
85 if sym in keywords: # operator keywords
85 if sym in keywords: # operator keywords
86 yield (sym, None, s)
86 yield (sym, None, s)
87 else:
87 else:
88 yield ('symbol', sym, s)
88 yield ('symbol', sym, s)
89 pos -= 1
89 pos -= 1
90 else:
90 else:
91 raise error.ParseError(_("syntax error"), pos)
91 raise error.ParseError(_("syntax error"), pos)
92 pos += 1
92 pos += 1
93 yield ('end', None, pos)
93 yield ('end', None, pos)
94
94
95 # helpers
95 # helpers
96
96
97 def getstring(x, err):
97 def getstring(x, err):
98 if x and (x[0] == 'string' or x[0] == 'symbol'):
98 if x and (x[0] == 'string' or x[0] == 'symbol'):
99 return x[1]
99 return x[1]
100 raise error.ParseError(err)
100 raise error.ParseError(err)
101
101
102 def getlist(x):
102 def getlist(x):
103 if not x:
103 if not x:
104 return []
104 return []
105 if x[0] == 'list':
105 if x[0] == 'list':
106 return getlist(x[1]) + [x[2]]
106 return getlist(x[1]) + [x[2]]
107 return [x]
107 return [x]
108
108
109 def getargs(x, min, max, err):
109 def getargs(x, min, max, err):
110 l = getlist(x)
110 l = getlist(x)
111 if len(l) < min or len(l) > max:
111 if len(l) < min or len(l) > max:
112 raise error.ParseError(err)
112 raise error.ParseError(err)
113 return l
113 return l
114
114
115 def getset(repo, subset, x):
115 def getset(repo, subset, x):
116 if not x:
116 if not x:
117 raise error.ParseError(_("missing argument"))
117 raise error.ParseError(_("missing argument"))
118 return methods[x[0]](repo, subset, *x[1:])
118 return methods[x[0]](repo, subset, *x[1:])
119
119
120 # operator methods
120 # operator methods
121
121
122 def stringset(repo, subset, x):
122 def stringset(repo, subset, x):
123 x = repo[x].rev()
123 x = repo[x].rev()
124 if x == -1 and len(subset) == len(repo):
124 if x == -1 and len(subset) == len(repo):
125 return [-1]
125 return [-1]
126 if x in subset:
126 if x in subset:
127 return [x]
127 return [x]
128 return []
128 return []
129
129
130 def symbolset(repo, subset, x):
130 def symbolset(repo, subset, x):
131 if x in symbols:
131 if x in symbols:
132 raise error.ParseError(_("can't use %s here") % x)
132 raise error.ParseError(_("can't use %s here") % x)
133 return stringset(repo, subset, x)
133 return stringset(repo, subset, x)
134
134
135 def rangeset(repo, subset, x, y):
135 def rangeset(repo, subset, x, y):
136 m = getset(repo, subset, x)
136 m = getset(repo, subset, x)
137 if not m:
137 if not m:
138 m = getset(repo, range(len(repo)), x)
138 m = getset(repo, range(len(repo)), x)
139
139
140 n = getset(repo, subset, y)
140 n = getset(repo, subset, y)
141 if not n:
141 if not n:
142 n = getset(repo, range(len(repo)), y)
142 n = getset(repo, range(len(repo)), y)
143
143
144 if not m or not n:
144 if not m or not n:
145 return []
145 return []
146 m, n = m[0], n[-1]
146 m, n = m[0], n[-1]
147
147
148 if m < n:
148 if m < n:
149 r = range(m, n + 1)
149 r = range(m, n + 1)
150 else:
150 else:
151 r = range(m, n - 1, -1)
151 r = range(m, n - 1, -1)
152 s = set(subset)
152 s = set(subset)
153 return [x for x in r if x in s]
153 return [x for x in r if x in s]
154
154
155 def andset(repo, subset, x, y):
155 def andset(repo, subset, x, y):
156 return getset(repo, getset(repo, subset, x), y)
156 return getset(repo, getset(repo, subset, x), y)
157
157
158 def orset(repo, subset, x, y):
158 def orset(repo, subset, x, y):
159 s = set(getset(repo, subset, x))
159 xl = getset(repo, subset, x)
160 s |= set(getset(repo, [r for r in subset if r not in s], y))
160 s = set(xl)
161 return [r for r in subset if r in s]
161 yl = getset(repo, [r for r in subset if r not in s], y)
162 return xl + yl
162
163
163 def notset(repo, subset, x):
164 def notset(repo, subset, x):
164 s = set(getset(repo, subset, x))
165 s = set(getset(repo, subset, x))
165 return [r for r in subset if r not in s]
166 return [r for r in subset if r not in s]
166
167
167 def listset(repo, subset, a, b):
168 def listset(repo, subset, a, b):
168 raise error.ParseError(_("can't use a list in this context"))
169 raise error.ParseError(_("can't use a list in this context"))
169
170
170 def func(repo, subset, a, b):
171 def func(repo, subset, a, b):
171 if a[0] == 'symbol' and a[1] in symbols:
172 if a[0] == 'symbol' and a[1] in symbols:
172 return symbols[a[1]](repo, subset, b)
173 return symbols[a[1]](repo, subset, b)
173 raise error.ParseError(_("not a function: %s") % a[1])
174 raise error.ParseError(_("not a function: %s") % a[1])
174
175
175 # functions
176 # functions
176
177
177 def adds(repo, subset, x):
178 def adds(repo, subset, x):
178 """``adds(pattern)``
179 """``adds(pattern)``
179 Changesets that add a file matching pattern.
180 Changesets that add a file matching pattern.
180 """
181 """
181 # i18n: "adds" is a keyword
182 # i18n: "adds" is a keyword
182 pat = getstring(x, _("adds requires a pattern"))
183 pat = getstring(x, _("adds requires a pattern"))
183 return checkstatus(repo, subset, pat, 1)
184 return checkstatus(repo, subset, pat, 1)
184
185
185 def ancestor(repo, subset, x):
186 def ancestor(repo, subset, x):
186 """``ancestor(single, single)``
187 """``ancestor(single, single)``
187 Greatest common ancestor of the two changesets.
188 Greatest common ancestor of the two changesets.
188 """
189 """
189 # i18n: "ancestor" is a keyword
190 # i18n: "ancestor" is a keyword
190 l = getargs(x, 2, 2, _("ancestor requires two arguments"))
191 l = getargs(x, 2, 2, _("ancestor requires two arguments"))
191 r = range(len(repo))
192 r = range(len(repo))
192 a = getset(repo, r, l[0])
193 a = getset(repo, r, l[0])
193 b = getset(repo, r, l[1])
194 b = getset(repo, r, l[1])
194 if len(a) != 1 or len(b) != 1:
195 if len(a) != 1 or len(b) != 1:
195 # i18n: "ancestor" is a keyword
196 # i18n: "ancestor" is a keyword
196 raise error.ParseError(_("ancestor arguments must be single revisions"))
197 raise error.ParseError(_("ancestor arguments must be single revisions"))
197 an = [repo[a[0]].ancestor(repo[b[0]]).rev()]
198 an = [repo[a[0]].ancestor(repo[b[0]]).rev()]
198
199
199 return [r for r in an if r in subset]
200 return [r for r in an if r in subset]
200
201
201 def ancestors(repo, subset, x):
202 def ancestors(repo, subset, x):
202 """``ancestors(set)``
203 """``ancestors(set)``
203 Changesets that are ancestors of a changeset in set.
204 Changesets that are ancestors of a changeset in set.
204 """
205 """
205 args = getset(repo, range(len(repo)), x)
206 args = getset(repo, range(len(repo)), x)
206 if not args:
207 if not args:
207 return []
208 return []
208 s = set(repo.changelog.ancestors(*args)) | set(args)
209 s = set(repo.changelog.ancestors(*args)) | set(args)
209 return [r for r in subset if r in s]
210 return [r for r in subset if r in s]
210
211
211 def author(repo, subset, x):
212 def author(repo, subset, x):
212 """``author(string)``
213 """``author(string)``
213 Alias for ``user(string)``.
214 Alias for ``user(string)``.
214 """
215 """
215 # i18n: "author" is a keyword
216 # i18n: "author" is a keyword
216 n = getstring(x, _("author requires a string")).lower()
217 n = getstring(x, _("author requires a string")).lower()
217 return [r for r in subset if n in repo[r].user().lower()]
218 return [r for r in subset if n in repo[r].user().lower()]
218
219
219 def bisected(repo, subset, x):
220 def bisected(repo, subset, x):
220 """``bisected(string)``
221 """``bisected(string)``
221 Changesets marked in the specified bisect state (good, bad, skip).
222 Changesets marked in the specified bisect state (good, bad, skip).
222 """
223 """
223 state = getstring(x, _("bisect requires a string")).lower()
224 state = getstring(x, _("bisect requires a string")).lower()
224 if state not in ('good', 'bad', 'skip', 'unknown'):
225 if state not in ('good', 'bad', 'skip', 'unknown'):
225 raise ParseError(_('invalid bisect state'))
226 raise ParseError(_('invalid bisect state'))
226 marked = set(repo.changelog.rev(n) for n in hbisect.load_state(repo)[state])
227 marked = set(repo.changelog.rev(n) for n in hbisect.load_state(repo)[state])
227 return [r for r in subset if r in marked]
228 return [r for r in subset if r in marked]
228
229
229 def bookmark(repo, subset, x):
230 def bookmark(repo, subset, x):
230 """``bookmark([name])``
231 """``bookmark([name])``
231 The named bookmark or all bookmarks.
232 The named bookmark or all bookmarks.
232 """
233 """
233 # i18n: "bookmark" is a keyword
234 # i18n: "bookmark" is a keyword
234 args = getargs(x, 0, 1, _('bookmark takes one or no arguments'))
235 args = getargs(x, 0, 1, _('bookmark takes one or no arguments'))
235 if args:
236 if args:
236 bm = getstring(args[0],
237 bm = getstring(args[0],
237 # i18n: "bookmark" is a keyword
238 # i18n: "bookmark" is a keyword
238 _('the argument to bookmark must be a string'))
239 _('the argument to bookmark must be a string'))
239 bmrev = bookmarksmod.listbookmarks(repo).get(bm, None)
240 bmrev = bookmarksmod.listbookmarks(repo).get(bm, None)
240 if not bmrev:
241 if not bmrev:
241 raise util.Abort(_("bookmark '%s' does not exist") % bm)
242 raise util.Abort(_("bookmark '%s' does not exist") % bm)
242 bmrev = repo[bmrev].rev()
243 bmrev = repo[bmrev].rev()
243 return [r for r in subset if r == bmrev]
244 return [r for r in subset if r == bmrev]
244 bms = set([repo[r].rev()
245 bms = set([repo[r].rev()
245 for r in bookmarksmod.listbookmarks(repo).values()])
246 for r in bookmarksmod.listbookmarks(repo).values()])
246 return [r for r in subset if r in bms]
247 return [r for r in subset if r in bms]
247
248
248 def branch(repo, subset, x):
249 def branch(repo, subset, x):
249 """``branch(string or set)``
250 """``branch(string or set)``
250 All changesets belonging to the given branch or the branches of the given
251 All changesets belonging to the given branch or the branches of the given
251 changesets.
252 changesets.
252 """
253 """
253 try:
254 try:
254 b = getstring(x, '')
255 b = getstring(x, '')
255 if b in repo.branchmap():
256 if b in repo.branchmap():
256 return [r for r in subset if repo[r].branch() == b]
257 return [r for r in subset if repo[r].branch() == b]
257 except error.ParseError:
258 except error.ParseError:
258 # not a string, but another revspec, e.g. tip()
259 # not a string, but another revspec, e.g. tip()
259 pass
260 pass
260
261
261 s = getset(repo, range(len(repo)), x)
262 s = getset(repo, range(len(repo)), x)
262 b = set()
263 b = set()
263 for r in s:
264 for r in s:
264 b.add(repo[r].branch())
265 b.add(repo[r].branch())
265 s = set(s)
266 s = set(s)
266 return [r for r in subset if r in s or repo[r].branch() in b]
267 return [r for r in subset if r in s or repo[r].branch() in b]
267
268
268 def checkstatus(repo, subset, pat, field):
269 def checkstatus(repo, subset, pat, field):
269 m = matchmod.match(repo.root, repo.getcwd(), [pat])
270 m = matchmod.match(repo.root, repo.getcwd(), [pat])
270 s = []
271 s = []
271 fast = (m.files() == [pat])
272 fast = (m.files() == [pat])
272 for r in subset:
273 for r in subset:
273 c = repo[r]
274 c = repo[r]
274 if fast:
275 if fast:
275 if pat not in c.files():
276 if pat not in c.files():
276 continue
277 continue
277 else:
278 else:
278 for f in c.files():
279 for f in c.files():
279 if m(f):
280 if m(f):
280 break
281 break
281 else:
282 else:
282 continue
283 continue
283 files = repo.status(c.p1().node(), c.node())[field]
284 files = repo.status(c.p1().node(), c.node())[field]
284 if fast:
285 if fast:
285 if pat in files:
286 if pat in files:
286 s.append(r)
287 s.append(r)
287 else:
288 else:
288 for f in files:
289 for f in files:
289 if m(f):
290 if m(f):
290 s.append(r)
291 s.append(r)
291 break
292 break
292 return s
293 return s
293
294
294 def children(repo, subset, x):
295 def children(repo, subset, x):
295 """``children(set)``
296 """``children(set)``
296 Child changesets of changesets in set.
297 Child changesets of changesets in set.
297 """
298 """
298 cs = set()
299 cs = set()
299 cl = repo.changelog
300 cl = repo.changelog
300 s = set(getset(repo, range(len(repo)), x))
301 s = set(getset(repo, range(len(repo)), x))
301 for r in xrange(0, len(repo)):
302 for r in xrange(0, len(repo)):
302 for p in cl.parentrevs(r):
303 for p in cl.parentrevs(r):
303 if p in s:
304 if p in s:
304 cs.add(r)
305 cs.add(r)
305 return [r for r in subset if r in cs]
306 return [r for r in subset if r in cs]
306
307
307 def closed(repo, subset, x):
308 def closed(repo, subset, x):
308 """``closed()``
309 """``closed()``
309 Changeset is closed.
310 Changeset is closed.
310 """
311 """
311 # i18n: "closed" is a keyword
312 # i18n: "closed" is a keyword
312 getargs(x, 0, 0, _("closed takes no arguments"))
313 getargs(x, 0, 0, _("closed takes no arguments"))
313 return [r for r in subset if repo[r].extra().get('close')]
314 return [r for r in subset if repo[r].extra().get('close')]
314
315
315 def contains(repo, subset, x):
316 def contains(repo, subset, x):
316 """``contains(pattern)``
317 """``contains(pattern)``
317 Revision contains pattern.
318 Revision contains pattern.
318 """
319 """
319 # i18n: "contains" is a keyword
320 # i18n: "contains" is a keyword
320 pat = getstring(x, _("contains requires a pattern"))
321 pat = getstring(x, _("contains requires a pattern"))
321 m = matchmod.match(repo.root, repo.getcwd(), [pat])
322 m = matchmod.match(repo.root, repo.getcwd(), [pat])
322 s = []
323 s = []
323 if m.files() == [pat]:
324 if m.files() == [pat]:
324 for r in subset:
325 for r in subset:
325 if pat in repo[r]:
326 if pat in repo[r]:
326 s.append(r)
327 s.append(r)
327 else:
328 else:
328 for r in subset:
329 for r in subset:
329 for f in repo[r].manifest():
330 for f in repo[r].manifest():
330 if m(f):
331 if m(f):
331 s.append(r)
332 s.append(r)
332 break
333 break
333 return s
334 return s
334
335
335 def date(repo, subset, x):
336 def date(repo, subset, x):
336 """``date(interval)``
337 """``date(interval)``
337 Changesets within the interval, see :hg:`help dates`.
338 Changesets within the interval, see :hg:`help dates`.
338 """
339 """
339 # i18n: "date" is a keyword
340 # i18n: "date" is a keyword
340 ds = getstring(x, _("date requires a string"))
341 ds = getstring(x, _("date requires a string"))
341 dm = util.matchdate(ds)
342 dm = util.matchdate(ds)
342 return [r for r in subset if dm(repo[r].date()[0])]
343 return [r for r in subset if dm(repo[r].date()[0])]
343
344
344 def descendants(repo, subset, x):
345 def descendants(repo, subset, x):
345 """``descendants(set)``
346 """``descendants(set)``
346 Changesets which are descendants of changesets in set.
347 Changesets which are descendants of changesets in set.
347 """
348 """
348 args = getset(repo, range(len(repo)), x)
349 args = getset(repo, range(len(repo)), x)
349 if not args:
350 if not args:
350 return []
351 return []
351 s = set(repo.changelog.descendants(*args)) | set(args)
352 s = set(repo.changelog.descendants(*args)) | set(args)
352 return [r for r in subset if r in s]
353 return [r for r in subset if r in s]
353
354
354 def follow(repo, subset, x):
355 def follow(repo, subset, x):
355 """``follow()``
356 """``follow()``
356 An alias for ``::.`` (ancestors of the working copy's first parent).
357 An alias for ``::.`` (ancestors of the working copy's first parent).
357 """
358 """
358 # i18n: "follow" is a keyword
359 # i18n: "follow" is a keyword
359 getargs(x, 0, 0, _("follow takes no arguments"))
360 getargs(x, 0, 0, _("follow takes no arguments"))
360 p = repo['.'].rev()
361 p = repo['.'].rev()
361 s = set(repo.changelog.ancestors(p)) | set([p])
362 s = set(repo.changelog.ancestors(p)) | set([p])
362 return [r for r in subset if r in s]
363 return [r for r in subset if r in s]
363
364
364 def getall(repo, subset, x):
365 def getall(repo, subset, x):
365 """``all()``
366 """``all()``
366 All changesets, the same as ``0:tip``.
367 All changesets, the same as ``0:tip``.
367 """
368 """
368 # i18n: "all" is a keyword
369 # i18n: "all" is a keyword
369 getargs(x, 0, 0, _("all takes no arguments"))
370 getargs(x, 0, 0, _("all takes no arguments"))
370 return subset
371 return subset
371
372
372 def grep(repo, subset, x):
373 def grep(repo, subset, x):
373 """``grep(regex)``
374 """``grep(regex)``
374 Like ``keyword(string)`` but accepts a regex. Use ``grep(r'...')``
375 Like ``keyword(string)`` but accepts a regex. Use ``grep(r'...')``
375 to ensure special escape characters are handled correctly.
376 to ensure special escape characters are handled correctly.
376 """
377 """
377 try:
378 try:
378 # i18n: "grep" is a keyword
379 # i18n: "grep" is a keyword
379 gr = re.compile(getstring(x, _("grep requires a string")))
380 gr = re.compile(getstring(x, _("grep requires a string")))
380 except re.error, e:
381 except re.error, e:
381 raise error.ParseError(_('invalid match pattern: %s') % e)
382 raise error.ParseError(_('invalid match pattern: %s') % e)
382 l = []
383 l = []
383 for r in subset:
384 for r in subset:
384 c = repo[r]
385 c = repo[r]
385 for e in c.files() + [c.user(), c.description()]:
386 for e in c.files() + [c.user(), c.description()]:
386 if gr.search(e):
387 if gr.search(e):
387 l.append(r)
388 l.append(r)
388 break
389 break
389 return l
390 return l
390
391
391 def hasfile(repo, subset, x):
392 def hasfile(repo, subset, x):
392 """``file(pattern)``
393 """``file(pattern)``
393 Changesets affecting files matched by pattern.
394 Changesets affecting files matched by pattern.
394 """
395 """
395 # i18n: "file" is a keyword
396 # i18n: "file" is a keyword
396 pat = getstring(x, _("file requires a pattern"))
397 pat = getstring(x, _("file requires a pattern"))
397 m = matchmod.match(repo.root, repo.getcwd(), [pat])
398 m = matchmod.match(repo.root, repo.getcwd(), [pat])
398 s = []
399 s = []
399 for r in subset:
400 for r in subset:
400 for f in repo[r].files():
401 for f in repo[r].files():
401 if m(f):
402 if m(f):
402 s.append(r)
403 s.append(r)
403 break
404 break
404 return s
405 return s
405
406
406 def head(repo, subset, x):
407 def head(repo, subset, x):
407 """``head()``
408 """``head()``
408 Changeset is a named branch head.
409 Changeset is a named branch head.
409 """
410 """
410 # i18n: "head" is a keyword
411 # i18n: "head" is a keyword
411 getargs(x, 0, 0, _("head takes no arguments"))
412 getargs(x, 0, 0, _("head takes no arguments"))
412 hs = set()
413 hs = set()
413 for b, ls in repo.branchmap().iteritems():
414 for b, ls in repo.branchmap().iteritems():
414 hs.update(repo[h].rev() for h in ls)
415 hs.update(repo[h].rev() for h in ls)
415 return [r for r in subset if r in hs]
416 return [r for r in subset if r in hs]
416
417
417 def heads(repo, subset, x):
418 def heads(repo, subset, x):
418 """``heads(set)``
419 """``heads(set)``
419 Members of set with no children in set.
420 Members of set with no children in set.
420 """
421 """
421 s = getset(repo, subset, x)
422 s = getset(repo, subset, x)
422 ps = set(parents(repo, subset, x))
423 ps = set(parents(repo, subset, x))
423 return [r for r in s if r not in ps]
424 return [r for r in s if r not in ps]
424
425
425 def keyword(repo, subset, x):
426 def keyword(repo, subset, x):
426 """``keyword(string)``
427 """``keyword(string)``
427 Search commit message, user name, and names of changed files for
428 Search commit message, user name, and names of changed files for
428 string.
429 string.
429 """
430 """
430 # i18n: "keyword" is a keyword
431 # i18n: "keyword" is a keyword
431 kw = getstring(x, _("keyword requires a string")).lower()
432 kw = getstring(x, _("keyword requires a string")).lower()
432 l = []
433 l = []
433 for r in subset:
434 for r in subset:
434 c = repo[r]
435 c = repo[r]
435 t = " ".join(c.files() + [c.user(), c.description()])
436 t = " ".join(c.files() + [c.user(), c.description()])
436 if kw in t.lower():
437 if kw in t.lower():
437 l.append(r)
438 l.append(r)
438 return l
439 return l
439
440
440 def limit(repo, subset, x):
441 def limit(repo, subset, x):
441 """``limit(set, n)``
442 """``limit(set, n)``
442 First n members of set.
443 First n members of set.
443 """
444 """
444 # i18n: "limit" is a keyword
445 # i18n: "limit" is a keyword
445 l = getargs(x, 2, 2, _("limit requires two arguments"))
446 l = getargs(x, 2, 2, _("limit requires two arguments"))
446 try:
447 try:
447 # i18n: "limit" is a keyword
448 # i18n: "limit" is a keyword
448 lim = int(getstring(l[1], _("limit requires a number")))
449 lim = int(getstring(l[1], _("limit requires a number")))
449 except ValueError:
450 except ValueError:
450 # i18n: "limit" is a keyword
451 # i18n: "limit" is a keyword
451 raise error.ParseError(_("limit expects a number"))
452 raise error.ParseError(_("limit expects a number"))
452 return getset(repo, subset, l[0])[:lim]
453 return getset(repo, subset, l[0])[:lim]
453
454
454 def maxrev(repo, subset, x):
455 def maxrev(repo, subset, x):
455 """``max(set)``
456 """``max(set)``
456 Changeset with highest revision number in set.
457 Changeset with highest revision number in set.
457 """
458 """
458 s = getset(repo, subset, x)
459 s = getset(repo, subset, x)
459 if s:
460 if s:
460 m = max(s)
461 m = max(s)
461 if m in subset:
462 if m in subset:
462 return [m]
463 return [m]
463 return []
464 return []
464
465
465 def merge(repo, subset, x):
466 def merge(repo, subset, x):
466 """``merge()``
467 """``merge()``
467 Changeset is a merge changeset.
468 Changeset is a merge changeset.
468 """
469 """
469 # i18n: "merge" is a keyword
470 # i18n: "merge" is a keyword
470 getargs(x, 0, 0, _("merge takes no arguments"))
471 getargs(x, 0, 0, _("merge takes no arguments"))
471 cl = repo.changelog
472 cl = repo.changelog
472 return [r for r in subset if cl.parentrevs(r)[1] != -1]
473 return [r for r in subset if cl.parentrevs(r)[1] != -1]
473
474
474 def minrev(repo, subset, x):
475 def minrev(repo, subset, x):
475 """``min(set)``
476 """``min(set)``
476 Changeset with lowest revision number in set.
477 Changeset with lowest revision number in set.
477 """
478 """
478 s = getset(repo, subset, x)
479 s = getset(repo, subset, x)
479 if s:
480 if s:
480 m = min(s)
481 m = min(s)
481 if m in subset:
482 if m in subset:
482 return [m]
483 return [m]
483 return []
484 return []
484
485
485 def modifies(repo, subset, x):
486 def modifies(repo, subset, x):
486 """``modifies(pattern)``
487 """``modifies(pattern)``
487 Changesets modifying files matched by pattern.
488 Changesets modifying files matched by pattern.
488 """
489 """
489 # i18n: "modifies" is a keyword
490 # i18n: "modifies" is a keyword
490 pat = getstring(x, _("modifies requires a pattern"))
491 pat = getstring(x, _("modifies requires a pattern"))
491 return checkstatus(repo, subset, pat, 0)
492 return checkstatus(repo, subset, pat, 0)
492
493
493 def node(repo, subset, x):
494 def node(repo, subset, x):
494 """``id(string)``
495 """``id(string)``
495 Revision non-ambiguously specified by the given hex string prefix.
496 Revision non-ambiguously specified by the given hex string prefix.
496 """
497 """
497 # i18n: "id" is a keyword
498 # i18n: "id" is a keyword
498 l = getargs(x, 1, 1, _("id requires one argument"))
499 l = getargs(x, 1, 1, _("id requires one argument"))
499 # i18n: "id" is a keyword
500 # i18n: "id" is a keyword
500 n = getstring(l[0], _("id requires a string"))
501 n = getstring(l[0], _("id requires a string"))
501 if len(n) == 40:
502 if len(n) == 40:
502 rn = repo[n].rev()
503 rn = repo[n].rev()
503 else:
504 else:
504 rn = repo.changelog.rev(repo.changelog._partialmatch(n))
505 rn = repo.changelog.rev(repo.changelog._partialmatch(n))
505 return [r for r in subset if r == rn]
506 return [r for r in subset if r == rn]
506
507
507 def outgoing(repo, subset, x):
508 def outgoing(repo, subset, x):
508 """``outgoing([path])``
509 """``outgoing([path])``
509 Changesets not found in the specified destination repository, or the
510 Changesets not found in the specified destination repository, or the
510 default push location.
511 default push location.
511 """
512 """
512 import hg # avoid start-up nasties
513 import hg # avoid start-up nasties
513 # i18n: "outgoing" is a keyword
514 # i18n: "outgoing" is a keyword
514 l = getargs(x, 0, 1, _("outgoing requires a repository path"))
515 l = getargs(x, 0, 1, _("outgoing requires a repository path"))
515 # i18n: "outgoing" is a keyword
516 # i18n: "outgoing" is a keyword
516 dest = l and getstring(l[0], _("outgoing requires a repository path")) or ''
517 dest = l and getstring(l[0], _("outgoing requires a repository path")) or ''
517 dest = repo.ui.expandpath(dest or 'default-push', dest or 'default')
518 dest = repo.ui.expandpath(dest or 'default-push', dest or 'default')
518 dest, branches = hg.parseurl(dest)
519 dest, branches = hg.parseurl(dest)
519 revs, checkout = hg.addbranchrevs(repo, repo, branches, [])
520 revs, checkout = hg.addbranchrevs(repo, repo, branches, [])
520 if revs:
521 if revs:
521 revs = [repo.lookup(rev) for rev in revs]
522 revs = [repo.lookup(rev) for rev in revs]
522 other = hg.repository(hg.remoteui(repo, {}), dest)
523 other = hg.repository(hg.remoteui(repo, {}), dest)
523 repo.ui.pushbuffer()
524 repo.ui.pushbuffer()
524 o = discovery.findoutgoing(repo, other)
525 o = discovery.findoutgoing(repo, other)
525 repo.ui.popbuffer()
526 repo.ui.popbuffer()
526 cl = repo.changelog
527 cl = repo.changelog
527 o = set([cl.rev(r) for r in repo.changelog.nodesbetween(o, revs)[0]])
528 o = set([cl.rev(r) for r in repo.changelog.nodesbetween(o, revs)[0]])
528 return [r for r in subset if r in o]
529 return [r for r in subset if r in o]
529
530
530 def p1(repo, subset, x):
531 def p1(repo, subset, x):
531 """``p1([set])``
532 """``p1([set])``
532 First parent of changesets in set, or the working directory.
533 First parent of changesets in set, or the working directory.
533 """
534 """
534 if x is None:
535 if x is None:
535 p = repo[x].p1().rev()
536 p = repo[x].p1().rev()
536 return [r for r in subset if r == p]
537 return [r for r in subset if r == p]
537
538
538 ps = set()
539 ps = set()
539 cl = repo.changelog
540 cl = repo.changelog
540 for r in getset(repo, range(len(repo)), x):
541 for r in getset(repo, range(len(repo)), x):
541 ps.add(cl.parentrevs(r)[0])
542 ps.add(cl.parentrevs(r)[0])
542 return [r for r in subset if r in ps]
543 return [r for r in subset if r in ps]
543
544
544 def p2(repo, subset, x):
545 def p2(repo, subset, x):
545 """``p2([set])``
546 """``p2([set])``
546 Second parent of changesets in set, or the working directory.
547 Second parent of changesets in set, or the working directory.
547 """
548 """
548 if x is None:
549 if x is None:
549 ps = repo[x].parents()
550 ps = repo[x].parents()
550 try:
551 try:
551 p = ps[1].rev()
552 p = ps[1].rev()
552 return [r for r in subset if r == p]
553 return [r for r in subset if r == p]
553 except IndexError:
554 except IndexError:
554 return []
555 return []
555
556
556 ps = set()
557 ps = set()
557 cl = repo.changelog
558 cl = repo.changelog
558 for r in getset(repo, range(len(repo)), x):
559 for r in getset(repo, range(len(repo)), x):
559 ps.add(cl.parentrevs(r)[1])
560 ps.add(cl.parentrevs(r)[1])
560 return [r for r in subset if r in ps]
561 return [r for r in subset if r in ps]
561
562
562 def parents(repo, subset, x):
563 def parents(repo, subset, x):
563 """``parents([set])``
564 """``parents([set])``
564 The set of all parents for all changesets in set, or the working directory.
565 The set of all parents for all changesets in set, or the working directory.
565 """
566 """
566 if x is None:
567 if x is None:
567 ps = tuple(p.rev() for p in repo[x].parents())
568 ps = tuple(p.rev() for p in repo[x].parents())
568 return [r for r in subset if r in ps]
569 return [r for r in subset if r in ps]
569
570
570 ps = set()
571 ps = set()
571 cl = repo.changelog
572 cl = repo.changelog
572 for r in getset(repo, range(len(repo)), x):
573 for r in getset(repo, range(len(repo)), x):
573 ps.update(cl.parentrevs(r))
574 ps.update(cl.parentrevs(r))
574 return [r for r in subset if r in ps]
575 return [r for r in subset if r in ps]
575
576
576 def present(repo, subset, x):
577 def present(repo, subset, x):
577 """``present(set)``
578 """``present(set)``
578 An empty set, if any revision in set isn't found; otherwise,
579 An empty set, if any revision in set isn't found; otherwise,
579 all revisions in set.
580 all revisions in set.
580 """
581 """
581 try:
582 try:
582 return getset(repo, subset, x)
583 return getset(repo, subset, x)
583 except error.RepoLookupError:
584 except error.RepoLookupError:
584 return []
585 return []
585
586
586 def removes(repo, subset, x):
587 def removes(repo, subset, x):
587 """``removes(pattern)``
588 """``removes(pattern)``
588 Changesets which remove files matching pattern.
589 Changesets which remove files matching pattern.
589 """
590 """
590 # i18n: "removes" is a keyword
591 # i18n: "removes" is a keyword
591 pat = getstring(x, _("removes requires a pattern"))
592 pat = getstring(x, _("removes requires a pattern"))
592 return checkstatus(repo, subset, pat, 2)
593 return checkstatus(repo, subset, pat, 2)
593
594
594 def rev(repo, subset, x):
595 def rev(repo, subset, x):
595 """``rev(number)``
596 """``rev(number)``
596 Revision with the given numeric identifier.
597 Revision with the given numeric identifier.
597 """
598 """
598 # i18n: "rev" is a keyword
599 # i18n: "rev" is a keyword
599 l = getargs(x, 1, 1, _("rev requires one argument"))
600 l = getargs(x, 1, 1, _("rev requires one argument"))
600 try:
601 try:
601 # i18n: "rev" is a keyword
602 # i18n: "rev" is a keyword
602 l = int(getstring(l[0], _("rev requires a number")))
603 l = int(getstring(l[0], _("rev requires a number")))
603 except ValueError:
604 except ValueError:
604 # i18n: "rev" is a keyword
605 # i18n: "rev" is a keyword
605 raise error.ParseError(_("rev expects a number"))
606 raise error.ParseError(_("rev expects a number"))
606 return [r for r in subset if r == l]
607 return [r for r in subset if r == l]
607
608
608 def reverse(repo, subset, x):
609 def reverse(repo, subset, x):
609 """``reverse(set)``
610 """``reverse(set)``
610 Reverse order of set.
611 Reverse order of set.
611 """
612 """
612 l = getset(repo, subset, x)
613 l = getset(repo, subset, x)
613 l.reverse()
614 l.reverse()
614 return l
615 return l
615
616
616 def roots(repo, subset, x):
617 def roots(repo, subset, x):
617 """``roots(set)``
618 """``roots(set)``
618 Changesets with no parent changeset in set.
619 Changesets with no parent changeset in set.
619 """
620 """
620 s = getset(repo, subset, x)
621 s = getset(repo, subset, x)
621 cs = set(children(repo, subset, x))
622 cs = set(children(repo, subset, x))
622 return [r for r in s if r not in cs]
623 return [r for r in s if r not in cs]
623
624
624 def sort(repo, subset, x):
625 def sort(repo, subset, x):
625 """``sort(set[, [-]key...])``
626 """``sort(set[, [-]key...])``
626 Sort set by keys. The default sort order is ascending, specify a key
627 Sort set by keys. The default sort order is ascending, specify a key
627 as ``-key`` to sort in descending order.
628 as ``-key`` to sort in descending order.
628
629
629 The keys can be:
630 The keys can be:
630
631
631 - ``rev`` for the revision number,
632 - ``rev`` for the revision number,
632 - ``branch`` for the branch name,
633 - ``branch`` for the branch name,
633 - ``desc`` for the commit message (description),
634 - ``desc`` for the commit message (description),
634 - ``user`` for user name (``author`` can be used as an alias),
635 - ``user`` for user name (``author`` can be used as an alias),
635 - ``date`` for the commit date
636 - ``date`` for the commit date
636 """
637 """
637 # i18n: "sort" is a keyword
638 # i18n: "sort" is a keyword
638 l = getargs(x, 1, 2, _("sort requires one or two arguments"))
639 l = getargs(x, 1, 2, _("sort requires one or two arguments"))
639 keys = "rev"
640 keys = "rev"
640 if len(l) == 2:
641 if len(l) == 2:
641 keys = getstring(l[1], _("sort spec must be a string"))
642 keys = getstring(l[1], _("sort spec must be a string"))
642
643
643 s = l[0]
644 s = l[0]
644 keys = keys.split()
645 keys = keys.split()
645 l = []
646 l = []
646 def invert(s):
647 def invert(s):
647 return "".join(chr(255 - ord(c)) for c in s)
648 return "".join(chr(255 - ord(c)) for c in s)
648 for r in getset(repo, subset, s):
649 for r in getset(repo, subset, s):
649 c = repo[r]
650 c = repo[r]
650 e = []
651 e = []
651 for k in keys:
652 for k in keys:
652 if k == 'rev':
653 if k == 'rev':
653 e.append(r)
654 e.append(r)
654 elif k == '-rev':
655 elif k == '-rev':
655 e.append(-r)
656 e.append(-r)
656 elif k == 'branch':
657 elif k == 'branch':
657 e.append(c.branch())
658 e.append(c.branch())
658 elif k == '-branch':
659 elif k == '-branch':
659 e.append(invert(c.branch()))
660 e.append(invert(c.branch()))
660 elif k == 'desc':
661 elif k == 'desc':
661 e.append(c.description())
662 e.append(c.description())
662 elif k == '-desc':
663 elif k == '-desc':
663 e.append(invert(c.description()))
664 e.append(invert(c.description()))
664 elif k in 'user author':
665 elif k in 'user author':
665 e.append(c.user())
666 e.append(c.user())
666 elif k in '-user -author':
667 elif k in '-user -author':
667 e.append(invert(c.user()))
668 e.append(invert(c.user()))
668 elif k == 'date':
669 elif k == 'date':
669 e.append(c.date()[0])
670 e.append(c.date()[0])
670 elif k == '-date':
671 elif k == '-date':
671 e.append(-c.date()[0])
672 e.append(-c.date()[0])
672 else:
673 else:
673 raise error.ParseError(_("unknown sort key %r") % k)
674 raise error.ParseError(_("unknown sort key %r") % k)
674 e.append(r)
675 e.append(r)
675 l.append(e)
676 l.append(e)
676 l.sort()
677 l.sort()
677 return [e[-1] for e in l]
678 return [e[-1] for e in l]
678
679
679 def tag(repo, subset, x):
680 def tag(repo, subset, x):
680 """``tag(name)``
681 """``tag(name)``
681 The specified tag by name, or all tagged revisions if no name is given.
682 The specified tag by name, or all tagged revisions if no name is given.
682 """
683 """
683 # i18n: "tag" is a keyword
684 # i18n: "tag" is a keyword
684 args = getargs(x, 0, 1, _("tag takes one or no arguments"))
685 args = getargs(x, 0, 1, _("tag takes one or no arguments"))
685 cl = repo.changelog
686 cl = repo.changelog
686 if args:
687 if args:
687 tn = getstring(args[0],
688 tn = getstring(args[0],
688 # i18n: "tag" is a keyword
689 # i18n: "tag" is a keyword
689 _('the argument to tag must be a string'))
690 _('the argument to tag must be a string'))
690 if not repo.tags().get(tn, None):
691 if not repo.tags().get(tn, None):
691 raise util.Abort(_("tag '%s' does not exist") % tn)
692 raise util.Abort(_("tag '%s' does not exist") % tn)
692 s = set([cl.rev(n) for t, n in repo.tagslist() if t == tn])
693 s = set([cl.rev(n) for t, n in repo.tagslist() if t == tn])
693 else:
694 else:
694 s = set([cl.rev(n) for t, n in repo.tagslist() if t != 'tip'])
695 s = set([cl.rev(n) for t, n in repo.tagslist() if t != 'tip'])
695 return [r for r in subset if r in s]
696 return [r for r in subset if r in s]
696
697
697 def tagged(repo, subset, x):
698 def tagged(repo, subset, x):
698 return tag(repo, subset, x)
699 return tag(repo, subset, x)
699
700
700 def user(repo, subset, x):
701 def user(repo, subset, x):
701 """``user(string)``
702 """``user(string)``
702 User name is string.
703 User name is string.
703 """
704 """
704 return author(repo, subset, x)
705 return author(repo, subset, x)
705
706
706 symbols = {
707 symbols = {
707 "adds": adds,
708 "adds": adds,
708 "all": getall,
709 "all": getall,
709 "ancestor": ancestor,
710 "ancestor": ancestor,
710 "ancestors": ancestors,
711 "ancestors": ancestors,
711 "author": author,
712 "author": author,
712 "bisected": bisected,
713 "bisected": bisected,
713 "bookmark": bookmark,
714 "bookmark": bookmark,
714 "branch": branch,
715 "branch": branch,
715 "children": children,
716 "children": children,
716 "closed": closed,
717 "closed": closed,
717 "contains": contains,
718 "contains": contains,
718 "date": date,
719 "date": date,
719 "descendants": descendants,
720 "descendants": descendants,
720 "file": hasfile,
721 "file": hasfile,
721 "follow": follow,
722 "follow": follow,
722 "grep": grep,
723 "grep": grep,
723 "head": head,
724 "head": head,
724 "heads": heads,
725 "heads": heads,
725 "keyword": keyword,
726 "keyword": keyword,
726 "limit": limit,
727 "limit": limit,
727 "max": maxrev,
728 "max": maxrev,
728 "min": minrev,
729 "min": minrev,
729 "merge": merge,
730 "merge": merge,
730 "modifies": modifies,
731 "modifies": modifies,
731 "id": node,
732 "id": node,
732 "outgoing": outgoing,
733 "outgoing": outgoing,
733 "p1": p1,
734 "p1": p1,
734 "p2": p2,
735 "p2": p2,
735 "parents": parents,
736 "parents": parents,
736 "present": present,
737 "present": present,
737 "removes": removes,
738 "removes": removes,
738 "reverse": reverse,
739 "reverse": reverse,
739 "rev": rev,
740 "rev": rev,
740 "roots": roots,
741 "roots": roots,
741 "sort": sort,
742 "sort": sort,
742 "tag": tag,
743 "tag": tag,
743 "tagged": tagged,
744 "tagged": tagged,
744 "user": user,
745 "user": user,
745 }
746 }
746
747
747 methods = {
748 methods = {
748 "range": rangeset,
749 "range": rangeset,
749 "string": stringset,
750 "string": stringset,
750 "symbol": symbolset,
751 "symbol": symbolset,
751 "and": andset,
752 "and": andset,
752 "or": orset,
753 "or": orset,
753 "not": notset,
754 "not": notset,
754 "list": listset,
755 "list": listset,
755 "func": func,
756 "func": func,
756 }
757 }
757
758
758 def optimize(x, small):
759 def optimize(x, small):
759 if x is None:
760 if x is None:
760 return 0, x
761 return 0, x
761
762
762 smallbonus = 1
763 smallbonus = 1
763 if small:
764 if small:
764 smallbonus = .5
765 smallbonus = .5
765
766
766 op = x[0]
767 op = x[0]
767 if op == 'minus':
768 if op == 'minus':
768 return optimize(('and', x[1], ('not', x[2])), small)
769 return optimize(('and', x[1], ('not', x[2])), small)
769 elif op == 'dagrange':
770 elif op == 'dagrange':
770 return optimize(('and', ('func', ('symbol', 'descendants'), x[1]),
771 return optimize(('and', ('func', ('symbol', 'descendants'), x[1]),
771 ('func', ('symbol', 'ancestors'), x[2])), small)
772 ('func', ('symbol', 'ancestors'), x[2])), small)
772 elif op == 'dagrangepre':
773 elif op == 'dagrangepre':
773 return optimize(('func', ('symbol', 'ancestors'), x[1]), small)
774 return optimize(('func', ('symbol', 'ancestors'), x[1]), small)
774 elif op == 'dagrangepost':
775 elif op == 'dagrangepost':
775 return optimize(('func', ('symbol', 'descendants'), x[1]), small)
776 return optimize(('func', ('symbol', 'descendants'), x[1]), small)
776 elif op == 'rangepre':
777 elif op == 'rangepre':
777 return optimize(('range', ('string', '0'), x[1]), small)
778 return optimize(('range', ('string', '0'), x[1]), small)
778 elif op == 'rangepost':
779 elif op == 'rangepost':
779 return optimize(('range', x[1], ('string', 'tip')), small)
780 return optimize(('range', x[1], ('string', 'tip')), small)
780 elif op == 'negate':
781 elif op == 'negate':
781 return optimize(('string',
782 return optimize(('string',
782 '-' + getstring(x[1], _("can't negate that"))), small)
783 '-' + getstring(x[1], _("can't negate that"))), small)
783 elif op in 'string symbol negate':
784 elif op in 'string symbol negate':
784 return smallbonus, x # single revisions are small
785 return smallbonus, x # single revisions are small
785 elif op == 'and' or op == 'dagrange':
786 elif op == 'and' or op == 'dagrange':
786 wa, ta = optimize(x[1], True)
787 wa, ta = optimize(x[1], True)
787 wb, tb = optimize(x[2], True)
788 wb, tb = optimize(x[2], True)
788 w = min(wa, wb)
789 w = min(wa, wb)
789 if wa > wb:
790 if wa > wb:
790 return w, (op, tb, ta)
791 return w, (op, tb, ta)
791 return w, (op, ta, tb)
792 return w, (op, ta, tb)
792 elif op == 'or':
793 elif op == 'or':
793 wa, ta = optimize(x[1], False)
794 wa, ta = optimize(x[1], False)
794 wb, tb = optimize(x[2], False)
795 wb, tb = optimize(x[2], False)
795 if wb < wa:
796 if wb < wa:
796 wb, wa = wa, wb
797 wb, wa = wa, wb
797 return max(wa, wb), (op, ta, tb)
798 return max(wa, wb), (op, ta, tb)
798 elif op == 'not':
799 elif op == 'not':
799 o = optimize(x[1], not small)
800 o = optimize(x[1], not small)
800 return o[0], (op, o[1])
801 return o[0], (op, o[1])
801 elif op == 'group':
802 elif op == 'group':
802 return optimize(x[1], small)
803 return optimize(x[1], small)
803 elif op in 'range list':
804 elif op in 'range list':
804 wa, ta = optimize(x[1], small)
805 wa, ta = optimize(x[1], small)
805 wb, tb = optimize(x[2], small)
806 wb, tb = optimize(x[2], small)
806 return wa + wb, (op, ta, tb)
807 return wa + wb, (op, ta, tb)
807 elif op == 'func':
808 elif op == 'func':
808 f = getstring(x[1], _("not a symbol"))
809 f = getstring(x[1], _("not a symbol"))
809 wa, ta = optimize(x[2], small)
810 wa, ta = optimize(x[2], small)
810 if f in "grep date user author keyword branch file outgoing closed":
811 if f in "grep date user author keyword branch file outgoing closed":
811 w = 10 # slow
812 w = 10 # slow
812 elif f in "modifies adds removes":
813 elif f in "modifies adds removes":
813 w = 30 # slower
814 w = 30 # slower
814 elif f == "contains":
815 elif f == "contains":
815 w = 100 # very slow
816 w = 100 # very slow
816 elif f == "ancestor":
817 elif f == "ancestor":
817 w = 1 * smallbonus
818 w = 1 * smallbonus
818 elif f in "reverse limit":
819 elif f in "reverse limit":
819 w = 0
820 w = 0
820 elif f in "sort":
821 elif f in "sort":
821 w = 10 # assume most sorts look at changelog
822 w = 10 # assume most sorts look at changelog
822 else:
823 else:
823 w = 1
824 w = 1
824 return w + wa, (op, x[1], ta)
825 return w + wa, (op, x[1], ta)
825 return 1, x
826 return 1, x
826
827
827 parse = parser.parser(tokenize, elements).parse
828 parse = parser.parser(tokenize, elements).parse
828
829
829 def match(spec):
830 def match(spec):
830 if not spec:
831 if not spec:
831 raise error.ParseError(_("empty query"))
832 raise error.ParseError(_("empty query"))
832 tree, pos = parse(spec)
833 tree, pos = parse(spec)
833 if (pos != len(spec)):
834 if (pos != len(spec)):
834 raise error.ParseError("invalid token", pos)
835 raise error.ParseError("invalid token", pos)
835 weight, tree = optimize(tree, True)
836 weight, tree = optimize(tree, True)
836 def mfunc(repo, subset):
837 def mfunc(repo, subset):
837 return getset(repo, subset, tree)
838 return getset(repo, subset, tree)
838 return mfunc
839 return mfunc
839
840
840 def makedoc(topic, doc):
841 def makedoc(topic, doc):
841 return help.makeitemsdoc(topic, doc, '.. predicatesmarker', symbols)
842 return help.makeitemsdoc(topic, doc, '.. predicatesmarker', symbols)
842
843
843 # tell hggettext to extract docstrings from these functions:
844 # tell hggettext to extract docstrings from these functions:
844 i18nfunctions = symbols.values()
845 i18nfunctions = symbols.values()
@@ -1,371 +1,376
1 $ HGENCODING=utf-8
1 $ HGENCODING=utf-8
2 $ export HGENCODING
2 $ export HGENCODING
3
3
4 $ try() {
4 $ try() {
5 > hg debugrevspec --debug $@
5 > hg debugrevspec --debug $@
6 > }
6 > }
7
7
8 $ log() {
8 $ log() {
9 > hg log --template '{rev}\n' -r "$1"
9 > hg log --template '{rev}\n' -r "$1"
10 > }
10 > }
11
11
12 $ hg init repo
12 $ hg init repo
13 $ cd repo
13 $ cd repo
14
14
15 $ echo a > a
15 $ echo a > a
16 $ hg branch a
16 $ hg branch a
17 marked working directory as branch a
17 marked working directory as branch a
18 $ hg ci -Aqm0
18 $ hg ci -Aqm0
19
19
20 $ echo b > b
20 $ echo b > b
21 $ hg branch b
21 $ hg branch b
22 marked working directory as branch b
22 marked working directory as branch b
23 $ hg ci -Aqm1
23 $ hg ci -Aqm1
24
24
25 $ rm a
25 $ rm a
26 $ hg branch a-b-c-
26 $ hg branch a-b-c-
27 marked working directory as branch a-b-c-
27 marked working directory as branch a-b-c-
28 $ hg ci -Aqm2 -u Bob
28 $ hg ci -Aqm2 -u Bob
29
29
30 $ hg co 1
30 $ hg co 1
31 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
31 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
32 $ hg branch +a+b+c+
32 $ hg branch +a+b+c+
33 marked working directory as branch +a+b+c+
33 marked working directory as branch +a+b+c+
34 $ hg ci -Aqm3
34 $ hg ci -Aqm3
35
35
36 $ hg co 2 # interleave
36 $ hg co 2 # interleave
37 0 files updated, 0 files merged, 1 files removed, 0 files unresolved
37 0 files updated, 0 files merged, 1 files removed, 0 files unresolved
38 $ echo bb > b
38 $ echo bb > b
39 $ hg branch -- -a-b-c-
39 $ hg branch -- -a-b-c-
40 marked working directory as branch -a-b-c-
40 marked working directory as branch -a-b-c-
41 $ hg ci -Aqm4 -d "May 12 2005"
41 $ hg ci -Aqm4 -d "May 12 2005"
42
42
43 $ hg co 3
43 $ hg co 3
44 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
44 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
45 $ hg branch /a/b/c/
45 $ hg branch /a/b/c/
46 marked working directory as branch /a/b/c/
46 marked working directory as branch /a/b/c/
47 $ hg ci -Aqm"5 bug"
47 $ hg ci -Aqm"5 bug"
48
48
49 $ hg merge 4
49 $ hg merge 4
50 1 files updated, 0 files merged, 1 files removed, 0 files unresolved
50 1 files updated, 0 files merged, 1 files removed, 0 files unresolved
51 (branch merge, don't forget to commit)
51 (branch merge, don't forget to commit)
52 $ hg branch _a_b_c_
52 $ hg branch _a_b_c_
53 marked working directory as branch _a_b_c_
53 marked working directory as branch _a_b_c_
54 $ hg ci -Aqm"6 issue619"
54 $ hg ci -Aqm"6 issue619"
55
55
56 $ hg branch .a.b.c.
56 $ hg branch .a.b.c.
57 marked working directory as branch .a.b.c.
57 marked working directory as branch .a.b.c.
58 $ hg ci -Aqm7
58 $ hg ci -Aqm7
59
59
60 $ hg branch all
60 $ hg branch all
61 marked working directory as branch all
61 marked working directory as branch all
62 $ hg ci --close-branch -Aqm8
62 $ hg ci --close-branch -Aqm8
63 abort: can only close branch heads
63 abort: can only close branch heads
64 [255]
64 [255]
65
65
66 $ hg co 4
66 $ hg co 4
67 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
67 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
68 $ hg branch é
68 $ hg branch é
69 marked working directory as branch \xc3\xa9 (esc)
69 marked working directory as branch \xc3\xa9 (esc)
70 $ hg ci -Aqm9
70 $ hg ci -Aqm9
71
71
72 $ hg tag -r6 1.0
72 $ hg tag -r6 1.0
73
73
74 $ hg clone --quiet -U -r 7 . ../remote1
74 $ hg clone --quiet -U -r 7 . ../remote1
75 $ hg clone --quiet -U -r 8 . ../remote2
75 $ hg clone --quiet -U -r 8 . ../remote2
76 $ echo "[paths]" >> .hg/hgrc
76 $ echo "[paths]" >> .hg/hgrc
77 $ echo "default = ../remote1" >> .hg/hgrc
77 $ echo "default = ../remote1" >> .hg/hgrc
78
78
79 names that should work without quoting
79 names that should work without quoting
80
80
81 $ try a
81 $ try a
82 ('symbol', 'a')
82 ('symbol', 'a')
83 0
83 0
84 $ try b-a
84 $ try b-a
85 ('minus', ('symbol', 'b'), ('symbol', 'a'))
85 ('minus', ('symbol', 'b'), ('symbol', 'a'))
86 1
86 1
87 $ try _a_b_c_
87 $ try _a_b_c_
88 ('symbol', '_a_b_c_')
88 ('symbol', '_a_b_c_')
89 6
89 6
90 $ try _a_b_c_-a
90 $ try _a_b_c_-a
91 ('minus', ('symbol', '_a_b_c_'), ('symbol', 'a'))
91 ('minus', ('symbol', '_a_b_c_'), ('symbol', 'a'))
92 6
92 6
93 $ try .a.b.c.
93 $ try .a.b.c.
94 ('symbol', '.a.b.c.')
94 ('symbol', '.a.b.c.')
95 7
95 7
96 $ try .a.b.c.-a
96 $ try .a.b.c.-a
97 ('minus', ('symbol', '.a.b.c.'), ('symbol', 'a'))
97 ('minus', ('symbol', '.a.b.c.'), ('symbol', 'a'))
98 7
98 7
99 $ try -- '-a-b-c-' # complains
99 $ try -- '-a-b-c-' # complains
100 hg: parse error at 7: not a prefix: end
100 hg: parse error at 7: not a prefix: end
101 [255]
101 [255]
102 $ log -a-b-c- # succeeds with fallback
102 $ log -a-b-c- # succeeds with fallback
103 4
103 4
104 $ try -- -a-b-c--a # complains
104 $ try -- -a-b-c--a # complains
105 ('minus', ('minus', ('minus', ('negate', ('symbol', 'a')), ('symbol', 'b')), ('symbol', 'c')), ('negate', ('symbol', 'a')))
105 ('minus', ('minus', ('minus', ('negate', ('symbol', 'a')), ('symbol', 'b')), ('symbol', 'c')), ('negate', ('symbol', 'a')))
106 abort: unknown revision '-a'!
106 abort: unknown revision '-a'!
107 [255]
107 [255]
108 $ try é
108 $ try é
109 ('symbol', '\xc3\xa9')
109 ('symbol', '\xc3\xa9')
110 9
110 9
111
111
112 quoting needed
112 quoting needed
113
113
114 $ try '"-a-b-c-"-a'
114 $ try '"-a-b-c-"-a'
115 ('minus', ('string', '-a-b-c-'), ('symbol', 'a'))
115 ('minus', ('string', '-a-b-c-'), ('symbol', 'a'))
116 4
116 4
117
117
118 $ log '1 or 2'
118 $ log '1 or 2'
119 1
119 1
120 2
120 2
121 $ log '1|2'
121 $ log '1|2'
122 1
122 1
123 2
123 2
124 $ log '1 and 2'
124 $ log '1 and 2'
125 $ log '1&2'
125 $ log '1&2'
126 $ try '1&2|3' # precedence - and is higher
126 $ try '1&2|3' # precedence - and is higher
127 ('or', ('and', ('symbol', '1'), ('symbol', '2')), ('symbol', '3'))
127 ('or', ('and', ('symbol', '1'), ('symbol', '2')), ('symbol', '3'))
128 3
128 3
129 $ try '1|2&3'
129 $ try '1|2&3'
130 ('or', ('symbol', '1'), ('and', ('symbol', '2'), ('symbol', '3')))
130 ('or', ('symbol', '1'), ('and', ('symbol', '2'), ('symbol', '3')))
131 1
131 1
132 $ try '1&2&3' # associativity
132 $ try '1&2&3' # associativity
133 ('and', ('and', ('symbol', '1'), ('symbol', '2')), ('symbol', '3'))
133 ('and', ('and', ('symbol', '1'), ('symbol', '2')), ('symbol', '3'))
134 $ try '1|(2|3)'
134 $ try '1|(2|3)'
135 ('or', ('symbol', '1'), ('group', ('or', ('symbol', '2'), ('symbol', '3'))))
135 ('or', ('symbol', '1'), ('group', ('or', ('symbol', '2'), ('symbol', '3'))))
136 1
136 1
137 2
137 2
138 3
138 3
139 $ log '1.0' # tag
139 $ log '1.0' # tag
140 6
140 6
141 $ log 'a' # branch
141 $ log 'a' # branch
142 0
142 0
143 $ log '2785f51ee'
143 $ log '2785f51ee'
144 0
144 0
145 $ log 'date(2005)'
145 $ log 'date(2005)'
146 4
146 4
147 $ log 'date(this is a test)'
147 $ log 'date(this is a test)'
148 hg: parse error at 10: unexpected token: symbol
148 hg: parse error at 10: unexpected token: symbol
149 [255]
149 [255]
150 $ log 'date()'
150 $ log 'date()'
151 hg: parse error: date requires a string
151 hg: parse error: date requires a string
152 [255]
152 [255]
153 $ log 'date'
153 $ log 'date'
154 hg: parse error: can't use date here
154 hg: parse error: can't use date here
155 [255]
155 [255]
156 $ log 'date('
156 $ log 'date('
157 hg: parse error at 5: not a prefix: end
157 hg: parse error at 5: not a prefix: end
158 [255]
158 [255]
159 $ log 'date(tip)'
159 $ log 'date(tip)'
160 abort: invalid date: 'tip'
160 abort: invalid date: 'tip'
161 [255]
161 [255]
162 $ log '"date"'
162 $ log '"date"'
163 abort: unknown revision 'date'!
163 abort: unknown revision 'date'!
164 [255]
164 [255]
165 $ log 'date(2005) and 1::'
165 $ log 'date(2005) and 1::'
166 4
166 4
167
167
168 $ log 'ancestor(1)'
168 $ log 'ancestor(1)'
169 hg: parse error: ancestor requires two arguments
169 hg: parse error: ancestor requires two arguments
170 [255]
170 [255]
171 $ log 'ancestor(4,5)'
171 $ log 'ancestor(4,5)'
172 1
172 1
173 $ log 'ancestor(4,5) and 4'
173 $ log 'ancestor(4,5) and 4'
174 $ log 'ancestors(5)'
174 $ log 'ancestors(5)'
175 0
175 0
176 1
176 1
177 3
177 3
178 5
178 5
179 $ log 'author(bob)'
179 $ log 'author(bob)'
180 2
180 2
181 $ log 'branch(é)'
181 $ log 'branch(é)'
182 8
182 8
183 9
183 9
184 $ log 'children(ancestor(4,5))'
184 $ log 'children(ancestor(4,5))'
185 2
185 2
186 3
186 3
187 $ log 'closed()'
187 $ log 'closed()'
188 $ log 'contains(a)'
188 $ log 'contains(a)'
189 0
189 0
190 1
190 1
191 3
191 3
192 5
192 5
193 $ log 'descendants(2 or 3)'
193 $ log 'descendants(2 or 3)'
194 2
194 2
195 3
195 3
196 4
196 4
197 5
197 5
198 6
198 6
199 7
199 7
200 8
200 8
201 9
201 9
202 $ log 'file(b)'
202 $ log 'file(b)'
203 1
203 1
204 4
204 4
205 $ log 'follow()'
205 $ log 'follow()'
206 0
206 0
207 1
207 1
208 2
208 2
209 4
209 4
210 8
210 8
211 9
211 9
212 $ log 'grep("issue\d+")'
212 $ log 'grep("issue\d+")'
213 6
213 6
214 $ try 'grep("(")' # invalid regular expression
214 $ try 'grep("(")' # invalid regular expression
215 ('func', ('symbol', 'grep'), ('string', '('))
215 ('func', ('symbol', 'grep'), ('string', '('))
216 hg: parse error: invalid match pattern: unbalanced parenthesis
216 hg: parse error: invalid match pattern: unbalanced parenthesis
217 [255]
217 [255]
218 $ try 'grep("\bissue\d+")'
218 $ try 'grep("\bissue\d+")'
219 ('func', ('symbol', 'grep'), ('string', '\x08issue\\d+'))
219 ('func', ('symbol', 'grep'), ('string', '\x08issue\\d+'))
220 $ try 'grep(r"\bissue\d+")'
220 $ try 'grep(r"\bissue\d+")'
221 ('func', ('symbol', 'grep'), ('string', '\\bissue\\d+'))
221 ('func', ('symbol', 'grep'), ('string', '\\bissue\\d+'))
222 6
222 6
223 $ try 'grep(r"\")'
223 $ try 'grep(r"\")'
224 hg: parse error at 7: unterminated string
224 hg: parse error at 7: unterminated string
225 [255]
225 [255]
226 $ log 'head()'
226 $ log 'head()'
227 0
227 0
228 1
228 1
229 2
229 2
230 3
230 3
231 4
231 4
232 5
232 5
233 6
233 6
234 7
234 7
235 9
235 9
236 $ log 'heads(6::)'
236 $ log 'heads(6::)'
237 7
237 7
238 $ log 'keyword(issue)'
238 $ log 'keyword(issue)'
239 6
239 6
240 $ log 'limit(head(), 1)'
240 $ log 'limit(head(), 1)'
241 0
241 0
242 $ log 'max(contains(a))'
242 $ log 'max(contains(a))'
243 5
243 5
244 $ log 'min(contains(a))'
244 $ log 'min(contains(a))'
245 0
245 0
246 $ log 'merge()'
246 $ log 'merge()'
247 6
247 6
248 $ log 'modifies(b)'
248 $ log 'modifies(b)'
249 4
249 4
250 $ log 'id(5)'
250 $ log 'id(5)'
251 2
251 2
252 $ log 'outgoing()'
252 $ log 'outgoing()'
253 8
253 8
254 9
254 9
255 $ log 'outgoing("../remote1")'
255 $ log 'outgoing("../remote1")'
256 8
256 8
257 9
257 9
258 $ log 'outgoing("../remote2")'
258 $ log 'outgoing("../remote2")'
259 3
259 3
260 5
260 5
261 6
261 6
262 7
262 7
263 9
263 9
264 $ log 'p1(merge())'
264 $ log 'p1(merge())'
265 5
265 5
266 $ log 'p2(merge())'
266 $ log 'p2(merge())'
267 4
267 4
268 $ log 'parents(merge())'
268 $ log 'parents(merge())'
269 4
269 4
270 5
270 5
271 $ log 'removes(a)'
271 $ log 'removes(a)'
272 2
272 2
273 6
273 6
274 $ log 'roots(all())'
274 $ log 'roots(all())'
275 0
275 0
276 $ log 'reverse(2 or 3 or 4 or 5)'
276 $ log 'reverse(2 or 3 or 4 or 5)'
277 5
277 5
278 4
278 4
279 3
279 3
280 2
280 2
281 $ log 'rev(5)'
281 $ log 'rev(5)'
282 5
282 5
283 $ log 'sort(limit(reverse(all()), 3))'
283 $ log 'sort(limit(reverse(all()), 3))'
284 7
284 7
285 8
285 8
286 9
286 9
287 $ log 'sort(2 or 3 or 4 or 5, date)'
287 $ log 'sort(2 or 3 or 4 or 5, date)'
288 2
288 2
289 3
289 3
290 5
290 5
291 4
291 4
292 $ log 'tagged()'
292 $ log 'tagged()'
293 6
293 6
294 $ log 'tag()'
294 $ log 'tag()'
295 6
295 6
296 $ log 'tag(1.0)'
296 $ log 'tag(1.0)'
297 6
297 6
298 $ log 'tag(tip)'
298 $ log 'tag(tip)'
299 9
299 9
300 $ log 'tag(unknown)'
300 $ log 'tag(unknown)'
301 abort: tag 'unknown' does not exist
301 abort: tag 'unknown' does not exist
302 [255]
302 [255]
303 $ log 'branch(unknown)'
303 $ log 'branch(unknown)'
304 abort: unknown revision 'unknown'!
304 abort: unknown revision 'unknown'!
305 [255]
305 [255]
306 $ log 'user(bob)'
306 $ log 'user(bob)'
307 2
307 2
308
308
309 $ log '4::8'
309 $ log '4::8'
310 4
310 4
311 8
311 8
312 $ log '4:8'
312 $ log '4:8'
313 4
313 4
314 5
314 5
315 6
315 6
316 7
316 7
317 8
317 8
318
318
319 $ log 'sort(!merge() & (modifies(b) | user(bob) | keyword(bug) | keyword(issue) & 1::9), "-date")'
319 $ log 'sort(!merge() & (modifies(b) | user(bob) | keyword(bug) | keyword(issue) & 1::9), "-date")'
320 4
320 4
321 2
321 2
322 5
322 5
323
323
324 $ log 'not 0 and 0:2'
324 $ log 'not 0 and 0:2'
325 1
325 1
326 2
326 2
327 $ log 'not 1 and 0:2'
327 $ log 'not 1 and 0:2'
328 0
328 0
329 2
329 2
330 $ log 'not 2 and 0:2'
330 $ log 'not 2 and 0:2'
331 0
331 0
332 1
332 1
333 $ log '(1 and 2)::'
333 $ log '(1 and 2)::'
334 $ log '(1 and 2):'
334 $ log '(1 and 2):'
335 $ log '(1 and 2):3'
335 $ log '(1 and 2):3'
336 $ log 'sort(head(), -rev)'
336 $ log 'sort(head(), -rev)'
337 9
337 9
338 7
338 7
339 6
339 6
340 5
340 5
341 4
341 4
342 3
342 3
343 2
343 2
344 1
344 1
345 0
345 0
346 $ log '4::8 - 8'
346 $ log '4::8 - 8'
347 4
347 4
348
348
349 issue2437
349 issue2437
350
350
351 $ log '3 and p1(5)'
351 $ log '3 and p1(5)'
352 3
352 3
353 $ log '4 and p2(6)'
353 $ log '4 and p2(6)'
354 4
354 4
355 $ log '1 and parents(:2)'
355 $ log '1 and parents(:2)'
356 1
356 1
357 $ log '2 and children(1:)'
357 $ log '2 and children(1:)'
358 2
358 2
359 $ log 'roots(all()) or roots(all())'
359 $ log 'roots(all()) or roots(all())'
360 0
360 0
361 $ log 'heads(branch(é)) or heads(branch(é))'
361 $ log 'heads(branch(é)) or heads(branch(é))'
362 9
362 9
363 $ log 'ancestors(8) and (heads(branch("-a-b-c-")) or heads(branch(é)))'
363 $ log 'ancestors(8) and (heads(branch("-a-b-c-")) or heads(branch(é)))'
364 4
364 4
365
365
366 issue2654: report a parse error if the revset was not completely parsed
366 issue2654: report a parse error if the revset was not completely parsed
367
367
368 $ log '1 OR 2'
368 $ log '1 OR 2'
369 hg: parse error at 2: invalid token
369 hg: parse error at 2: invalid token
370 [255]
370 [255]
371
371
372 or operator should preserve ordering:
373 $ log 'reverse(2::4) or tip'
374 4
375 2
376 9
General Comments 0
You need to be logged in to leave comments. Login now