##// END OF EJS Templates
templates: add 'bisect' keyword to return a cset's bisect status...
"Yann E. MORIN" -
r15155:f4a8d754 default
parent child Browse files
Show More
@@ -0,0 +1,117 b''
1 # Here we create a simple DAG which has just enough of the required
2 # topology to test all the bisection status labels:
3 #
4 # 13--14
5 # /
6 # 0--1--2--3---------9--10--11--12
7 # \ /
8 # 4--5--6--7--8
9
10
11 $ hg init
12
13 $ echo '0' >a
14 $ hg add a
15 $ hg ci -u test -d '0 0' -m '0'
16 $ echo '1' >a
17 $ hg ci -u test -d '0 1' -m '1'
18
19 branch 2-3
20
21 $ echo '2' >b
22 $ hg add b
23 $ hg ci -u test -d '0 2' -m '2'
24 $ echo '3' >b
25 $ hg ci -u test -d '0 3' -m '3'
26
27 branch 4-8
28
29 $ hg up -r 1
30 0 files updated, 0 files merged, 1 files removed, 0 files unresolved
31 $ echo '4' >c
32 $ hg add c
33 $ hg ci -u test -d '0 4' -m '4'
34 created new head
35 $ echo '5' >c
36 $ hg ci -u test -d '0 5' -m '5'
37 $ echo '6' >c
38 $ hg ci -u test -d '0 6' -m '6'
39 $ echo '7' >c
40 $ hg ci -u test -d '0 7' -m '7'
41 $ echo '8' >c
42 $ hg ci -u test -d '0 8' -m '8'
43
44 merge
45
46 $ hg merge -r 3
47 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
48 (branch merge, don't forget to commit)
49 $ hg ci -u test -d '0 9' -m '9=8+3'
50
51 $ echo '10' >a
52 $ hg ci -u test -d '0 10' -m '10'
53 $ echo '11' >a
54 $ hg ci -u test -d '0 11' -m '11'
55 $ echo '12' >a
56 $ hg ci -u test -d '0 12' -m '12'
57
58 unrelated branch
59
60 $ hg up -r 3
61 1 files updated, 0 files merged, 1 files removed, 0 files unresolved
62 $ echo '13' >d
63 $ hg add d
64 $ hg ci -u test -d '0 13' -m '13'
65 created new head
66 $ echo '14' >d
67 $ hg ci -u test -d '0 14' -m '14'
68
69 mark changesets
70
71 $ hg bisect --reset
72 $ hg bisect --good 4
73 $ hg bisect --good 6
74 $ hg bisect --bad 12
75 Testing changeset 9:8bcbdb072033 (6 changesets remaining, ~2 tests)
76 1 files updated, 0 files merged, 1 files removed, 0 files unresolved
77 $ hg bisect --bad 10
78 Testing changeset 8:3cd112f87d77 (4 changesets remaining, ~2 tests)
79 0 files updated, 0 files merged, 1 files removed, 0 files unresolved
80 $ hg bisect --skip 7
81 Testing changeset 8:3cd112f87d77 (4 changesets remaining, ~2 tests)
82 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
83
84 test template
85
86 $ hg log --template '{rev}:{node|short} {bisect}\n'
87 14:cecd84203acc
88 13:86f7c8cdb6df
89 12:a76089b5f47c bad
90 11:5c3eb122d29c bad (implicit)
91 10:b097cef2be03 bad
92 9:8bcbdb072033 untested
93 8:3cd112f87d77 untested
94 7:577e237a73bd skipped
95 6:e597fa2707c5 good
96 5:b9cea37a76bc good (implicit)
97 4:da6b357259d7 good
98 3:e7f031aee8ca ignored
99 2:b1ad1b6bcc5c ignored
100 1:37f42ae8b45e good (implicit)
101 0:b4e73ffab476 good (implicit)
102 $ hg log --template '{bisect|shortbisect} {rev}:{node|short}\n'
103 14:cecd84203acc
104 13:86f7c8cdb6df
105 B 12:a76089b5f47c
106 B 11:5c3eb122d29c
107 B 10:b097cef2be03
108 U 9:8bcbdb072033
109 U 8:3cd112f87d77
110 S 7:577e237a73bd
111 G 6:e597fa2707c5
112 G 5:b9cea37a76bc
113 G 4:da6b357259d7
114 I 3:e7f031aee8ca
115 I 2:b1ad1b6bcc5c
116 G 1:37f42ae8b45e
117 G 0:b4e73ffab476
@@ -1,362 +1,372 b''
1 # template-filters.py - common template expansion filters
1 # template-filters.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 import cgi, re, os, time, urllib
8 import cgi, re, os, time, urllib
9 import encoding, node, util
9 import encoding, node, util
10 import hbisect
10
11
11 def addbreaks(text):
12 def addbreaks(text):
12 """:addbreaks: Any text. Add an XHTML "<br />" tag before the end of
13 """:addbreaks: Any text. Add an XHTML "<br />" tag before the end of
13 every line except the last.
14 every line except the last.
14 """
15 """
15 return text.replace('\n', '<br/>\n')
16 return text.replace('\n', '<br/>\n')
16
17
17 agescales = [("year", 3600 * 24 * 365),
18 agescales = [("year", 3600 * 24 * 365),
18 ("month", 3600 * 24 * 30),
19 ("month", 3600 * 24 * 30),
19 ("week", 3600 * 24 * 7),
20 ("week", 3600 * 24 * 7),
20 ("day", 3600 * 24),
21 ("day", 3600 * 24),
21 ("hour", 3600),
22 ("hour", 3600),
22 ("minute", 60),
23 ("minute", 60),
23 ("second", 1)]
24 ("second", 1)]
24
25
25 def age(date):
26 def age(date):
26 """:age: Date. Returns a human-readable date/time difference between the
27 """:age: Date. Returns a human-readable date/time difference between the
27 given date/time and the current date/time.
28 given date/time and the current date/time.
28 """
29 """
29
30
30 def plural(t, c):
31 def plural(t, c):
31 if c == 1:
32 if c == 1:
32 return t
33 return t
33 return t + "s"
34 return t + "s"
34 def fmt(t, c):
35 def fmt(t, c):
35 return "%d %s" % (c, plural(t, c))
36 return "%d %s" % (c, plural(t, c))
36
37
37 now = time.time()
38 now = time.time()
38 then = date[0]
39 then = date[0]
39 future = False
40 future = False
40 if then > now:
41 if then > now:
41 future = True
42 future = True
42 delta = max(1, int(then - now))
43 delta = max(1, int(then - now))
43 if delta > agescales[0][1] * 30:
44 if delta > agescales[0][1] * 30:
44 return 'in the distant future'
45 return 'in the distant future'
45 else:
46 else:
46 delta = max(1, int(now - then))
47 delta = max(1, int(now - then))
47 if delta > agescales[0][1] * 2:
48 if delta > agescales[0][1] * 2:
48 return util.shortdate(date)
49 return util.shortdate(date)
49
50
50 for t, s in agescales:
51 for t, s in agescales:
51 n = delta // s
52 n = delta // s
52 if n >= 2 or s == 1:
53 if n >= 2 or s == 1:
53 if future:
54 if future:
54 return '%s from now' % fmt(t, n)
55 return '%s from now' % fmt(t, n)
55 return '%s ago' % fmt(t, n)
56 return '%s ago' % fmt(t, n)
56
57
57 def basename(path):
58 def basename(path):
58 """:basename: Any text. Treats the text as a path, and returns the last
59 """:basename: Any text. Treats the text as a path, and returns the last
59 component of the path after splitting by the path separator
60 component of the path after splitting by the path separator
60 (ignoring trailing separators). For example, "foo/bar/baz" becomes
61 (ignoring trailing separators). For example, "foo/bar/baz" becomes
61 "baz" and "foo/bar//" becomes "bar".
62 "baz" and "foo/bar//" becomes "bar".
62 """
63 """
63 return os.path.basename(path)
64 return os.path.basename(path)
64
65
65 def datefilter(text):
66 def datefilter(text):
66 """:date: Date. Returns a date in a Unix date format, including the
67 """:date: Date. Returns a date in a Unix date format, including the
67 timezone: "Mon Sep 04 15:13:13 2006 0700".
68 timezone: "Mon Sep 04 15:13:13 2006 0700".
68 """
69 """
69 return util.datestr(text)
70 return util.datestr(text)
70
71
71 def domain(author):
72 def domain(author):
72 """:domain: Any text. Finds the first string that looks like an email
73 """:domain: Any text. Finds the first string that looks like an email
73 address, and extracts just the domain component. Example: ``User
74 address, and extracts just the domain component. Example: ``User
74 <user@example.com>`` becomes ``example.com``.
75 <user@example.com>`` becomes ``example.com``.
75 """
76 """
76 f = author.find('@')
77 f = author.find('@')
77 if f == -1:
78 if f == -1:
78 return ''
79 return ''
79 author = author[f + 1:]
80 author = author[f + 1:]
80 f = author.find('>')
81 f = author.find('>')
81 if f >= 0:
82 if f >= 0:
82 author = author[:f]
83 author = author[:f]
83 return author
84 return author
84
85
85 def email(text):
86 def email(text):
86 """:email: Any text. Extracts the first string that looks like an email
87 """:email: Any text. Extracts the first string that looks like an email
87 address. Example: ``User <user@example.com>`` becomes
88 address. Example: ``User <user@example.com>`` becomes
88 ``user@example.com``.
89 ``user@example.com``.
89 """
90 """
90 return util.email(text)
91 return util.email(text)
91
92
92 def escape(text):
93 def escape(text):
93 """:escape: Any text. Replaces the special XML/XHTML characters "&", "<"
94 """:escape: Any text. Replaces the special XML/XHTML characters "&", "<"
94 and ">" with XML entities.
95 and ">" with XML entities.
95 """
96 """
96 return cgi.escape(text, True)
97 return cgi.escape(text, True)
97
98
98 para_re = None
99 para_re = None
99 space_re = None
100 space_re = None
100
101
101 def fill(text, width):
102 def fill(text, width):
102 '''fill many paragraphs.'''
103 '''fill many paragraphs.'''
103 global para_re, space_re
104 global para_re, space_re
104 if para_re is None:
105 if para_re is None:
105 para_re = re.compile('(\n\n|\n\\s*[-*]\\s*)', re.M)
106 para_re = re.compile('(\n\n|\n\\s*[-*]\\s*)', re.M)
106 space_re = re.compile(r' +')
107 space_re = re.compile(r' +')
107
108
108 def findparas():
109 def findparas():
109 start = 0
110 start = 0
110 while True:
111 while True:
111 m = para_re.search(text, start)
112 m = para_re.search(text, start)
112 if not m:
113 if not m:
113 uctext = unicode(text[start:], encoding.encoding)
114 uctext = unicode(text[start:], encoding.encoding)
114 w = len(uctext)
115 w = len(uctext)
115 while 0 < w and uctext[w - 1].isspace():
116 while 0 < w and uctext[w - 1].isspace():
116 w -= 1
117 w -= 1
117 yield (uctext[:w].encode(encoding.encoding),
118 yield (uctext[:w].encode(encoding.encoding),
118 uctext[w:].encode(encoding.encoding))
119 uctext[w:].encode(encoding.encoding))
119 break
120 break
120 yield text[start:m.start(0)], m.group(1)
121 yield text[start:m.start(0)], m.group(1)
121 start = m.end(1)
122 start = m.end(1)
122
123
123 return "".join([space_re.sub(' ', util.wrap(para, width=width)) + rest
124 return "".join([space_re.sub(' ', util.wrap(para, width=width)) + rest
124 for para, rest in findparas()])
125 for para, rest in findparas()])
125
126
126 def fill68(text):
127 def fill68(text):
127 """:fill68: Any text. Wraps the text to fit in 68 columns."""
128 """:fill68: Any text. Wraps the text to fit in 68 columns."""
128 return fill(text, 68)
129 return fill(text, 68)
129
130
130 def fill76(text):
131 def fill76(text):
131 """:fill76: Any text. Wraps the text to fit in 76 columns."""
132 """:fill76: Any text. Wraps the text to fit in 76 columns."""
132 return fill(text, 76)
133 return fill(text, 76)
133
134
134 def firstline(text):
135 def firstline(text):
135 """:firstline: Any text. Returns the first line of text."""
136 """:firstline: Any text. Returns the first line of text."""
136 try:
137 try:
137 return text.splitlines(True)[0].rstrip('\r\n')
138 return text.splitlines(True)[0].rstrip('\r\n')
138 except IndexError:
139 except IndexError:
139 return ''
140 return ''
140
141
141 def hexfilter(text):
142 def hexfilter(text):
142 """:hex: Any text. Convert a binary Mercurial node identifier into
143 """:hex: Any text. Convert a binary Mercurial node identifier into
143 its long hexadecimal representation.
144 its long hexadecimal representation.
144 """
145 """
145 return node.hex(text)
146 return node.hex(text)
146
147
147 def hgdate(text):
148 def hgdate(text):
148 """:hgdate: Date. Returns the date as a pair of numbers: "1157407993
149 """:hgdate: Date. Returns the date as a pair of numbers: "1157407993
149 25200" (Unix timestamp, timezone offset).
150 25200" (Unix timestamp, timezone offset).
150 """
151 """
151 return "%d %d" % text
152 return "%d %d" % text
152
153
153 def isodate(text):
154 def isodate(text):
154 """:isodate: Date. Returns the date in ISO 8601 format: "2009-08-18 13:00
155 """:isodate: Date. Returns the date in ISO 8601 format: "2009-08-18 13:00
155 +0200".
156 +0200".
156 """
157 """
157 return util.datestr(text, '%Y-%m-%d %H:%M %1%2')
158 return util.datestr(text, '%Y-%m-%d %H:%M %1%2')
158
159
159 def isodatesec(text):
160 def isodatesec(text):
160 """:isodatesec: Date. Returns the date in ISO 8601 format, including
161 """:isodatesec: Date. Returns the date in ISO 8601 format, including
161 seconds: "2009-08-18 13:00:13 +0200". See also the rfc3339date
162 seconds: "2009-08-18 13:00:13 +0200". See also the rfc3339date
162 filter.
163 filter.
163 """
164 """
164 return util.datestr(text, '%Y-%m-%d %H:%M:%S %1%2')
165 return util.datestr(text, '%Y-%m-%d %H:%M:%S %1%2')
165
166
166 def indent(text, prefix):
167 def indent(text, prefix):
167 '''indent each non-empty line of text after first with prefix.'''
168 '''indent each non-empty line of text after first with prefix.'''
168 lines = text.splitlines()
169 lines = text.splitlines()
169 num_lines = len(lines)
170 num_lines = len(lines)
170 endswithnewline = text[-1:] == '\n'
171 endswithnewline = text[-1:] == '\n'
171 def indenter():
172 def indenter():
172 for i in xrange(num_lines):
173 for i in xrange(num_lines):
173 l = lines[i]
174 l = lines[i]
174 if i and l.strip():
175 if i and l.strip():
175 yield prefix
176 yield prefix
176 yield l
177 yield l
177 if i < num_lines - 1 or endswithnewline:
178 if i < num_lines - 1 or endswithnewline:
178 yield '\n'
179 yield '\n'
179 return "".join(indenter())
180 return "".join(indenter())
180
181
181 def json(obj):
182 def json(obj):
182 if obj is None or obj is False or obj is True:
183 if obj is None or obj is False or obj is True:
183 return {None: 'null', False: 'false', True: 'true'}[obj]
184 return {None: 'null', False: 'false', True: 'true'}[obj]
184 elif isinstance(obj, int) or isinstance(obj, float):
185 elif isinstance(obj, int) or isinstance(obj, float):
185 return str(obj)
186 return str(obj)
186 elif isinstance(obj, str):
187 elif isinstance(obj, str):
187 u = unicode(obj, encoding.encoding, 'replace')
188 u = unicode(obj, encoding.encoding, 'replace')
188 return '"%s"' % jsonescape(u)
189 return '"%s"' % jsonescape(u)
189 elif isinstance(obj, unicode):
190 elif isinstance(obj, unicode):
190 return '"%s"' % jsonescape(obj)
191 return '"%s"' % jsonescape(obj)
191 elif util.safehasattr(obj, 'keys'):
192 elif util.safehasattr(obj, 'keys'):
192 out = []
193 out = []
193 for k, v in obj.iteritems():
194 for k, v in obj.iteritems():
194 s = '%s: %s' % (json(k), json(v))
195 s = '%s: %s' % (json(k), json(v))
195 out.append(s)
196 out.append(s)
196 return '{' + ', '.join(out) + '}'
197 return '{' + ', '.join(out) + '}'
197 elif util.safehasattr(obj, '__iter__'):
198 elif util.safehasattr(obj, '__iter__'):
198 out = []
199 out = []
199 for i in obj:
200 for i in obj:
200 out.append(json(i))
201 out.append(json(i))
201 return '[' + ', '.join(out) + ']'
202 return '[' + ', '.join(out) + ']'
202 else:
203 else:
203 raise TypeError('cannot encode type %s' % obj.__class__.__name__)
204 raise TypeError('cannot encode type %s' % obj.__class__.__name__)
204
205
205 def _uescape(c):
206 def _uescape(c):
206 if ord(c) < 0x80:
207 if ord(c) < 0x80:
207 return c
208 return c
208 else:
209 else:
209 return '\\u%04x' % ord(c)
210 return '\\u%04x' % ord(c)
210
211
211 _escapes = [
212 _escapes = [
212 ('\\', '\\\\'), ('"', '\\"'), ('\t', '\\t'), ('\n', '\\n'),
213 ('\\', '\\\\'), ('"', '\\"'), ('\t', '\\t'), ('\n', '\\n'),
213 ('\r', '\\r'), ('\f', '\\f'), ('\b', '\\b'),
214 ('\r', '\\r'), ('\f', '\\f'), ('\b', '\\b'),
214 ]
215 ]
215
216
216 def jsonescape(s):
217 def jsonescape(s):
217 for k, v in _escapes:
218 for k, v in _escapes:
218 s = s.replace(k, v)
219 s = s.replace(k, v)
219 return ''.join(_uescape(c) for c in s)
220 return ''.join(_uescape(c) for c in s)
220
221
221 def localdate(text):
222 def localdate(text):
222 """:localdate: Date. Converts a date to local date."""
223 """:localdate: Date. Converts a date to local date."""
223 return (text[0], util.makedate()[1])
224 return (text[0], util.makedate()[1])
224
225
225 def nonempty(str):
226 def nonempty(str):
226 """:nonempty: Any text. Returns '(none)' if the string is empty."""
227 """:nonempty: Any text. Returns '(none)' if the string is empty."""
227 return str or "(none)"
228 return str or "(none)"
228
229
229 def obfuscate(text):
230 def obfuscate(text):
230 """:obfuscate: Any text. Returns the input text rendered as a sequence of
231 """:obfuscate: Any text. Returns the input text rendered as a sequence of
231 XML entities.
232 XML entities.
232 """
233 """
233 text = unicode(text, encoding.encoding, 'replace')
234 text = unicode(text, encoding.encoding, 'replace')
234 return ''.join(['&#%d;' % ord(c) for c in text])
235 return ''.join(['&#%d;' % ord(c) for c in text])
235
236
236 def permissions(flags):
237 def permissions(flags):
237 if "l" in flags:
238 if "l" in flags:
238 return "lrwxrwxrwx"
239 return "lrwxrwxrwx"
239 if "x" in flags:
240 if "x" in flags:
240 return "-rwxr-xr-x"
241 return "-rwxr-xr-x"
241 return "-rw-r--r--"
242 return "-rw-r--r--"
242
243
243 def person(author):
244 def person(author):
244 """:person: Any text. Returns the text before an email address."""
245 """:person: Any text. Returns the text before an email address."""
245 if not '@' in author:
246 if not '@' in author:
246 return author
247 return author
247 f = author.find('<')
248 f = author.find('<')
248 if f != -1:
249 if f != -1:
249 return author[:f].rstrip()
250 return author[:f].rstrip()
250 f = author.find('@')
251 f = author.find('@')
251 return author[:f].replace('.', ' ')
252 return author[:f].replace('.', ' ')
252
253
253 def rfc3339date(text):
254 def rfc3339date(text):
254 """:rfc3339date: Date. Returns a date using the Internet date format
255 """:rfc3339date: Date. Returns a date using the Internet date format
255 specified in RFC 3339: "2009-08-18T13:00:13+02:00".
256 specified in RFC 3339: "2009-08-18T13:00:13+02:00".
256 """
257 """
257 return util.datestr(text, "%Y-%m-%dT%H:%M:%S%1:%2")
258 return util.datestr(text, "%Y-%m-%dT%H:%M:%S%1:%2")
258
259
259 def rfc822date(text):
260 def rfc822date(text):
260 """:rfc822date: Date. Returns a date using the same format used in email
261 """:rfc822date: Date. Returns a date using the same format used in email
261 headers: "Tue, 18 Aug 2009 13:00:13 +0200".
262 headers: "Tue, 18 Aug 2009 13:00:13 +0200".
262 """
263 """
263 return util.datestr(text, "%a, %d %b %Y %H:%M:%S %1%2")
264 return util.datestr(text, "%a, %d %b %Y %H:%M:%S %1%2")
264
265
265 def short(text):
266 def short(text):
266 """:short: Changeset hash. Returns the short form of a changeset hash,
267 """:short: Changeset hash. Returns the short form of a changeset hash,
267 i.e. a 12 hexadecimal digit string.
268 i.e. a 12 hexadecimal digit string.
268 """
269 """
269 return text[:12]
270 return text[:12]
270
271
272 def shortbisect(text):
273 """:shortbisect: Any text. Treats `text` as a bisection status, and
274 returns a single-character representing the status (G: good, B: bad,
275 S: skipped, U: untested, I: ignored). Returns single space if `text`
276 is not a valid bisection status.
277 """
278 return hbisect.shortlabel(text) or ' '
279
271 def shortdate(text):
280 def shortdate(text):
272 """:shortdate: Date. Returns a date like "2006-09-18"."""
281 """:shortdate: Date. Returns a date like "2006-09-18"."""
273 return util.shortdate(text)
282 return util.shortdate(text)
274
283
275 def stringescape(text):
284 def stringescape(text):
276 return text.encode('string_escape')
285 return text.encode('string_escape')
277
286
278 def stringify(thing):
287 def stringify(thing):
279 """:stringify: Any type. Turns the value into text by converting values into
288 """:stringify: Any type. Turns the value into text by converting values into
280 text and concatenating them.
289 text and concatenating them.
281 """
290 """
282 if util.safehasattr(thing, '__iter__') and not isinstance(thing, str):
291 if util.safehasattr(thing, '__iter__') and not isinstance(thing, str):
283 return "".join([stringify(t) for t in thing if t is not None])
292 return "".join([stringify(t) for t in thing if t is not None])
284 return str(thing)
293 return str(thing)
285
294
286 def strip(text):
295 def strip(text):
287 """:strip: Any text. Strips all leading and trailing whitespace."""
296 """:strip: Any text. Strips all leading and trailing whitespace."""
288 return text.strip()
297 return text.strip()
289
298
290 def stripdir(text):
299 def stripdir(text):
291 """:stripdir: Treat the text as path and strip a directory level, if
300 """:stripdir: Treat the text as path and strip a directory level, if
292 possible. For example, "foo" and "foo/bar" becomes "foo".
301 possible. For example, "foo" and "foo/bar" becomes "foo".
293 """
302 """
294 dir = os.path.dirname(text)
303 dir = os.path.dirname(text)
295 if dir == "":
304 if dir == "":
296 return os.path.basename(text)
305 return os.path.basename(text)
297 else:
306 else:
298 return dir
307 return dir
299
308
300 def tabindent(text):
309 def tabindent(text):
301 """:tabindent: Any text. Returns the text, with every line except the
310 """:tabindent: Any text. Returns the text, with every line except the
302 first starting with a tab character.
311 first starting with a tab character.
303 """
312 """
304 return indent(text, '\t')
313 return indent(text, '\t')
305
314
306 def urlescape(text):
315 def urlescape(text):
307 """:urlescape: Any text. Escapes all "special" characters. For example,
316 """:urlescape: Any text. Escapes all "special" characters. For example,
308 "foo bar" becomes "foo%20bar".
317 "foo bar" becomes "foo%20bar".
309 """
318 """
310 return urllib.quote(text)
319 return urllib.quote(text)
311
320
312 def userfilter(text):
321 def userfilter(text):
313 """:user: Any text. Returns the user portion of an email address."""
322 """:user: Any text. Returns the user portion of an email address."""
314 return util.shortuser(text)
323 return util.shortuser(text)
315
324
316 def xmlescape(text):
325 def xmlescape(text):
317 text = (text
326 text = (text
318 .replace('&', '&amp;')
327 .replace('&', '&amp;')
319 .replace('<', '&lt;')
328 .replace('<', '&lt;')
320 .replace('>', '&gt;')
329 .replace('>', '&gt;')
321 .replace('"', '&quot;')
330 .replace('"', '&quot;')
322 .replace("'", '&#39;')) # &apos; invalid in HTML
331 .replace("'", '&#39;')) # &apos; invalid in HTML
323 return re.sub('[\x00-\x08\x0B\x0C\x0E-\x1F]', ' ', text)
332 return re.sub('[\x00-\x08\x0B\x0C\x0E-\x1F]', ' ', text)
324
333
325 filters = {
334 filters = {
326 "addbreaks": addbreaks,
335 "addbreaks": addbreaks,
327 "age": age,
336 "age": age,
328 "basename": basename,
337 "basename": basename,
329 "date": datefilter,
338 "date": datefilter,
330 "domain": domain,
339 "domain": domain,
331 "email": email,
340 "email": email,
332 "escape": escape,
341 "escape": escape,
333 "fill68": fill68,
342 "fill68": fill68,
334 "fill76": fill76,
343 "fill76": fill76,
335 "firstline": firstline,
344 "firstline": firstline,
336 "hex": hexfilter,
345 "hex": hexfilter,
337 "hgdate": hgdate,
346 "hgdate": hgdate,
338 "isodate": isodate,
347 "isodate": isodate,
339 "isodatesec": isodatesec,
348 "isodatesec": isodatesec,
340 "json": json,
349 "json": json,
341 "jsonescape": jsonescape,
350 "jsonescape": jsonescape,
342 "localdate": localdate,
351 "localdate": localdate,
343 "nonempty": nonempty,
352 "nonempty": nonempty,
344 "obfuscate": obfuscate,
353 "obfuscate": obfuscate,
345 "permissions": permissions,
354 "permissions": permissions,
346 "person": person,
355 "person": person,
347 "rfc3339date": rfc3339date,
356 "rfc3339date": rfc3339date,
348 "rfc822date": rfc822date,
357 "rfc822date": rfc822date,
349 "short": short,
358 "short": short,
359 "shortbisect": shortbisect,
350 "shortdate": shortdate,
360 "shortdate": shortdate,
351 "stringescape": stringescape,
361 "stringescape": stringescape,
352 "stringify": stringify,
362 "stringify": stringify,
353 "strip": strip,
363 "strip": strip,
354 "stripdir": stripdir,
364 "stripdir": stripdir,
355 "tabindent": tabindent,
365 "tabindent": tabindent,
356 "urlescape": urlescape,
366 "urlescape": urlescape,
357 "user": userfilter,
367 "user": userfilter,
358 "xmlescape": xmlescape,
368 "xmlescape": xmlescape,
359 }
369 }
360
370
361 # tell hggettext to extract docstrings from these functions:
371 # tell hggettext to extract docstrings from these functions:
362 i18nfunctions = filters.values()
372 i18nfunctions = filters.values()
@@ -1,314 +1,320 b''
1 # templatekw.py - common changeset template keywords
1 # templatekw.py - common changeset template keywords
2 #
2 #
3 # Copyright 2005-2009 Matt Mackall <mpm@selenic.com>
3 # Copyright 2005-2009 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 node import hex
8 from node import hex
9 import patch, util, error
9 import patch, util, error
10 import hbisect
10
11
11 def showlist(name, values, plural=None, **args):
12 def showlist(name, values, plural=None, **args):
12 '''expand set of values.
13 '''expand set of values.
13 name is name of key in template map.
14 name is name of key in template map.
14 values is list of strings or dicts.
15 values is list of strings or dicts.
15 plural is plural of name, if not simply name + 's'.
16 plural is plural of name, if not simply name + 's'.
16
17
17 expansion works like this, given name 'foo'.
18 expansion works like this, given name 'foo'.
18
19
19 if values is empty, expand 'no_foos'.
20 if values is empty, expand 'no_foos'.
20
21
21 if 'foo' not in template map, return values as a string,
22 if 'foo' not in template map, return values as a string,
22 joined by space.
23 joined by space.
23
24
24 expand 'start_foos'.
25 expand 'start_foos'.
25
26
26 for each value, expand 'foo'. if 'last_foo' in template
27 for each value, expand 'foo'. if 'last_foo' in template
27 map, expand it instead of 'foo' for last key.
28 map, expand it instead of 'foo' for last key.
28
29
29 expand 'end_foos'.
30 expand 'end_foos'.
30 '''
31 '''
31 templ = args['templ']
32 templ = args['templ']
32 if plural:
33 if plural:
33 names = plural
34 names = plural
34 else: names = name + 's'
35 else: names = name + 's'
35 if not values:
36 if not values:
36 noname = 'no_' + names
37 noname = 'no_' + names
37 if noname in templ:
38 if noname in templ:
38 yield templ(noname, **args)
39 yield templ(noname, **args)
39 return
40 return
40 if name not in templ:
41 if name not in templ:
41 if isinstance(values[0], str):
42 if isinstance(values[0], str):
42 yield ' '.join(values)
43 yield ' '.join(values)
43 else:
44 else:
44 for v in values:
45 for v in values:
45 yield dict(v, **args)
46 yield dict(v, **args)
46 return
47 return
47 startname = 'start_' + names
48 startname = 'start_' + names
48 if startname in templ:
49 if startname in templ:
49 yield templ(startname, **args)
50 yield templ(startname, **args)
50 vargs = args.copy()
51 vargs = args.copy()
51 def one(v, tag=name):
52 def one(v, tag=name):
52 try:
53 try:
53 vargs.update(v)
54 vargs.update(v)
54 except (AttributeError, ValueError):
55 except (AttributeError, ValueError):
55 try:
56 try:
56 for a, b in v:
57 for a, b in v:
57 vargs[a] = b
58 vargs[a] = b
58 except ValueError:
59 except ValueError:
59 vargs[name] = v
60 vargs[name] = v
60 return templ(tag, **vargs)
61 return templ(tag, **vargs)
61 lastname = 'last_' + name
62 lastname = 'last_' + name
62 if lastname in templ:
63 if lastname in templ:
63 last = values.pop()
64 last = values.pop()
64 else:
65 else:
65 last = None
66 last = None
66 for v in values:
67 for v in values:
67 yield one(v)
68 yield one(v)
68 if last is not None:
69 if last is not None:
69 yield one(last, tag=lastname)
70 yield one(last, tag=lastname)
70 endname = 'end_' + names
71 endname = 'end_' + names
71 if endname in templ:
72 if endname in templ:
72 yield templ(endname, **args)
73 yield templ(endname, **args)
73
74
74 def getfiles(repo, ctx, revcache):
75 def getfiles(repo, ctx, revcache):
75 if 'files' not in revcache:
76 if 'files' not in revcache:
76 revcache['files'] = repo.status(ctx.p1().node(), ctx.node())[:3]
77 revcache['files'] = repo.status(ctx.p1().node(), ctx.node())[:3]
77 return revcache['files']
78 return revcache['files']
78
79
79 def getlatesttags(repo, ctx, cache):
80 def getlatesttags(repo, ctx, cache):
80 '''return date, distance and name for the latest tag of rev'''
81 '''return date, distance and name for the latest tag of rev'''
81
82
82 if 'latesttags' not in cache:
83 if 'latesttags' not in cache:
83 # Cache mapping from rev to a tuple with tag date, tag
84 # Cache mapping from rev to a tuple with tag date, tag
84 # distance and tag name
85 # distance and tag name
85 cache['latesttags'] = {-1: (0, 0, 'null')}
86 cache['latesttags'] = {-1: (0, 0, 'null')}
86 latesttags = cache['latesttags']
87 latesttags = cache['latesttags']
87
88
88 rev = ctx.rev()
89 rev = ctx.rev()
89 todo = [rev]
90 todo = [rev]
90 while todo:
91 while todo:
91 rev = todo.pop()
92 rev = todo.pop()
92 if rev in latesttags:
93 if rev in latesttags:
93 continue
94 continue
94 ctx = repo[rev]
95 ctx = repo[rev]
95 tags = [t for t in ctx.tags() if repo.tagtype(t) == 'global']
96 tags = [t for t in ctx.tags() if repo.tagtype(t) == 'global']
96 if tags:
97 if tags:
97 latesttags[rev] = ctx.date()[0], 0, ':'.join(sorted(tags))
98 latesttags[rev] = ctx.date()[0], 0, ':'.join(sorted(tags))
98 continue
99 continue
99 try:
100 try:
100 # The tuples are laid out so the right one can be found by
101 # The tuples are laid out so the right one can be found by
101 # comparison.
102 # comparison.
102 pdate, pdist, ptag = max(
103 pdate, pdist, ptag = max(
103 latesttags[p.rev()] for p in ctx.parents())
104 latesttags[p.rev()] for p in ctx.parents())
104 except KeyError:
105 except KeyError:
105 # Cache miss - recurse
106 # Cache miss - recurse
106 todo.append(rev)
107 todo.append(rev)
107 todo.extend(p.rev() for p in ctx.parents())
108 todo.extend(p.rev() for p in ctx.parents())
108 continue
109 continue
109 latesttags[rev] = pdate, pdist + 1, ptag
110 latesttags[rev] = pdate, pdist + 1, ptag
110 return latesttags[rev]
111 return latesttags[rev]
111
112
112 def getrenamedfn(repo, endrev=None):
113 def getrenamedfn(repo, endrev=None):
113 rcache = {}
114 rcache = {}
114 if endrev is None:
115 if endrev is None:
115 endrev = len(repo)
116 endrev = len(repo)
116
117
117 def getrenamed(fn, rev):
118 def getrenamed(fn, rev):
118 '''looks up all renames for a file (up to endrev) the first
119 '''looks up all renames for a file (up to endrev) the first
119 time the file is given. It indexes on the changerev and only
120 time the file is given. It indexes on the changerev and only
120 parses the manifest if linkrev != changerev.
121 parses the manifest if linkrev != changerev.
121 Returns rename info for fn at changerev rev.'''
122 Returns rename info for fn at changerev rev.'''
122 if fn not in rcache:
123 if fn not in rcache:
123 rcache[fn] = {}
124 rcache[fn] = {}
124 fl = repo.file(fn)
125 fl = repo.file(fn)
125 for i in fl:
126 for i in fl:
126 lr = fl.linkrev(i)
127 lr = fl.linkrev(i)
127 renamed = fl.renamed(fl.node(i))
128 renamed = fl.renamed(fl.node(i))
128 rcache[fn][lr] = renamed
129 rcache[fn][lr] = renamed
129 if lr >= endrev:
130 if lr >= endrev:
130 break
131 break
131 if rev in rcache[fn]:
132 if rev in rcache[fn]:
132 return rcache[fn][rev]
133 return rcache[fn][rev]
133
134
134 # If linkrev != rev (i.e. rev not found in rcache) fallback to
135 # If linkrev != rev (i.e. rev not found in rcache) fallback to
135 # filectx logic.
136 # filectx logic.
136 try:
137 try:
137 return repo[rev][fn].renamed()
138 return repo[rev][fn].renamed()
138 except error.LookupError:
139 except error.LookupError:
139 return None
140 return None
140
141
141 return getrenamed
142 return getrenamed
142
143
143
144
144 def showauthor(repo, ctx, templ, **args):
145 def showauthor(repo, ctx, templ, **args):
145 """:author: String. The unmodified author of the changeset."""
146 """:author: String. The unmodified author of the changeset."""
146 return ctx.user()
147 return ctx.user()
147
148
149 def showbisect(repo, ctx, templ, **args):
150 """:bisect: String. The changeset bisection status."""
151 return hbisect.label(repo, ctx.node())
152
148 def showbranch(**args):
153 def showbranch(**args):
149 """:branch: String. The name of the branch on which the changeset was
154 """:branch: String. The name of the branch on which the changeset was
150 committed.
155 committed.
151 """
156 """
152 return args['ctx'].branch()
157 return args['ctx'].branch()
153
158
154 def showbranches(**args):
159 def showbranches(**args):
155 """:branches: List of strings. The name of the branch on which the
160 """:branches: List of strings. The name of the branch on which the
156 changeset was committed. Will be empty if the branch name was
161 changeset was committed. Will be empty if the branch name was
157 default.
162 default.
158 """
163 """
159 branch = args['ctx'].branch()
164 branch = args['ctx'].branch()
160 if branch != 'default':
165 if branch != 'default':
161 return showlist('branch', [branch], plural='branches', **args)
166 return showlist('branch', [branch], plural='branches', **args)
162
167
163 def showbookmarks(**args):
168 def showbookmarks(**args):
164 """:bookmarks: List of strings. Any bookmarks associated with the
169 """:bookmarks: List of strings. Any bookmarks associated with the
165 changeset.
170 changeset.
166 """
171 """
167 bookmarks = args['ctx'].bookmarks()
172 bookmarks = args['ctx'].bookmarks()
168 return showlist('bookmark', bookmarks, **args)
173 return showlist('bookmark', bookmarks, **args)
169
174
170 def showchildren(**args):
175 def showchildren(**args):
171 """:children: List of strings. The children of the changeset."""
176 """:children: List of strings. The children of the changeset."""
172 ctx = args['ctx']
177 ctx = args['ctx']
173 childrevs = ['%d:%s' % (cctx, cctx) for cctx in ctx.children()]
178 childrevs = ['%d:%s' % (cctx, cctx) for cctx in ctx.children()]
174 return showlist('children', childrevs, **args)
179 return showlist('children', childrevs, **args)
175
180
176 def showdate(repo, ctx, templ, **args):
181 def showdate(repo, ctx, templ, **args):
177 """:date: Date information. The date when the changeset was committed."""
182 """:date: Date information. The date when the changeset was committed."""
178 return ctx.date()
183 return ctx.date()
179
184
180 def showdescription(repo, ctx, templ, **args):
185 def showdescription(repo, ctx, templ, **args):
181 """:desc: String. The text of the changeset description."""
186 """:desc: String. The text of the changeset description."""
182 return ctx.description().strip()
187 return ctx.description().strip()
183
188
184 def showdiffstat(repo, ctx, templ, **args):
189 def showdiffstat(repo, ctx, templ, **args):
185 """:diffstat: String. Statistics of changes with the following format:
190 """:diffstat: String. Statistics of changes with the following format:
186 "modified files: +added/-removed lines"
191 "modified files: +added/-removed lines"
187 """
192 """
188 stats = patch.diffstatdata(util.iterlines(ctx.diff()))
193 stats = patch.diffstatdata(util.iterlines(ctx.diff()))
189 maxname, maxtotal, adds, removes, binary = patch.diffstatsum(stats)
194 maxname, maxtotal, adds, removes, binary = patch.diffstatsum(stats)
190 return '%s: +%s/-%s' % (len(stats), adds, removes)
195 return '%s: +%s/-%s' % (len(stats), adds, removes)
191
196
192 def showextras(**args):
197 def showextras(**args):
193 templ = args['templ']
198 templ = args['templ']
194 for key, value in sorted(args['ctx'].extra().items()):
199 for key, value in sorted(args['ctx'].extra().items()):
195 args = args.copy()
200 args = args.copy()
196 args.update(dict(key=key, value=value))
201 args.update(dict(key=key, value=value))
197 yield templ('extra', **args)
202 yield templ('extra', **args)
198
203
199 def showfileadds(**args):
204 def showfileadds(**args):
200 """:file_adds: List of strings. Files added by this changeset."""
205 """:file_adds: List of strings. Files added by this changeset."""
201 repo, ctx, revcache = args['repo'], args['ctx'], args['revcache']
206 repo, ctx, revcache = args['repo'], args['ctx'], args['revcache']
202 return showlist('file_add', getfiles(repo, ctx, revcache)[1], **args)
207 return showlist('file_add', getfiles(repo, ctx, revcache)[1], **args)
203
208
204 def showfilecopies(**args):
209 def showfilecopies(**args):
205 """:file_copies: List of strings. Files copied in this changeset with
210 """:file_copies: List of strings. Files copied in this changeset with
206 their sources.
211 their sources.
207 """
212 """
208 cache, ctx = args['cache'], args['ctx']
213 cache, ctx = args['cache'], args['ctx']
209 copies = args['revcache'].get('copies')
214 copies = args['revcache'].get('copies')
210 if copies is None:
215 if copies is None:
211 if 'getrenamed' not in cache:
216 if 'getrenamed' not in cache:
212 cache['getrenamed'] = getrenamedfn(args['repo'])
217 cache['getrenamed'] = getrenamedfn(args['repo'])
213 copies = []
218 copies = []
214 getrenamed = cache['getrenamed']
219 getrenamed = cache['getrenamed']
215 for fn in ctx.files():
220 for fn in ctx.files():
216 rename = getrenamed(fn, ctx.rev())
221 rename = getrenamed(fn, ctx.rev())
217 if rename:
222 if rename:
218 copies.append((fn, rename[0]))
223 copies.append((fn, rename[0]))
219
224
220 c = [{'name': x[0], 'source': x[1]} for x in copies]
225 c = [{'name': x[0], 'source': x[1]} for x in copies]
221 return showlist('file_copy', c, plural='file_copies', **args)
226 return showlist('file_copy', c, plural='file_copies', **args)
222
227
223 # showfilecopiesswitch() displays file copies only if copy records are
228 # showfilecopiesswitch() displays file copies only if copy records are
224 # provided before calling the templater, usually with a --copies
229 # provided before calling the templater, usually with a --copies
225 # command line switch.
230 # command line switch.
226 def showfilecopiesswitch(**args):
231 def showfilecopiesswitch(**args):
227 """:file_copies_switch: List of strings. Like "file_copies" but displayed
232 """:file_copies_switch: List of strings. Like "file_copies" but displayed
228 only if the --copied switch is set.
233 only if the --copied switch is set.
229 """
234 """
230 copies = args['revcache'].get('copies') or []
235 copies = args['revcache'].get('copies') or []
231 c = [{'name': x[0], 'source': x[1]} for x in copies]
236 c = [{'name': x[0], 'source': x[1]} for x in copies]
232 return showlist('file_copy', c, plural='file_copies', **args)
237 return showlist('file_copy', c, plural='file_copies', **args)
233
238
234 def showfiledels(**args):
239 def showfiledels(**args):
235 """:file_dels: List of strings. Files removed by this changeset."""
240 """:file_dels: List of strings. Files removed by this changeset."""
236 repo, ctx, revcache = args['repo'], args['ctx'], args['revcache']
241 repo, ctx, revcache = args['repo'], args['ctx'], args['revcache']
237 return showlist('file_del', getfiles(repo, ctx, revcache)[2], **args)
242 return showlist('file_del', getfiles(repo, ctx, revcache)[2], **args)
238
243
239 def showfilemods(**args):
244 def showfilemods(**args):
240 """:file_mods: List of strings. Files modified by this changeset."""
245 """:file_mods: List of strings. Files modified by this changeset."""
241 repo, ctx, revcache = args['repo'], args['ctx'], args['revcache']
246 repo, ctx, revcache = args['repo'], args['ctx'], args['revcache']
242 return showlist('file_mod', getfiles(repo, ctx, revcache)[0], **args)
247 return showlist('file_mod', getfiles(repo, ctx, revcache)[0], **args)
243
248
244 def showfiles(**args):
249 def showfiles(**args):
245 """:files: List of strings. All files modified, added, or removed by this
250 """:files: List of strings. All files modified, added, or removed by this
246 changeset.
251 changeset.
247 """
252 """
248 return showlist('file', args['ctx'].files(), **args)
253 return showlist('file', args['ctx'].files(), **args)
249
254
250 def showlatesttag(repo, ctx, templ, cache, **args):
255 def showlatesttag(repo, ctx, templ, cache, **args):
251 """:latesttag: String. Most recent global tag in the ancestors of this
256 """:latesttag: String. Most recent global tag in the ancestors of this
252 changeset.
257 changeset.
253 """
258 """
254 return getlatesttags(repo, ctx, cache)[2]
259 return getlatesttags(repo, ctx, cache)[2]
255
260
256 def showlatesttagdistance(repo, ctx, templ, cache, **args):
261 def showlatesttagdistance(repo, ctx, templ, cache, **args):
257 """:latesttagdistance: Integer. Longest path to the latest tag."""
262 """:latesttagdistance: Integer. Longest path to the latest tag."""
258 return getlatesttags(repo, ctx, cache)[1]
263 return getlatesttags(repo, ctx, cache)[1]
259
264
260 def showmanifest(**args):
265 def showmanifest(**args):
261 repo, ctx, templ = args['repo'], args['ctx'], args['templ']
266 repo, ctx, templ = args['repo'], args['ctx'], args['templ']
262 args = args.copy()
267 args = args.copy()
263 args.update(dict(rev=repo.manifest.rev(ctx.changeset()[0]),
268 args.update(dict(rev=repo.manifest.rev(ctx.changeset()[0]),
264 node=hex(ctx.changeset()[0])))
269 node=hex(ctx.changeset()[0])))
265 return templ('manifest', **args)
270 return templ('manifest', **args)
266
271
267 def shownode(repo, ctx, templ, **args):
272 def shownode(repo, ctx, templ, **args):
268 """:node: String. The changeset identification hash, as a 40 hexadecimal
273 """:node: String. The changeset identification hash, as a 40 hexadecimal
269 digit string.
274 digit string.
270 """
275 """
271 return ctx.hex()
276 return ctx.hex()
272
277
273 def showrev(repo, ctx, templ, **args):
278 def showrev(repo, ctx, templ, **args):
274 """:rev: Integer. The repository-local changeset revision number."""
279 """:rev: Integer. The repository-local changeset revision number."""
275 return ctx.rev()
280 return ctx.rev()
276
281
277 def showtags(**args):
282 def showtags(**args):
278 """:tags: List of strings. Any tags associated with the changeset."""
283 """:tags: List of strings. Any tags associated with the changeset."""
279 return showlist('tag', args['ctx'].tags(), **args)
284 return showlist('tag', args['ctx'].tags(), **args)
280
285
281 # keywords are callables like:
286 # keywords are callables like:
282 # fn(repo, ctx, templ, cache, revcache, **args)
287 # fn(repo, ctx, templ, cache, revcache, **args)
283 # with:
288 # with:
284 # repo - current repository instance
289 # repo - current repository instance
285 # ctx - the changectx being displayed
290 # ctx - the changectx being displayed
286 # templ - the templater instance
291 # templ - the templater instance
287 # cache - a cache dictionary for the whole templater run
292 # cache - a cache dictionary for the whole templater run
288 # revcache - a cache dictionary for the current revision
293 # revcache - a cache dictionary for the current revision
289 keywords = {
294 keywords = {
290 'author': showauthor,
295 'author': showauthor,
296 'bisect': showbisect,
291 'branch': showbranch,
297 'branch': showbranch,
292 'branches': showbranches,
298 'branches': showbranches,
293 'bookmarks': showbookmarks,
299 'bookmarks': showbookmarks,
294 'children': showchildren,
300 'children': showchildren,
295 'date': showdate,
301 'date': showdate,
296 'desc': showdescription,
302 'desc': showdescription,
297 'diffstat': showdiffstat,
303 'diffstat': showdiffstat,
298 'extras': showextras,
304 'extras': showextras,
299 'file_adds': showfileadds,
305 'file_adds': showfileadds,
300 'file_copies': showfilecopies,
306 'file_copies': showfilecopies,
301 'file_copies_switch': showfilecopiesswitch,
307 'file_copies_switch': showfilecopiesswitch,
302 'file_dels': showfiledels,
308 'file_dels': showfiledels,
303 'file_mods': showfilemods,
309 'file_mods': showfilemods,
304 'files': showfiles,
310 'files': showfiles,
305 'latesttag': showlatesttag,
311 'latesttag': showlatesttag,
306 'latesttagdistance': showlatesttagdistance,
312 'latesttagdistance': showlatesttagdistance,
307 'manifest': showmanifest,
313 'manifest': showmanifest,
308 'node': shownode,
314 'node': shownode,
309 'rev': showrev,
315 'rev': showrev,
310 'tags': showtags,
316 'tags': showtags,
311 }
317 }
312
318
313 # tell hggettext to extract docstrings from these functions:
319 # tell hggettext to extract docstrings from these functions:
314 i18nfunctions = keywords.values()
320 i18nfunctions = keywords.values()
General Comments 0
You need to be logged in to leave comments. Login now