##// END OF EJS Templates
templates: make {indent("", " ")} be empty...
Martin von Zweigbergk -
r44093:fa246ada default
parent child Browse files
Show More
@@ -1,549 +1,549 b''
1 # templatefilters.py - common template expansion filters
1 # templatefilters.py - common template expansion filters
2 #
2 #
3 # Copyright 2005-2008 Matt Mackall <mpm@selenic.com>
3 # Copyright 2005-2008 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 from __future__ import absolute_import
8 from __future__ import absolute_import
9
9
10 import os
10 import os
11 import re
11 import re
12 import time
12 import time
13
13
14 from .i18n import _
14 from .i18n import _
15 from . import (
15 from . import (
16 encoding,
16 encoding,
17 error,
17 error,
18 node,
18 node,
19 pycompat,
19 pycompat,
20 registrar,
20 registrar,
21 templateutil,
21 templateutil,
22 url,
22 url,
23 util,
23 util,
24 )
24 )
25 from .utils import (
25 from .utils import (
26 cborutil,
26 cborutil,
27 dateutil,
27 dateutil,
28 stringutil,
28 stringutil,
29 )
29 )
30
30
31 urlerr = util.urlerr
31 urlerr = util.urlerr
32 urlreq = util.urlreq
32 urlreq = util.urlreq
33
33
34 # filters are callables like:
34 # filters are callables like:
35 # fn(obj)
35 # fn(obj)
36 # with:
36 # with:
37 # obj - object to be filtered (text, date, list and so on)
37 # obj - object to be filtered (text, date, list and so on)
38 filters = {}
38 filters = {}
39
39
40 templatefilter = registrar.templatefilter(filters)
40 templatefilter = registrar.templatefilter(filters)
41
41
42
42
43 @templatefilter(b'addbreaks', intype=bytes)
43 @templatefilter(b'addbreaks', intype=bytes)
44 def addbreaks(text):
44 def addbreaks(text):
45 """Any text. Add an XHTML "<br />" tag before the end of
45 """Any text. Add an XHTML "<br />" tag before the end of
46 every line except the last.
46 every line except the last.
47 """
47 """
48 return text.replace(b'\n', b'<br/>\n')
48 return text.replace(b'\n', b'<br/>\n')
49
49
50
50
51 agescales = [
51 agescales = [
52 (b"year", 3600 * 24 * 365, b'Y'),
52 (b"year", 3600 * 24 * 365, b'Y'),
53 (b"month", 3600 * 24 * 30, b'M'),
53 (b"month", 3600 * 24 * 30, b'M'),
54 (b"week", 3600 * 24 * 7, b'W'),
54 (b"week", 3600 * 24 * 7, b'W'),
55 (b"day", 3600 * 24, b'd'),
55 (b"day", 3600 * 24, b'd'),
56 (b"hour", 3600, b'h'),
56 (b"hour", 3600, b'h'),
57 (b"minute", 60, b'm'),
57 (b"minute", 60, b'm'),
58 (b"second", 1, b's'),
58 (b"second", 1, b's'),
59 ]
59 ]
60
60
61
61
62 @templatefilter(b'age', intype=templateutil.date)
62 @templatefilter(b'age', intype=templateutil.date)
63 def age(date, abbrev=False):
63 def age(date, abbrev=False):
64 """Date. Returns a human-readable date/time difference between the
64 """Date. Returns a human-readable date/time difference between the
65 given date/time and the current date/time.
65 given date/time and the current date/time.
66 """
66 """
67
67
68 def plural(t, c):
68 def plural(t, c):
69 if c == 1:
69 if c == 1:
70 return t
70 return t
71 return t + b"s"
71 return t + b"s"
72
72
73 def fmt(t, c, a):
73 def fmt(t, c, a):
74 if abbrev:
74 if abbrev:
75 return b"%d%s" % (c, a)
75 return b"%d%s" % (c, a)
76 return b"%d %s" % (c, plural(t, c))
76 return b"%d %s" % (c, plural(t, c))
77
77
78 now = time.time()
78 now = time.time()
79 then = date[0]
79 then = date[0]
80 future = False
80 future = False
81 if then > now:
81 if then > now:
82 future = True
82 future = True
83 delta = max(1, int(then - now))
83 delta = max(1, int(then - now))
84 if delta > agescales[0][1] * 30:
84 if delta > agescales[0][1] * 30:
85 return b'in the distant future'
85 return b'in the distant future'
86 else:
86 else:
87 delta = max(1, int(now - then))
87 delta = max(1, int(now - then))
88 if delta > agescales[0][1] * 2:
88 if delta > agescales[0][1] * 2:
89 return dateutil.shortdate(date)
89 return dateutil.shortdate(date)
90
90
91 for t, s, a in agescales:
91 for t, s, a in agescales:
92 n = delta // s
92 n = delta // s
93 if n >= 2 or s == 1:
93 if n >= 2 or s == 1:
94 if future:
94 if future:
95 return b'%s from now' % fmt(t, n, a)
95 return b'%s from now' % fmt(t, n, a)
96 return b'%s ago' % fmt(t, n, a)
96 return b'%s ago' % fmt(t, n, a)
97
97
98
98
99 @templatefilter(b'basename', intype=bytes)
99 @templatefilter(b'basename', intype=bytes)
100 def basename(path):
100 def basename(path):
101 """Any text. Treats the text as a path, and returns the last
101 """Any text. Treats the text as a path, and returns the last
102 component of the path after splitting by the path separator.
102 component of the path after splitting by the path separator.
103 For example, "foo/bar/baz" becomes "baz" and "foo/bar//" becomes "".
103 For example, "foo/bar/baz" becomes "baz" and "foo/bar//" becomes "".
104 """
104 """
105 return os.path.basename(path)
105 return os.path.basename(path)
106
106
107
107
108 @templatefilter(b'cbor')
108 @templatefilter(b'cbor')
109 def cbor(obj):
109 def cbor(obj):
110 """Any object. Serializes the object to CBOR bytes."""
110 """Any object. Serializes the object to CBOR bytes."""
111 return b''.join(cborutil.streamencode(obj))
111 return b''.join(cborutil.streamencode(obj))
112
112
113
113
114 @templatefilter(b'commondir')
114 @templatefilter(b'commondir')
115 def commondir(filelist):
115 def commondir(filelist):
116 """List of text. Treats each list item as file name with /
116 """List of text. Treats each list item as file name with /
117 as path separator and returns the longest common directory
117 as path separator and returns the longest common directory
118 prefix shared by all list items.
118 prefix shared by all list items.
119 Returns the empty string if no common prefix exists.
119 Returns the empty string if no common prefix exists.
120
120
121 The list items are not normalized, i.e. "foo/../bar" is handled as
121 The list items are not normalized, i.e. "foo/../bar" is handled as
122 file "bar" in the directory "foo/..". Leading slashes are ignored.
122 file "bar" in the directory "foo/..". Leading slashes are ignored.
123
123
124 For example, ["foo/bar/baz", "foo/baz/bar"] becomes "foo" and
124 For example, ["foo/bar/baz", "foo/baz/bar"] becomes "foo" and
125 ["foo/bar", "baz"] becomes "".
125 ["foo/bar", "baz"] becomes "".
126 """
126 """
127
127
128 def common(a, b):
128 def common(a, b):
129 if len(a) > len(b):
129 if len(a) > len(b):
130 a = b[: len(a)]
130 a = b[: len(a)]
131 elif len(b) > len(a):
131 elif len(b) > len(a):
132 b = b[: len(a)]
132 b = b[: len(a)]
133 if a == b:
133 if a == b:
134 return a
134 return a
135 for i in pycompat.xrange(len(a)):
135 for i in pycompat.xrange(len(a)):
136 if a[i] != b[i]:
136 if a[i] != b[i]:
137 return a[:i]
137 return a[:i]
138 return a
138 return a
139
139
140 try:
140 try:
141 if not filelist:
141 if not filelist:
142 return b""
142 return b""
143 dirlist = [f.lstrip(b'/').split(b'/')[:-1] for f in filelist]
143 dirlist = [f.lstrip(b'/').split(b'/')[:-1] for f in filelist]
144 if len(dirlist) == 1:
144 if len(dirlist) == 1:
145 return b'/'.join(dirlist[0])
145 return b'/'.join(dirlist[0])
146 a = min(dirlist)
146 a = min(dirlist)
147 b = max(dirlist)
147 b = max(dirlist)
148 # The common prefix of a and b is shared with all
148 # The common prefix of a and b is shared with all
149 # elements of the list since Python sorts lexicographical
149 # elements of the list since Python sorts lexicographical
150 # and [1, x] after [1].
150 # and [1, x] after [1].
151 return b'/'.join(common(a, b))
151 return b'/'.join(common(a, b))
152 except TypeError:
152 except TypeError:
153 raise error.ParseError(_(b'argument is not a list of text'))
153 raise error.ParseError(_(b'argument is not a list of text'))
154
154
155
155
156 @templatefilter(b'count')
156 @templatefilter(b'count')
157 def count(i):
157 def count(i):
158 """List or text. Returns the length as an integer."""
158 """List or text. Returns the length as an integer."""
159 try:
159 try:
160 return len(i)
160 return len(i)
161 except TypeError:
161 except TypeError:
162 raise error.ParseError(_(b'not countable'))
162 raise error.ParseError(_(b'not countable'))
163
163
164
164
165 @templatefilter(b'dirname', intype=bytes)
165 @templatefilter(b'dirname', intype=bytes)
166 def dirname(path):
166 def dirname(path):
167 """Any text. Treats the text as a path, and strips the last
167 """Any text. Treats the text as a path, and strips the last
168 component of the path after splitting by the path separator.
168 component of the path after splitting by the path separator.
169 """
169 """
170 return os.path.dirname(path)
170 return os.path.dirname(path)
171
171
172
172
173 @templatefilter(b'domain', intype=bytes)
173 @templatefilter(b'domain', intype=bytes)
174 def domain(author):
174 def domain(author):
175 """Any text. Finds the first string that looks like an email
175 """Any text. Finds the first string that looks like an email
176 address, and extracts just the domain component. Example: ``User
176 address, and extracts just the domain component. Example: ``User
177 <user@example.com>`` becomes ``example.com``.
177 <user@example.com>`` becomes ``example.com``.
178 """
178 """
179 f = author.find(b'@')
179 f = author.find(b'@')
180 if f == -1:
180 if f == -1:
181 return b''
181 return b''
182 author = author[f + 1 :]
182 author = author[f + 1 :]
183 f = author.find(b'>')
183 f = author.find(b'>')
184 if f >= 0:
184 if f >= 0:
185 author = author[:f]
185 author = author[:f]
186 return author
186 return author
187
187
188
188
189 @templatefilter(b'email', intype=bytes)
189 @templatefilter(b'email', intype=bytes)
190 def email(text):
190 def email(text):
191 """Any text. Extracts the first string that looks like an email
191 """Any text. Extracts the first string that looks like an email
192 address. Example: ``User <user@example.com>`` becomes
192 address. Example: ``User <user@example.com>`` becomes
193 ``user@example.com``.
193 ``user@example.com``.
194 """
194 """
195 return stringutil.email(text)
195 return stringutil.email(text)
196
196
197
197
198 @templatefilter(b'escape', intype=bytes)
198 @templatefilter(b'escape', intype=bytes)
199 def escape(text):
199 def escape(text):
200 """Any text. Replaces the special XML/XHTML characters "&", "<"
200 """Any text. Replaces the special XML/XHTML characters "&", "<"
201 and ">" with XML entities, and filters out NUL characters.
201 and ">" with XML entities, and filters out NUL characters.
202 """
202 """
203 return url.escape(text.replace(b'\0', b''), True)
203 return url.escape(text.replace(b'\0', b''), True)
204
204
205
205
206 para_re = None
206 para_re = None
207 space_re = None
207 space_re = None
208
208
209
209
210 def fill(text, width, initindent=b'', hangindent=b''):
210 def fill(text, width, initindent=b'', hangindent=b''):
211 '''fill many paragraphs with optional indentation.'''
211 '''fill many paragraphs with optional indentation.'''
212 global para_re, space_re
212 global para_re, space_re
213 if para_re is None:
213 if para_re is None:
214 para_re = re.compile(b'(\n\n|\n\\s*[-*]\\s*)', re.M)
214 para_re = re.compile(b'(\n\n|\n\\s*[-*]\\s*)', re.M)
215 space_re = re.compile(br' +')
215 space_re = re.compile(br' +')
216
216
217 def findparas():
217 def findparas():
218 start = 0
218 start = 0
219 while True:
219 while True:
220 m = para_re.search(text, start)
220 m = para_re.search(text, start)
221 if not m:
221 if not m:
222 uctext = encoding.unifromlocal(text[start:])
222 uctext = encoding.unifromlocal(text[start:])
223 w = len(uctext)
223 w = len(uctext)
224 while w > 0 and uctext[w - 1].isspace():
224 while w > 0 and uctext[w - 1].isspace():
225 w -= 1
225 w -= 1
226 yield (
226 yield (
227 encoding.unitolocal(uctext[:w]),
227 encoding.unitolocal(uctext[:w]),
228 encoding.unitolocal(uctext[w:]),
228 encoding.unitolocal(uctext[w:]),
229 )
229 )
230 break
230 break
231 yield text[start : m.start(0)], m.group(1)
231 yield text[start : m.start(0)], m.group(1)
232 start = m.end(1)
232 start = m.end(1)
233
233
234 return b"".join(
234 return b"".join(
235 [
235 [
236 stringutil.wrap(
236 stringutil.wrap(
237 space_re.sub(b' ', stringutil.wrap(para, width)),
237 space_re.sub(b' ', stringutil.wrap(para, width)),
238 width,
238 width,
239 initindent,
239 initindent,
240 hangindent,
240 hangindent,
241 )
241 )
242 + rest
242 + rest
243 for para, rest in findparas()
243 for para, rest in findparas()
244 ]
244 ]
245 )
245 )
246
246
247
247
248 @templatefilter(b'fill68', intype=bytes)
248 @templatefilter(b'fill68', intype=bytes)
249 def fill68(text):
249 def fill68(text):
250 """Any text. Wraps the text to fit in 68 columns."""
250 """Any text. Wraps the text to fit in 68 columns."""
251 return fill(text, 68)
251 return fill(text, 68)
252
252
253
253
254 @templatefilter(b'fill76', intype=bytes)
254 @templatefilter(b'fill76', intype=bytes)
255 def fill76(text):
255 def fill76(text):
256 """Any text. Wraps the text to fit in 76 columns."""
256 """Any text. Wraps the text to fit in 76 columns."""
257 return fill(text, 76)
257 return fill(text, 76)
258
258
259
259
260 @templatefilter(b'firstline', intype=bytes)
260 @templatefilter(b'firstline', intype=bytes)
261 def firstline(text):
261 def firstline(text):
262 """Any text. Returns the first line of text."""
262 """Any text. Returns the first line of text."""
263 try:
263 try:
264 return text.splitlines(True)[0].rstrip(b'\r\n')
264 return text.splitlines(True)[0].rstrip(b'\r\n')
265 except IndexError:
265 except IndexError:
266 return b''
266 return b''
267
267
268
268
269 @templatefilter(b'hex', intype=bytes)
269 @templatefilter(b'hex', intype=bytes)
270 def hexfilter(text):
270 def hexfilter(text):
271 """Any text. Convert a binary Mercurial node identifier into
271 """Any text. Convert a binary Mercurial node identifier into
272 its long hexadecimal representation.
272 its long hexadecimal representation.
273 """
273 """
274 return node.hex(text)
274 return node.hex(text)
275
275
276
276
277 @templatefilter(b'hgdate', intype=templateutil.date)
277 @templatefilter(b'hgdate', intype=templateutil.date)
278 def hgdate(text):
278 def hgdate(text):
279 """Date. Returns the date as a pair of numbers: "1157407993
279 """Date. Returns the date as a pair of numbers: "1157407993
280 25200" (Unix timestamp, timezone offset).
280 25200" (Unix timestamp, timezone offset).
281 """
281 """
282 return b"%d %d" % text
282 return b"%d %d" % text
283
283
284
284
285 @templatefilter(b'isodate', intype=templateutil.date)
285 @templatefilter(b'isodate', intype=templateutil.date)
286 def isodate(text):
286 def isodate(text):
287 """Date. Returns the date in ISO 8601 format: "2009-08-18 13:00
287 """Date. Returns the date in ISO 8601 format: "2009-08-18 13:00
288 +0200".
288 +0200".
289 """
289 """
290 return dateutil.datestr(text, b'%Y-%m-%d %H:%M %1%2')
290 return dateutil.datestr(text, b'%Y-%m-%d %H:%M %1%2')
291
291
292
292
293 @templatefilter(b'isodatesec', intype=templateutil.date)
293 @templatefilter(b'isodatesec', intype=templateutil.date)
294 def isodatesec(text):
294 def isodatesec(text):
295 """Date. Returns the date in ISO 8601 format, including
295 """Date. Returns the date in ISO 8601 format, including
296 seconds: "2009-08-18 13:00:13 +0200". See also the rfc3339date
296 seconds: "2009-08-18 13:00:13 +0200". See also the rfc3339date
297 filter.
297 filter.
298 """
298 """
299 return dateutil.datestr(text, b'%Y-%m-%d %H:%M:%S %1%2')
299 return dateutil.datestr(text, b'%Y-%m-%d %H:%M:%S %1%2')
300
300
301
301
302 def indent(text, prefix):
302 def indent(text, prefix, firstline=b''):
303 '''indent each non-empty line of text after first with prefix.'''
303 '''indent each non-empty line of text after first with prefix.'''
304 lines = text.splitlines()
304 lines = text.splitlines()
305 num_lines = len(lines)
305 num_lines = len(lines)
306 endswithnewline = text[-1:] == b'\n'
306 endswithnewline = text[-1:] == b'\n'
307
307
308 def indenter():
308 def indenter():
309 for i in pycompat.xrange(num_lines):
309 for i in pycompat.xrange(num_lines):
310 l = lines[i]
310 l = lines[i]
311 if i and l.strip():
311 if l.strip():
312 yield prefix
312 yield prefix if i else firstline
313 yield l
313 yield l
314 if i < num_lines - 1 or endswithnewline:
314 if i < num_lines - 1 or endswithnewline:
315 yield b'\n'
315 yield b'\n'
316
316
317 return b"".join(indenter())
317 return b"".join(indenter())
318
318
319
319
320 @templatefilter(b'json')
320 @templatefilter(b'json')
321 def json(obj, paranoid=True):
321 def json(obj, paranoid=True):
322 """Any object. Serializes the object to a JSON formatted text."""
322 """Any object. Serializes the object to a JSON formatted text."""
323 if obj is None:
323 if obj is None:
324 return b'null'
324 return b'null'
325 elif obj is False:
325 elif obj is False:
326 return b'false'
326 return b'false'
327 elif obj is True:
327 elif obj is True:
328 return b'true'
328 return b'true'
329 elif isinstance(obj, (int, pycompat.long, float)):
329 elif isinstance(obj, (int, pycompat.long, float)):
330 return pycompat.bytestr(obj)
330 return pycompat.bytestr(obj)
331 elif isinstance(obj, bytes):
331 elif isinstance(obj, bytes):
332 return b'"%s"' % encoding.jsonescape(obj, paranoid=paranoid)
332 return b'"%s"' % encoding.jsonescape(obj, paranoid=paranoid)
333 elif isinstance(obj, type(u'')):
333 elif isinstance(obj, type(u'')):
334 raise error.ProgrammingError(
334 raise error.ProgrammingError(
335 b'Mercurial only does output with bytes: %r' % obj
335 b'Mercurial only does output with bytes: %r' % obj
336 )
336 )
337 elif util.safehasattr(obj, b'keys'):
337 elif util.safehasattr(obj, b'keys'):
338 out = [
338 out = [
339 b'"%s": %s'
339 b'"%s": %s'
340 % (encoding.jsonescape(k, paranoid=paranoid), json(v, paranoid))
340 % (encoding.jsonescape(k, paranoid=paranoid), json(v, paranoid))
341 for k, v in sorted(pycompat.iteritems(obj))
341 for k, v in sorted(pycompat.iteritems(obj))
342 ]
342 ]
343 return b'{' + b', '.join(out) + b'}'
343 return b'{' + b', '.join(out) + b'}'
344 elif util.safehasattr(obj, b'__iter__'):
344 elif util.safehasattr(obj, b'__iter__'):
345 out = [json(i, paranoid) for i in obj]
345 out = [json(i, paranoid) for i in obj]
346 return b'[' + b', '.join(out) + b']'
346 return b'[' + b', '.join(out) + b']'
347 raise error.ProgrammingError(b'cannot encode %r' % obj)
347 raise error.ProgrammingError(b'cannot encode %r' % obj)
348
348
349
349
350 @templatefilter(b'lower', intype=bytes)
350 @templatefilter(b'lower', intype=bytes)
351 def lower(text):
351 def lower(text):
352 """Any text. Converts the text to lowercase."""
352 """Any text. Converts the text to lowercase."""
353 return encoding.lower(text)
353 return encoding.lower(text)
354
354
355
355
356 @templatefilter(b'nonempty', intype=bytes)
356 @templatefilter(b'nonempty', intype=bytes)
357 def nonempty(text):
357 def nonempty(text):
358 """Any text. Returns '(none)' if the string is empty."""
358 """Any text. Returns '(none)' if the string is empty."""
359 return text or b"(none)"
359 return text or b"(none)"
360
360
361
361
362 @templatefilter(b'obfuscate', intype=bytes)
362 @templatefilter(b'obfuscate', intype=bytes)
363 def obfuscate(text):
363 def obfuscate(text):
364 """Any text. Returns the input text rendered as a sequence of
364 """Any text. Returns the input text rendered as a sequence of
365 XML entities.
365 XML entities.
366 """
366 """
367 text = pycompat.unicode(
367 text = pycompat.unicode(
368 text, pycompat.sysstr(encoding.encoding), r'replace'
368 text, pycompat.sysstr(encoding.encoding), r'replace'
369 )
369 )
370 return b''.join([b'&#%d;' % ord(c) for c in text])
370 return b''.join([b'&#%d;' % ord(c) for c in text])
371
371
372
372
373 @templatefilter(b'permissions', intype=bytes)
373 @templatefilter(b'permissions', intype=bytes)
374 def permissions(flags):
374 def permissions(flags):
375 if b"l" in flags:
375 if b"l" in flags:
376 return b"lrwxrwxrwx"
376 return b"lrwxrwxrwx"
377 if b"x" in flags:
377 if b"x" in flags:
378 return b"-rwxr-xr-x"
378 return b"-rwxr-xr-x"
379 return b"-rw-r--r--"
379 return b"-rw-r--r--"
380
380
381
381
382 @templatefilter(b'person', intype=bytes)
382 @templatefilter(b'person', intype=bytes)
383 def person(author):
383 def person(author):
384 """Any text. Returns the name before an email address,
384 """Any text. Returns the name before an email address,
385 interpreting it as per RFC 5322.
385 interpreting it as per RFC 5322.
386 """
386 """
387 return stringutil.person(author)
387 return stringutil.person(author)
388
388
389
389
390 @templatefilter(b'revescape', intype=bytes)
390 @templatefilter(b'revescape', intype=bytes)
391 def revescape(text):
391 def revescape(text):
392 """Any text. Escapes all "special" characters, except @.
392 """Any text. Escapes all "special" characters, except @.
393 Forward slashes are escaped twice to prevent web servers from prematurely
393 Forward slashes are escaped twice to prevent web servers from prematurely
394 unescaping them. For example, "@foo bar/baz" becomes "@foo%20bar%252Fbaz".
394 unescaping them. For example, "@foo bar/baz" becomes "@foo%20bar%252Fbaz".
395 """
395 """
396 return urlreq.quote(text, safe=b'/@').replace(b'/', b'%252F')
396 return urlreq.quote(text, safe=b'/@').replace(b'/', b'%252F')
397
397
398
398
399 @templatefilter(b'rfc3339date', intype=templateutil.date)
399 @templatefilter(b'rfc3339date', intype=templateutil.date)
400 def rfc3339date(text):
400 def rfc3339date(text):
401 """Date. Returns a date using the Internet date format
401 """Date. Returns a date using the Internet date format
402 specified in RFC 3339: "2009-08-18T13:00:13+02:00".
402 specified in RFC 3339: "2009-08-18T13:00:13+02:00".
403 """
403 """
404 return dateutil.datestr(text, b"%Y-%m-%dT%H:%M:%S%1:%2")
404 return dateutil.datestr(text, b"%Y-%m-%dT%H:%M:%S%1:%2")
405
405
406
406
407 @templatefilter(b'rfc822date', intype=templateutil.date)
407 @templatefilter(b'rfc822date', intype=templateutil.date)
408 def rfc822date(text):
408 def rfc822date(text):
409 """Date. Returns a date using the same format used in email
409 """Date. Returns a date using the same format used in email
410 headers: "Tue, 18 Aug 2009 13:00:13 +0200".
410 headers: "Tue, 18 Aug 2009 13:00:13 +0200".
411 """
411 """
412 return dateutil.datestr(text, b"%a, %d %b %Y %H:%M:%S %1%2")
412 return dateutil.datestr(text, b"%a, %d %b %Y %H:%M:%S %1%2")
413
413
414
414
415 @templatefilter(b'short', intype=bytes)
415 @templatefilter(b'short', intype=bytes)
416 def short(text):
416 def short(text):
417 """Changeset hash. Returns the short form of a changeset hash,
417 """Changeset hash. Returns the short form of a changeset hash,
418 i.e. a 12 hexadecimal digit string.
418 i.e. a 12 hexadecimal digit string.
419 """
419 """
420 return text[:12]
420 return text[:12]
421
421
422
422
423 @templatefilter(b'shortbisect', intype=bytes)
423 @templatefilter(b'shortbisect', intype=bytes)
424 def shortbisect(label):
424 def shortbisect(label):
425 """Any text. Treats `label` as a bisection status, and
425 """Any text. Treats `label` as a bisection status, and
426 returns a single-character representing the status (G: good, B: bad,
426 returns a single-character representing the status (G: good, B: bad,
427 S: skipped, U: untested, I: ignored). Returns single space if `text`
427 S: skipped, U: untested, I: ignored). Returns single space if `text`
428 is not a valid bisection status.
428 is not a valid bisection status.
429 """
429 """
430 if label:
430 if label:
431 return label[0:1].upper()
431 return label[0:1].upper()
432 return b' '
432 return b' '
433
433
434
434
435 @templatefilter(b'shortdate', intype=templateutil.date)
435 @templatefilter(b'shortdate', intype=templateutil.date)
436 def shortdate(text):
436 def shortdate(text):
437 """Date. Returns a date like "2006-09-18"."""
437 """Date. Returns a date like "2006-09-18"."""
438 return dateutil.shortdate(text)
438 return dateutil.shortdate(text)
439
439
440
440
441 @templatefilter(b'slashpath', intype=bytes)
441 @templatefilter(b'slashpath', intype=bytes)
442 def slashpath(path):
442 def slashpath(path):
443 """Any text. Replaces the native path separator with slash."""
443 """Any text. Replaces the native path separator with slash."""
444 return util.pconvert(path)
444 return util.pconvert(path)
445
445
446
446
447 @templatefilter(b'splitlines', intype=bytes)
447 @templatefilter(b'splitlines', intype=bytes)
448 def splitlines(text):
448 def splitlines(text):
449 """Any text. Split text into a list of lines."""
449 """Any text. Split text into a list of lines."""
450 return templateutil.hybridlist(text.splitlines(), name=b'line')
450 return templateutil.hybridlist(text.splitlines(), name=b'line')
451
451
452
452
453 @templatefilter(b'stringescape', intype=bytes)
453 @templatefilter(b'stringescape', intype=bytes)
454 def stringescape(text):
454 def stringescape(text):
455 return stringutil.escapestr(text)
455 return stringutil.escapestr(text)
456
456
457
457
458 @templatefilter(b'stringify', intype=bytes)
458 @templatefilter(b'stringify', intype=bytes)
459 def stringify(thing):
459 def stringify(thing):
460 """Any type. Turns the value into text by converting values into
460 """Any type. Turns the value into text by converting values into
461 text and concatenating them.
461 text and concatenating them.
462 """
462 """
463 return thing # coerced by the intype
463 return thing # coerced by the intype
464
464
465
465
466 @templatefilter(b'stripdir', intype=bytes)
466 @templatefilter(b'stripdir', intype=bytes)
467 def stripdir(text):
467 def stripdir(text):
468 """Treat the text as path and strip a directory level, if
468 """Treat the text as path and strip a directory level, if
469 possible. For example, "foo" and "foo/bar" becomes "foo".
469 possible. For example, "foo" and "foo/bar" becomes "foo".
470 """
470 """
471 dir = os.path.dirname(text)
471 dir = os.path.dirname(text)
472 if dir == b"":
472 if dir == b"":
473 return os.path.basename(text)
473 return os.path.basename(text)
474 else:
474 else:
475 return dir
475 return dir
476
476
477
477
478 @templatefilter(b'tabindent', intype=bytes)
478 @templatefilter(b'tabindent', intype=bytes)
479 def tabindent(text):
479 def tabindent(text):
480 """Any text. Returns the text, with every non-empty line
480 """Any text. Returns the text, with every non-empty line
481 except the first starting with a tab character.
481 except the first starting with a tab character.
482 """
482 """
483 return indent(text, b'\t')
483 return indent(text, b'\t')
484
484
485
485
486 @templatefilter(b'upper', intype=bytes)
486 @templatefilter(b'upper', intype=bytes)
487 def upper(text):
487 def upper(text):
488 """Any text. Converts the text to uppercase."""
488 """Any text. Converts the text to uppercase."""
489 return encoding.upper(text)
489 return encoding.upper(text)
490
490
491
491
492 @templatefilter(b'urlescape', intype=bytes)
492 @templatefilter(b'urlescape', intype=bytes)
493 def urlescape(text):
493 def urlescape(text):
494 """Any text. Escapes all "special" characters. For example,
494 """Any text. Escapes all "special" characters. For example,
495 "foo bar" becomes "foo%20bar".
495 "foo bar" becomes "foo%20bar".
496 """
496 """
497 return urlreq.quote(text)
497 return urlreq.quote(text)
498
498
499
499
500 @templatefilter(b'user', intype=bytes)
500 @templatefilter(b'user', intype=bytes)
501 def userfilter(text):
501 def userfilter(text):
502 """Any text. Returns a short representation of a user name or email
502 """Any text. Returns a short representation of a user name or email
503 address."""
503 address."""
504 return stringutil.shortuser(text)
504 return stringutil.shortuser(text)
505
505
506
506
507 @templatefilter(b'emailuser', intype=bytes)
507 @templatefilter(b'emailuser', intype=bytes)
508 def emailuser(text):
508 def emailuser(text):
509 """Any text. Returns the user portion of an email address."""
509 """Any text. Returns the user portion of an email address."""
510 return stringutil.emailuser(text)
510 return stringutil.emailuser(text)
511
511
512
512
513 @templatefilter(b'utf8', intype=bytes)
513 @templatefilter(b'utf8', intype=bytes)
514 def utf8(text):
514 def utf8(text):
515 """Any text. Converts from the local character encoding to UTF-8."""
515 """Any text. Converts from the local character encoding to UTF-8."""
516 return encoding.fromlocal(text)
516 return encoding.fromlocal(text)
517
517
518
518
519 @templatefilter(b'xmlescape', intype=bytes)
519 @templatefilter(b'xmlescape', intype=bytes)
520 def xmlescape(text):
520 def xmlescape(text):
521 text = (
521 text = (
522 text.replace(b'&', b'&amp;')
522 text.replace(b'&', b'&amp;')
523 .replace(b'<', b'&lt;')
523 .replace(b'<', b'&lt;')
524 .replace(b'>', b'&gt;')
524 .replace(b'>', b'&gt;')
525 .replace(b'"', b'&quot;')
525 .replace(b'"', b'&quot;')
526 .replace(b"'", b'&#39;')
526 .replace(b"'", b'&#39;')
527 ) # &apos; invalid in HTML
527 ) # &apos; invalid in HTML
528 return re.sub(b'[\x00-\x08\x0B\x0C\x0E-\x1F]', b' ', text)
528 return re.sub(b'[\x00-\x08\x0B\x0C\x0E-\x1F]', b' ', text)
529
529
530
530
531 def websub(text, websubtable):
531 def websub(text, websubtable):
532 """:websub: Any text. Only applies to hgweb. Applies the regular
532 """:websub: Any text. Only applies to hgweb. Applies the regular
533 expression replacements defined in the websub section.
533 expression replacements defined in the websub section.
534 """
534 """
535 if websubtable:
535 if websubtable:
536 for regexp, format in websubtable:
536 for regexp, format in websubtable:
537 text = regexp.sub(format, text)
537 text = regexp.sub(format, text)
538 return text
538 return text
539
539
540
540
541 def loadfilter(ui, extname, registrarobj):
541 def loadfilter(ui, extname, registrarobj):
542 """Load template filter from specified registrarobj
542 """Load template filter from specified registrarobj
543 """
543 """
544 for name, func in pycompat.iteritems(registrarobj._table):
544 for name, func in pycompat.iteritems(registrarobj._table):
545 filters[name] = func
545 filters[name] = func
546
546
547
547
548 # tell hggettext to extract docstrings from these functions:
548 # tell hggettext to extract docstrings from these functions:
549 i18nfunctions = filters.values()
549 i18nfunctions = filters.values()
@@ -1,882 +1,880 b''
1 # templatefuncs.py - common template functions
1 # templatefuncs.py - common template functions
2 #
2 #
3 # Copyright 2005, 2006 Matt Mackall <mpm@selenic.com>
3 # Copyright 2005, 2006 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 from __future__ import absolute_import
8 from __future__ import absolute_import
9
9
10 import re
10 import re
11
11
12 from .i18n import _
12 from .i18n import _
13 from .node import (
13 from .node import (
14 bin,
14 bin,
15 wdirid,
15 wdirid,
16 )
16 )
17 from . import (
17 from . import (
18 color,
18 color,
19 diffutil,
19 diffutil,
20 encoding,
20 encoding,
21 error,
21 error,
22 minirst,
22 minirst,
23 obsutil,
23 obsutil,
24 pycompat,
24 pycompat,
25 registrar,
25 registrar,
26 revset as revsetmod,
26 revset as revsetmod,
27 revsetlang,
27 revsetlang,
28 scmutil,
28 scmutil,
29 templatefilters,
29 templatefilters,
30 templatekw,
30 templatekw,
31 templateutil,
31 templateutil,
32 util,
32 util,
33 )
33 )
34 from .utils import (
34 from .utils import (
35 dateutil,
35 dateutil,
36 stringutil,
36 stringutil,
37 )
37 )
38
38
39 evalrawexp = templateutil.evalrawexp
39 evalrawexp = templateutil.evalrawexp
40 evalwrapped = templateutil.evalwrapped
40 evalwrapped = templateutil.evalwrapped
41 evalfuncarg = templateutil.evalfuncarg
41 evalfuncarg = templateutil.evalfuncarg
42 evalboolean = templateutil.evalboolean
42 evalboolean = templateutil.evalboolean
43 evaldate = templateutil.evaldate
43 evaldate = templateutil.evaldate
44 evalinteger = templateutil.evalinteger
44 evalinteger = templateutil.evalinteger
45 evalstring = templateutil.evalstring
45 evalstring = templateutil.evalstring
46 evalstringliteral = templateutil.evalstringliteral
46 evalstringliteral = templateutil.evalstringliteral
47
47
48 # dict of template built-in functions
48 # dict of template built-in functions
49 funcs = {}
49 funcs = {}
50 templatefunc = registrar.templatefunc(funcs)
50 templatefunc = registrar.templatefunc(funcs)
51
51
52
52
53 @templatefunc(b'date(date[, fmt])')
53 @templatefunc(b'date(date[, fmt])')
54 def date(context, mapping, args):
54 def date(context, mapping, args):
55 """Format a date. See :hg:`help dates` for formatting
55 """Format a date. See :hg:`help dates` for formatting
56 strings. The default is a Unix date format, including the timezone:
56 strings. The default is a Unix date format, including the timezone:
57 "Mon Sep 04 15:13:13 2006 0700"."""
57 "Mon Sep 04 15:13:13 2006 0700"."""
58 if not (1 <= len(args) <= 2):
58 if not (1 <= len(args) <= 2):
59 # i18n: "date" is a keyword
59 # i18n: "date" is a keyword
60 raise error.ParseError(_(b"date expects one or two arguments"))
60 raise error.ParseError(_(b"date expects one or two arguments"))
61
61
62 date = evaldate(
62 date = evaldate(
63 context,
63 context,
64 mapping,
64 mapping,
65 args[0],
65 args[0],
66 # i18n: "date" is a keyword
66 # i18n: "date" is a keyword
67 _(b"date expects a date information"),
67 _(b"date expects a date information"),
68 )
68 )
69 fmt = None
69 fmt = None
70 if len(args) == 2:
70 if len(args) == 2:
71 fmt = evalstring(context, mapping, args[1])
71 fmt = evalstring(context, mapping, args[1])
72 if fmt is None:
72 if fmt is None:
73 return dateutil.datestr(date)
73 return dateutil.datestr(date)
74 else:
74 else:
75 return dateutil.datestr(date, fmt)
75 return dateutil.datestr(date, fmt)
76
76
77
77
78 @templatefunc(b'dict([[key=]value...])', argspec=b'*args **kwargs')
78 @templatefunc(b'dict([[key=]value...])', argspec=b'*args **kwargs')
79 def dict_(context, mapping, args):
79 def dict_(context, mapping, args):
80 """Construct a dict from key-value pairs. A key may be omitted if
80 """Construct a dict from key-value pairs. A key may be omitted if
81 a value expression can provide an unambiguous name."""
81 a value expression can provide an unambiguous name."""
82 data = util.sortdict()
82 data = util.sortdict()
83
83
84 for v in args[b'args']:
84 for v in args[b'args']:
85 k = templateutil.findsymbolicname(v)
85 k = templateutil.findsymbolicname(v)
86 if not k:
86 if not k:
87 raise error.ParseError(_(b'dict key cannot be inferred'))
87 raise error.ParseError(_(b'dict key cannot be inferred'))
88 if k in data or k in args[b'kwargs']:
88 if k in data or k in args[b'kwargs']:
89 raise error.ParseError(_(b"duplicated dict key '%s' inferred") % k)
89 raise error.ParseError(_(b"duplicated dict key '%s' inferred") % k)
90 data[k] = evalfuncarg(context, mapping, v)
90 data[k] = evalfuncarg(context, mapping, v)
91
91
92 data.update(
92 data.update(
93 (k, evalfuncarg(context, mapping, v))
93 (k, evalfuncarg(context, mapping, v))
94 for k, v in pycompat.iteritems(args[b'kwargs'])
94 for k, v in pycompat.iteritems(args[b'kwargs'])
95 )
95 )
96 return templateutil.hybriddict(data)
96 return templateutil.hybriddict(data)
97
97
98
98
99 @templatefunc(
99 @templatefunc(
100 b'diff([includepattern [, excludepattern]])', requires={b'ctx', b'ui'}
100 b'diff([includepattern [, excludepattern]])', requires={b'ctx', b'ui'}
101 )
101 )
102 def diff(context, mapping, args):
102 def diff(context, mapping, args):
103 """Show a diff, optionally
103 """Show a diff, optionally
104 specifying files to include or exclude."""
104 specifying files to include or exclude."""
105 if len(args) > 2:
105 if len(args) > 2:
106 # i18n: "diff" is a keyword
106 # i18n: "diff" is a keyword
107 raise error.ParseError(_(b"diff expects zero, one, or two arguments"))
107 raise error.ParseError(_(b"diff expects zero, one, or two arguments"))
108
108
109 def getpatterns(i):
109 def getpatterns(i):
110 if i < len(args):
110 if i < len(args):
111 s = evalstring(context, mapping, args[i]).strip()
111 s = evalstring(context, mapping, args[i]).strip()
112 if s:
112 if s:
113 return [s]
113 return [s]
114 return []
114 return []
115
115
116 ctx = context.resource(mapping, b'ctx')
116 ctx = context.resource(mapping, b'ctx')
117 ui = context.resource(mapping, b'ui')
117 ui = context.resource(mapping, b'ui')
118 diffopts = diffutil.diffallopts(ui)
118 diffopts = diffutil.diffallopts(ui)
119 chunks = ctx.diff(
119 chunks = ctx.diff(
120 match=ctx.match([], getpatterns(0), getpatterns(1)), opts=diffopts
120 match=ctx.match([], getpatterns(0), getpatterns(1)), opts=diffopts
121 )
121 )
122
122
123 return b''.join(chunks)
123 return b''.join(chunks)
124
124
125
125
126 @templatefunc(
126 @templatefunc(
127 b'extdata(source)', argspec=b'source', requires={b'ctx', b'cache'}
127 b'extdata(source)', argspec=b'source', requires={b'ctx', b'cache'}
128 )
128 )
129 def extdata(context, mapping, args):
129 def extdata(context, mapping, args):
130 """Show a text read from the specified extdata source. (EXPERIMENTAL)"""
130 """Show a text read from the specified extdata source. (EXPERIMENTAL)"""
131 if b'source' not in args:
131 if b'source' not in args:
132 # i18n: "extdata" is a keyword
132 # i18n: "extdata" is a keyword
133 raise error.ParseError(_(b'extdata expects one argument'))
133 raise error.ParseError(_(b'extdata expects one argument'))
134
134
135 source = evalstring(context, mapping, args[b'source'])
135 source = evalstring(context, mapping, args[b'source'])
136 if not source:
136 if not source:
137 sym = templateutil.findsymbolicname(args[b'source'])
137 sym = templateutil.findsymbolicname(args[b'source'])
138 if sym:
138 if sym:
139 raise error.ParseError(
139 raise error.ParseError(
140 _(b'empty data source specified'),
140 _(b'empty data source specified'),
141 hint=_(b"did you mean extdata('%s')?") % sym,
141 hint=_(b"did you mean extdata('%s')?") % sym,
142 )
142 )
143 else:
143 else:
144 raise error.ParseError(_(b'empty data source specified'))
144 raise error.ParseError(_(b'empty data source specified'))
145 cache = context.resource(mapping, b'cache').setdefault(b'extdata', {})
145 cache = context.resource(mapping, b'cache').setdefault(b'extdata', {})
146 ctx = context.resource(mapping, b'ctx')
146 ctx = context.resource(mapping, b'ctx')
147 if source in cache:
147 if source in cache:
148 data = cache[source]
148 data = cache[source]
149 else:
149 else:
150 data = cache[source] = scmutil.extdatasource(ctx.repo(), source)
150 data = cache[source] = scmutil.extdatasource(ctx.repo(), source)
151 return data.get(ctx.rev(), b'')
151 return data.get(ctx.rev(), b'')
152
152
153
153
154 @templatefunc(b'files(pattern)', requires={b'ctx'})
154 @templatefunc(b'files(pattern)', requires={b'ctx'})
155 def files(context, mapping, args):
155 def files(context, mapping, args):
156 """All files of the current changeset matching the pattern. See
156 """All files of the current changeset matching the pattern. See
157 :hg:`help patterns`."""
157 :hg:`help patterns`."""
158 if not len(args) == 1:
158 if not len(args) == 1:
159 # i18n: "files" is a keyword
159 # i18n: "files" is a keyword
160 raise error.ParseError(_(b"files expects one argument"))
160 raise error.ParseError(_(b"files expects one argument"))
161
161
162 raw = evalstring(context, mapping, args[0])
162 raw = evalstring(context, mapping, args[0])
163 ctx = context.resource(mapping, b'ctx')
163 ctx = context.resource(mapping, b'ctx')
164 m = ctx.match([raw])
164 m = ctx.match([raw])
165 files = list(ctx.matches(m))
165 files = list(ctx.matches(m))
166 return templateutil.compatfileslist(context, mapping, b"file", files)
166 return templateutil.compatfileslist(context, mapping, b"file", files)
167
167
168
168
169 @templatefunc(b'fill(text[, width[, initialident[, hangindent]]])')
169 @templatefunc(b'fill(text[, width[, initialident[, hangindent]]])')
170 def fill(context, mapping, args):
170 def fill(context, mapping, args):
171 """Fill many
171 """Fill many
172 paragraphs with optional indentation. See the "fill" filter."""
172 paragraphs with optional indentation. See the "fill" filter."""
173 if not (1 <= len(args) <= 4):
173 if not (1 <= len(args) <= 4):
174 # i18n: "fill" is a keyword
174 # i18n: "fill" is a keyword
175 raise error.ParseError(_(b"fill expects one to four arguments"))
175 raise error.ParseError(_(b"fill expects one to four arguments"))
176
176
177 text = evalstring(context, mapping, args[0])
177 text = evalstring(context, mapping, args[0])
178 width = 76
178 width = 76
179 initindent = b''
179 initindent = b''
180 hangindent = b''
180 hangindent = b''
181 if 2 <= len(args) <= 4:
181 if 2 <= len(args) <= 4:
182 width = evalinteger(
182 width = evalinteger(
183 context,
183 context,
184 mapping,
184 mapping,
185 args[1],
185 args[1],
186 # i18n: "fill" is a keyword
186 # i18n: "fill" is a keyword
187 _(b"fill expects an integer width"),
187 _(b"fill expects an integer width"),
188 )
188 )
189 try:
189 try:
190 initindent = evalstring(context, mapping, args[2])
190 initindent = evalstring(context, mapping, args[2])
191 hangindent = evalstring(context, mapping, args[3])
191 hangindent = evalstring(context, mapping, args[3])
192 except IndexError:
192 except IndexError:
193 pass
193 pass
194
194
195 return templatefilters.fill(text, width, initindent, hangindent)
195 return templatefilters.fill(text, width, initindent, hangindent)
196
196
197
197
198 @templatefunc(b'filter(iterable[, expr])')
198 @templatefunc(b'filter(iterable[, expr])')
199 def filter_(context, mapping, args):
199 def filter_(context, mapping, args):
200 """Remove empty elements from a list or a dict. If expr specified, it's
200 """Remove empty elements from a list or a dict. If expr specified, it's
201 applied to each element to test emptiness."""
201 applied to each element to test emptiness."""
202 if not (1 <= len(args) <= 2):
202 if not (1 <= len(args) <= 2):
203 # i18n: "filter" is a keyword
203 # i18n: "filter" is a keyword
204 raise error.ParseError(_(b"filter expects one or two arguments"))
204 raise error.ParseError(_(b"filter expects one or two arguments"))
205 iterable = evalwrapped(context, mapping, args[0])
205 iterable = evalwrapped(context, mapping, args[0])
206 if len(args) == 1:
206 if len(args) == 1:
207
207
208 def select(w):
208 def select(w):
209 return w.tobool(context, mapping)
209 return w.tobool(context, mapping)
210
210
211 else:
211 else:
212
212
213 def select(w):
213 def select(w):
214 if not isinstance(w, templateutil.mappable):
214 if not isinstance(w, templateutil.mappable):
215 raise error.ParseError(_(b"not filterable by expression"))
215 raise error.ParseError(_(b"not filterable by expression"))
216 lm = context.overlaymap(mapping, w.tomap(context))
216 lm = context.overlaymap(mapping, w.tomap(context))
217 return evalboolean(context, lm, args[1])
217 return evalboolean(context, lm, args[1])
218
218
219 return iterable.filter(context, mapping, select)
219 return iterable.filter(context, mapping, select)
220
220
221
221
222 @templatefunc(b'formatnode(node)', requires={b'ui'})
222 @templatefunc(b'formatnode(node)', requires={b'ui'})
223 def formatnode(context, mapping, args):
223 def formatnode(context, mapping, args):
224 """Obtain the preferred form of a changeset hash. (DEPRECATED)"""
224 """Obtain the preferred form of a changeset hash. (DEPRECATED)"""
225 if len(args) != 1:
225 if len(args) != 1:
226 # i18n: "formatnode" is a keyword
226 # i18n: "formatnode" is a keyword
227 raise error.ParseError(_(b"formatnode expects one argument"))
227 raise error.ParseError(_(b"formatnode expects one argument"))
228
228
229 ui = context.resource(mapping, b'ui')
229 ui = context.resource(mapping, b'ui')
230 node = evalstring(context, mapping, args[0])
230 node = evalstring(context, mapping, args[0])
231 if ui.debugflag:
231 if ui.debugflag:
232 return node
232 return node
233 return templatefilters.short(node)
233 return templatefilters.short(node)
234
234
235
235
236 @templatefunc(b'mailmap(author)', requires={b'repo', b'cache'})
236 @templatefunc(b'mailmap(author)', requires={b'repo', b'cache'})
237 def mailmap(context, mapping, args):
237 def mailmap(context, mapping, args):
238 """Return the author, updated according to the value
238 """Return the author, updated according to the value
239 set in the .mailmap file"""
239 set in the .mailmap file"""
240 if len(args) != 1:
240 if len(args) != 1:
241 raise error.ParseError(_(b"mailmap expects one argument"))
241 raise error.ParseError(_(b"mailmap expects one argument"))
242
242
243 author = evalstring(context, mapping, args[0])
243 author = evalstring(context, mapping, args[0])
244
244
245 cache = context.resource(mapping, b'cache')
245 cache = context.resource(mapping, b'cache')
246 repo = context.resource(mapping, b'repo')
246 repo = context.resource(mapping, b'repo')
247
247
248 if b'mailmap' not in cache:
248 if b'mailmap' not in cache:
249 data = repo.wvfs.tryread(b'.mailmap')
249 data = repo.wvfs.tryread(b'.mailmap')
250 cache[b'mailmap'] = stringutil.parsemailmap(data)
250 cache[b'mailmap'] = stringutil.parsemailmap(data)
251
251
252 return stringutil.mapname(cache[b'mailmap'], author)
252 return stringutil.mapname(cache[b'mailmap'], author)
253
253
254
254
255 @templatefunc(
255 @templatefunc(
256 b'pad(text, width[, fillchar=\' \'[, left=False[, truncate=False]]])',
256 b'pad(text, width[, fillchar=\' \'[, left=False[, truncate=False]]])',
257 argspec=b'text width fillchar left truncate',
257 argspec=b'text width fillchar left truncate',
258 )
258 )
259 def pad(context, mapping, args):
259 def pad(context, mapping, args):
260 """Pad text with a
260 """Pad text with a
261 fill character."""
261 fill character."""
262 if b'text' not in args or b'width' not in args:
262 if b'text' not in args or b'width' not in args:
263 # i18n: "pad" is a keyword
263 # i18n: "pad" is a keyword
264 raise error.ParseError(_(b"pad() expects two to four arguments"))
264 raise error.ParseError(_(b"pad() expects two to four arguments"))
265
265
266 width = evalinteger(
266 width = evalinteger(
267 context,
267 context,
268 mapping,
268 mapping,
269 args[b'width'],
269 args[b'width'],
270 # i18n: "pad" is a keyword
270 # i18n: "pad" is a keyword
271 _(b"pad() expects an integer width"),
271 _(b"pad() expects an integer width"),
272 )
272 )
273
273
274 text = evalstring(context, mapping, args[b'text'])
274 text = evalstring(context, mapping, args[b'text'])
275
275
276 truncate = False
276 truncate = False
277 left = False
277 left = False
278 fillchar = b' '
278 fillchar = b' '
279 if b'fillchar' in args:
279 if b'fillchar' in args:
280 fillchar = evalstring(context, mapping, args[b'fillchar'])
280 fillchar = evalstring(context, mapping, args[b'fillchar'])
281 if len(color.stripeffects(fillchar)) != 1:
281 if len(color.stripeffects(fillchar)) != 1:
282 # i18n: "pad" is a keyword
282 # i18n: "pad" is a keyword
283 raise error.ParseError(_(b"pad() expects a single fill character"))
283 raise error.ParseError(_(b"pad() expects a single fill character"))
284 if b'left' in args:
284 if b'left' in args:
285 left = evalboolean(context, mapping, args[b'left'])
285 left = evalboolean(context, mapping, args[b'left'])
286 if b'truncate' in args:
286 if b'truncate' in args:
287 truncate = evalboolean(context, mapping, args[b'truncate'])
287 truncate = evalboolean(context, mapping, args[b'truncate'])
288
288
289 fillwidth = width - encoding.colwidth(color.stripeffects(text))
289 fillwidth = width - encoding.colwidth(color.stripeffects(text))
290 if fillwidth < 0 and truncate:
290 if fillwidth < 0 and truncate:
291 return encoding.trim(color.stripeffects(text), width, leftside=left)
291 return encoding.trim(color.stripeffects(text), width, leftside=left)
292 if fillwidth <= 0:
292 if fillwidth <= 0:
293 return text
293 return text
294 if left:
294 if left:
295 return fillchar * fillwidth + text
295 return fillchar * fillwidth + text
296 else:
296 else:
297 return text + fillchar * fillwidth
297 return text + fillchar * fillwidth
298
298
299
299
300 @templatefunc(b'indent(text, indentchars[, firstline])')
300 @templatefunc(b'indent(text, indentchars[, firstline])')
301 def indent(context, mapping, args):
301 def indent(context, mapping, args):
302 """Indents all non-empty lines
302 """Indents all non-empty lines
303 with the characters given in the indentchars string. An optional
303 with the characters given in the indentchars string. An optional
304 third parameter will override the indent for the first line only
304 third parameter will override the indent for the first line only
305 if present."""
305 if present."""
306 if not (2 <= len(args) <= 3):
306 if not (2 <= len(args) <= 3):
307 # i18n: "indent" is a keyword
307 # i18n: "indent" is a keyword
308 raise error.ParseError(_(b"indent() expects two or three arguments"))
308 raise error.ParseError(_(b"indent() expects two or three arguments"))
309
309
310 text = evalstring(context, mapping, args[0])
310 text = evalstring(context, mapping, args[0])
311 indent = evalstring(context, mapping, args[1])
311 indent = evalstring(context, mapping, args[1])
312
312
313 firstline = indent
313 if len(args) == 3:
314 if len(args) == 3:
314 firstline = evalstring(context, mapping, args[2])
315 firstline = evalstring(context, mapping, args[2])
315 else:
316 firstline = indent
317
316
318 # the indent function doesn't indent the first line, so we do it here
317 return templatefilters.indent(text, indent, firstline=firstline)
319 return templatefilters.indent(firstline + text, indent)
320
318
321
319
322 @templatefunc(b'get(dict, key)')
320 @templatefunc(b'get(dict, key)')
323 def get(context, mapping, args):
321 def get(context, mapping, args):
324 """Get an attribute/key from an object. Some keywords
322 """Get an attribute/key from an object. Some keywords
325 are complex types. This function allows you to obtain the value of an
323 are complex types. This function allows you to obtain the value of an
326 attribute on these types."""
324 attribute on these types."""
327 if len(args) != 2:
325 if len(args) != 2:
328 # i18n: "get" is a keyword
326 # i18n: "get" is a keyword
329 raise error.ParseError(_(b"get() expects two arguments"))
327 raise error.ParseError(_(b"get() expects two arguments"))
330
328
331 dictarg = evalwrapped(context, mapping, args[0])
329 dictarg = evalwrapped(context, mapping, args[0])
332 key = evalrawexp(context, mapping, args[1])
330 key = evalrawexp(context, mapping, args[1])
333 try:
331 try:
334 return dictarg.getmember(context, mapping, key)
332 return dictarg.getmember(context, mapping, key)
335 except error.ParseError as err:
333 except error.ParseError as err:
336 # i18n: "get" is a keyword
334 # i18n: "get" is a keyword
337 hint = _(b"get() expects a dict as first argument")
335 hint = _(b"get() expects a dict as first argument")
338 raise error.ParseError(bytes(err), hint=hint)
336 raise error.ParseError(bytes(err), hint=hint)
339
337
340
338
341 @templatefunc(b'config(section, name[, default])', requires={b'ui'})
339 @templatefunc(b'config(section, name[, default])', requires={b'ui'})
342 def config(context, mapping, args):
340 def config(context, mapping, args):
343 """Returns the requested hgrc config option as a string."""
341 """Returns the requested hgrc config option as a string."""
344 fn = context.resource(mapping, b'ui').config
342 fn = context.resource(mapping, b'ui').config
345 return _config(context, mapping, args, fn, evalstring)
343 return _config(context, mapping, args, fn, evalstring)
346
344
347
345
348 @templatefunc(b'configbool(section, name[, default])', requires={b'ui'})
346 @templatefunc(b'configbool(section, name[, default])', requires={b'ui'})
349 def configbool(context, mapping, args):
347 def configbool(context, mapping, args):
350 """Returns the requested hgrc config option as a boolean."""
348 """Returns the requested hgrc config option as a boolean."""
351 fn = context.resource(mapping, b'ui').configbool
349 fn = context.resource(mapping, b'ui').configbool
352 return _config(context, mapping, args, fn, evalboolean)
350 return _config(context, mapping, args, fn, evalboolean)
353
351
354
352
355 @templatefunc(b'configint(section, name[, default])', requires={b'ui'})
353 @templatefunc(b'configint(section, name[, default])', requires={b'ui'})
356 def configint(context, mapping, args):
354 def configint(context, mapping, args):
357 """Returns the requested hgrc config option as an integer."""
355 """Returns the requested hgrc config option as an integer."""
358 fn = context.resource(mapping, b'ui').configint
356 fn = context.resource(mapping, b'ui').configint
359 return _config(context, mapping, args, fn, evalinteger)
357 return _config(context, mapping, args, fn, evalinteger)
360
358
361
359
362 def _config(context, mapping, args, configfn, defaultfn):
360 def _config(context, mapping, args, configfn, defaultfn):
363 if not (2 <= len(args) <= 3):
361 if not (2 <= len(args) <= 3):
364 raise error.ParseError(_(b"config expects two or three arguments"))
362 raise error.ParseError(_(b"config expects two or three arguments"))
365
363
366 # The config option can come from any section, though we specifically
364 # The config option can come from any section, though we specifically
367 # reserve the [templateconfig] section for dynamically defining options
365 # reserve the [templateconfig] section for dynamically defining options
368 # for this function without also requiring an extension.
366 # for this function without also requiring an extension.
369 section = evalstringliteral(context, mapping, args[0])
367 section = evalstringliteral(context, mapping, args[0])
370 name = evalstringliteral(context, mapping, args[1])
368 name = evalstringliteral(context, mapping, args[1])
371 if len(args) == 3:
369 if len(args) == 3:
372 default = defaultfn(context, mapping, args[2])
370 default = defaultfn(context, mapping, args[2])
373 return configfn(section, name, default)
371 return configfn(section, name, default)
374 else:
372 else:
375 return configfn(section, name)
373 return configfn(section, name)
376
374
377
375
378 @templatefunc(b'if(expr, then[, else])')
376 @templatefunc(b'if(expr, then[, else])')
379 def if_(context, mapping, args):
377 def if_(context, mapping, args):
380 """Conditionally execute based on the result of
378 """Conditionally execute based on the result of
381 an expression."""
379 an expression."""
382 if not (2 <= len(args) <= 3):
380 if not (2 <= len(args) <= 3):
383 # i18n: "if" is a keyword
381 # i18n: "if" is a keyword
384 raise error.ParseError(_(b"if expects two or three arguments"))
382 raise error.ParseError(_(b"if expects two or three arguments"))
385
383
386 test = evalboolean(context, mapping, args[0])
384 test = evalboolean(context, mapping, args[0])
387 if test:
385 if test:
388 return evalrawexp(context, mapping, args[1])
386 return evalrawexp(context, mapping, args[1])
389 elif len(args) == 3:
387 elif len(args) == 3:
390 return evalrawexp(context, mapping, args[2])
388 return evalrawexp(context, mapping, args[2])
391
389
392
390
393 @templatefunc(b'ifcontains(needle, haystack, then[, else])')
391 @templatefunc(b'ifcontains(needle, haystack, then[, else])')
394 def ifcontains(context, mapping, args):
392 def ifcontains(context, mapping, args):
395 """Conditionally execute based
393 """Conditionally execute based
396 on whether the item "needle" is in "haystack"."""
394 on whether the item "needle" is in "haystack"."""
397 if not (3 <= len(args) <= 4):
395 if not (3 <= len(args) <= 4):
398 # i18n: "ifcontains" is a keyword
396 # i18n: "ifcontains" is a keyword
399 raise error.ParseError(_(b"ifcontains expects three or four arguments"))
397 raise error.ParseError(_(b"ifcontains expects three or four arguments"))
400
398
401 haystack = evalwrapped(context, mapping, args[1])
399 haystack = evalwrapped(context, mapping, args[1])
402 try:
400 try:
403 needle = evalrawexp(context, mapping, args[0])
401 needle = evalrawexp(context, mapping, args[0])
404 found = haystack.contains(context, mapping, needle)
402 found = haystack.contains(context, mapping, needle)
405 except error.ParseError:
403 except error.ParseError:
406 found = False
404 found = False
407
405
408 if found:
406 if found:
409 return evalrawexp(context, mapping, args[2])
407 return evalrawexp(context, mapping, args[2])
410 elif len(args) == 4:
408 elif len(args) == 4:
411 return evalrawexp(context, mapping, args[3])
409 return evalrawexp(context, mapping, args[3])
412
410
413
411
414 @templatefunc(b'ifeq(expr1, expr2, then[, else])')
412 @templatefunc(b'ifeq(expr1, expr2, then[, else])')
415 def ifeq(context, mapping, args):
413 def ifeq(context, mapping, args):
416 """Conditionally execute based on
414 """Conditionally execute based on
417 whether 2 items are equivalent."""
415 whether 2 items are equivalent."""
418 if not (3 <= len(args) <= 4):
416 if not (3 <= len(args) <= 4):
419 # i18n: "ifeq" is a keyword
417 # i18n: "ifeq" is a keyword
420 raise error.ParseError(_(b"ifeq expects three or four arguments"))
418 raise error.ParseError(_(b"ifeq expects three or four arguments"))
421
419
422 test = evalstring(context, mapping, args[0])
420 test = evalstring(context, mapping, args[0])
423 match = evalstring(context, mapping, args[1])
421 match = evalstring(context, mapping, args[1])
424 if test == match:
422 if test == match:
425 return evalrawexp(context, mapping, args[2])
423 return evalrawexp(context, mapping, args[2])
426 elif len(args) == 4:
424 elif len(args) == 4:
427 return evalrawexp(context, mapping, args[3])
425 return evalrawexp(context, mapping, args[3])
428
426
429
427
430 @templatefunc(b'join(list, sep)')
428 @templatefunc(b'join(list, sep)')
431 def join(context, mapping, args):
429 def join(context, mapping, args):
432 """Join items in a list with a delimiter."""
430 """Join items in a list with a delimiter."""
433 if not (1 <= len(args) <= 2):
431 if not (1 <= len(args) <= 2):
434 # i18n: "join" is a keyword
432 # i18n: "join" is a keyword
435 raise error.ParseError(_(b"join expects one or two arguments"))
433 raise error.ParseError(_(b"join expects one or two arguments"))
436
434
437 joinset = evalwrapped(context, mapping, args[0])
435 joinset = evalwrapped(context, mapping, args[0])
438 joiner = b" "
436 joiner = b" "
439 if len(args) > 1:
437 if len(args) > 1:
440 joiner = evalstring(context, mapping, args[1])
438 joiner = evalstring(context, mapping, args[1])
441 return joinset.join(context, mapping, joiner)
439 return joinset.join(context, mapping, joiner)
442
440
443
441
444 @templatefunc(b'label(label, expr)', requires={b'ui'})
442 @templatefunc(b'label(label, expr)', requires={b'ui'})
445 def label(context, mapping, args):
443 def label(context, mapping, args):
446 """Apply a label to generated content. Content with
444 """Apply a label to generated content. Content with
447 a label applied can result in additional post-processing, such as
445 a label applied can result in additional post-processing, such as
448 automatic colorization."""
446 automatic colorization."""
449 if len(args) != 2:
447 if len(args) != 2:
450 # i18n: "label" is a keyword
448 # i18n: "label" is a keyword
451 raise error.ParseError(_(b"label expects two arguments"))
449 raise error.ParseError(_(b"label expects two arguments"))
452
450
453 ui = context.resource(mapping, b'ui')
451 ui = context.resource(mapping, b'ui')
454 thing = evalstring(context, mapping, args[1])
452 thing = evalstring(context, mapping, args[1])
455 # preserve unknown symbol as literal so effects like 'red', 'bold',
453 # preserve unknown symbol as literal so effects like 'red', 'bold',
456 # etc. don't need to be quoted
454 # etc. don't need to be quoted
457 label = evalstringliteral(context, mapping, args[0])
455 label = evalstringliteral(context, mapping, args[0])
458
456
459 return ui.label(thing, label)
457 return ui.label(thing, label)
460
458
461
459
462 @templatefunc(b'latesttag([pattern])')
460 @templatefunc(b'latesttag([pattern])')
463 def latesttag(context, mapping, args):
461 def latesttag(context, mapping, args):
464 """The global tags matching the given pattern on the
462 """The global tags matching the given pattern on the
465 most recent globally tagged ancestor of this changeset.
463 most recent globally tagged ancestor of this changeset.
466 If no such tags exist, the "{tag}" template resolves to
464 If no such tags exist, the "{tag}" template resolves to
467 the string "null". See :hg:`help revisions.patterns` for the pattern
465 the string "null". See :hg:`help revisions.patterns` for the pattern
468 syntax.
466 syntax.
469 """
467 """
470 if len(args) > 1:
468 if len(args) > 1:
471 # i18n: "latesttag" is a keyword
469 # i18n: "latesttag" is a keyword
472 raise error.ParseError(_(b"latesttag expects at most one argument"))
470 raise error.ParseError(_(b"latesttag expects at most one argument"))
473
471
474 pattern = None
472 pattern = None
475 if len(args) == 1:
473 if len(args) == 1:
476 pattern = evalstring(context, mapping, args[0])
474 pattern = evalstring(context, mapping, args[0])
477 return templatekw.showlatesttags(context, mapping, pattern)
475 return templatekw.showlatesttags(context, mapping, pattern)
478
476
479
477
480 @templatefunc(b'localdate(date[, tz])')
478 @templatefunc(b'localdate(date[, tz])')
481 def localdate(context, mapping, args):
479 def localdate(context, mapping, args):
482 """Converts a date to the specified timezone.
480 """Converts a date to the specified timezone.
483 The default is local date."""
481 The default is local date."""
484 if not (1 <= len(args) <= 2):
482 if not (1 <= len(args) <= 2):
485 # i18n: "localdate" is a keyword
483 # i18n: "localdate" is a keyword
486 raise error.ParseError(_(b"localdate expects one or two arguments"))
484 raise error.ParseError(_(b"localdate expects one or two arguments"))
487
485
488 date = evaldate(
486 date = evaldate(
489 context,
487 context,
490 mapping,
488 mapping,
491 args[0],
489 args[0],
492 # i18n: "localdate" is a keyword
490 # i18n: "localdate" is a keyword
493 _(b"localdate expects a date information"),
491 _(b"localdate expects a date information"),
494 )
492 )
495 if len(args) >= 2:
493 if len(args) >= 2:
496 tzoffset = None
494 tzoffset = None
497 tz = evalfuncarg(context, mapping, args[1])
495 tz = evalfuncarg(context, mapping, args[1])
498 if isinstance(tz, bytes):
496 if isinstance(tz, bytes):
499 tzoffset, remainder = dateutil.parsetimezone(tz)
497 tzoffset, remainder = dateutil.parsetimezone(tz)
500 if remainder:
498 if remainder:
501 tzoffset = None
499 tzoffset = None
502 if tzoffset is None:
500 if tzoffset is None:
503 try:
501 try:
504 tzoffset = int(tz)
502 tzoffset = int(tz)
505 except (TypeError, ValueError):
503 except (TypeError, ValueError):
506 # i18n: "localdate" is a keyword
504 # i18n: "localdate" is a keyword
507 raise error.ParseError(_(b"localdate expects a timezone"))
505 raise error.ParseError(_(b"localdate expects a timezone"))
508 else:
506 else:
509 tzoffset = dateutil.makedate()[1]
507 tzoffset = dateutil.makedate()[1]
510 return templateutil.date((date[0], tzoffset))
508 return templateutil.date((date[0], tzoffset))
511
509
512
510
513 @templatefunc(b'max(iterable)')
511 @templatefunc(b'max(iterable)')
514 def max_(context, mapping, args, **kwargs):
512 def max_(context, mapping, args, **kwargs):
515 """Return the max of an iterable"""
513 """Return the max of an iterable"""
516 if len(args) != 1:
514 if len(args) != 1:
517 # i18n: "max" is a keyword
515 # i18n: "max" is a keyword
518 raise error.ParseError(_(b"max expects one argument"))
516 raise error.ParseError(_(b"max expects one argument"))
519
517
520 iterable = evalwrapped(context, mapping, args[0])
518 iterable = evalwrapped(context, mapping, args[0])
521 try:
519 try:
522 return iterable.getmax(context, mapping)
520 return iterable.getmax(context, mapping)
523 except error.ParseError as err:
521 except error.ParseError as err:
524 # i18n: "max" is a keyword
522 # i18n: "max" is a keyword
525 hint = _(b"max first argument should be an iterable")
523 hint = _(b"max first argument should be an iterable")
526 raise error.ParseError(bytes(err), hint=hint)
524 raise error.ParseError(bytes(err), hint=hint)
527
525
528
526
529 @templatefunc(b'min(iterable)')
527 @templatefunc(b'min(iterable)')
530 def min_(context, mapping, args, **kwargs):
528 def min_(context, mapping, args, **kwargs):
531 """Return the min of an iterable"""
529 """Return the min of an iterable"""
532 if len(args) != 1:
530 if len(args) != 1:
533 # i18n: "min" is a keyword
531 # i18n: "min" is a keyword
534 raise error.ParseError(_(b"min expects one argument"))
532 raise error.ParseError(_(b"min expects one argument"))
535
533
536 iterable = evalwrapped(context, mapping, args[0])
534 iterable = evalwrapped(context, mapping, args[0])
537 try:
535 try:
538 return iterable.getmin(context, mapping)
536 return iterable.getmin(context, mapping)
539 except error.ParseError as err:
537 except error.ParseError as err:
540 # i18n: "min" is a keyword
538 # i18n: "min" is a keyword
541 hint = _(b"min first argument should be an iterable")
539 hint = _(b"min first argument should be an iterable")
542 raise error.ParseError(bytes(err), hint=hint)
540 raise error.ParseError(bytes(err), hint=hint)
543
541
544
542
545 @templatefunc(b'mod(a, b)')
543 @templatefunc(b'mod(a, b)')
546 def mod(context, mapping, args):
544 def mod(context, mapping, args):
547 """Calculate a mod b such that a / b + a mod b == a"""
545 """Calculate a mod b such that a / b + a mod b == a"""
548 if not len(args) == 2:
546 if not len(args) == 2:
549 # i18n: "mod" is a keyword
547 # i18n: "mod" is a keyword
550 raise error.ParseError(_(b"mod expects two arguments"))
548 raise error.ParseError(_(b"mod expects two arguments"))
551
549
552 func = lambda a, b: a % b
550 func = lambda a, b: a % b
553 return templateutil.runarithmetic(
551 return templateutil.runarithmetic(
554 context, mapping, (func, args[0], args[1])
552 context, mapping, (func, args[0], args[1])
555 )
553 )
556
554
557
555
558 @templatefunc(b'obsfateoperations(markers)')
556 @templatefunc(b'obsfateoperations(markers)')
559 def obsfateoperations(context, mapping, args):
557 def obsfateoperations(context, mapping, args):
560 """Compute obsfate related information based on markers (EXPERIMENTAL)"""
558 """Compute obsfate related information based on markers (EXPERIMENTAL)"""
561 if len(args) != 1:
559 if len(args) != 1:
562 # i18n: "obsfateoperations" is a keyword
560 # i18n: "obsfateoperations" is a keyword
563 raise error.ParseError(_(b"obsfateoperations expects one argument"))
561 raise error.ParseError(_(b"obsfateoperations expects one argument"))
564
562
565 markers = evalfuncarg(context, mapping, args[0])
563 markers = evalfuncarg(context, mapping, args[0])
566
564
567 try:
565 try:
568 data = obsutil.markersoperations(markers)
566 data = obsutil.markersoperations(markers)
569 return templateutil.hybridlist(data, name=b'operation')
567 return templateutil.hybridlist(data, name=b'operation')
570 except (TypeError, KeyError):
568 except (TypeError, KeyError):
571 # i18n: "obsfateoperations" is a keyword
569 # i18n: "obsfateoperations" is a keyword
572 errmsg = _(b"obsfateoperations first argument should be an iterable")
570 errmsg = _(b"obsfateoperations first argument should be an iterable")
573 raise error.ParseError(errmsg)
571 raise error.ParseError(errmsg)
574
572
575
573
576 @templatefunc(b'obsfatedate(markers)')
574 @templatefunc(b'obsfatedate(markers)')
577 def obsfatedate(context, mapping, args):
575 def obsfatedate(context, mapping, args):
578 """Compute obsfate related information based on markers (EXPERIMENTAL)"""
576 """Compute obsfate related information based on markers (EXPERIMENTAL)"""
579 if len(args) != 1:
577 if len(args) != 1:
580 # i18n: "obsfatedate" is a keyword
578 # i18n: "obsfatedate" is a keyword
581 raise error.ParseError(_(b"obsfatedate expects one argument"))
579 raise error.ParseError(_(b"obsfatedate expects one argument"))
582
580
583 markers = evalfuncarg(context, mapping, args[0])
581 markers = evalfuncarg(context, mapping, args[0])
584
582
585 try:
583 try:
586 # TODO: maybe this has to be a wrapped list of date wrappers?
584 # TODO: maybe this has to be a wrapped list of date wrappers?
587 data = obsutil.markersdates(markers)
585 data = obsutil.markersdates(markers)
588 return templateutil.hybridlist(data, name=b'date', fmt=b'%d %d')
586 return templateutil.hybridlist(data, name=b'date', fmt=b'%d %d')
589 except (TypeError, KeyError):
587 except (TypeError, KeyError):
590 # i18n: "obsfatedate" is a keyword
588 # i18n: "obsfatedate" is a keyword
591 errmsg = _(b"obsfatedate first argument should be an iterable")
589 errmsg = _(b"obsfatedate first argument should be an iterable")
592 raise error.ParseError(errmsg)
590 raise error.ParseError(errmsg)
593
591
594
592
595 @templatefunc(b'obsfateusers(markers)')
593 @templatefunc(b'obsfateusers(markers)')
596 def obsfateusers(context, mapping, args):
594 def obsfateusers(context, mapping, args):
597 """Compute obsfate related information based on markers (EXPERIMENTAL)"""
595 """Compute obsfate related information based on markers (EXPERIMENTAL)"""
598 if len(args) != 1:
596 if len(args) != 1:
599 # i18n: "obsfateusers" is a keyword
597 # i18n: "obsfateusers" is a keyword
600 raise error.ParseError(_(b"obsfateusers expects one argument"))
598 raise error.ParseError(_(b"obsfateusers expects one argument"))
601
599
602 markers = evalfuncarg(context, mapping, args[0])
600 markers = evalfuncarg(context, mapping, args[0])
603
601
604 try:
602 try:
605 data = obsutil.markersusers(markers)
603 data = obsutil.markersusers(markers)
606 return templateutil.hybridlist(data, name=b'user')
604 return templateutil.hybridlist(data, name=b'user')
607 except (TypeError, KeyError, ValueError):
605 except (TypeError, KeyError, ValueError):
608 # i18n: "obsfateusers" is a keyword
606 # i18n: "obsfateusers" is a keyword
609 msg = _(
607 msg = _(
610 b"obsfateusers first argument should be an iterable of "
608 b"obsfateusers first argument should be an iterable of "
611 b"obsmakers"
609 b"obsmakers"
612 )
610 )
613 raise error.ParseError(msg)
611 raise error.ParseError(msg)
614
612
615
613
616 @templatefunc(b'obsfateverb(successors, markers)')
614 @templatefunc(b'obsfateverb(successors, markers)')
617 def obsfateverb(context, mapping, args):
615 def obsfateverb(context, mapping, args):
618 """Compute obsfate related information based on successors (EXPERIMENTAL)"""
616 """Compute obsfate related information based on successors (EXPERIMENTAL)"""
619 if len(args) != 2:
617 if len(args) != 2:
620 # i18n: "obsfateverb" is a keyword
618 # i18n: "obsfateverb" is a keyword
621 raise error.ParseError(_(b"obsfateverb expects two arguments"))
619 raise error.ParseError(_(b"obsfateverb expects two arguments"))
622
620
623 successors = evalfuncarg(context, mapping, args[0])
621 successors = evalfuncarg(context, mapping, args[0])
624 markers = evalfuncarg(context, mapping, args[1])
622 markers = evalfuncarg(context, mapping, args[1])
625
623
626 try:
624 try:
627 return obsutil.obsfateverb(successors, markers)
625 return obsutil.obsfateverb(successors, markers)
628 except TypeError:
626 except TypeError:
629 # i18n: "obsfateverb" is a keyword
627 # i18n: "obsfateverb" is a keyword
630 errmsg = _(b"obsfateverb first argument should be countable")
628 errmsg = _(b"obsfateverb first argument should be countable")
631 raise error.ParseError(errmsg)
629 raise error.ParseError(errmsg)
632
630
633
631
634 @templatefunc(b'relpath(path)', requires={b'repo'})
632 @templatefunc(b'relpath(path)', requires={b'repo'})
635 def relpath(context, mapping, args):
633 def relpath(context, mapping, args):
636 """Convert a repository-absolute path into a filesystem path relative to
634 """Convert a repository-absolute path into a filesystem path relative to
637 the current working directory."""
635 the current working directory."""
638 if len(args) != 1:
636 if len(args) != 1:
639 # i18n: "relpath" is a keyword
637 # i18n: "relpath" is a keyword
640 raise error.ParseError(_(b"relpath expects one argument"))
638 raise error.ParseError(_(b"relpath expects one argument"))
641
639
642 repo = context.resource(mapping, b'repo')
640 repo = context.resource(mapping, b'repo')
643 path = evalstring(context, mapping, args[0])
641 path = evalstring(context, mapping, args[0])
644 return repo.pathto(path)
642 return repo.pathto(path)
645
643
646
644
647 @templatefunc(b'revset(query[, formatargs...])', requires={b'repo', b'cache'})
645 @templatefunc(b'revset(query[, formatargs...])', requires={b'repo', b'cache'})
648 def revset(context, mapping, args):
646 def revset(context, mapping, args):
649 """Execute a revision set query. See
647 """Execute a revision set query. See
650 :hg:`help revset`."""
648 :hg:`help revset`."""
651 if not len(args) > 0:
649 if not len(args) > 0:
652 # i18n: "revset" is a keyword
650 # i18n: "revset" is a keyword
653 raise error.ParseError(_(b"revset expects one or more arguments"))
651 raise error.ParseError(_(b"revset expects one or more arguments"))
654
652
655 raw = evalstring(context, mapping, args[0])
653 raw = evalstring(context, mapping, args[0])
656 repo = context.resource(mapping, b'repo')
654 repo = context.resource(mapping, b'repo')
657
655
658 def query(expr):
656 def query(expr):
659 m = revsetmod.match(repo.ui, expr, lookup=revsetmod.lookupfn(repo))
657 m = revsetmod.match(repo.ui, expr, lookup=revsetmod.lookupfn(repo))
660 return m(repo)
658 return m(repo)
661
659
662 if len(args) > 1:
660 if len(args) > 1:
663 formatargs = [evalfuncarg(context, mapping, a) for a in args[1:]]
661 formatargs = [evalfuncarg(context, mapping, a) for a in args[1:]]
664 revs = query(revsetlang.formatspec(raw, *formatargs))
662 revs = query(revsetlang.formatspec(raw, *formatargs))
665 else:
663 else:
666 cache = context.resource(mapping, b'cache')
664 cache = context.resource(mapping, b'cache')
667 revsetcache = cache.setdefault(b"revsetcache", {})
665 revsetcache = cache.setdefault(b"revsetcache", {})
668 if raw in revsetcache:
666 if raw in revsetcache:
669 revs = revsetcache[raw]
667 revs = revsetcache[raw]
670 else:
668 else:
671 revs = query(raw)
669 revs = query(raw)
672 revsetcache[raw] = revs
670 revsetcache[raw] = revs
673 return templatekw.showrevslist(context, mapping, b"revision", revs)
671 return templatekw.showrevslist(context, mapping, b"revision", revs)
674
672
675
673
676 @templatefunc(b'rstdoc(text, style)')
674 @templatefunc(b'rstdoc(text, style)')
677 def rstdoc(context, mapping, args):
675 def rstdoc(context, mapping, args):
678 """Format reStructuredText."""
676 """Format reStructuredText."""
679 if len(args) != 2:
677 if len(args) != 2:
680 # i18n: "rstdoc" is a keyword
678 # i18n: "rstdoc" is a keyword
681 raise error.ParseError(_(b"rstdoc expects two arguments"))
679 raise error.ParseError(_(b"rstdoc expects two arguments"))
682
680
683 text = evalstring(context, mapping, args[0])
681 text = evalstring(context, mapping, args[0])
684 style = evalstring(context, mapping, args[1])
682 style = evalstring(context, mapping, args[1])
685
683
686 return minirst.format(text, style=style, keep=[b'verbose'])
684 return minirst.format(text, style=style, keep=[b'verbose'])
687
685
688
686
689 @templatefunc(b'search(pattern, text)')
687 @templatefunc(b'search(pattern, text)')
690 def search(context, mapping, args):
688 def search(context, mapping, args):
691 """Look for the first text matching the regular expression pattern.
689 """Look for the first text matching the regular expression pattern.
692 Groups are accessible as ``{1}``, ``{2}``, ... in %-mapped template."""
690 Groups are accessible as ``{1}``, ``{2}``, ... in %-mapped template."""
693 if len(args) != 2:
691 if len(args) != 2:
694 # i18n: "search" is a keyword
692 # i18n: "search" is a keyword
695 raise error.ParseError(_(b'search expects two arguments'))
693 raise error.ParseError(_(b'search expects two arguments'))
696
694
697 pat = evalstring(context, mapping, args[0])
695 pat = evalstring(context, mapping, args[0])
698 src = evalstring(context, mapping, args[1])
696 src = evalstring(context, mapping, args[1])
699 try:
697 try:
700 patre = re.compile(pat)
698 patre = re.compile(pat)
701 except re.error:
699 except re.error:
702 # i18n: "search" is a keyword
700 # i18n: "search" is a keyword
703 raise error.ParseError(_(b'search got an invalid pattern: %s') % pat)
701 raise error.ParseError(_(b'search got an invalid pattern: %s') % pat)
704 # named groups shouldn't shadow *reserved* resource keywords
702 # named groups shouldn't shadow *reserved* resource keywords
705 badgroups = context.knownresourcekeys() & set(
703 badgroups = context.knownresourcekeys() & set(
706 pycompat.byteskwargs(patre.groupindex)
704 pycompat.byteskwargs(patre.groupindex)
707 )
705 )
708 if badgroups:
706 if badgroups:
709 raise error.ParseError(
707 raise error.ParseError(
710 # i18n: "search" is a keyword
708 # i18n: "search" is a keyword
711 _(b'invalid group %(group)s in search pattern: %(pat)s')
709 _(b'invalid group %(group)s in search pattern: %(pat)s')
712 % {
710 % {
713 b'group': b', '.join(b"'%s'" % g for g in sorted(badgroups)),
711 b'group': b', '.join(b"'%s'" % g for g in sorted(badgroups)),
714 b'pat': pat,
712 b'pat': pat,
715 }
713 }
716 )
714 )
717
715
718 match = patre.search(src)
716 match = patre.search(src)
719 if not match:
717 if not match:
720 return templateutil.mappingnone()
718 return templateutil.mappingnone()
721
719
722 lm = {b'0': match.group(0)}
720 lm = {b'0': match.group(0)}
723 lm.update((b'%d' % i, v) for i, v in enumerate(match.groups(), 1))
721 lm.update((b'%d' % i, v) for i, v in enumerate(match.groups(), 1))
724 lm.update(pycompat.byteskwargs(match.groupdict()))
722 lm.update(pycompat.byteskwargs(match.groupdict()))
725 return templateutil.mappingdict(lm, tmpl=b'{0}')
723 return templateutil.mappingdict(lm, tmpl=b'{0}')
726
724
727
725
728 @templatefunc(b'separate(sep, args...)', argspec=b'sep *args')
726 @templatefunc(b'separate(sep, args...)', argspec=b'sep *args')
729 def separate(context, mapping, args):
727 def separate(context, mapping, args):
730 """Add a separator between non-empty arguments."""
728 """Add a separator between non-empty arguments."""
731 if b'sep' not in args:
729 if b'sep' not in args:
732 # i18n: "separate" is a keyword
730 # i18n: "separate" is a keyword
733 raise error.ParseError(_(b"separate expects at least one argument"))
731 raise error.ParseError(_(b"separate expects at least one argument"))
734
732
735 sep = evalstring(context, mapping, args[b'sep'])
733 sep = evalstring(context, mapping, args[b'sep'])
736 first = True
734 first = True
737 for arg in args[b'args']:
735 for arg in args[b'args']:
738 argstr = evalstring(context, mapping, arg)
736 argstr = evalstring(context, mapping, arg)
739 if not argstr:
737 if not argstr:
740 continue
738 continue
741 if first:
739 if first:
742 first = False
740 first = False
743 else:
741 else:
744 yield sep
742 yield sep
745 yield argstr
743 yield argstr
746
744
747
745
748 @templatefunc(b'shortest(node, minlength=4)', requires={b'repo', b'cache'})
746 @templatefunc(b'shortest(node, minlength=4)', requires={b'repo', b'cache'})
749 def shortest(context, mapping, args):
747 def shortest(context, mapping, args):
750 """Obtain the shortest representation of
748 """Obtain the shortest representation of
751 a node."""
749 a node."""
752 if not (1 <= len(args) <= 2):
750 if not (1 <= len(args) <= 2):
753 # i18n: "shortest" is a keyword
751 # i18n: "shortest" is a keyword
754 raise error.ParseError(_(b"shortest() expects one or two arguments"))
752 raise error.ParseError(_(b"shortest() expects one or two arguments"))
755
753
756 hexnode = evalstring(context, mapping, args[0])
754 hexnode = evalstring(context, mapping, args[0])
757
755
758 minlength = 4
756 minlength = 4
759 if len(args) > 1:
757 if len(args) > 1:
760 minlength = evalinteger(
758 minlength = evalinteger(
761 context,
759 context,
762 mapping,
760 mapping,
763 args[1],
761 args[1],
764 # i18n: "shortest" is a keyword
762 # i18n: "shortest" is a keyword
765 _(b"shortest() expects an integer minlength"),
763 _(b"shortest() expects an integer minlength"),
766 )
764 )
767
765
768 repo = context.resource(mapping, b'repo')
766 repo = context.resource(mapping, b'repo')
769 if len(hexnode) > 40:
767 if len(hexnode) > 40:
770 return hexnode
768 return hexnode
771 elif len(hexnode) == 40:
769 elif len(hexnode) == 40:
772 try:
770 try:
773 node = bin(hexnode)
771 node = bin(hexnode)
774 except TypeError:
772 except TypeError:
775 return hexnode
773 return hexnode
776 else:
774 else:
777 try:
775 try:
778 node = scmutil.resolvehexnodeidprefix(repo, hexnode)
776 node = scmutil.resolvehexnodeidprefix(repo, hexnode)
779 except error.WdirUnsupported:
777 except error.WdirUnsupported:
780 node = wdirid
778 node = wdirid
781 except error.LookupError:
779 except error.LookupError:
782 return hexnode
780 return hexnode
783 if not node:
781 if not node:
784 return hexnode
782 return hexnode
785 cache = context.resource(mapping, b'cache')
783 cache = context.resource(mapping, b'cache')
786 try:
784 try:
787 return scmutil.shortesthexnodeidprefix(repo, node, minlength, cache)
785 return scmutil.shortesthexnodeidprefix(repo, node, minlength, cache)
788 except error.RepoLookupError:
786 except error.RepoLookupError:
789 return hexnode
787 return hexnode
790
788
791
789
792 @templatefunc(b'strip(text[, chars])')
790 @templatefunc(b'strip(text[, chars])')
793 def strip(context, mapping, args):
791 def strip(context, mapping, args):
794 """Strip characters from a string. By default,
792 """Strip characters from a string. By default,
795 strips all leading and trailing whitespace."""
793 strips all leading and trailing whitespace."""
796 if not (1 <= len(args) <= 2):
794 if not (1 <= len(args) <= 2):
797 # i18n: "strip" is a keyword
795 # i18n: "strip" is a keyword
798 raise error.ParseError(_(b"strip expects one or two arguments"))
796 raise error.ParseError(_(b"strip expects one or two arguments"))
799
797
800 text = evalstring(context, mapping, args[0])
798 text = evalstring(context, mapping, args[0])
801 if len(args) == 2:
799 if len(args) == 2:
802 chars = evalstring(context, mapping, args[1])
800 chars = evalstring(context, mapping, args[1])
803 return text.strip(chars)
801 return text.strip(chars)
804 return text.strip()
802 return text.strip()
805
803
806
804
807 @templatefunc(b'sub(pattern, replacement, expression)')
805 @templatefunc(b'sub(pattern, replacement, expression)')
808 def sub(context, mapping, args):
806 def sub(context, mapping, args):
809 """Perform text substitution
807 """Perform text substitution
810 using regular expressions."""
808 using regular expressions."""
811 if len(args) != 3:
809 if len(args) != 3:
812 # i18n: "sub" is a keyword
810 # i18n: "sub" is a keyword
813 raise error.ParseError(_(b"sub expects three arguments"))
811 raise error.ParseError(_(b"sub expects three arguments"))
814
812
815 pat = evalstring(context, mapping, args[0])
813 pat = evalstring(context, mapping, args[0])
816 rpl = evalstring(context, mapping, args[1])
814 rpl = evalstring(context, mapping, args[1])
817 src = evalstring(context, mapping, args[2])
815 src = evalstring(context, mapping, args[2])
818 try:
816 try:
819 patre = re.compile(pat)
817 patre = re.compile(pat)
820 except re.error:
818 except re.error:
821 # i18n: "sub" is a keyword
819 # i18n: "sub" is a keyword
822 raise error.ParseError(_(b"sub got an invalid pattern: %s") % pat)
820 raise error.ParseError(_(b"sub got an invalid pattern: %s") % pat)
823 try:
821 try:
824 yield patre.sub(rpl, src)
822 yield patre.sub(rpl, src)
825 except re.error:
823 except re.error:
826 # i18n: "sub" is a keyword
824 # i18n: "sub" is a keyword
827 raise error.ParseError(_(b"sub got an invalid replacement: %s") % rpl)
825 raise error.ParseError(_(b"sub got an invalid replacement: %s") % rpl)
828
826
829
827
830 @templatefunc(b'startswith(pattern, text)')
828 @templatefunc(b'startswith(pattern, text)')
831 def startswith(context, mapping, args):
829 def startswith(context, mapping, args):
832 """Returns the value from the "text" argument
830 """Returns the value from the "text" argument
833 if it begins with the content from the "pattern" argument."""
831 if it begins with the content from the "pattern" argument."""
834 if len(args) != 2:
832 if len(args) != 2:
835 # i18n: "startswith" is a keyword
833 # i18n: "startswith" is a keyword
836 raise error.ParseError(_(b"startswith expects two arguments"))
834 raise error.ParseError(_(b"startswith expects two arguments"))
837
835
838 patn = evalstring(context, mapping, args[0])
836 patn = evalstring(context, mapping, args[0])
839 text = evalstring(context, mapping, args[1])
837 text = evalstring(context, mapping, args[1])
840 if text.startswith(patn):
838 if text.startswith(patn):
841 return text
839 return text
842 return b''
840 return b''
843
841
844
842
845 @templatefunc(b'word(number, text[, separator])')
843 @templatefunc(b'word(number, text[, separator])')
846 def word(context, mapping, args):
844 def word(context, mapping, args):
847 """Return the nth word from a string."""
845 """Return the nth word from a string."""
848 if not (2 <= len(args) <= 3):
846 if not (2 <= len(args) <= 3):
849 # i18n: "word" is a keyword
847 # i18n: "word" is a keyword
850 raise error.ParseError(
848 raise error.ParseError(
851 _(b"word expects two or three arguments, got %d") % len(args)
849 _(b"word expects two or three arguments, got %d") % len(args)
852 )
850 )
853
851
854 num = evalinteger(
852 num = evalinteger(
855 context,
853 context,
856 mapping,
854 mapping,
857 args[0],
855 args[0],
858 # i18n: "word" is a keyword
856 # i18n: "word" is a keyword
859 _(b"word expects an integer index"),
857 _(b"word expects an integer index"),
860 )
858 )
861 text = evalstring(context, mapping, args[1])
859 text = evalstring(context, mapping, args[1])
862 if len(args) == 3:
860 if len(args) == 3:
863 splitter = evalstring(context, mapping, args[2])
861 splitter = evalstring(context, mapping, args[2])
864 else:
862 else:
865 splitter = None
863 splitter = None
866
864
867 tokens = text.split(splitter)
865 tokens = text.split(splitter)
868 if num >= len(tokens) or num < -len(tokens):
866 if num >= len(tokens) or num < -len(tokens):
869 return b''
867 return b''
870 else:
868 else:
871 return tokens[num]
869 return tokens[num]
872
870
873
871
874 def loadfunction(ui, extname, registrarobj):
872 def loadfunction(ui, extname, registrarobj):
875 """Load template function from specified registrarobj
873 """Load template function from specified registrarobj
876 """
874 """
877 for name, func in pycompat.iteritems(registrarobj._table):
875 for name, func in pycompat.iteritems(registrarobj._table):
878 funcs[name] = func
876 funcs[name] = func
879
877
880
878
881 # tell hggettext to extract docstrings from these functions:
879 # tell hggettext to extract docstrings from these functions:
882 i18nfunctions = funcs.values()
880 i18nfunctions = funcs.values()
@@ -1,14 +1,17 b''
1 == New Features ==
1 == New Features ==
2
2
3
3
4 == New Experimental Features ==
4 == New Experimental Features ==
5
5
6
6
7 == Bug Fixes ==
7 == Bug Fixes ==
8
8
9 * The `indent()` template function was documented to not indent empty lines,
10 but it still indented the first line even if it was empty. It no longer does
11 that.
9
12
10 == Backwards Compatibility Changes ==
13 == Backwards Compatibility Changes ==
11
14
12
15
13 == Internal API Changes ==
16 == Internal API Changes ==
14
17
@@ -1,1653 +1,1653 b''
1 Test template filters and functions
1 Test template filters and functions
2 ===================================
2 ===================================
3
3
4 $ hg init a
4 $ hg init a
5 $ cd a
5 $ cd a
6 $ echo a > a
6 $ echo a > a
7 $ hg add a
7 $ hg add a
8 $ echo line 1 > b
8 $ echo line 1 > b
9 $ echo line 2 >> b
9 $ echo line 2 >> b
10 $ hg commit -l b -d '1000000 0' -u 'User Name <user@hostname>'
10 $ hg commit -l b -d '1000000 0' -u 'User Name <user@hostname>'
11
11
12 $ hg add b
12 $ hg add b
13 $ echo other 1 > c
13 $ echo other 1 > c
14 $ echo other 2 >> c
14 $ echo other 2 >> c
15 $ echo >> c
15 $ echo >> c
16 $ echo other 3 >> c
16 $ echo other 3 >> c
17 $ hg commit -l c -d '1100000 0' -u 'A. N. Other <other@place>'
17 $ hg commit -l c -d '1100000 0' -u 'A. N. Other <other@place>'
18
18
19 $ hg add c
19 $ hg add c
20 $ hg commit -m 'no person' -d '1200000 0' -u 'other@place'
20 $ hg commit -m 'no person' -d '1200000 0' -u 'other@place'
21 $ echo c >> c
21 $ echo c >> c
22 $ hg commit -m 'no user, no domain' -d '1300000 0' -u 'person'
22 $ hg commit -m 'no user, no domain' -d '1300000 0' -u 'person'
23
23
24 $ echo foo > .hg/branch
24 $ echo foo > .hg/branch
25 $ hg commit -m 'new branch' -d '1400000 0' -u 'person'
25 $ hg commit -m 'new branch' -d '1400000 0' -u 'person'
26
26
27 $ hg co -q 3
27 $ hg co -q 3
28 $ echo other 4 >> d
28 $ echo other 4 >> d
29 $ hg add d
29 $ hg add d
30 $ hg commit -m 'new head' -d '1500000 0' -u 'person'
30 $ hg commit -m 'new head' -d '1500000 0' -u 'person'
31
31
32 $ hg merge -q foo
32 $ hg merge -q foo
33 $ hg commit -m 'merge' -d '1500001 0' -u 'person'
33 $ hg commit -m 'merge' -d '1500001 0' -u 'person'
34
34
35 Second branch starting at nullrev:
35 Second branch starting at nullrev:
36
36
37 $ hg update null
37 $ hg update null
38 0 files updated, 0 files merged, 4 files removed, 0 files unresolved
38 0 files updated, 0 files merged, 4 files removed, 0 files unresolved
39 $ echo second > second
39 $ echo second > second
40 $ hg add second
40 $ hg add second
41 $ hg commit -m second -d '1000000 0' -u 'User Name <user@hostname>'
41 $ hg commit -m second -d '1000000 0' -u 'User Name <user@hostname>'
42 created new head
42 created new head
43
43
44 $ echo third > third
44 $ echo third > third
45 $ hg add third
45 $ hg add third
46 $ hg mv second fourth
46 $ hg mv second fourth
47 $ hg commit -m third -d "2020-01-01 10:01"
47 $ hg commit -m third -d "2020-01-01 10:01"
48
48
49 $ hg phase -r 5 --public
49 $ hg phase -r 5 --public
50 $ hg phase -r 7 --secret --force
50 $ hg phase -r 7 --secret --force
51
51
52 Filters work:
52 Filters work:
53
53
54 $ hg log --template '{author|domain}\n'
54 $ hg log --template '{author|domain}\n'
55
55
56 hostname
56 hostname
57
57
58
58
59
59
60
60
61 place
61 place
62 place
62 place
63 hostname
63 hostname
64
64
65 $ hg log --template '{author|person}\n'
65 $ hg log --template '{author|person}\n'
66 test
66 test
67 User Name
67 User Name
68 person
68 person
69 person
69 person
70 person
70 person
71 person
71 person
72 other
72 other
73 A. N. Other
73 A. N. Other
74 User Name
74 User Name
75
75
76 $ hg log --template '{author|user}\n'
76 $ hg log --template '{author|user}\n'
77 test
77 test
78 user
78 user
79 person
79 person
80 person
80 person
81 person
81 person
82 person
82 person
83 other
83 other
84 other
84 other
85 user
85 user
86
86
87 $ hg log --template '{date|date}\n'
87 $ hg log --template '{date|date}\n'
88 Wed Jan 01 10:01:00 2020 +0000
88 Wed Jan 01 10:01:00 2020 +0000
89 Mon Jan 12 13:46:40 1970 +0000
89 Mon Jan 12 13:46:40 1970 +0000
90 Sun Jan 18 08:40:01 1970 +0000
90 Sun Jan 18 08:40:01 1970 +0000
91 Sun Jan 18 08:40:00 1970 +0000
91 Sun Jan 18 08:40:00 1970 +0000
92 Sat Jan 17 04:53:20 1970 +0000
92 Sat Jan 17 04:53:20 1970 +0000
93 Fri Jan 16 01:06:40 1970 +0000
93 Fri Jan 16 01:06:40 1970 +0000
94 Wed Jan 14 21:20:00 1970 +0000
94 Wed Jan 14 21:20:00 1970 +0000
95 Tue Jan 13 17:33:20 1970 +0000
95 Tue Jan 13 17:33:20 1970 +0000
96 Mon Jan 12 13:46:40 1970 +0000
96 Mon Jan 12 13:46:40 1970 +0000
97
97
98 $ hg log --template '{date|isodate}\n'
98 $ hg log --template '{date|isodate}\n'
99 2020-01-01 10:01 +0000
99 2020-01-01 10:01 +0000
100 1970-01-12 13:46 +0000
100 1970-01-12 13:46 +0000
101 1970-01-18 08:40 +0000
101 1970-01-18 08:40 +0000
102 1970-01-18 08:40 +0000
102 1970-01-18 08:40 +0000
103 1970-01-17 04:53 +0000
103 1970-01-17 04:53 +0000
104 1970-01-16 01:06 +0000
104 1970-01-16 01:06 +0000
105 1970-01-14 21:20 +0000
105 1970-01-14 21:20 +0000
106 1970-01-13 17:33 +0000
106 1970-01-13 17:33 +0000
107 1970-01-12 13:46 +0000
107 1970-01-12 13:46 +0000
108
108
109 $ hg log --template '{date|isodatesec}\n'
109 $ hg log --template '{date|isodatesec}\n'
110 2020-01-01 10:01:00 +0000
110 2020-01-01 10:01:00 +0000
111 1970-01-12 13:46:40 +0000
111 1970-01-12 13:46:40 +0000
112 1970-01-18 08:40:01 +0000
112 1970-01-18 08:40:01 +0000
113 1970-01-18 08:40:00 +0000
113 1970-01-18 08:40:00 +0000
114 1970-01-17 04:53:20 +0000
114 1970-01-17 04:53:20 +0000
115 1970-01-16 01:06:40 +0000
115 1970-01-16 01:06:40 +0000
116 1970-01-14 21:20:00 +0000
116 1970-01-14 21:20:00 +0000
117 1970-01-13 17:33:20 +0000
117 1970-01-13 17:33:20 +0000
118 1970-01-12 13:46:40 +0000
118 1970-01-12 13:46:40 +0000
119
119
120 $ hg log --template '{date|rfc822date}\n'
120 $ hg log --template '{date|rfc822date}\n'
121 Wed, 01 Jan 2020 10:01:00 +0000
121 Wed, 01 Jan 2020 10:01:00 +0000
122 Mon, 12 Jan 1970 13:46:40 +0000
122 Mon, 12 Jan 1970 13:46:40 +0000
123 Sun, 18 Jan 1970 08:40:01 +0000
123 Sun, 18 Jan 1970 08:40:01 +0000
124 Sun, 18 Jan 1970 08:40:00 +0000
124 Sun, 18 Jan 1970 08:40:00 +0000
125 Sat, 17 Jan 1970 04:53:20 +0000
125 Sat, 17 Jan 1970 04:53:20 +0000
126 Fri, 16 Jan 1970 01:06:40 +0000
126 Fri, 16 Jan 1970 01:06:40 +0000
127 Wed, 14 Jan 1970 21:20:00 +0000
127 Wed, 14 Jan 1970 21:20:00 +0000
128 Tue, 13 Jan 1970 17:33:20 +0000
128 Tue, 13 Jan 1970 17:33:20 +0000
129 Mon, 12 Jan 1970 13:46:40 +0000
129 Mon, 12 Jan 1970 13:46:40 +0000
130
130
131 $ hg log --template '{desc|firstline}\n'
131 $ hg log --template '{desc|firstline}\n'
132 third
132 third
133 second
133 second
134 merge
134 merge
135 new head
135 new head
136 new branch
136 new branch
137 no user, no domain
137 no user, no domain
138 no person
138 no person
139 other 1
139 other 1
140 line 1
140 line 1
141
141
142 $ hg log --template '{node|short}\n'
142 $ hg log --template '{node|short}\n'
143 95c24699272e
143 95c24699272e
144 29114dbae42b
144 29114dbae42b
145 d41e714fe50d
145 d41e714fe50d
146 13207e5a10d9
146 13207e5a10d9
147 bbe44766e73d
147 bbe44766e73d
148 10e46f2dcbf4
148 10e46f2dcbf4
149 97054abb4ab8
149 97054abb4ab8
150 b608e9d1a3f0
150 b608e9d1a3f0
151 1e4e1b8f71e0
151 1e4e1b8f71e0
152
152
153 $ hg log --template '<changeset author="{author|xmlescape}"/>\n'
153 $ hg log --template '<changeset author="{author|xmlescape}"/>\n'
154 <changeset author="test"/>
154 <changeset author="test"/>
155 <changeset author="User Name &lt;user@hostname&gt;"/>
155 <changeset author="User Name &lt;user@hostname&gt;"/>
156 <changeset author="person"/>
156 <changeset author="person"/>
157 <changeset author="person"/>
157 <changeset author="person"/>
158 <changeset author="person"/>
158 <changeset author="person"/>
159 <changeset author="person"/>
159 <changeset author="person"/>
160 <changeset author="other@place"/>
160 <changeset author="other@place"/>
161 <changeset author="A. N. Other &lt;other@place&gt;"/>
161 <changeset author="A. N. Other &lt;other@place&gt;"/>
162 <changeset author="User Name &lt;user@hostname&gt;"/>
162 <changeset author="User Name &lt;user@hostname&gt;"/>
163
163
164 $ hg log --template '{rev}: {children}\n'
164 $ hg log --template '{rev}: {children}\n'
165 8:
165 8:
166 7: 8:95c24699272e
166 7: 8:95c24699272e
167 6:
167 6:
168 5: 6:d41e714fe50d
168 5: 6:d41e714fe50d
169 4: 6:d41e714fe50d
169 4: 6:d41e714fe50d
170 3: 4:bbe44766e73d 5:13207e5a10d9
170 3: 4:bbe44766e73d 5:13207e5a10d9
171 2: 3:10e46f2dcbf4
171 2: 3:10e46f2dcbf4
172 1: 2:97054abb4ab8
172 1: 2:97054abb4ab8
173 0: 1:b608e9d1a3f0
173 0: 1:b608e9d1a3f0
174
174
175 Formatnode filter works:
175 Formatnode filter works:
176
176
177 $ hg -q log -r 0 --template '{node|formatnode}\n'
177 $ hg -q log -r 0 --template '{node|formatnode}\n'
178 1e4e1b8f71e0
178 1e4e1b8f71e0
179
179
180 $ hg log -r 0 --template '{node|formatnode}\n'
180 $ hg log -r 0 --template '{node|formatnode}\n'
181 1e4e1b8f71e0
181 1e4e1b8f71e0
182
182
183 $ hg -v log -r 0 --template '{node|formatnode}\n'
183 $ hg -v log -r 0 --template '{node|formatnode}\n'
184 1e4e1b8f71e0
184 1e4e1b8f71e0
185
185
186 $ hg --debug log -r 0 --template '{node|formatnode}\n'
186 $ hg --debug log -r 0 --template '{node|formatnode}\n'
187 1e4e1b8f71e05681d422154f5421e385fec3454f
187 1e4e1b8f71e05681d422154f5421e385fec3454f
188
188
189 Age filter:
189 Age filter:
190
190
191 $ hg init unstable-hash
191 $ hg init unstable-hash
192 $ cd unstable-hash
192 $ cd unstable-hash
193 $ hg log --template '{date|age}\n' > /dev/null || exit 1
193 $ hg log --template '{date|age}\n' > /dev/null || exit 1
194
194
195 >>> from __future__ import absolute_import
195 >>> from __future__ import absolute_import
196 >>> import datetime
196 >>> import datetime
197 >>> fp = open('a', 'wb')
197 >>> fp = open('a', 'wb')
198 >>> n = datetime.datetime.now() + datetime.timedelta(366 * 7)
198 >>> n = datetime.datetime.now() + datetime.timedelta(366 * 7)
199 >>> fp.write(b'%d-%d-%d 00:00' % (n.year, n.month, n.day)) and None
199 >>> fp.write(b'%d-%d-%d 00:00' % (n.year, n.month, n.day)) and None
200 >>> fp.close()
200 >>> fp.close()
201 $ hg add a
201 $ hg add a
202 $ hg commit -m future -d "`cat a`"
202 $ hg commit -m future -d "`cat a`"
203
203
204 $ hg log -l1 --template '{date|age}\n'
204 $ hg log -l1 --template '{date|age}\n'
205 7 years from now
205 7 years from now
206
206
207 $ cd ..
207 $ cd ..
208 $ rm -rf unstable-hash
208 $ rm -rf unstable-hash
209
209
210 Filename filters:
210 Filename filters:
211
211
212 $ hg debugtemplate '{"foo/bar"|basename}|{"foo/"|basename}|{"foo"|basename}|\n'
212 $ hg debugtemplate '{"foo/bar"|basename}|{"foo/"|basename}|{"foo"|basename}|\n'
213 bar||foo|
213 bar||foo|
214 $ hg debugtemplate '{"foo/bar"|dirname}|{"foo/"|dirname}|{"foo"|dirname}|\n'
214 $ hg debugtemplate '{"foo/bar"|dirname}|{"foo/"|dirname}|{"foo"|dirname}|\n'
215 foo|foo||
215 foo|foo||
216 $ hg debugtemplate '{"foo/bar"|stripdir}|{"foo/"|stripdir}|{"foo"|stripdir}|\n'
216 $ hg debugtemplate '{"foo/bar"|stripdir}|{"foo/"|stripdir}|{"foo"|stripdir}|\n'
217 foo|foo|foo|
217 foo|foo|foo|
218
218
219 commondir() filter:
219 commondir() filter:
220
220
221 $ hg debugtemplate '{""|splitlines|commondir}\n'
221 $ hg debugtemplate '{""|splitlines|commondir}\n'
222
222
223 $ hg debugtemplate '{"foo/bar\nfoo/baz\nfoo/foobar\n"|splitlines|commondir}\n'
223 $ hg debugtemplate '{"foo/bar\nfoo/baz\nfoo/foobar\n"|splitlines|commondir}\n'
224 foo
224 foo
225 $ hg debugtemplate '{"foo/bar\nfoo/bar\n"|splitlines|commondir}\n'
225 $ hg debugtemplate '{"foo/bar\nfoo/bar\n"|splitlines|commondir}\n'
226 foo
226 foo
227 $ hg debugtemplate '{"/foo/bar\n/foo/bar\n"|splitlines|commondir}\n'
227 $ hg debugtemplate '{"/foo/bar\n/foo/bar\n"|splitlines|commondir}\n'
228 foo
228 foo
229 $ hg debugtemplate '{"/foo\n/foo\n"|splitlines|commondir}\n'
229 $ hg debugtemplate '{"/foo\n/foo\n"|splitlines|commondir}\n'
230
230
231 $ hg debugtemplate '{"foo/bar\nbar/baz"|splitlines|commondir}\n'
231 $ hg debugtemplate '{"foo/bar\nbar/baz"|splitlines|commondir}\n'
232
232
233 $ hg debugtemplate '{"foo/bar\nbar/baz\nbar/foo\n"|splitlines|commondir}\n'
233 $ hg debugtemplate '{"foo/bar\nbar/baz\nbar/foo\n"|splitlines|commondir}\n'
234
234
235 $ hg debugtemplate '{"foo/../bar\nfoo/bar"|splitlines|commondir}\n'
235 $ hg debugtemplate '{"foo/../bar\nfoo/bar"|splitlines|commondir}\n'
236 foo
236 foo
237 $ hg debugtemplate '{"foo\n/foo"|splitlines|commondir}\n'
237 $ hg debugtemplate '{"foo\n/foo"|splitlines|commondir}\n'
238
238
239
239
240 $ hg log -r null -T '{rev|commondir}'
240 $ hg log -r null -T '{rev|commondir}'
241 hg: parse error: argument is not a list of text
241 hg: parse error: argument is not a list of text
242 (template filter 'commondir' is not compatible with keyword 'rev')
242 (template filter 'commondir' is not compatible with keyword 'rev')
243 [255]
243 [255]
244
244
245 Add a dummy commit to make up for the instability of the above:
245 Add a dummy commit to make up for the instability of the above:
246
246
247 $ echo a > a
247 $ echo a > a
248 $ hg add a
248 $ hg add a
249 $ hg ci -m future
249 $ hg ci -m future
250
250
251 Count filter:
251 Count filter:
252
252
253 $ hg log -l1 --template '{node|count} {node|short|count}\n'
253 $ hg log -l1 --template '{node|count} {node|short|count}\n'
254 40 12
254 40 12
255
255
256 $ hg log -l1 --template '{revset("null^")|count} {revset(".")|count} {revset("0::3")|count}\n'
256 $ hg log -l1 --template '{revset("null^")|count} {revset(".")|count} {revset("0::3")|count}\n'
257 0 1 4
257 0 1 4
258
258
259 $ hg log -G --template '{rev}: children: {children|count}, \
259 $ hg log -G --template '{rev}: children: {children|count}, \
260 > tags: {tags|count}, file_adds: {file_adds|count}, \
260 > tags: {tags|count}, file_adds: {file_adds|count}, \
261 > ancestors: {revset("ancestors(%s)", rev)|count}'
261 > ancestors: {revset("ancestors(%s)", rev)|count}'
262 @ 9: children: 0, tags: 1, file_adds: 1, ancestors: 3
262 @ 9: children: 0, tags: 1, file_adds: 1, ancestors: 3
263 |
263 |
264 o 8: children: 1, tags: 0, file_adds: 2, ancestors: 2
264 o 8: children: 1, tags: 0, file_adds: 2, ancestors: 2
265 |
265 |
266 o 7: children: 1, tags: 0, file_adds: 1, ancestors: 1
266 o 7: children: 1, tags: 0, file_adds: 1, ancestors: 1
267
267
268 o 6: children: 0, tags: 0, file_adds: 0, ancestors: 7
268 o 6: children: 0, tags: 0, file_adds: 0, ancestors: 7
269 |\
269 |\
270 | o 5: children: 1, tags: 0, file_adds: 1, ancestors: 5
270 | o 5: children: 1, tags: 0, file_adds: 1, ancestors: 5
271 | |
271 | |
272 o | 4: children: 1, tags: 0, file_adds: 0, ancestors: 5
272 o | 4: children: 1, tags: 0, file_adds: 0, ancestors: 5
273 |/
273 |/
274 o 3: children: 2, tags: 0, file_adds: 0, ancestors: 4
274 o 3: children: 2, tags: 0, file_adds: 0, ancestors: 4
275 |
275 |
276 o 2: children: 1, tags: 0, file_adds: 1, ancestors: 3
276 o 2: children: 1, tags: 0, file_adds: 1, ancestors: 3
277 |
277 |
278 o 1: children: 1, tags: 0, file_adds: 1, ancestors: 2
278 o 1: children: 1, tags: 0, file_adds: 1, ancestors: 2
279 |
279 |
280 o 0: children: 1, tags: 0, file_adds: 1, ancestors: 1
280 o 0: children: 1, tags: 0, file_adds: 1, ancestors: 1
281
281
282
282
283 $ hg log -l1 -T '{termwidth|count}\n'
283 $ hg log -l1 -T '{termwidth|count}\n'
284 hg: parse error: not countable
284 hg: parse error: not countable
285 (template filter 'count' is not compatible with keyword 'termwidth')
285 (template filter 'count' is not compatible with keyword 'termwidth')
286 [255]
286 [255]
287
287
288 Upper/lower filters:
288 Upper/lower filters:
289
289
290 $ hg log -r0 --template '{branch|upper}\n'
290 $ hg log -r0 --template '{branch|upper}\n'
291 DEFAULT
291 DEFAULT
292 $ hg log -r0 --template '{author|lower}\n'
292 $ hg log -r0 --template '{author|lower}\n'
293 user name <user@hostname>
293 user name <user@hostname>
294 $ hg log -r0 --template '{date|upper}\n'
294 $ hg log -r0 --template '{date|upper}\n'
295 1000000.00
295 1000000.00
296
296
297 Add a commit that does all possible modifications at once
297 Add a commit that does all possible modifications at once
298
298
299 $ echo modify >> third
299 $ echo modify >> third
300 $ touch b
300 $ touch b
301 $ hg add b
301 $ hg add b
302 $ hg mv fourth fifth
302 $ hg mv fourth fifth
303 $ hg rm a
303 $ hg rm a
304 $ hg ci -m "Modify, add, remove, rename"
304 $ hg ci -m "Modify, add, remove, rename"
305
305
306 Pass generator object created by template function to filter
306 Pass generator object created by template function to filter
307
307
308 $ hg log -l 1 --template '{if(author, author)|user}\n'
308 $ hg log -l 1 --template '{if(author, author)|user}\n'
309 test
309 test
310
310
311 Test diff function:
311 Test diff function:
312
312
313 $ hg diff -c 8
313 $ hg diff -c 8
314 diff -r 29114dbae42b -r 95c24699272e fourth
314 diff -r 29114dbae42b -r 95c24699272e fourth
315 --- /dev/null Thu Jan 01 00:00:00 1970 +0000
315 --- /dev/null Thu Jan 01 00:00:00 1970 +0000
316 +++ b/fourth Wed Jan 01 10:01:00 2020 +0000
316 +++ b/fourth Wed Jan 01 10:01:00 2020 +0000
317 @@ -0,0 +1,1 @@
317 @@ -0,0 +1,1 @@
318 +second
318 +second
319 diff -r 29114dbae42b -r 95c24699272e second
319 diff -r 29114dbae42b -r 95c24699272e second
320 --- a/second Mon Jan 12 13:46:40 1970 +0000
320 --- a/second Mon Jan 12 13:46:40 1970 +0000
321 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000
321 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000
322 @@ -1,1 +0,0 @@
322 @@ -1,1 +0,0 @@
323 -second
323 -second
324 diff -r 29114dbae42b -r 95c24699272e third
324 diff -r 29114dbae42b -r 95c24699272e third
325 --- /dev/null Thu Jan 01 00:00:00 1970 +0000
325 --- /dev/null Thu Jan 01 00:00:00 1970 +0000
326 +++ b/third Wed Jan 01 10:01:00 2020 +0000
326 +++ b/third Wed Jan 01 10:01:00 2020 +0000
327 @@ -0,0 +1,1 @@
327 @@ -0,0 +1,1 @@
328 +third
328 +third
329
329
330 $ hg log -r 8 -T "{diff()}"
330 $ hg log -r 8 -T "{diff()}"
331 diff -r 29114dbae42b -r 95c24699272e fourth
331 diff -r 29114dbae42b -r 95c24699272e fourth
332 --- /dev/null Thu Jan 01 00:00:00 1970 +0000
332 --- /dev/null Thu Jan 01 00:00:00 1970 +0000
333 +++ b/fourth Wed Jan 01 10:01:00 2020 +0000
333 +++ b/fourth Wed Jan 01 10:01:00 2020 +0000
334 @@ -0,0 +1,1 @@
334 @@ -0,0 +1,1 @@
335 +second
335 +second
336 diff -r 29114dbae42b -r 95c24699272e second
336 diff -r 29114dbae42b -r 95c24699272e second
337 --- a/second Mon Jan 12 13:46:40 1970 +0000
337 --- a/second Mon Jan 12 13:46:40 1970 +0000
338 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000
338 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000
339 @@ -1,1 +0,0 @@
339 @@ -1,1 +0,0 @@
340 -second
340 -second
341 diff -r 29114dbae42b -r 95c24699272e third
341 diff -r 29114dbae42b -r 95c24699272e third
342 --- /dev/null Thu Jan 01 00:00:00 1970 +0000
342 --- /dev/null Thu Jan 01 00:00:00 1970 +0000
343 +++ b/third Wed Jan 01 10:01:00 2020 +0000
343 +++ b/third Wed Jan 01 10:01:00 2020 +0000
344 @@ -0,0 +1,1 @@
344 @@ -0,0 +1,1 @@
345 +third
345 +third
346
346
347 $ hg log -r 8 -T "{diff('glob:f*')}"
347 $ hg log -r 8 -T "{diff('glob:f*')}"
348 diff -r 29114dbae42b -r 95c24699272e fourth
348 diff -r 29114dbae42b -r 95c24699272e fourth
349 --- /dev/null Thu Jan 01 00:00:00 1970 +0000
349 --- /dev/null Thu Jan 01 00:00:00 1970 +0000
350 +++ b/fourth Wed Jan 01 10:01:00 2020 +0000
350 +++ b/fourth Wed Jan 01 10:01:00 2020 +0000
351 @@ -0,0 +1,1 @@
351 @@ -0,0 +1,1 @@
352 +second
352 +second
353
353
354 $ hg log -r 8 -T "{diff('', 'glob:f*')}"
354 $ hg log -r 8 -T "{diff('', 'glob:f*')}"
355 diff -r 29114dbae42b -r 95c24699272e second
355 diff -r 29114dbae42b -r 95c24699272e second
356 --- a/second Mon Jan 12 13:46:40 1970 +0000
356 --- a/second Mon Jan 12 13:46:40 1970 +0000
357 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000
357 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000
358 @@ -1,1 +0,0 @@
358 @@ -1,1 +0,0 @@
359 -second
359 -second
360 diff -r 29114dbae42b -r 95c24699272e third
360 diff -r 29114dbae42b -r 95c24699272e third
361 --- /dev/null Thu Jan 01 00:00:00 1970 +0000
361 --- /dev/null Thu Jan 01 00:00:00 1970 +0000
362 +++ b/third Wed Jan 01 10:01:00 2020 +0000
362 +++ b/third Wed Jan 01 10:01:00 2020 +0000
363 @@ -0,0 +1,1 @@
363 @@ -0,0 +1,1 @@
364 +third
364 +third
365
365
366 $ hg log -r 8 -T "{diff('FOURTH'|lower)}"
366 $ hg log -r 8 -T "{diff('FOURTH'|lower)}"
367 diff -r 29114dbae42b -r 95c24699272e fourth
367 diff -r 29114dbae42b -r 95c24699272e fourth
368 --- /dev/null Thu Jan 01 00:00:00 1970 +0000
368 --- /dev/null Thu Jan 01 00:00:00 1970 +0000
369 +++ b/fourth Wed Jan 01 10:01:00 2020 +0000
369 +++ b/fourth Wed Jan 01 10:01:00 2020 +0000
370 @@ -0,0 +1,1 @@
370 @@ -0,0 +1,1 @@
371 +second
371 +second
372
372
373 $ hg --config diff.git=true log -r 8 -T "{diff()}"
373 $ hg --config diff.git=true log -r 8 -T "{diff()}"
374 diff --git a/second b/fourth
374 diff --git a/second b/fourth
375 rename from second
375 rename from second
376 rename to fourth
376 rename to fourth
377 diff --git a/third b/third
377 diff --git a/third b/third
378 new file mode 100644
378 new file mode 100644
379 --- /dev/null
379 --- /dev/null
380 +++ b/third
380 +++ b/third
381 @@ -0,0 +1,1 @@
381 @@ -0,0 +1,1 @@
382 +third
382 +third
383
383
384 $ cd ..
384 $ cd ..
385
385
386 latesttag() function:
386 latesttag() function:
387
387
388 $ hg init latesttag
388 $ hg init latesttag
389 $ cd latesttag
389 $ cd latesttag
390
390
391 $ echo a > file
391 $ echo a > file
392 $ hg ci -Am a -d '0 0'
392 $ hg ci -Am a -d '0 0'
393 adding file
393 adding file
394
394
395 $ echo b >> file
395 $ echo b >> file
396 $ hg ci -m b -d '1 0'
396 $ hg ci -m b -d '1 0'
397
397
398 $ echo c >> head1
398 $ echo c >> head1
399 $ hg ci -Am h1c -d '2 0'
399 $ hg ci -Am h1c -d '2 0'
400 adding head1
400 adding head1
401
401
402 $ hg update -q 1
402 $ hg update -q 1
403 $ echo d >> head2
403 $ echo d >> head2
404 $ hg ci -Am h2d -d '3 0'
404 $ hg ci -Am h2d -d '3 0'
405 adding head2
405 adding head2
406 created new head
406 created new head
407
407
408 $ echo e >> head2
408 $ echo e >> head2
409 $ hg ci -m h2e -d '4 0'
409 $ hg ci -m h2e -d '4 0'
410
410
411 $ hg merge -q
411 $ hg merge -q
412 $ hg ci -m merge -d '5 -3600'
412 $ hg ci -m merge -d '5 -3600'
413
413
414 $ hg tag -r 1 -m t1 -d '6 0' t1
414 $ hg tag -r 1 -m t1 -d '6 0' t1
415 $ hg tag -r 2 -m t2 -d '7 0' t2
415 $ hg tag -r 2 -m t2 -d '7 0' t2
416 $ hg tag -r 3 -m t3 -d '8 0' t3
416 $ hg tag -r 3 -m t3 -d '8 0' t3
417 $ hg tag -r 4 -m t4 -d '4 0' t4 # older than t2, but should not matter
417 $ hg tag -r 4 -m t4 -d '4 0' t4 # older than t2, but should not matter
418 $ hg tag -r 5 -m t5 -d '9 0' t5
418 $ hg tag -r 5 -m t5 -d '9 0' t5
419 $ hg tag -r 3 -m at3 -d '10 0' at3
419 $ hg tag -r 3 -m at3 -d '10 0' at3
420
420
421 $ hg log -G --template "{rev}: {latesttag('re:^t[13]$') % '{tag}, C: {changes}, D: {distance}'}\n"
421 $ hg log -G --template "{rev}: {latesttag('re:^t[13]$') % '{tag}, C: {changes}, D: {distance}'}\n"
422 @ 11: t3, C: 9, D: 8
422 @ 11: t3, C: 9, D: 8
423 |
423 |
424 o 10: t3, C: 8, D: 7
424 o 10: t3, C: 8, D: 7
425 |
425 |
426 o 9: t3, C: 7, D: 6
426 o 9: t3, C: 7, D: 6
427 |
427 |
428 o 8: t3, C: 6, D: 5
428 o 8: t3, C: 6, D: 5
429 |
429 |
430 o 7: t3, C: 5, D: 4
430 o 7: t3, C: 5, D: 4
431 |
431 |
432 o 6: t3, C: 4, D: 3
432 o 6: t3, C: 4, D: 3
433 |
433 |
434 o 5: t3, C: 3, D: 2
434 o 5: t3, C: 3, D: 2
435 |\
435 |\
436 | o 4: t3, C: 1, D: 1
436 | o 4: t3, C: 1, D: 1
437 | |
437 | |
438 | o 3: t3, C: 0, D: 0
438 | o 3: t3, C: 0, D: 0
439 | |
439 | |
440 o | 2: t1, C: 1, D: 1
440 o | 2: t1, C: 1, D: 1
441 |/
441 |/
442 o 1: t1, C: 0, D: 0
442 o 1: t1, C: 0, D: 0
443 |
443 |
444 o 0: null, C: 1, D: 1
444 o 0: null, C: 1, D: 1
445
445
446
446
447 $ cd ..
447 $ cd ..
448
448
449 Test filter() empty values:
449 Test filter() empty values:
450
450
451 $ hg log -R a -r 1 -T '{filter(desc|splitlines) % "{line}\n"}'
451 $ hg log -R a -r 1 -T '{filter(desc|splitlines) % "{line}\n"}'
452 other 1
452 other 1
453 other 2
453 other 2
454 other 3
454 other 3
455 $ hg log -R a -r 0 -T '{filter(dict(a=0, b=1) % "{ifeq(key, "a", "{value}\n")}")}'
455 $ hg log -R a -r 0 -T '{filter(dict(a=0, b=1) % "{ifeq(key, "a", "{value}\n")}")}'
456 0
456 0
457
457
458 0 should not be falsy
458 0 should not be falsy
459
459
460 $ hg log -R a -r 0 -T '{filter(revset("0:2"))}\n'
460 $ hg log -R a -r 0 -T '{filter(revset("0:2"))}\n'
461 0 1 2
461 0 1 2
462
462
463 Test filter() by expression:
463 Test filter() by expression:
464
464
465 $ hg log -R a -r 1 -T '{filter(desc|splitlines, ifcontains("1", line, "t"))}\n'
465 $ hg log -R a -r 1 -T '{filter(desc|splitlines, ifcontains("1", line, "t"))}\n'
466 other 1
466 other 1
467 $ hg log -R a -r 0 -T '{filter(dict(a=0, b=1), ifeq(key, "b", "t"))}\n'
467 $ hg log -R a -r 0 -T '{filter(dict(a=0, b=1), ifeq(key, "b", "t"))}\n'
468 b=1
468 b=1
469
469
470 Test filter() shouldn't crash:
470 Test filter() shouldn't crash:
471
471
472 $ hg log -R a -r 0 -T '{filter(extras)}\n'
472 $ hg log -R a -r 0 -T '{filter(extras)}\n'
473 branch=default
473 branch=default
474 $ hg log -R a -r 0 -T '{filter(files)}\n'
474 $ hg log -R a -r 0 -T '{filter(files)}\n'
475 a
475 a
476
476
477 Test filter() unsupported arguments:
477 Test filter() unsupported arguments:
478
478
479 $ hg log -R a -r 0 -T '{filter()}\n'
479 $ hg log -R a -r 0 -T '{filter()}\n'
480 hg: parse error: filter expects one or two arguments
480 hg: parse error: filter expects one or two arguments
481 [255]
481 [255]
482 $ hg log -R a -r 0 -T '{filter(date)}\n'
482 $ hg log -R a -r 0 -T '{filter(date)}\n'
483 hg: parse error: date is not iterable
483 hg: parse error: date is not iterable
484 [255]
484 [255]
485 $ hg log -R a -r 0 -T '{filter(rev)}\n'
485 $ hg log -R a -r 0 -T '{filter(rev)}\n'
486 hg: parse error: 0 is not iterable
486 hg: parse error: 0 is not iterable
487 [255]
487 [255]
488 $ hg log -R a -r 0 -T '{filter(desc|firstline)}\n'
488 $ hg log -R a -r 0 -T '{filter(desc|firstline)}\n'
489 hg: parse error: 'line 1' is not filterable
489 hg: parse error: 'line 1' is not filterable
490 [255]
490 [255]
491 $ hg log -R a -r 0 -T '{filter(manifest)}\n'
491 $ hg log -R a -r 0 -T '{filter(manifest)}\n'
492 hg: parse error: '0:a0c8bcbbb45c' is not filterable
492 hg: parse error: '0:a0c8bcbbb45c' is not filterable
493 [255]
493 [255]
494 $ hg log -R a -r 0 -T '{filter(succsandmarkers)}\n'
494 $ hg log -R a -r 0 -T '{filter(succsandmarkers)}\n'
495 hg: parse error: not filterable without template
495 hg: parse error: not filterable without template
496 [255]
496 [255]
497 $ hg log -R a -r 0 -T '{filter(desc|splitlines % "{line}", "")}\n'
497 $ hg log -R a -r 0 -T '{filter(desc|splitlines % "{line}", "")}\n'
498 hg: parse error: not filterable by expression
498 hg: parse error: not filterable by expression
499 [255]
499 [255]
500
500
501 Test manifest/get() can be join()-ed as string, though it's silly:
501 Test manifest/get() can be join()-ed as string, though it's silly:
502
502
503 $ hg log -R latesttag -r tip -T '{join(manifest, ".")}\n'
503 $ hg log -R latesttag -r tip -T '{join(manifest, ".")}\n'
504 1.1.:.2.b.c.6.e.9.0.0.6.c.e.2
504 1.1.:.2.b.c.6.e.9.0.0.6.c.e.2
505 $ hg log -R latesttag -r tip -T '{join(get(extras, "branch"), ".")}\n'
505 $ hg log -R latesttag -r tip -T '{join(get(extras, "branch"), ".")}\n'
506 d.e.f.a.u.l.t
506 d.e.f.a.u.l.t
507
507
508 Test join() over string
508 Test join() over string
509
509
510 $ hg log -R latesttag -r tip -T '{join(rev|stringify, ".")}\n'
510 $ hg log -R latesttag -r tip -T '{join(rev|stringify, ".")}\n'
511 1.1
511 1.1
512
512
513 Test join() over uniterable
513 Test join() over uniterable
514
514
515 $ hg log -R latesttag -r tip -T '{join(rev, "")}\n'
515 $ hg log -R latesttag -r tip -T '{join(rev, "")}\n'
516 hg: parse error: 11 is not iterable
516 hg: parse error: 11 is not iterable
517 [255]
517 [255]
518
518
519 Test min/max of integers
519 Test min/max of integers
520
520
521 $ hg log -R latesttag -l1 -T '{min(revset("9:10"))}\n'
521 $ hg log -R latesttag -l1 -T '{min(revset("9:10"))}\n'
522 9
522 9
523 $ hg log -R latesttag -l1 -T '{max(revset("9:10"))}\n'
523 $ hg log -R latesttag -l1 -T '{max(revset("9:10"))}\n'
524 10
524 10
525
525
526 Test min/max over map operation:
526 Test min/max over map operation:
527
527
528 $ hg log -R latesttag -r3 -T '{min(tags % "{tag}")}\n'
528 $ hg log -R latesttag -r3 -T '{min(tags % "{tag}")}\n'
529 at3
529 at3
530 $ hg log -R latesttag -r3 -T '{max(tags % "{tag}")}\n'
530 $ hg log -R latesttag -r3 -T '{max(tags % "{tag}")}\n'
531 t3
531 t3
532
532
533 Test min/max of strings:
533 Test min/max of strings:
534
534
535 $ hg log -R latesttag -l1 -T '{min(desc)}\n'
535 $ hg log -R latesttag -l1 -T '{min(desc)}\n'
536 3
536 3
537 $ hg log -R latesttag -l1 -T '{max(desc)}\n'
537 $ hg log -R latesttag -l1 -T '{max(desc)}\n'
538 t
538 t
539
539
540 Test min/max of non-iterable:
540 Test min/max of non-iterable:
541
541
542 $ hg debugtemplate '{min(1)}'
542 $ hg debugtemplate '{min(1)}'
543 hg: parse error: 1 is not iterable
543 hg: parse error: 1 is not iterable
544 (min first argument should be an iterable)
544 (min first argument should be an iterable)
545 [255]
545 [255]
546 $ hg debugtemplate '{max(2)}'
546 $ hg debugtemplate '{max(2)}'
547 hg: parse error: 2 is not iterable
547 hg: parse error: 2 is not iterable
548 (max first argument should be an iterable)
548 (max first argument should be an iterable)
549 [255]
549 [255]
550
550
551 $ hg log -R latesttag -l1 -T '{min(date)}'
551 $ hg log -R latesttag -l1 -T '{min(date)}'
552 hg: parse error: date is not iterable
552 hg: parse error: date is not iterable
553 (min first argument should be an iterable)
553 (min first argument should be an iterable)
554 [255]
554 [255]
555 $ hg log -R latesttag -l1 -T '{max(date)}'
555 $ hg log -R latesttag -l1 -T '{max(date)}'
556 hg: parse error: date is not iterable
556 hg: parse error: date is not iterable
557 (max first argument should be an iterable)
557 (max first argument should be an iterable)
558 [255]
558 [255]
559
559
560 Test min/max of empty sequence:
560 Test min/max of empty sequence:
561
561
562 $ hg debugtemplate '{min("")}'
562 $ hg debugtemplate '{min("")}'
563 hg: parse error: empty string
563 hg: parse error: empty string
564 (min first argument should be an iterable)
564 (min first argument should be an iterable)
565 [255]
565 [255]
566 $ hg debugtemplate '{max("")}'
566 $ hg debugtemplate '{max("")}'
567 hg: parse error: empty string
567 hg: parse error: empty string
568 (max first argument should be an iterable)
568 (max first argument should be an iterable)
569 [255]
569 [255]
570 $ hg debugtemplate '{min(dict())}'
570 $ hg debugtemplate '{min(dict())}'
571 hg: parse error: empty sequence
571 hg: parse error: empty sequence
572 (min first argument should be an iterable)
572 (min first argument should be an iterable)
573 [255]
573 [255]
574 $ hg debugtemplate '{max(dict())}'
574 $ hg debugtemplate '{max(dict())}'
575 hg: parse error: empty sequence
575 hg: parse error: empty sequence
576 (max first argument should be an iterable)
576 (max first argument should be an iterable)
577 [255]
577 [255]
578 $ hg debugtemplate '{min(dict() % "")}'
578 $ hg debugtemplate '{min(dict() % "")}'
579 hg: parse error: empty sequence
579 hg: parse error: empty sequence
580 (min first argument should be an iterable)
580 (min first argument should be an iterable)
581 [255]
581 [255]
582 $ hg debugtemplate '{max(dict() % "")}'
582 $ hg debugtemplate '{max(dict() % "")}'
583 hg: parse error: empty sequence
583 hg: parse error: empty sequence
584 (max first argument should be an iterable)
584 (max first argument should be an iterable)
585 [255]
585 [255]
586
586
587 Test min/max of if() result
587 Test min/max of if() result
588
588
589 $ cd latesttag
589 $ cd latesttag
590 $ hg log -l1 -T '{min(if(true, revset("9:10"), ""))}\n'
590 $ hg log -l1 -T '{min(if(true, revset("9:10"), ""))}\n'
591 9
591 9
592 $ hg log -l1 -T '{max(if(false, "", revset("9:10")))}\n'
592 $ hg log -l1 -T '{max(if(false, "", revset("9:10")))}\n'
593 10
593 10
594 $ hg log -l1 -T '{min(ifcontains("a", "aa", revset("9:10"), ""))}\n'
594 $ hg log -l1 -T '{min(ifcontains("a", "aa", revset("9:10"), ""))}\n'
595 9
595 9
596 $ hg log -l1 -T '{max(ifcontains("a", "bb", "", revset("9:10")))}\n'
596 $ hg log -l1 -T '{max(ifcontains("a", "bb", "", revset("9:10")))}\n'
597 10
597 10
598 $ hg log -l1 -T '{min(ifeq(0, 0, revset("9:10"), ""))}\n'
598 $ hg log -l1 -T '{min(ifeq(0, 0, revset("9:10"), ""))}\n'
599 9
599 9
600 $ hg log -l1 -T '{max(ifeq(0, 1, "", revset("9:10")))}\n'
600 $ hg log -l1 -T '{max(ifeq(0, 1, "", revset("9:10")))}\n'
601 10
601 10
602 $ cd ..
602 $ cd ..
603
603
604 Test laziness of if() then/else clause
604 Test laziness of if() then/else clause
605
605
606 $ hg debugtemplate '{count(0)}'
606 $ hg debugtemplate '{count(0)}'
607 hg: parse error: not countable
607 hg: parse error: not countable
608 (incompatible use of template filter 'count')
608 (incompatible use of template filter 'count')
609 [255]
609 [255]
610 $ hg debugtemplate '{if(true, "", count(0))}'
610 $ hg debugtemplate '{if(true, "", count(0))}'
611 $ hg debugtemplate '{if(false, count(0), "")}'
611 $ hg debugtemplate '{if(false, count(0), "")}'
612 $ hg debugtemplate '{ifcontains("a", "aa", "", count(0))}'
612 $ hg debugtemplate '{ifcontains("a", "aa", "", count(0))}'
613 $ hg debugtemplate '{ifcontains("a", "bb", count(0), "")}'
613 $ hg debugtemplate '{ifcontains("a", "bb", count(0), "")}'
614 $ hg debugtemplate '{ifeq(0, 0, "", count(0))}'
614 $ hg debugtemplate '{ifeq(0, 0, "", count(0))}'
615 $ hg debugtemplate '{ifeq(0, 1, count(0), "")}'
615 $ hg debugtemplate '{ifeq(0, 1, count(0), "")}'
616
616
617 Test search() function:
617 Test search() function:
618
618
619 $ hg log -R a -r2 -T '{desc}\n'
619 $ hg log -R a -r2 -T '{desc}\n'
620 no person
620 no person
621
621
622 $ hg log -R a -r2 -T '{search(r"p.*", desc)}\n'
622 $ hg log -R a -r2 -T '{search(r"p.*", desc)}\n'
623 person
623 person
624
624
625 as bool
625 as bool
626
626
627 $ hg log -R a -r2 -T '{if(search(r"p.*", desc), "", "not ")}found\n'
627 $ hg log -R a -r2 -T '{if(search(r"p.*", desc), "", "not ")}found\n'
628 found
628 found
629 $ hg log -R a -r2 -T '{if(search(r"q", desc), "", "not ")}found\n'
629 $ hg log -R a -r2 -T '{if(search(r"q", desc), "", "not ")}found\n'
630 not found
630 not found
631
631
632 match as json
632 match as json
633
633
634 $ hg log -R a -r2 -T '{search(r"(no) p.*", desc)|json}\n'
634 $ hg log -R a -r2 -T '{search(r"(no) p.*", desc)|json}\n'
635 {"0": "no person", "1": "no"}
635 {"0": "no person", "1": "no"}
636 $ hg log -R a -r2 -T '{search(r"q", desc)|json}\n'
636 $ hg log -R a -r2 -T '{search(r"q", desc)|json}\n'
637 null
637 null
638
638
639 group reference
639 group reference
640
640
641 $ hg log -R a -r2 -T '{search(r"(no) (p.*)", desc) % "{1|upper} {2|hex}"}\n'
641 $ hg log -R a -r2 -T '{search(r"(no) (p.*)", desc) % "{1|upper} {2|hex}"}\n'
642 NO 706572736f6e
642 NO 706572736f6e
643 $ hg log -R a -r2 -T '{search(r"(?P<foo>[a-z]*)", desc) % "{foo}"}\n'
643 $ hg log -R a -r2 -T '{search(r"(?P<foo>[a-z]*)", desc) % "{foo}"}\n'
644 no
644 no
645 $ hg log -R a -r2 -T '{search(r"(?P<foo>[a-z]*)", desc).foo}\n'
645 $ hg log -R a -r2 -T '{search(r"(?P<foo>[a-z]*)", desc).foo}\n'
646 no
646 no
647
647
648 group reference with no match
648 group reference with no match
649
649
650 $ hg log -R a -r2 -T '{search(r"q", desc) % "match: {0}"}\n'
650 $ hg log -R a -r2 -T '{search(r"q", desc) % "match: {0}"}\n'
651
651
652
652
653 bad group names
653 bad group names
654
654
655 $ hg log -R a -r2 -T '{search(r"(?P<0>.)", desc) % "{0}"}\n'
655 $ hg log -R a -r2 -T '{search(r"(?P<0>.)", desc) % "{0}"}\n'
656 hg: parse error: search got an invalid pattern: (?P<0>.)
656 hg: parse error: search got an invalid pattern: (?P<0>.)
657 [255]
657 [255]
658 $ hg log -R a -r2 -T '{search(r"(?P<repo>.)", desc) % "{repo}"}\n'
658 $ hg log -R a -r2 -T '{search(r"(?P<repo>.)", desc) % "{repo}"}\n'
659 hg: parse error: invalid group 'repo' in search pattern: (?P<repo>.)
659 hg: parse error: invalid group 'repo' in search pattern: (?P<repo>.)
660 [255]
660 [255]
661
661
662 Test the sub function of templating for expansion:
662 Test the sub function of templating for expansion:
663
663
664 $ hg log -R latesttag -r 10 --template '{sub("[0-9]", "x", "{rev}")}\n'
664 $ hg log -R latesttag -r 10 --template '{sub("[0-9]", "x", "{rev}")}\n'
665 xx
665 xx
666
666
667 $ hg log -R latesttag -r 10 -T '{sub("[", "x", rev)}\n'
667 $ hg log -R latesttag -r 10 -T '{sub("[", "x", rev)}\n'
668 hg: parse error: sub got an invalid pattern: [
668 hg: parse error: sub got an invalid pattern: [
669 [255]
669 [255]
670 $ hg log -R latesttag -r 10 -T '{sub("[0-9]", r"\1", rev)}\n'
670 $ hg log -R latesttag -r 10 -T '{sub("[0-9]", r"\1", rev)}\n'
671 hg: parse error: sub got an invalid replacement: \1
671 hg: parse error: sub got an invalid replacement: \1
672 [255]
672 [255]
673
673
674 Test the strip function with chars specified:
674 Test the strip function with chars specified:
675
675
676 $ hg log -R latesttag --template '{desc}\n'
676 $ hg log -R latesttag --template '{desc}\n'
677 at3
677 at3
678 t5
678 t5
679 t4
679 t4
680 t3
680 t3
681 t2
681 t2
682 t1
682 t1
683 merge
683 merge
684 h2e
684 h2e
685 h2d
685 h2d
686 h1c
686 h1c
687 b
687 b
688 a
688 a
689
689
690 $ hg log -R latesttag --template '{strip(desc, "te")}\n'
690 $ hg log -R latesttag --template '{strip(desc, "te")}\n'
691 at3
691 at3
692 5
692 5
693 4
693 4
694 3
694 3
695 2
695 2
696 1
696 1
697 merg
697 merg
698 h2
698 h2
699 h2d
699 h2d
700 h1c
700 h1c
701 b
701 b
702 a
702 a
703
703
704 Test date format:
704 Test date format:
705
705
706 $ hg log -R latesttag --template 'date: {date(date, "%y %m %d %S %z")}\n'
706 $ hg log -R latesttag --template 'date: {date(date, "%y %m %d %S %z")}\n'
707 date: 70 01 01 10 +0000
707 date: 70 01 01 10 +0000
708 date: 70 01 01 09 +0000
708 date: 70 01 01 09 +0000
709 date: 70 01 01 04 +0000
709 date: 70 01 01 04 +0000
710 date: 70 01 01 08 +0000
710 date: 70 01 01 08 +0000
711 date: 70 01 01 07 +0000
711 date: 70 01 01 07 +0000
712 date: 70 01 01 06 +0000
712 date: 70 01 01 06 +0000
713 date: 70 01 01 05 +0100
713 date: 70 01 01 05 +0100
714 date: 70 01 01 04 +0000
714 date: 70 01 01 04 +0000
715 date: 70 01 01 03 +0000
715 date: 70 01 01 03 +0000
716 date: 70 01 01 02 +0000
716 date: 70 01 01 02 +0000
717 date: 70 01 01 01 +0000
717 date: 70 01 01 01 +0000
718 date: 70 01 01 00 +0000
718 date: 70 01 01 00 +0000
719
719
720 Test invalid date:
720 Test invalid date:
721
721
722 $ hg log -R latesttag -T '{date(rev)}\n'
722 $ hg log -R latesttag -T '{date(rev)}\n'
723 hg: parse error: date expects a date information
723 hg: parse error: date expects a date information
724 [255]
724 [255]
725
725
726 Set up repository containing template fragments in commit metadata:
726 Set up repository containing template fragments in commit metadata:
727
727
728 $ hg init r
728 $ hg init r
729 $ cd r
729 $ cd r
730 $ echo a > a
730 $ echo a > a
731 $ hg ci -Am '{rev}'
731 $ hg ci -Am '{rev}'
732 adding a
732 adding a
733
733
734 $ hg branch -q 'text.{rev}'
734 $ hg branch -q 'text.{rev}'
735 $ echo aa >> aa
735 $ echo aa >> aa
736 $ hg ci -u '{node|short}' -m 'desc to be wrapped desc to be wrapped'
736 $ hg ci -u '{node|short}' -m 'desc to be wrapped desc to be wrapped'
737
737
738 color effect can be specified without quoting:
738 color effect can be specified without quoting:
739
739
740 $ hg log --color=always -l 1 --template '{label(red, "text\n")}'
740 $ hg log --color=always -l 1 --template '{label(red, "text\n")}'
741 \x1b[0;31mtext\x1b[0m (esc)
741 \x1b[0;31mtext\x1b[0m (esc)
742
742
743 color effects can be nested (issue5413)
743 color effects can be nested (issue5413)
744
744
745 $ hg debugtemplate --color=always \
745 $ hg debugtemplate --color=always \
746 > '{label(red, "red{label(magenta, "ma{label(cyan, "cyan")}{label(yellow, "yellow")}genta")}")}\n'
746 > '{label(red, "red{label(magenta, "ma{label(cyan, "cyan")}{label(yellow, "yellow")}genta")}")}\n'
747 \x1b[0;31mred\x1b[0;35mma\x1b[0;36mcyan\x1b[0m\x1b[0;31m\x1b[0;35m\x1b[0;33myellow\x1b[0m\x1b[0;31m\x1b[0;35mgenta\x1b[0m (esc)
747 \x1b[0;31mred\x1b[0;35mma\x1b[0;36mcyan\x1b[0m\x1b[0;31m\x1b[0;35m\x1b[0;33myellow\x1b[0m\x1b[0;31m\x1b[0;35mgenta\x1b[0m (esc)
748
748
749 pad() should interact well with color codes (issue5416)
749 pad() should interact well with color codes (issue5416)
750
750
751 $ hg debugtemplate --color=always \
751 $ hg debugtemplate --color=always \
752 > '{pad(label(red, "red"), 5, label(cyan, "-"))}\n'
752 > '{pad(label(red, "red"), 5, label(cyan, "-"))}\n'
753 \x1b[0;31mred\x1b[0m\x1b[0;36m-\x1b[0m\x1b[0;36m-\x1b[0m (esc)
753 \x1b[0;31mred\x1b[0m\x1b[0;36m-\x1b[0m\x1b[0;36m-\x1b[0m (esc)
754
754
755 pad() with truncate has to strip color codes, though
755 pad() with truncate has to strip color codes, though
756
756
757 $ hg debugtemplate --color=always \
757 $ hg debugtemplate --color=always \
758 > '{pad(label(red, "scarlet"), 5, truncate=true)}\n'
758 > '{pad(label(red, "scarlet"), 5, truncate=true)}\n'
759 scarl
759 scarl
760
760
761 label should be no-op if color is disabled:
761 label should be no-op if color is disabled:
762
762
763 $ hg log --color=never -l 1 --template '{label(red, "text\n")}'
763 $ hg log --color=never -l 1 --template '{label(red, "text\n")}'
764 text
764 text
765 $ hg log --config extensions.color=! -l 1 --template '{label(red, "text\n")}'
765 $ hg log --config extensions.color=! -l 1 --template '{label(red, "text\n")}'
766 text
766 text
767
767
768 Test branches inside if statement:
768 Test branches inside if statement:
769
769
770 $ hg log -r 0 --template '{if(branches, "yes", "no")}\n'
770 $ hg log -r 0 --template '{if(branches, "yes", "no")}\n'
771 no
771 no
772
772
773 Test dict constructor:
773 Test dict constructor:
774
774
775 $ hg log -r 0 -T '{dict(y=node|short, x=rev)}\n'
775 $ hg log -r 0 -T '{dict(y=node|short, x=rev)}\n'
776 y=f7769ec2ab97 x=0
776 y=f7769ec2ab97 x=0
777 $ hg log -r 0 -T '{dict(x=rev, y=node|short) % "{key}={value}\n"}'
777 $ hg log -r 0 -T '{dict(x=rev, y=node|short) % "{key}={value}\n"}'
778 x=0
778 x=0
779 y=f7769ec2ab97
779 y=f7769ec2ab97
780 $ hg log -r 0 -T '{dict(x=rev, y=node|short)|json}\n'
780 $ hg log -r 0 -T '{dict(x=rev, y=node|short)|json}\n'
781 {"x": 0, "y": "f7769ec2ab97"}
781 {"x": 0, "y": "f7769ec2ab97"}
782 $ hg log -r 0 -T '{dict()|json}\n'
782 $ hg log -r 0 -T '{dict()|json}\n'
783 {}
783 {}
784
784
785 $ hg log -r 0 -T '{dict(rev, node=node|short)}\n'
785 $ hg log -r 0 -T '{dict(rev, node=node|short)}\n'
786 rev=0 node=f7769ec2ab97
786 rev=0 node=f7769ec2ab97
787 $ hg log -r 0 -T '{dict(rev, node|short)}\n'
787 $ hg log -r 0 -T '{dict(rev, node|short)}\n'
788 rev=0 node=f7769ec2ab97
788 rev=0 node=f7769ec2ab97
789
789
790 $ hg log -r 0 -T '{dict(rev, rev=rev)}\n'
790 $ hg log -r 0 -T '{dict(rev, rev=rev)}\n'
791 hg: parse error: duplicated dict key 'rev' inferred
791 hg: parse error: duplicated dict key 'rev' inferred
792 [255]
792 [255]
793 $ hg log -r 0 -T '{dict(node, node|short)}\n'
793 $ hg log -r 0 -T '{dict(node, node|short)}\n'
794 hg: parse error: duplicated dict key 'node' inferred
794 hg: parse error: duplicated dict key 'node' inferred
795 [255]
795 [255]
796 $ hg log -r 0 -T '{dict(1 + 2)}'
796 $ hg log -r 0 -T '{dict(1 + 2)}'
797 hg: parse error: dict key cannot be inferred
797 hg: parse error: dict key cannot be inferred
798 [255]
798 [255]
799
799
800 $ hg log -r 0 -T '{dict(x=rev, x=node)}'
800 $ hg log -r 0 -T '{dict(x=rev, x=node)}'
801 hg: parse error: dict got multiple values for keyword argument 'x'
801 hg: parse error: dict got multiple values for keyword argument 'x'
802 [255]
802 [255]
803
803
804 Test get function:
804 Test get function:
805
805
806 $ hg log -r 0 --template '{get(extras, "branch")}\n'
806 $ hg log -r 0 --template '{get(extras, "branch")}\n'
807 default
807 default
808 $ hg log -r 0 --template '{get(extras, "br{"anch"}")}\n'
808 $ hg log -r 0 --template '{get(extras, "br{"anch"}")}\n'
809 default
809 default
810 $ hg log -r 0 --template '{get(files, "should_fail")}\n'
810 $ hg log -r 0 --template '{get(files, "should_fail")}\n'
811 hg: parse error: not a dictionary
811 hg: parse error: not a dictionary
812 (get() expects a dict as first argument)
812 (get() expects a dict as first argument)
813 [255]
813 [255]
814
814
815 Test json filter applied to wrapped object:
815 Test json filter applied to wrapped object:
816
816
817 $ hg log -r0 -T '{files|json}\n'
817 $ hg log -r0 -T '{files|json}\n'
818 ["a"]
818 ["a"]
819 $ hg log -r0 -T '{extras|json}\n'
819 $ hg log -r0 -T '{extras|json}\n'
820 {"branch": "default"}
820 {"branch": "default"}
821 $ hg log -r0 -T '{date|json}\n'
821 $ hg log -r0 -T '{date|json}\n'
822 [0, 0]
822 [0, 0]
823
823
824 Test json filter applied to map result:
824 Test json filter applied to map result:
825
825
826 $ hg log -r0 -T '{json(extras % "{key}")}\n'
826 $ hg log -r0 -T '{json(extras % "{key}")}\n'
827 ["branch"]
827 ["branch"]
828
828
829 Test localdate(date, tz) function:
829 Test localdate(date, tz) function:
830
830
831 $ TZ=JST-09 hg log -r0 -T '{date|localdate|isodate}\n'
831 $ TZ=JST-09 hg log -r0 -T '{date|localdate|isodate}\n'
832 1970-01-01 09:00 +0900
832 1970-01-01 09:00 +0900
833 $ TZ=JST-09 hg log -r0 -T '{localdate(date, "UTC")|isodate}\n'
833 $ TZ=JST-09 hg log -r0 -T '{localdate(date, "UTC")|isodate}\n'
834 1970-01-01 00:00 +0000
834 1970-01-01 00:00 +0000
835 $ TZ=JST-09 hg log -r0 -T '{localdate(date, "blahUTC")|isodate}\n'
835 $ TZ=JST-09 hg log -r0 -T '{localdate(date, "blahUTC")|isodate}\n'
836 hg: parse error: localdate expects a timezone
836 hg: parse error: localdate expects a timezone
837 [255]
837 [255]
838 $ TZ=JST-09 hg log -r0 -T '{localdate(date, "+0200")|isodate}\n'
838 $ TZ=JST-09 hg log -r0 -T '{localdate(date, "+0200")|isodate}\n'
839 1970-01-01 02:00 +0200
839 1970-01-01 02:00 +0200
840 $ TZ=JST-09 hg log -r0 -T '{localdate(date, "0")|isodate}\n'
840 $ TZ=JST-09 hg log -r0 -T '{localdate(date, "0")|isodate}\n'
841 1970-01-01 00:00 +0000
841 1970-01-01 00:00 +0000
842 $ TZ=JST-09 hg log -r0 -T '{localdate(date, 0)|isodate}\n'
842 $ TZ=JST-09 hg log -r0 -T '{localdate(date, 0)|isodate}\n'
843 1970-01-01 00:00 +0000
843 1970-01-01 00:00 +0000
844 $ hg log -r0 -T '{localdate(date, "invalid")|isodate}\n'
844 $ hg log -r0 -T '{localdate(date, "invalid")|isodate}\n'
845 hg: parse error: localdate expects a timezone
845 hg: parse error: localdate expects a timezone
846 [255]
846 [255]
847 $ hg log -r0 -T '{localdate(date, date)|isodate}\n'
847 $ hg log -r0 -T '{localdate(date, date)|isodate}\n'
848 hg: parse error: localdate expects a timezone
848 hg: parse error: localdate expects a timezone
849 [255]
849 [255]
850
850
851 Test shortest(node) function:
851 Test shortest(node) function:
852
852
853 $ echo b > b
853 $ echo b > b
854 $ hg ci -qAm b
854 $ hg ci -qAm b
855 $ hg log --template '{shortest(node)}\n'
855 $ hg log --template '{shortest(node)}\n'
856 e777
856 e777
857 bcc7
857 bcc7
858 f776
858 f776
859 $ hg log --template '{shortest(node, 10)}\n'
859 $ hg log --template '{shortest(node, 10)}\n'
860 e777603221
860 e777603221
861 bcc7ff960b
861 bcc7ff960b
862 f7769ec2ab
862 f7769ec2ab
863 $ hg log --template '{shortest(node, 1)}\n' -r null
863 $ hg log --template '{shortest(node, 1)}\n' -r null
864 00
864 00
865 $ hg log --template '{node|shortest}\n' -l1
865 $ hg log --template '{node|shortest}\n' -l1
866 e777
866 e777
867
867
868 $ hg log -r 0 -T '{shortest(node, "1{"0"}")}\n'
868 $ hg log -r 0 -T '{shortest(node, "1{"0"}")}\n'
869 f7769ec2ab
869 f7769ec2ab
870 $ hg log -r 0 -T '{shortest(node, "not an int")}\n'
870 $ hg log -r 0 -T '{shortest(node, "not an int")}\n'
871 hg: parse error: shortest() expects an integer minlength
871 hg: parse error: shortest() expects an integer minlength
872 [255]
872 [255]
873
873
874 $ hg log -r 'wdir()' -T '{node|shortest}\n'
874 $ hg log -r 'wdir()' -T '{node|shortest}\n'
875 ffff
875 ffff
876
876
877 $ hg log --template '{shortest("f")}\n' -l1
877 $ hg log --template '{shortest("f")}\n' -l1
878 f
878 f
879
879
880 $ hg log --template '{shortest("0123456789012345678901234567890123456789")}\n' -l1
880 $ hg log --template '{shortest("0123456789012345678901234567890123456789")}\n' -l1
881 0123456789012345678901234567890123456789
881 0123456789012345678901234567890123456789
882
882
883 $ hg log --template '{shortest("01234567890123456789012345678901234567890123456789")}\n' -l1
883 $ hg log --template '{shortest("01234567890123456789012345678901234567890123456789")}\n' -l1
884 01234567890123456789012345678901234567890123456789
884 01234567890123456789012345678901234567890123456789
885
885
886 $ hg log --template '{shortest("not a hex string")}\n' -l1
886 $ hg log --template '{shortest("not a hex string")}\n' -l1
887 not a hex string
887 not a hex string
888
888
889 $ hg log --template '{shortest("not a hex string, but it'\''s 40 bytes long")}\n' -l1
889 $ hg log --template '{shortest("not a hex string, but it'\''s 40 bytes long")}\n' -l1
890 not a hex string, but it's 40 bytes long
890 not a hex string, but it's 40 bytes long
891
891
892 $ hg log --template '{shortest("ffffffffffffffffffffffffffffffffffffffff")}\n' -l1
892 $ hg log --template '{shortest("ffffffffffffffffffffffffffffffffffffffff")}\n' -l1
893 ffff
893 ffff
894
894
895 $ hg log --template '{shortest("fffffff")}\n' -l1
895 $ hg log --template '{shortest("fffffff")}\n' -l1
896 ffff
896 ffff
897
897
898 $ hg log --template '{shortest("ff")}\n' -l1
898 $ hg log --template '{shortest("ff")}\n' -l1
899 ffff
899 ffff
900
900
901 $ cd ..
901 $ cd ..
902
902
903 Test shortest(node) with the repo having short hash collision:
903 Test shortest(node) with the repo having short hash collision:
904
904
905 $ hg init hashcollision
905 $ hg init hashcollision
906 $ cd hashcollision
906 $ cd hashcollision
907 $ cat <<EOF >> .hg/hgrc
907 $ cat <<EOF >> .hg/hgrc
908 > [experimental]
908 > [experimental]
909 > evolution.createmarkers=True
909 > evolution.createmarkers=True
910 > EOF
910 > EOF
911 $ echo 0 > a
911 $ echo 0 > a
912 $ hg ci -qAm 0
912 $ hg ci -qAm 0
913 $ for i in 17 129 248 242 480 580 617 1057 2857 4025; do
913 $ for i in 17 129 248 242 480 580 617 1057 2857 4025; do
914 > hg up -q 0
914 > hg up -q 0
915 > echo $i > a
915 > echo $i > a
916 > hg ci -qm $i
916 > hg ci -qm $i
917 > done
917 > done
918 $ hg up -q null
918 $ hg up -q null
919 $ hg log -r0: -T '{rev}:{node}\n'
919 $ hg log -r0: -T '{rev}:{node}\n'
920 0:b4e73ffab476aa0ee32ed81ca51e07169844bc6a
920 0:b4e73ffab476aa0ee32ed81ca51e07169844bc6a
921 1:11424df6dc1dd4ea255eae2b58eaca7831973bbc
921 1:11424df6dc1dd4ea255eae2b58eaca7831973bbc
922 2:11407b3f1b9c3e76a79c1ec5373924df096f0499
922 2:11407b3f1b9c3e76a79c1ec5373924df096f0499
923 3:11dd92fe0f39dfdaacdaa5f3997edc533875cfc4
923 3:11dd92fe0f39dfdaacdaa5f3997edc533875cfc4
924 4:10776689e627b465361ad5c296a20a487e153ca4
924 4:10776689e627b465361ad5c296a20a487e153ca4
925 5:a00be79088084cb3aff086ab799f8790e01a976b
925 5:a00be79088084cb3aff086ab799f8790e01a976b
926 6:a0b0acd79b4498d0052993d35a6a748dd51d13e6
926 6:a0b0acd79b4498d0052993d35a6a748dd51d13e6
927 7:a0457b3450b8e1b778f1163b31a435802987fe5d
927 7:a0457b3450b8e1b778f1163b31a435802987fe5d
928 8:c56256a09cd28e5764f32e8e2810d0f01e2e357a
928 8:c56256a09cd28e5764f32e8e2810d0f01e2e357a
929 9:c5623987d205cd6d9d8389bfc40fff9dbb670b48
929 9:c5623987d205cd6d9d8389bfc40fff9dbb670b48
930 10:c562ddd9c94164376c20b86b0b4991636a3bf84f
930 10:c562ddd9c94164376c20b86b0b4991636a3bf84f
931 $ hg debugobsolete a00be79088084cb3aff086ab799f8790e01a976b
931 $ hg debugobsolete a00be79088084cb3aff086ab799f8790e01a976b
932 1 new obsolescence markers
932 1 new obsolescence markers
933 obsoleted 1 changesets
933 obsoleted 1 changesets
934 $ hg debugobsolete c5623987d205cd6d9d8389bfc40fff9dbb670b48
934 $ hg debugobsolete c5623987d205cd6d9d8389bfc40fff9dbb670b48
935 1 new obsolescence markers
935 1 new obsolescence markers
936 obsoleted 1 changesets
936 obsoleted 1 changesets
937 $ hg debugobsolete c562ddd9c94164376c20b86b0b4991636a3bf84f
937 $ hg debugobsolete c562ddd9c94164376c20b86b0b4991636a3bf84f
938 1 new obsolescence markers
938 1 new obsolescence markers
939 obsoleted 1 changesets
939 obsoleted 1 changesets
940
940
941 nodes starting with '11' (we don't have the revision number '11' though)
941 nodes starting with '11' (we don't have the revision number '11' though)
942
942
943 $ hg log -r 1:3 -T '{rev}:{shortest(node, 0)}\n'
943 $ hg log -r 1:3 -T '{rev}:{shortest(node, 0)}\n'
944 1:1142
944 1:1142
945 2:1140
945 2:1140
946 3:11d
946 3:11d
947
947
948 '5:a00' is hidden, but still we have two nodes starting with 'a0'
948 '5:a00' is hidden, but still we have two nodes starting with 'a0'
949
949
950 $ hg log -r 6:7 -T '{rev}:{shortest(node, 0)}\n'
950 $ hg log -r 6:7 -T '{rev}:{shortest(node, 0)}\n'
951 6:a0b
951 6:a0b
952 7:a04
952 7:a04
953
953
954 node '10' conflicts with the revision number '10' even if it is hidden
954 node '10' conflicts with the revision number '10' even if it is hidden
955 (we could exclude hidden revision numbers, but currently we don't)
955 (we could exclude hidden revision numbers, but currently we don't)
956
956
957 $ hg log -r 4 -T '{rev}:{shortest(node, 0)}\n'
957 $ hg log -r 4 -T '{rev}:{shortest(node, 0)}\n'
958 4:107
958 4:107
959 $ hg log -r 4 -T '{rev}:{shortest(node, 0)}\n' --hidden
959 $ hg log -r 4 -T '{rev}:{shortest(node, 0)}\n' --hidden
960 4:107
960 4:107
961
961
962 $ hg --config experimental.revisions.prefixhexnode=yes log -r 4 -T '{rev}:{shortest(node, 0)}\n'
962 $ hg --config experimental.revisions.prefixhexnode=yes log -r 4 -T '{rev}:{shortest(node, 0)}\n'
963 4:x10
963 4:x10
964 $ hg --config experimental.revisions.prefixhexnode=yes log -r 4 -T '{rev}:{shortest(node, 0)}\n' --hidden
964 $ hg --config experimental.revisions.prefixhexnode=yes log -r 4 -T '{rev}:{shortest(node, 0)}\n' --hidden
965 4:x10
965 4:x10
966
966
967 node 'c562' should be unique if the other 'c562' nodes are hidden
967 node 'c562' should be unique if the other 'c562' nodes are hidden
968 (but we don't try the slow path to filter out hidden nodes for now)
968 (but we don't try the slow path to filter out hidden nodes for now)
969
969
970 $ hg log -r 8 -T '{rev}:{node|shortest}\n'
970 $ hg log -r 8 -T '{rev}:{node|shortest}\n'
971 8:c5625
971 8:c5625
972 $ hg log -r 8:10 -T '{rev}:{node|shortest}\n' --hidden
972 $ hg log -r 8:10 -T '{rev}:{node|shortest}\n' --hidden
973 8:c5625
973 8:c5625
974 9:c5623
974 9:c5623
975 10:c562d
975 10:c562d
976
976
977 $ cd ..
977 $ cd ..
978
978
979 Test prefixhexnode when the first character of the hash is 0.
979 Test prefixhexnode when the first character of the hash is 0.
980 $ hg init hashcollision2
980 $ hg init hashcollision2
981 $ cd hashcollision2
981 $ cd hashcollision2
982 $ cat <<EOF >> .hg/hgrc
982 $ cat <<EOF >> .hg/hgrc
983 > [experimental]
983 > [experimental]
984 > evolution.createmarkers=True
984 > evolution.createmarkers=True
985 > EOF
985 > EOF
986 $ echo 0 > a
986 $ echo 0 > a
987 $ hg ci -qAm 0
987 $ hg ci -qAm 0
988 $ echo 21 > a
988 $ echo 21 > a
989 $ hg ci -qm 21
989 $ hg ci -qm 21
990 $ hg up -q null
990 $ hg up -q null
991 $ hg log -r0: -T '{rev}:{node}\n'
991 $ hg log -r0: -T '{rev}:{node}\n'
992 0:b4e73ffab476aa0ee32ed81ca51e07169844bc6a
992 0:b4e73ffab476aa0ee32ed81ca51e07169844bc6a
993 1:0cf177ba2b1dc3862a00fb81715fec90950201be
993 1:0cf177ba2b1dc3862a00fb81715fec90950201be
994
994
995 we need the 'x' prefix to ensure we aren't colliding with rev0. We identify
995 we need the 'x' prefix to ensure we aren't colliding with rev0. We identify
996 the collision with nullid if we aren't using disambiguatewithin, so we need to set
996 the collision with nullid if we aren't using disambiguatewithin, so we need to set
997 that as well.
997 that as well.
998 $ hg --config experimental.revisions.disambiguatewithin='descendants(0)' \
998 $ hg --config experimental.revisions.disambiguatewithin='descendants(0)' \
999 > --config experimental.revisions.prefixhexnode=yes \
999 > --config experimental.revisions.prefixhexnode=yes \
1000 > log -r 1 -T '{rev}:{shortest(node, 0)}\n'
1000 > log -r 1 -T '{rev}:{shortest(node, 0)}\n'
1001 1:x0
1001 1:x0
1002
1002
1003 $ hg debugobsolete 0cf177ba2b1dc3862a00fb81715fec90950201be
1003 $ hg debugobsolete 0cf177ba2b1dc3862a00fb81715fec90950201be
1004 1 new obsolescence markers
1004 1 new obsolescence markers
1005 obsoleted 1 changesets
1005 obsoleted 1 changesets
1006 $ hg up -q 0
1006 $ hg up -q 0
1007 $ echo 61 > a
1007 $ echo 61 > a
1008 $ hg ci -m 61
1008 $ hg ci -m 61
1009 $ hg log -r0: -T '{rev}:{node}\n'
1009 $ hg log -r0: -T '{rev}:{node}\n'
1010 0:b4e73ffab476aa0ee32ed81ca51e07169844bc6a
1010 0:b4e73ffab476aa0ee32ed81ca51e07169844bc6a
1011 2:01384dde84b3a511ae0835f35ac40bd806c99bb8
1011 2:01384dde84b3a511ae0835f35ac40bd806c99bb8
1012
1012
1013 we still have the 'x' prefix because '0' is still the shortest prefix, since
1013 we still have the 'x' prefix because '0' is still the shortest prefix, since
1014 rev1's '0c' is hidden.
1014 rev1's '0c' is hidden.
1015 $ hg --config experimental.revisions.disambiguatewithin=0:-1-0 \
1015 $ hg --config experimental.revisions.disambiguatewithin=0:-1-0 \
1016 > --config experimental.revisions.prefixhexnode=yes \
1016 > --config experimental.revisions.prefixhexnode=yes \
1017 > log -r 0:-1-0 -T '{rev}:{shortest(node, 0)}\n'
1017 > log -r 0:-1-0 -T '{rev}:{shortest(node, 0)}\n'
1018 2:x0
1018 2:x0
1019
1019
1020 we don't have the 'x' prefix on 2 because '01' is not a synonym for rev1.
1020 we don't have the 'x' prefix on 2 because '01' is not a synonym for rev1.
1021 $ hg --config experimental.revisions.disambiguatewithin=0:-1-0 \
1021 $ hg --config experimental.revisions.disambiguatewithin=0:-1-0 \
1022 > --config experimental.revisions.prefixhexnode=yes \
1022 > --config experimental.revisions.prefixhexnode=yes \
1023 > log -r 0:-1-0 -T '{rev}:{shortest(node, 0)}\n' --hidden
1023 > log -r 0:-1-0 -T '{rev}:{shortest(node, 0)}\n' --hidden
1024 1:0c
1024 1:0c
1025 2:01
1025 2:01
1026
1026
1027 $ cd ..
1027 $ cd ..
1028
1028
1029 Test pad function
1029 Test pad function
1030
1030
1031 $ cd r
1031 $ cd r
1032
1032
1033 $ hg log --template '{pad(rev, 20)} {author|user}\n'
1033 $ hg log --template '{pad(rev, 20)} {author|user}\n'
1034 2 test
1034 2 test
1035 1 {node|short}
1035 1 {node|short}
1036 0 test
1036 0 test
1037
1037
1038 $ hg log --template '{pad(rev, 20, " ", True)} {author|user}\n'
1038 $ hg log --template '{pad(rev, 20, " ", True)} {author|user}\n'
1039 2 test
1039 2 test
1040 1 {node|short}
1040 1 {node|short}
1041 0 test
1041 0 test
1042
1042
1043 $ hg log --template '{pad(rev, 20, "-", False)} {author|user}\n'
1043 $ hg log --template '{pad(rev, 20, "-", False)} {author|user}\n'
1044 2------------------- test
1044 2------------------- test
1045 1------------------- {node|short}
1045 1------------------- {node|short}
1046 0------------------- test
1046 0------------------- test
1047
1047
1048 $ hg log --template '{pad(author, 5, "-", False, True)}\n'
1048 $ hg log --template '{pad(author, 5, "-", False, True)}\n'
1049 test-
1049 test-
1050 {node
1050 {node
1051 test-
1051 test-
1052 $ hg log --template '{pad(author, 5, "-", True, True)}\n'
1052 $ hg log --template '{pad(author, 5, "-", True, True)}\n'
1053 -test
1053 -test
1054 hort}
1054 hort}
1055 -test
1055 -test
1056
1056
1057 Test template string in pad function
1057 Test template string in pad function
1058
1058
1059 $ hg log -r 0 -T '{pad("\{{rev}}", 10)} {author|user}\n'
1059 $ hg log -r 0 -T '{pad("\{{rev}}", 10)} {author|user}\n'
1060 {0} test
1060 {0} test
1061
1061
1062 $ hg log -r 0 -T '{pad(r"\{rev}", 10)} {author|user}\n'
1062 $ hg log -r 0 -T '{pad(r"\{rev}", 10)} {author|user}\n'
1063 \{rev} test
1063 \{rev} test
1064
1064
1065 Test width argument passed to pad function
1065 Test width argument passed to pad function
1066
1066
1067 $ hg log -r 0 -T '{pad(rev, "1{"0"}")} {author|user}\n'
1067 $ hg log -r 0 -T '{pad(rev, "1{"0"}")} {author|user}\n'
1068 0 test
1068 0 test
1069 $ hg log -r 0 -T '{pad(rev, "not an int")}\n'
1069 $ hg log -r 0 -T '{pad(rev, "not an int")}\n'
1070 hg: parse error: pad() expects an integer width
1070 hg: parse error: pad() expects an integer width
1071 [255]
1071 [255]
1072
1072
1073 Test invalid fillchar passed to pad function
1073 Test invalid fillchar passed to pad function
1074
1074
1075 $ hg log -r 0 -T '{pad(rev, 10, "")}\n'
1075 $ hg log -r 0 -T '{pad(rev, 10, "")}\n'
1076 hg: parse error: pad() expects a single fill character
1076 hg: parse error: pad() expects a single fill character
1077 [255]
1077 [255]
1078 $ hg log -r 0 -T '{pad(rev, 10, "--")}\n'
1078 $ hg log -r 0 -T '{pad(rev, 10, "--")}\n'
1079 hg: parse error: pad() expects a single fill character
1079 hg: parse error: pad() expects a single fill character
1080 [255]
1080 [255]
1081
1081
1082 Test boolean argument passed to pad function
1082 Test boolean argument passed to pad function
1083
1083
1084 no crash
1084 no crash
1085
1085
1086 $ hg log -r 0 -T '{pad(rev, 10, "-", "f{"oo"}")}\n'
1086 $ hg log -r 0 -T '{pad(rev, 10, "-", "f{"oo"}")}\n'
1087 ---------0
1087 ---------0
1088
1088
1089 string/literal
1089 string/literal
1090
1090
1091 $ hg log -r 0 -T '{pad(rev, 10, "-", "false")}\n'
1091 $ hg log -r 0 -T '{pad(rev, 10, "-", "false")}\n'
1092 ---------0
1092 ---------0
1093 $ hg log -r 0 -T '{pad(rev, 10, "-", false)}\n'
1093 $ hg log -r 0 -T '{pad(rev, 10, "-", false)}\n'
1094 0---------
1094 0---------
1095 $ hg log -r 0 -T '{pad(rev, 10, "-", "")}\n'
1095 $ hg log -r 0 -T '{pad(rev, 10, "-", "")}\n'
1096 0---------
1096 0---------
1097
1097
1098 unknown keyword is evaluated to ''
1098 unknown keyword is evaluated to ''
1099
1099
1100 $ hg log -r 0 -T '{pad(rev, 10, "-", unknownkeyword)}\n'
1100 $ hg log -r 0 -T '{pad(rev, 10, "-", unknownkeyword)}\n'
1101 0---------
1101 0---------
1102
1102
1103 Test separate function
1103 Test separate function
1104
1104
1105 $ hg log -r 0 -T '{separate("-", "", "a", "b", "", "", "c", "")}\n'
1105 $ hg log -r 0 -T '{separate("-", "", "a", "b", "", "", "c", "")}\n'
1106 a-b-c
1106 a-b-c
1107 $ hg log -r 0 -T '{separate(" ", "{rev}:{node|short}", author|user, branch)}\n'
1107 $ hg log -r 0 -T '{separate(" ", "{rev}:{node|short}", author|user, branch)}\n'
1108 0:f7769ec2ab97 test default
1108 0:f7769ec2ab97 test default
1109 $ hg log -r 0 --color=always -T '{separate(" ", "a", label(red, "b"), "c", label(red, ""), "d")}\n'
1109 $ hg log -r 0 --color=always -T '{separate(" ", "a", label(red, "b"), "c", label(red, ""), "d")}\n'
1110 a \x1b[0;31mb\x1b[0m c d (esc)
1110 a \x1b[0;31mb\x1b[0m c d (esc)
1111
1111
1112 Test boolean expression/literal passed to if function
1112 Test boolean expression/literal passed to if function
1113
1113
1114 $ hg log -r 0 -T '{if(rev, "rev 0 is True")}\n'
1114 $ hg log -r 0 -T '{if(rev, "rev 0 is True")}\n'
1115 rev 0 is True
1115 rev 0 is True
1116 $ hg log -r 0 -T '{if(0, "literal 0 is True as well")}\n'
1116 $ hg log -r 0 -T '{if(0, "literal 0 is True as well")}\n'
1117 literal 0 is True as well
1117 literal 0 is True as well
1118 $ hg log -r 0 -T '{if(min(revset(r"0")), "0 of hybriditem is also True")}\n'
1118 $ hg log -r 0 -T '{if(min(revset(r"0")), "0 of hybriditem is also True")}\n'
1119 0 of hybriditem is also True
1119 0 of hybriditem is also True
1120 $ hg log -r 0 -T '{if("", "", "empty string is False")}\n'
1120 $ hg log -r 0 -T '{if("", "", "empty string is False")}\n'
1121 empty string is False
1121 empty string is False
1122 $ hg log -r 0 -T '{if(revset(r"0 - 0"), "", "empty list is False")}\n'
1122 $ hg log -r 0 -T '{if(revset(r"0 - 0"), "", "empty list is False")}\n'
1123 empty list is False
1123 empty list is False
1124 $ hg log -r 0 -T '{if(revset(r"0"), "non-empty list is True")}\n'
1124 $ hg log -r 0 -T '{if(revset(r"0"), "non-empty list is True")}\n'
1125 non-empty list is True
1125 non-empty list is True
1126 $ hg log -r 0 -T '{if(revset(r"0") % "", "list of empty strings is True")}\n'
1126 $ hg log -r 0 -T '{if(revset(r"0") % "", "list of empty strings is True")}\n'
1127 list of empty strings is True
1127 list of empty strings is True
1128 $ hg log -r 0 -T '{if(true, "true is True")}\n'
1128 $ hg log -r 0 -T '{if(true, "true is True")}\n'
1129 true is True
1129 true is True
1130 $ hg log -r 0 -T '{if(false, "", "false is False")}\n'
1130 $ hg log -r 0 -T '{if(false, "", "false is False")}\n'
1131 false is False
1131 false is False
1132 $ hg log -r 0 -T '{if("false", "non-empty string is True")}\n'
1132 $ hg log -r 0 -T '{if("false", "non-empty string is True")}\n'
1133 non-empty string is True
1133 non-empty string is True
1134
1134
1135 Test ifcontains function
1135 Test ifcontains function
1136
1136
1137 $ hg log --template '{rev} {ifcontains(rev, "2 two 0", "is in the string", "is not")}\n'
1137 $ hg log --template '{rev} {ifcontains(rev, "2 two 0", "is in the string", "is not")}\n'
1138 2 is in the string
1138 2 is in the string
1139 1 is not
1139 1 is not
1140 0 is in the string
1140 0 is in the string
1141
1141
1142 $ hg log -T '{rev} {ifcontains(rev, "2 two{" 0"}", "is in the string", "is not")}\n'
1142 $ hg log -T '{rev} {ifcontains(rev, "2 two{" 0"}", "is in the string", "is not")}\n'
1143 2 is in the string
1143 2 is in the string
1144 1 is not
1144 1 is not
1145 0 is in the string
1145 0 is in the string
1146
1146
1147 $ hg log --template '{rev} {ifcontains("a", file_adds, "added a", "did not add a")}\n'
1147 $ hg log --template '{rev} {ifcontains("a", file_adds, "added a", "did not add a")}\n'
1148 2 did not add a
1148 2 did not add a
1149 1 did not add a
1149 1 did not add a
1150 0 added a
1150 0 added a
1151
1151
1152 $ hg log --debug -T '{rev}{ifcontains(1, parents, " is parent of 1")}\n'
1152 $ hg log --debug -T '{rev}{ifcontains(1, parents, " is parent of 1")}\n'
1153 2 is parent of 1
1153 2 is parent of 1
1154 1
1154 1
1155 0
1155 0
1156
1156
1157 $ hg log -l1 -T '{ifcontains("branch", extras, "t", "f")}\n'
1157 $ hg log -l1 -T '{ifcontains("branch", extras, "t", "f")}\n'
1158 t
1158 t
1159 $ hg log -l1 -T '{ifcontains("branch", extras % "{key}", "t", "f")}\n'
1159 $ hg log -l1 -T '{ifcontains("branch", extras % "{key}", "t", "f")}\n'
1160 t
1160 t
1161 $ hg log -l1 -T '{ifcontains("branc", extras % "{key}", "t", "f")}\n'
1161 $ hg log -l1 -T '{ifcontains("branc", extras % "{key}", "t", "f")}\n'
1162 f
1162 f
1163 $ hg log -l1 -T '{ifcontains("branc", stringify(extras % "{key}"), "t", "f")}\n'
1163 $ hg log -l1 -T '{ifcontains("branc", stringify(extras % "{key}"), "t", "f")}\n'
1164 t
1164 t
1165
1165
1166 Test revset function
1166 Test revset function
1167
1167
1168 $ hg log --template '{rev} {ifcontains(rev, revset("."), "current rev", "not current rev")}\n'
1168 $ hg log --template '{rev} {ifcontains(rev, revset("."), "current rev", "not current rev")}\n'
1169 2 current rev
1169 2 current rev
1170 1 not current rev
1170 1 not current rev
1171 0 not current rev
1171 0 not current rev
1172
1172
1173 $ hg log --template '{rev} {ifcontains(rev, revset(". + .^"), "match rev", "not match rev")}\n'
1173 $ hg log --template '{rev} {ifcontains(rev, revset(". + .^"), "match rev", "not match rev")}\n'
1174 2 match rev
1174 2 match rev
1175 1 match rev
1175 1 match rev
1176 0 not match rev
1176 0 not match rev
1177
1177
1178 $ hg log -T '{ifcontains(desc, revset(":"), "", "type not match")}\n' -l1
1178 $ hg log -T '{ifcontains(desc, revset(":"), "", "type not match")}\n' -l1
1179 type not match
1179 type not match
1180
1180
1181 $ hg log --template '{rev} Parents: {revset("parents(%s)", rev)}\n'
1181 $ hg log --template '{rev} Parents: {revset("parents(%s)", rev)}\n'
1182 2 Parents: 1
1182 2 Parents: 1
1183 1 Parents: 0
1183 1 Parents: 0
1184 0 Parents:
1184 0 Parents:
1185
1185
1186 $ cat >> .hg/hgrc <<EOF
1186 $ cat >> .hg/hgrc <<EOF
1187 > [revsetalias]
1187 > [revsetalias]
1188 > myparents(\$1) = parents(\$1)
1188 > myparents(\$1) = parents(\$1)
1189 > EOF
1189 > EOF
1190 $ hg log --template '{rev} Parents: {revset("myparents(%s)", rev)}\n'
1190 $ hg log --template '{rev} Parents: {revset("myparents(%s)", rev)}\n'
1191 2 Parents: 1
1191 2 Parents: 1
1192 1 Parents: 0
1192 1 Parents: 0
1193 0 Parents:
1193 0 Parents:
1194
1194
1195 $ hg log --template 'Rev: {rev}\n{revset("::%s", rev) % "Ancestor: {revision}\n"}\n'
1195 $ hg log --template 'Rev: {rev}\n{revset("::%s", rev) % "Ancestor: {revision}\n"}\n'
1196 Rev: 2
1196 Rev: 2
1197 Ancestor: 0
1197 Ancestor: 0
1198 Ancestor: 1
1198 Ancestor: 1
1199 Ancestor: 2
1199 Ancestor: 2
1200
1200
1201 Rev: 1
1201 Rev: 1
1202 Ancestor: 0
1202 Ancestor: 0
1203 Ancestor: 1
1203 Ancestor: 1
1204
1204
1205 Rev: 0
1205 Rev: 0
1206 Ancestor: 0
1206 Ancestor: 0
1207
1207
1208 $ hg log --template '{revset("TIP"|lower)}\n' -l1
1208 $ hg log --template '{revset("TIP"|lower)}\n' -l1
1209 2
1209 2
1210
1210
1211 $ hg log -T '{revset("%s", "t{"ip"}")}\n' -l1
1211 $ hg log -T '{revset("%s", "t{"ip"}")}\n' -l1
1212 2
1212 2
1213
1213
1214 a list template is evaluated for each item of revset/parents
1214 a list template is evaluated for each item of revset/parents
1215
1215
1216 $ hg log -T '{rev} p: {revset("p1(%s)", rev) % "{rev}:{node|short}"}\n'
1216 $ hg log -T '{rev} p: {revset("p1(%s)", rev) % "{rev}:{node|short}"}\n'
1217 2 p: 1:bcc7ff960b8e
1217 2 p: 1:bcc7ff960b8e
1218 1 p: 0:f7769ec2ab97
1218 1 p: 0:f7769ec2ab97
1219 0 p:
1219 0 p:
1220
1220
1221 $ hg log --debug -T '{rev} p:{parents % " {rev}:{node|short}"}\n'
1221 $ hg log --debug -T '{rev} p:{parents % " {rev}:{node|short}"}\n'
1222 2 p: 1:bcc7ff960b8e -1:000000000000
1222 2 p: 1:bcc7ff960b8e -1:000000000000
1223 1 p: 0:f7769ec2ab97 -1:000000000000
1223 1 p: 0:f7769ec2ab97 -1:000000000000
1224 0 p: -1:000000000000 -1:000000000000
1224 0 p: -1:000000000000 -1:000000000000
1225
1225
1226 therefore, 'revcache' should be recreated for each rev
1226 therefore, 'revcache' should be recreated for each rev
1227
1227
1228 $ hg log -T '{rev} {file_adds}\np {revset("p1(%s)", rev) % "{file_adds}"}\n'
1228 $ hg log -T '{rev} {file_adds}\np {revset("p1(%s)", rev) % "{file_adds}"}\n'
1229 2 aa b
1229 2 aa b
1230 p
1230 p
1231 1
1231 1
1232 p a
1232 p a
1233 0 a
1233 0 a
1234 p
1234 p
1235
1235
1236 $ hg log --debug -T '{rev} {file_adds}\np {parents % "{file_adds}"}\n'
1236 $ hg log --debug -T '{rev} {file_adds}\np {parents % "{file_adds}"}\n'
1237 2 aa b
1237 2 aa b
1238 p
1238 p
1239 1
1239 1
1240 p a
1240 p a
1241 0 a
1241 0 a
1242 p
1242 p
1243
1243
1244 a revset item must be evaluated as an integer revision, not an offset from tip
1244 a revset item must be evaluated as an integer revision, not an offset from tip
1245
1245
1246 $ hg log -l 1 -T '{revset("null") % "{rev}:{node|short}"}\n'
1246 $ hg log -l 1 -T '{revset("null") % "{rev}:{node|short}"}\n'
1247 -1:000000000000
1247 -1:000000000000
1248 $ hg log -l 1 -T '{revset("%s", "null") % "{rev}:{node|short}"}\n'
1248 $ hg log -l 1 -T '{revset("%s", "null") % "{rev}:{node|short}"}\n'
1249 -1:000000000000
1249 -1:000000000000
1250
1250
1251 join() should pick '{rev}' from revset items:
1251 join() should pick '{rev}' from revset items:
1252
1252
1253 $ hg log -R ../a -T '{join(revset("parents(%d)", rev), ", ")}\n' -r6
1253 $ hg log -R ../a -T '{join(revset("parents(%d)", rev), ", ")}\n' -r6
1254 4, 5
1254 4, 5
1255
1255
1256 on the other hand, parents are formatted as '{rev}:{node|formatnode}' by
1256 on the other hand, parents are formatted as '{rev}:{node|formatnode}' by
1257 default. join() should agree with the default formatting:
1257 default. join() should agree with the default formatting:
1258
1258
1259 $ hg log -R ../a -T '{join(parents, ", ")}\n' -r6
1259 $ hg log -R ../a -T '{join(parents, ", ")}\n' -r6
1260 5:13207e5a10d9, 4:bbe44766e73d
1260 5:13207e5a10d9, 4:bbe44766e73d
1261
1261
1262 $ hg log -R ../a -T '{join(parents, ",\n")}\n' -r6 --debug
1262 $ hg log -R ../a -T '{join(parents, ",\n")}\n' -r6 --debug
1263 5:13207e5a10d9fd28ec424934298e176197f2c67f,
1263 5:13207e5a10d9fd28ec424934298e176197f2c67f,
1264 4:bbe44766e73d5f11ed2177f1838de10c53ef3e74
1264 4:bbe44766e73d5f11ed2177f1838de10c53ef3e74
1265
1265
1266 Invalid arguments passed to revset()
1266 Invalid arguments passed to revset()
1267
1267
1268 $ hg log -T '{revset("%whatever", 0)}\n'
1268 $ hg log -T '{revset("%whatever", 0)}\n'
1269 hg: parse error: unexpected revspec format character w
1269 hg: parse error: unexpected revspec format character w
1270 [255]
1270 [255]
1271 $ hg log -T '{revset("%lwhatever", files)}\n'
1271 $ hg log -T '{revset("%lwhatever", files)}\n'
1272 hg: parse error: unexpected revspec format character w
1272 hg: parse error: unexpected revspec format character w
1273 [255]
1273 [255]
1274 $ hg log -T '{revset("%s %s", 0)}\n'
1274 $ hg log -T '{revset("%s %s", 0)}\n'
1275 hg: parse error: missing argument for revspec
1275 hg: parse error: missing argument for revspec
1276 [255]
1276 [255]
1277 $ hg log -T '{revset("", 0)}\n'
1277 $ hg log -T '{revset("", 0)}\n'
1278 hg: parse error: too many revspec arguments specified
1278 hg: parse error: too many revspec arguments specified
1279 [255]
1279 [255]
1280 $ hg log -T '{revset("%s", 0, 1)}\n'
1280 $ hg log -T '{revset("%s", 0, 1)}\n'
1281 hg: parse error: too many revspec arguments specified
1281 hg: parse error: too many revspec arguments specified
1282 [255]
1282 [255]
1283 $ hg log -T '{revset("%", 0)}\n'
1283 $ hg log -T '{revset("%", 0)}\n'
1284 hg: parse error: incomplete revspec format character
1284 hg: parse error: incomplete revspec format character
1285 [255]
1285 [255]
1286 $ hg log -T '{revset("%l", 0)}\n'
1286 $ hg log -T '{revset("%l", 0)}\n'
1287 hg: parse error: incomplete revspec format character
1287 hg: parse error: incomplete revspec format character
1288 [255]
1288 [255]
1289 $ hg log -T '{revset("%d", 'foo')}\n'
1289 $ hg log -T '{revset("%d", 'foo')}\n'
1290 hg: parse error: invalid argument for revspec
1290 hg: parse error: invalid argument for revspec
1291 [255]
1291 [255]
1292 $ hg log -T '{revset("%ld", files)}\n'
1292 $ hg log -T '{revset("%ld", files)}\n'
1293 hg: parse error: invalid argument for revspec
1293 hg: parse error: invalid argument for revspec
1294 [255]
1294 [255]
1295 $ hg log -T '{revset("%ls", 0)}\n'
1295 $ hg log -T '{revset("%ls", 0)}\n'
1296 hg: parse error: invalid argument for revspec
1296 hg: parse error: invalid argument for revspec
1297 [255]
1297 [255]
1298 $ hg log -T '{revset("%b", 'foo')}\n'
1298 $ hg log -T '{revset("%b", 'foo')}\n'
1299 hg: parse error: invalid argument for revspec
1299 hg: parse error: invalid argument for revspec
1300 [255]
1300 [255]
1301 $ hg log -T '{revset("%lb", files)}\n'
1301 $ hg log -T '{revset("%lb", files)}\n'
1302 hg: parse error: invalid argument for revspec
1302 hg: parse error: invalid argument for revspec
1303 [255]
1303 [255]
1304 $ hg log -T '{revset("%r", 0)}\n'
1304 $ hg log -T '{revset("%r", 0)}\n'
1305 hg: parse error: invalid argument for revspec
1305 hg: parse error: invalid argument for revspec
1306 [255]
1306 [255]
1307
1307
1308 Test files function
1308 Test files function
1309
1309
1310 $ hg log -T "{rev}\n{join(files('*'), '\n')}\n"
1310 $ hg log -T "{rev}\n{join(files('*'), '\n')}\n"
1311 2
1311 2
1312 a
1312 a
1313 aa
1313 aa
1314 b
1314 b
1315 1
1315 1
1316 a
1316 a
1317 0
1317 0
1318 a
1318 a
1319
1319
1320 $ hg log -T "{rev}\n{join(files('aa'), '\n')}\n"
1320 $ hg log -T "{rev}\n{join(files('aa'), '\n')}\n"
1321 2
1321 2
1322 aa
1322 aa
1323 1
1323 1
1324
1324
1325 0
1325 0
1326
1326
1327
1327
1328 $ hg log -l1 -T "{files('aa') % '{file}\n'}"
1328 $ hg log -l1 -T "{files('aa') % '{file}\n'}"
1329 aa
1329 aa
1330 $ hg log -l1 -T "{files('aa') % '{path}\n'}"
1330 $ hg log -l1 -T "{files('aa') % '{path}\n'}"
1331 aa
1331 aa
1332
1332
1333 $ hg rm a
1333 $ hg rm a
1334 $ hg log -r "wdir()" -T "{rev}\n{join(files('*'), '\n')}\n"
1334 $ hg log -r "wdir()" -T "{rev}\n{join(files('*'), '\n')}\n"
1335 2147483647
1335 2147483647
1336 aa
1336 aa
1337 b
1337 b
1338 $ hg revert a
1338 $ hg revert a
1339
1339
1340 Test relpath function
1340 Test relpath function
1341
1341
1342 $ hg log -r0 -T '{files % "{file|relpath}\n"}'
1342 $ hg log -r0 -T '{files % "{file|relpath}\n"}'
1343 a
1343 a
1344 $ cd ..
1344 $ cd ..
1345 $ hg log -R r -r0 -T '{files % "{file|relpath}\n"}'
1345 $ hg log -R r -r0 -T '{files % "{file|relpath}\n"}'
1346 r/a
1346 r/a
1347
1347
1348 Test stringify on sub expressions
1348 Test stringify on sub expressions
1349
1349
1350 $ hg log -R a -r 8 --template '{join(files, if("1", if("1", ", ")))}\n'
1350 $ hg log -R a -r 8 --template '{join(files, if("1", if("1", ", ")))}\n'
1351 fourth, second, third
1351 fourth, second, third
1352 $ hg log -R a -r 8 --template '{strip(if("1", if("1", "-abc-")), if("1", if("1", "-")))}\n'
1352 $ hg log -R a -r 8 --template '{strip(if("1", if("1", "-abc-")), if("1", if("1", "-")))}\n'
1353 abc
1353 abc
1354
1354
1355 Test splitlines
1355 Test splitlines
1356
1356
1357 $ hg log -Gv -R a --template "{splitlines(desc) % 'foo {line}\n'}"
1357 $ hg log -Gv -R a --template "{splitlines(desc) % 'foo {line}\n'}"
1358 @ foo Modify, add, remove, rename
1358 @ foo Modify, add, remove, rename
1359 |
1359 |
1360 o foo future
1360 o foo future
1361 |
1361 |
1362 o foo third
1362 o foo third
1363 |
1363 |
1364 o foo second
1364 o foo second
1365
1365
1366 o foo merge
1366 o foo merge
1367 |\
1367 |\
1368 | o foo new head
1368 | o foo new head
1369 | |
1369 | |
1370 o | foo new branch
1370 o | foo new branch
1371 |/
1371 |/
1372 o foo no user, no domain
1372 o foo no user, no domain
1373 |
1373 |
1374 o foo no person
1374 o foo no person
1375 |
1375 |
1376 o foo other 1
1376 o foo other 1
1377 | foo other 2
1377 | foo other 2
1378 | foo
1378 | foo
1379 | foo other 3
1379 | foo other 3
1380 o foo line 1
1380 o foo line 1
1381 foo line 2
1381 foo line 2
1382
1382
1383 $ hg log -R a -r0 -T '{desc|splitlines}\n'
1383 $ hg log -R a -r0 -T '{desc|splitlines}\n'
1384 line 1 line 2
1384 line 1 line 2
1385 $ hg log -R a -r0 -T '{join(desc|splitlines, "|")}\n'
1385 $ hg log -R a -r0 -T '{join(desc|splitlines, "|")}\n'
1386 line 1|line 2
1386 line 1|line 2
1387
1387
1388 Test startswith
1388 Test startswith
1389 $ hg log -Gv -R a --template "{startswith(desc)}"
1389 $ hg log -Gv -R a --template "{startswith(desc)}"
1390 hg: parse error: startswith expects two arguments
1390 hg: parse error: startswith expects two arguments
1391 [255]
1391 [255]
1392
1392
1393 $ hg log -Gv -R a --template "{startswith('line', desc)}"
1393 $ hg log -Gv -R a --template "{startswith('line', desc)}"
1394 @
1394 @
1395 |
1395 |
1396 o
1396 o
1397 |
1397 |
1398 o
1398 o
1399 |
1399 |
1400 o
1400 o
1401
1401
1402 o
1402 o
1403 |\
1403 |\
1404 | o
1404 | o
1405 | |
1405 | |
1406 o |
1406 o |
1407 |/
1407 |/
1408 o
1408 o
1409 |
1409 |
1410 o
1410 o
1411 |
1411 |
1412 o
1412 o
1413 |
1413 |
1414 o line 1
1414 o line 1
1415 line 2
1415 line 2
1416
1416
1417 Test word function (including index out of bounds graceful failure)
1417 Test word function (including index out of bounds graceful failure)
1418
1418
1419 $ hg log -Gv -R a --template "{word('1', desc)}"
1419 $ hg log -Gv -R a --template "{word('1', desc)}"
1420 @ add,
1420 @ add,
1421 |
1421 |
1422 o
1422 o
1423 |
1423 |
1424 o
1424 o
1425 |
1425 |
1426 o
1426 o
1427
1427
1428 o
1428 o
1429 |\
1429 |\
1430 | o head
1430 | o head
1431 | |
1431 | |
1432 o | branch
1432 o | branch
1433 |/
1433 |/
1434 o user,
1434 o user,
1435 |
1435 |
1436 o person
1436 o person
1437 |
1437 |
1438 o 1
1438 o 1
1439 |
1439 |
1440 o 1
1440 o 1
1441
1441
1442
1442
1443 Test word third parameter used as splitter
1443 Test word third parameter used as splitter
1444
1444
1445 $ hg log -Gv -R a --template "{word('0', desc, 'o')}"
1445 $ hg log -Gv -R a --template "{word('0', desc, 'o')}"
1446 @ M
1446 @ M
1447 |
1447 |
1448 o future
1448 o future
1449 |
1449 |
1450 o third
1450 o third
1451 |
1451 |
1452 o sec
1452 o sec
1453
1453
1454 o merge
1454 o merge
1455 |\
1455 |\
1456 | o new head
1456 | o new head
1457 | |
1457 | |
1458 o | new branch
1458 o | new branch
1459 |/
1459 |/
1460 o n
1460 o n
1461 |
1461 |
1462 o n
1462 o n
1463 |
1463 |
1464 o
1464 o
1465 |
1465 |
1466 o line 1
1466 o line 1
1467 line 2
1467 line 2
1468
1468
1469 Test word error messages for not enough and too many arguments
1469 Test word error messages for not enough and too many arguments
1470
1470
1471 $ hg log -Gv -R a --template "{word('0')}"
1471 $ hg log -Gv -R a --template "{word('0')}"
1472 hg: parse error: word expects two or three arguments, got 1
1472 hg: parse error: word expects two or three arguments, got 1
1473 [255]
1473 [255]
1474
1474
1475 $ hg log -Gv -R a --template "{word('0', desc, 'o', 'h', 'b', 'o', 'y')}"
1475 $ hg log -Gv -R a --template "{word('0', desc, 'o', 'h', 'b', 'o', 'y')}"
1476 hg: parse error: word expects two or three arguments, got 7
1476 hg: parse error: word expects two or three arguments, got 7
1477 [255]
1477 [255]
1478
1478
1479 Test word for integer literal
1479 Test word for integer literal
1480
1480
1481 $ hg log -R a --template "{word(2, desc)}\n" -r0
1481 $ hg log -R a --template "{word(2, desc)}\n" -r0
1482 line
1482 line
1483
1483
1484 Test word for invalid numbers
1484 Test word for invalid numbers
1485
1485
1486 $ hg log -Gv -R a --template "{word('a', desc)}"
1486 $ hg log -Gv -R a --template "{word('a', desc)}"
1487 hg: parse error: word expects an integer index
1487 hg: parse error: word expects an integer index
1488 [255]
1488 [255]
1489
1489
1490 Test word for out of range
1490 Test word for out of range
1491
1491
1492 $ hg log -R a --template "{word(10000, desc)}"
1492 $ hg log -R a --template "{word(10000, desc)}"
1493 $ hg log -R a --template "{word(-10000, desc)}"
1493 $ hg log -R a --template "{word(-10000, desc)}"
1494
1494
1495 Test indent and not adding to empty lines
1495 Test indent and not adding to empty lines
1496
1496
1497 $ hg log -T "-----\n{indent(desc, '>> ', ' > ')}\n" -r 0:1 -R a
1497 $ hg log -T "-----\n{indent(desc, '>> ', ' > ')}\n" -r 0:1 -R a
1498 -----
1498 -----
1499 > line 1
1499 > line 1
1500 >> line 2
1500 >> line 2
1501 -----
1501 -----
1502 > other 1
1502 > other 1
1503 >> other 2
1503 >> other 2
1504
1504
1505 >> other 3
1505 >> other 3
1506
1506
1507 Test indent with empty first line
1507 Test indent with empty first line
1508
1508
1509 $ hg version -T "{indent('', '>> ')}\n"
1509 $ hg version -T "{indent('', '>> ')}\n"
1510 >>
1510
1511
1511
1512 $ hg version -T "{indent('
1512 $ hg version -T "{indent('
1513 > second', '>> ')}\n"
1513 > second', '>> ')}\n"
1514 >>
1514
1515 >> second
1515 >> second
1516
1516
1517 $ hg version -T "{indent('
1517 $ hg version -T "{indent('
1518 > second', '>> ', ' > ')}\n"
1518 > second', '>> ', ' > ')}\n"
1519 >
1519
1520 >> second
1520 >> second
1521
1521
1522 Test with non-strings like dates
1522 Test with non-strings like dates
1523
1523
1524 $ hg log -T "{indent(date, ' ')}\n" -r 2:3 -R a
1524 $ hg log -T "{indent(date, ' ')}\n" -r 2:3 -R a
1525 1200000.00
1525 1200000.00
1526 1300000.00
1526 1300000.00
1527
1527
1528 Test cbor filter:
1528 Test cbor filter:
1529
1529
1530 $ cat <<'EOF' > "$TESTTMP/decodecbor.py"
1530 $ cat <<'EOF' > "$TESTTMP/decodecbor.py"
1531 > from __future__ import absolute_import
1531 > from __future__ import absolute_import
1532 > from mercurial import (
1532 > from mercurial import (
1533 > dispatch,
1533 > dispatch,
1534 > pycompat,
1534 > pycompat,
1535 > )
1535 > )
1536 > from mercurial.utils import (
1536 > from mercurial.utils import (
1537 > cborutil,
1537 > cborutil,
1538 > stringutil,
1538 > stringutil,
1539 > )
1539 > )
1540 > dispatch.initstdio()
1540 > dispatch.initstdio()
1541 > items = cborutil.decodeall(pycompat.stdin.read())
1541 > items = cborutil.decodeall(pycompat.stdin.read())
1542 > pycompat.stdout.write(stringutil.pprint(items, indent=1) + b'\n')
1542 > pycompat.stdout.write(stringutil.pprint(items, indent=1) + b'\n')
1543 > EOF
1543 > EOF
1544
1544
1545 $ hg log -T "{rev|cbor}" -R a -l2 | "$PYTHON" "$TESTTMP/decodecbor.py"
1545 $ hg log -T "{rev|cbor}" -R a -l2 | "$PYTHON" "$TESTTMP/decodecbor.py"
1546 [
1546 [
1547 10,
1547 10,
1548 9
1548 9
1549 ]
1549 ]
1550
1550
1551 $ hg log -T "{extras|cbor}" -R a -l1 | "$PYTHON" "$TESTTMP/decodecbor.py"
1551 $ hg log -T "{extras|cbor}" -R a -l1 | "$PYTHON" "$TESTTMP/decodecbor.py"
1552 [
1552 [
1553 {
1553 {
1554 'branch': 'default'
1554 'branch': 'default'
1555 }
1555 }
1556 ]
1556 ]
1557
1557
1558 json filter should escape HTML tags so that the output can be embedded in hgweb:
1558 json filter should escape HTML tags so that the output can be embedded in hgweb:
1559
1559
1560 $ hg log -T "{'<foo@example.org>'|json}\n" -R a -l1
1560 $ hg log -T "{'<foo@example.org>'|json}\n" -R a -l1
1561 "\u003cfoo@example.org\u003e"
1561 "\u003cfoo@example.org\u003e"
1562
1562
1563 Set up repository for non-ascii encoding tests:
1563 Set up repository for non-ascii encoding tests:
1564
1564
1565 $ hg init nonascii
1565 $ hg init nonascii
1566 $ cd nonascii
1566 $ cd nonascii
1567 $ "$PYTHON" <<EOF
1567 $ "$PYTHON" <<EOF
1568 > open('latin1', 'wb').write(b'\xe9')
1568 > open('latin1', 'wb').write(b'\xe9')
1569 > open('utf-8', 'wb').write(b'\xc3\xa9')
1569 > open('utf-8', 'wb').write(b'\xc3\xa9')
1570 > EOF
1570 > EOF
1571 $ HGENCODING=utf-8 hg branch -q `cat utf-8`
1571 $ HGENCODING=utf-8 hg branch -q `cat utf-8`
1572 $ HGENCODING=utf-8 hg ci -qAm "non-ascii branch: `cat utf-8`" utf-8
1572 $ HGENCODING=utf-8 hg ci -qAm "non-ascii branch: `cat utf-8`" utf-8
1573
1573
1574 json filter should try round-trip conversion to utf-8:
1574 json filter should try round-trip conversion to utf-8:
1575
1575
1576 $ HGENCODING=ascii hg log -T "{branch|json}\n" -r0
1576 $ HGENCODING=ascii hg log -T "{branch|json}\n" -r0
1577 "\u00e9"
1577 "\u00e9"
1578 $ HGENCODING=ascii hg log -T "{desc|json}\n" -r0
1578 $ HGENCODING=ascii hg log -T "{desc|json}\n" -r0
1579 "non-ascii branch: \u00e9"
1579 "non-ascii branch: \u00e9"
1580
1580
1581 json filter should take input as utf-8 if it was converted from utf-8:
1581 json filter should take input as utf-8 if it was converted from utf-8:
1582
1582
1583 $ HGENCODING=latin-1 hg log -T "{branch|json}\n" -r0
1583 $ HGENCODING=latin-1 hg log -T "{branch|json}\n" -r0
1584 "\u00e9"
1584 "\u00e9"
1585 $ HGENCODING=latin-1 hg log -T "{desc|json}\n" -r0
1585 $ HGENCODING=latin-1 hg log -T "{desc|json}\n" -r0
1586 "non-ascii branch: \u00e9"
1586 "non-ascii branch: \u00e9"
1587
1587
1588 json filter takes input as utf-8b:
1588 json filter takes input as utf-8b:
1589
1589
1590 $ HGENCODING=ascii hg log -T "{'`cat utf-8`'|json}\n" -l1
1590 $ HGENCODING=ascii hg log -T "{'`cat utf-8`'|json}\n" -l1
1591 "\u00e9"
1591 "\u00e9"
1592 $ HGENCODING=ascii hg log -T "{'`cat latin1`'|json}\n" -l1
1592 $ HGENCODING=ascii hg log -T "{'`cat latin1`'|json}\n" -l1
1593 "\udce9"
1593 "\udce9"
1594
1594
1595 cbor filter is bytes transparent, which should handle bytes subtypes
1595 cbor filter is bytes transparent, which should handle bytes subtypes
1596 as bytes:
1596 as bytes:
1597
1597
1598 $ HGENCODING=ascii hg log -T "{branch|cbor}" -r0 \
1598 $ HGENCODING=ascii hg log -T "{branch|cbor}" -r0 \
1599 > | "$PYTHON" "$TESTTMP/decodecbor.py"
1599 > | "$PYTHON" "$TESTTMP/decodecbor.py"
1600 [
1600 [
1601 '?'
1601 '?'
1602 ]
1602 ]
1603 $ HGENCODING=latin-1 hg log -T "{branch|cbor}" -r0 \
1603 $ HGENCODING=latin-1 hg log -T "{branch|cbor}" -r0 \
1604 > | "$PYTHON" "$TESTTMP/decodecbor.py"
1604 > | "$PYTHON" "$TESTTMP/decodecbor.py"
1605 [
1605 [
1606 '\xe9'
1606 '\xe9'
1607 ]
1607 ]
1608
1608
1609 utf8 filter:
1609 utf8 filter:
1610
1610
1611 $ HGENCODING=ascii hg log -T "round-trip: {branch|utf8|hex}\n" -r0
1611 $ HGENCODING=ascii hg log -T "round-trip: {branch|utf8|hex}\n" -r0
1612 round-trip: c3a9
1612 round-trip: c3a9
1613 $ HGENCODING=latin1 hg log -T "decoded: {'`cat latin1`'|utf8|hex}\n" -l1
1613 $ HGENCODING=latin1 hg log -T "decoded: {'`cat latin1`'|utf8|hex}\n" -l1
1614 decoded: c3a9
1614 decoded: c3a9
1615 $ HGENCODING=ascii hg log -T "replaced: {'`cat latin1`'|utf8|hex}\n" -l1
1615 $ HGENCODING=ascii hg log -T "replaced: {'`cat latin1`'|utf8|hex}\n" -l1
1616 abort: decoding near * (glob)
1616 abort: decoding near * (glob)
1617 [255]
1617 [255]
1618 $ hg log -T "coerced to string: {rev|utf8}\n" -r0
1618 $ hg log -T "coerced to string: {rev|utf8}\n" -r0
1619 coerced to string: 0
1619 coerced to string: 0
1620
1620
1621 pad width:
1621 pad width:
1622
1622
1623 $ HGENCODING=utf-8 hg debugtemplate "{pad('`cat utf-8`', 2, '-')}\n"
1623 $ HGENCODING=utf-8 hg debugtemplate "{pad('`cat utf-8`', 2, '-')}\n"
1624 \xc3\xa9- (esc)
1624 \xc3\xa9- (esc)
1625
1625
1626 read config options:
1626 read config options:
1627
1627
1628 $ hg log -T "{config('templateconfig', 'knob', 'foo')}\n"
1628 $ hg log -T "{config('templateconfig', 'knob', 'foo')}\n"
1629 foo
1629 foo
1630 $ hg log -T "{config('templateconfig', 'knob', 'foo')}\n" \
1630 $ hg log -T "{config('templateconfig', 'knob', 'foo')}\n" \
1631 > --config templateconfig.knob=bar
1631 > --config templateconfig.knob=bar
1632 bar
1632 bar
1633 $ hg log -T "{configbool('templateconfig', 'knob', True)}\n"
1633 $ hg log -T "{configbool('templateconfig', 'knob', True)}\n"
1634 True
1634 True
1635 $ hg log -T "{configbool('templateconfig', 'knob', True)}\n" \
1635 $ hg log -T "{configbool('templateconfig', 'knob', True)}\n" \
1636 > --config templateconfig.knob=0
1636 > --config templateconfig.knob=0
1637 False
1637 False
1638 $ hg log -T "{configint('templateconfig', 'knob', 123)}\n"
1638 $ hg log -T "{configint('templateconfig', 'knob', 123)}\n"
1639 123
1639 123
1640 $ hg log -T "{configint('templateconfig', 'knob', 123)}\n" \
1640 $ hg log -T "{configint('templateconfig', 'knob', 123)}\n" \
1641 > --config templateconfig.knob=456
1641 > --config templateconfig.knob=456
1642 456
1642 456
1643 $ hg log -T "{config('templateconfig', 'knob')}\n"
1643 $ hg log -T "{config('templateconfig', 'knob')}\n"
1644 devel-warn: config item requires an explicit default value: 'templateconfig.knob' at: * (glob)
1644 devel-warn: config item requires an explicit default value: 'templateconfig.knob' at: * (glob)
1645
1645
1646 $ hg log -T "{configbool('ui', 'interactive')}\n"
1646 $ hg log -T "{configbool('ui', 'interactive')}\n"
1647 False
1647 False
1648 $ hg log -T "{configbool('ui', 'interactive')}\n" --config ui.interactive=1
1648 $ hg log -T "{configbool('ui', 'interactive')}\n" --config ui.interactive=1
1649 True
1649 True
1650 $ hg log -T "{config('templateconfig', 'knob', if(true, 'foo', 'bar'))}\n"
1650 $ hg log -T "{config('templateconfig', 'knob', if(true, 'foo', 'bar'))}\n"
1651 foo
1651 foo
1652
1652
1653 $ cd ..
1653 $ cd ..
General Comments 0
You need to be logged in to leave comments. Login now