##// END OF EJS Templates
templates: extract function to `stringutil` for getting first line of text...
Martin von Zweigbergk -
r49885:51aed118 default
parent child Browse files
Show More
@@ -1,554 +1,551 b''
1 # templatefilters.py - common template expansion filters
1 # templatefilters.py - common template expansion filters
2 #
2 #
3 # Copyright 2005-2008 Olivia Mackall <olivia@selenic.com>
3 # Copyright 2005-2008 Olivia Mackall <olivia@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
8
9 import os
9 import os
10 import re
10 import re
11 import time
11 import time
12
12
13 from .i18n import _
13 from .i18n import _
14 from .node import hex
14 from .node import hex
15 from . import (
15 from . import (
16 encoding,
16 encoding,
17 error,
17 error,
18 pycompat,
18 pycompat,
19 registrar,
19 registrar,
20 smartset,
20 smartset,
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 def _tocborencodable(obj):
108 def _tocborencodable(obj):
109 if isinstance(obj, smartset.abstractsmartset):
109 if isinstance(obj, smartset.abstractsmartset):
110 return list(obj)
110 return list(obj)
111 return obj
111 return obj
112
112
113
113
114 @templatefilter(b'cbor')
114 @templatefilter(b'cbor')
115 def cbor(obj):
115 def cbor(obj):
116 """Any object. Serializes the object to CBOR bytes."""
116 """Any object. Serializes the object to CBOR bytes."""
117 # cborutil is stricter about type than json() filter
117 # cborutil is stricter about type than json() filter
118 obj = pycompat.rapply(_tocborencodable, obj)
118 obj = pycompat.rapply(_tocborencodable, obj)
119 return b''.join(cborutil.streamencode(obj))
119 return b''.join(cborutil.streamencode(obj))
120
120
121
121
122 @templatefilter(b'commondir')
122 @templatefilter(b'commondir')
123 def commondir(filelist):
123 def commondir(filelist):
124 """List of text. Treats each list item as file name with /
124 """List of text. Treats each list item as file name with /
125 as path separator and returns the longest common directory
125 as path separator and returns the longest common directory
126 prefix shared by all list items.
126 prefix shared by all list items.
127 Returns the empty string if no common prefix exists.
127 Returns the empty string if no common prefix exists.
128
128
129 The list items are not normalized, i.e. "foo/../bar" is handled as
129 The list items are not normalized, i.e. "foo/../bar" is handled as
130 file "bar" in the directory "foo/..". Leading slashes are ignored.
130 file "bar" in the directory "foo/..". Leading slashes are ignored.
131
131
132 For example, ["foo/bar/baz", "foo/baz/bar"] becomes "foo" and
132 For example, ["foo/bar/baz", "foo/baz/bar"] becomes "foo" and
133 ["foo/bar", "baz"] becomes "".
133 ["foo/bar", "baz"] becomes "".
134 """
134 """
135
135
136 def common(a, b):
136 def common(a, b):
137 if len(a) > len(b):
137 if len(a) > len(b):
138 a = b[: len(a)]
138 a = b[: len(a)]
139 elif len(b) > len(a):
139 elif len(b) > len(a):
140 b = b[: len(a)]
140 b = b[: len(a)]
141 if a == b:
141 if a == b:
142 return a
142 return a
143 for i in pycompat.xrange(len(a)):
143 for i in pycompat.xrange(len(a)):
144 if a[i] != b[i]:
144 if a[i] != b[i]:
145 return a[:i]
145 return a[:i]
146 return a
146 return a
147
147
148 try:
148 try:
149 if not filelist:
149 if not filelist:
150 return b""
150 return b""
151 dirlist = [f.lstrip(b'/').split(b'/')[:-1] for f in filelist]
151 dirlist = [f.lstrip(b'/').split(b'/')[:-1] for f in filelist]
152 if len(dirlist) == 1:
152 if len(dirlist) == 1:
153 return b'/'.join(dirlist[0])
153 return b'/'.join(dirlist[0])
154 a = min(dirlist)
154 a = min(dirlist)
155 b = max(dirlist)
155 b = max(dirlist)
156 # The common prefix of a and b is shared with all
156 # The common prefix of a and b is shared with all
157 # elements of the list since Python sorts lexicographical
157 # elements of the list since Python sorts lexicographical
158 # and [1, x] after [1].
158 # and [1, x] after [1].
159 return b'/'.join(common(a, b))
159 return b'/'.join(common(a, b))
160 except TypeError:
160 except TypeError:
161 raise error.ParseError(_(b'argument is not a list of text'))
161 raise error.ParseError(_(b'argument is not a list of text'))
162
162
163
163
164 @templatefilter(b'count')
164 @templatefilter(b'count')
165 def count(i):
165 def count(i):
166 """List or text. Returns the length as an integer."""
166 """List or text. Returns the length as an integer."""
167 try:
167 try:
168 return len(i)
168 return len(i)
169 except TypeError:
169 except TypeError:
170 raise error.ParseError(_(b'not countable'))
170 raise error.ParseError(_(b'not countable'))
171
171
172
172
173 @templatefilter(b'dirname', intype=bytes)
173 @templatefilter(b'dirname', intype=bytes)
174 def dirname(path):
174 def dirname(path):
175 """Any text. Treats the text as a path, and strips the last
175 """Any text. Treats the text as a path, and strips the last
176 component of the path after splitting by the path separator.
176 component of the path after splitting by the path separator.
177 """
177 """
178 return os.path.dirname(path)
178 return os.path.dirname(path)
179
179
180
180
181 @templatefilter(b'domain', intype=bytes)
181 @templatefilter(b'domain', intype=bytes)
182 def domain(author):
182 def domain(author):
183 """Any text. Finds the first string that looks like an email
183 """Any text. Finds the first string that looks like an email
184 address, and extracts just the domain component. Example: ``User
184 address, and extracts just the domain component. Example: ``User
185 <user@example.com>`` becomes ``example.com``.
185 <user@example.com>`` becomes ``example.com``.
186 """
186 """
187 f = author.find(b'@')
187 f = author.find(b'@')
188 if f == -1:
188 if f == -1:
189 return b''
189 return b''
190 author = author[f + 1 :]
190 author = author[f + 1 :]
191 f = author.find(b'>')
191 f = author.find(b'>')
192 if f >= 0:
192 if f >= 0:
193 author = author[:f]
193 author = author[:f]
194 return author
194 return author
195
195
196
196
197 @templatefilter(b'email', intype=bytes)
197 @templatefilter(b'email', intype=bytes)
198 def email(text):
198 def email(text):
199 """Any text. Extracts the first string that looks like an email
199 """Any text. Extracts the first string that looks like an email
200 address. Example: ``User <user@example.com>`` becomes
200 address. Example: ``User <user@example.com>`` becomes
201 ``user@example.com``.
201 ``user@example.com``.
202 """
202 """
203 return stringutil.email(text)
203 return stringutil.email(text)
204
204
205
205
206 @templatefilter(b'escape', intype=bytes)
206 @templatefilter(b'escape', intype=bytes)
207 def escape(text):
207 def escape(text):
208 """Any text. Replaces the special XML/XHTML characters "&", "<"
208 """Any text. Replaces the special XML/XHTML characters "&", "<"
209 and ">" with XML entities, and filters out NUL characters.
209 and ">" with XML entities, and filters out NUL characters.
210 """
210 """
211 return url.escape(text.replace(b'\0', b''), True)
211 return url.escape(text.replace(b'\0', b''), True)
212
212
213
213
214 para_re = None
214 para_re = None
215 space_re = None
215 space_re = None
216
216
217
217
218 def fill(text, width, initindent=b'', hangindent=b''):
218 def fill(text, width, initindent=b'', hangindent=b''):
219 '''fill many paragraphs with optional indentation.'''
219 '''fill many paragraphs with optional indentation.'''
220 global para_re, space_re
220 global para_re, space_re
221 if para_re is None:
221 if para_re is None:
222 para_re = re.compile(b'(\n\n|\n\\s*[-*]\\s*)', re.M)
222 para_re = re.compile(b'(\n\n|\n\\s*[-*]\\s*)', re.M)
223 space_re = re.compile(br' +')
223 space_re = re.compile(br' +')
224
224
225 def findparas():
225 def findparas():
226 start = 0
226 start = 0
227 while True:
227 while True:
228 m = para_re.search(text, start)
228 m = para_re.search(text, start)
229 if not m:
229 if not m:
230 uctext = encoding.unifromlocal(text[start:])
230 uctext = encoding.unifromlocal(text[start:])
231 w = len(uctext)
231 w = len(uctext)
232 while w > 0 and uctext[w - 1].isspace():
232 while w > 0 and uctext[w - 1].isspace():
233 w -= 1
233 w -= 1
234 yield (
234 yield (
235 encoding.unitolocal(uctext[:w]),
235 encoding.unitolocal(uctext[:w]),
236 encoding.unitolocal(uctext[w:]),
236 encoding.unitolocal(uctext[w:]),
237 )
237 )
238 break
238 break
239 yield text[start : m.start(0)], m.group(1)
239 yield text[start : m.start(0)], m.group(1)
240 start = m.end(1)
240 start = m.end(1)
241
241
242 return b"".join(
242 return b"".join(
243 [
243 [
244 stringutil.wrap(
244 stringutil.wrap(
245 space_re.sub(b' ', stringutil.wrap(para, width)),
245 space_re.sub(b' ', stringutil.wrap(para, width)),
246 width,
246 width,
247 initindent,
247 initindent,
248 hangindent,
248 hangindent,
249 )
249 )
250 + rest
250 + rest
251 for para, rest in findparas()
251 for para, rest in findparas()
252 ]
252 ]
253 )
253 )
254
254
255
255
256 @templatefilter(b'fill68', intype=bytes)
256 @templatefilter(b'fill68', intype=bytes)
257 def fill68(text):
257 def fill68(text):
258 """Any text. Wraps the text to fit in 68 columns."""
258 """Any text. Wraps the text to fit in 68 columns."""
259 return fill(text, 68)
259 return fill(text, 68)
260
260
261
261
262 @templatefilter(b'fill76', intype=bytes)
262 @templatefilter(b'fill76', intype=bytes)
263 def fill76(text):
263 def fill76(text):
264 """Any text. Wraps the text to fit in 76 columns."""
264 """Any text. Wraps the text to fit in 76 columns."""
265 return fill(text, 76)
265 return fill(text, 76)
266
266
267
267
268 @templatefilter(b'firstline', intype=bytes)
268 @templatefilter(b'firstline', intype=bytes)
269 def firstline(text):
269 def firstline(text):
270 """Any text. Returns the first line of text."""
270 """Any text. Returns the first line of text."""
271 try:
271 return stringutil.firstline(text)
272 return text.splitlines()[0]
273 except IndexError:
274 return b''
275
272
276
273
277 @templatefilter(b'hex', intype=bytes)
274 @templatefilter(b'hex', intype=bytes)
278 def hexfilter(text):
275 def hexfilter(text):
279 """Any text. Convert a binary Mercurial node identifier into
276 """Any text. Convert a binary Mercurial node identifier into
280 its long hexadecimal representation.
277 its long hexadecimal representation.
281 """
278 """
282 return hex(text)
279 return hex(text)
283
280
284
281
285 @templatefilter(b'hgdate', intype=templateutil.date)
282 @templatefilter(b'hgdate', intype=templateutil.date)
286 def hgdate(text):
283 def hgdate(text):
287 """Date. Returns the date as a pair of numbers: "1157407993
284 """Date. Returns the date as a pair of numbers: "1157407993
288 25200" (Unix timestamp, timezone offset).
285 25200" (Unix timestamp, timezone offset).
289 """
286 """
290 return b"%d %d" % text
287 return b"%d %d" % text
291
288
292
289
293 @templatefilter(b'isodate', intype=templateutil.date)
290 @templatefilter(b'isodate', intype=templateutil.date)
294 def isodate(text):
291 def isodate(text):
295 """Date. Returns the date in ISO 8601 format: "2009-08-18 13:00
292 """Date. Returns the date in ISO 8601 format: "2009-08-18 13:00
296 +0200".
293 +0200".
297 """
294 """
298 return dateutil.datestr(text, b'%Y-%m-%d %H:%M %1%2')
295 return dateutil.datestr(text, b'%Y-%m-%d %H:%M %1%2')
299
296
300
297
301 @templatefilter(b'isodatesec', intype=templateutil.date)
298 @templatefilter(b'isodatesec', intype=templateutil.date)
302 def isodatesec(text):
299 def isodatesec(text):
303 """Date. Returns the date in ISO 8601 format, including
300 """Date. Returns the date in ISO 8601 format, including
304 seconds: "2009-08-18 13:00:13 +0200". See also the rfc3339date
301 seconds: "2009-08-18 13:00:13 +0200". See also the rfc3339date
305 filter.
302 filter.
306 """
303 """
307 return dateutil.datestr(text, b'%Y-%m-%d %H:%M:%S %1%2')
304 return dateutil.datestr(text, b'%Y-%m-%d %H:%M:%S %1%2')
308
305
309
306
310 def indent(text, prefix, firstline=b''):
307 def indent(text, prefix, firstline=b''):
311 '''indent each non-empty line of text after first with prefix.'''
308 '''indent each non-empty line of text after first with prefix.'''
312 lines = text.splitlines()
309 lines = text.splitlines()
313 num_lines = len(lines)
310 num_lines = len(lines)
314 endswithnewline = text[-1:] == b'\n'
311 endswithnewline = text[-1:] == b'\n'
315
312
316 def indenter():
313 def indenter():
317 for i in pycompat.xrange(num_lines):
314 for i in pycompat.xrange(num_lines):
318 l = lines[i]
315 l = lines[i]
319 if l.strip():
316 if l.strip():
320 yield prefix if i else firstline
317 yield prefix if i else firstline
321 yield l
318 yield l
322 if i < num_lines - 1 or endswithnewline:
319 if i < num_lines - 1 or endswithnewline:
323 yield b'\n'
320 yield b'\n'
324
321
325 return b"".join(indenter())
322 return b"".join(indenter())
326
323
327
324
328 @templatefilter(b'json')
325 @templatefilter(b'json')
329 def json(obj, paranoid=True):
326 def json(obj, paranoid=True):
330 """Any object. Serializes the object to a JSON formatted text."""
327 """Any object. Serializes the object to a JSON formatted text."""
331 if obj is None:
328 if obj is None:
332 return b'null'
329 return b'null'
333 elif obj is False:
330 elif obj is False:
334 return b'false'
331 return b'false'
335 elif obj is True:
332 elif obj is True:
336 return b'true'
333 return b'true'
337 elif isinstance(obj, (int, int, float)):
334 elif isinstance(obj, (int, int, float)):
338 return pycompat.bytestr(obj)
335 return pycompat.bytestr(obj)
339 elif isinstance(obj, bytes):
336 elif isinstance(obj, bytes):
340 return b'"%s"' % encoding.jsonescape(obj, paranoid=paranoid)
337 return b'"%s"' % encoding.jsonescape(obj, paranoid=paranoid)
341 elif isinstance(obj, type(u'')):
338 elif isinstance(obj, type(u'')):
342 raise error.ProgrammingError(
339 raise error.ProgrammingError(
343 b'Mercurial only does output with bytes: %r' % obj
340 b'Mercurial only does output with bytes: %r' % obj
344 )
341 )
345 elif util.safehasattr(obj, b'keys'):
342 elif util.safehasattr(obj, b'keys'):
346 out = [
343 out = [
347 b'"%s": %s'
344 b'"%s": %s'
348 % (encoding.jsonescape(k, paranoid=paranoid), json(v, paranoid))
345 % (encoding.jsonescape(k, paranoid=paranoid), json(v, paranoid))
349 for k, v in sorted(obj.items())
346 for k, v in sorted(obj.items())
350 ]
347 ]
351 return b'{' + b', '.join(out) + b'}'
348 return b'{' + b', '.join(out) + b'}'
352 elif util.safehasattr(obj, b'__iter__'):
349 elif util.safehasattr(obj, b'__iter__'):
353 out = [json(i, paranoid) for i in obj]
350 out = [json(i, paranoid) for i in obj]
354 return b'[' + b', '.join(out) + b']'
351 return b'[' + b', '.join(out) + b']'
355 raise error.ProgrammingError(b'cannot encode %r' % obj)
352 raise error.ProgrammingError(b'cannot encode %r' % obj)
356
353
357
354
358 @templatefilter(b'lower', intype=bytes)
355 @templatefilter(b'lower', intype=bytes)
359 def lower(text):
356 def lower(text):
360 """Any text. Converts the text to lowercase."""
357 """Any text. Converts the text to lowercase."""
361 return encoding.lower(text)
358 return encoding.lower(text)
362
359
363
360
364 @templatefilter(b'nonempty', intype=bytes)
361 @templatefilter(b'nonempty', intype=bytes)
365 def nonempty(text):
362 def nonempty(text):
366 """Any text. Returns '(none)' if the string is empty."""
363 """Any text. Returns '(none)' if the string is empty."""
367 return text or b"(none)"
364 return text or b"(none)"
368
365
369
366
370 @templatefilter(b'obfuscate', intype=bytes)
367 @templatefilter(b'obfuscate', intype=bytes)
371 def obfuscate(text):
368 def obfuscate(text):
372 """Any text. Returns the input text rendered as a sequence of
369 """Any text. Returns the input text rendered as a sequence of
373 XML entities.
370 XML entities.
374 """
371 """
375 text = str(text, pycompat.sysstr(encoding.encoding), r'replace')
372 text = str(text, pycompat.sysstr(encoding.encoding), r'replace')
376 return b''.join([b'&#%d;' % ord(c) for c in text])
373 return b''.join([b'&#%d;' % ord(c) for c in text])
377
374
378
375
379 @templatefilter(b'permissions', intype=bytes)
376 @templatefilter(b'permissions', intype=bytes)
380 def permissions(flags):
377 def permissions(flags):
381 if b"l" in flags:
378 if b"l" in flags:
382 return b"lrwxrwxrwx"
379 return b"lrwxrwxrwx"
383 if b"x" in flags:
380 if b"x" in flags:
384 return b"-rwxr-xr-x"
381 return b"-rwxr-xr-x"
385 return b"-rw-r--r--"
382 return b"-rw-r--r--"
386
383
387
384
388 @templatefilter(b'person', intype=bytes)
385 @templatefilter(b'person', intype=bytes)
389 def person(author):
386 def person(author):
390 """Any text. Returns the name before an email address,
387 """Any text. Returns the name before an email address,
391 interpreting it as per RFC 5322.
388 interpreting it as per RFC 5322.
392 """
389 """
393 return stringutil.person(author)
390 return stringutil.person(author)
394
391
395
392
396 @templatefilter(b'revescape', intype=bytes)
393 @templatefilter(b'revescape', intype=bytes)
397 def revescape(text):
394 def revescape(text):
398 """Any text. Escapes all "special" characters, except @.
395 """Any text. Escapes all "special" characters, except @.
399 Forward slashes are escaped twice to prevent web servers from prematurely
396 Forward slashes are escaped twice to prevent web servers from prematurely
400 unescaping them. For example, "@foo bar/baz" becomes "@foo%20bar%252Fbaz".
397 unescaping them. For example, "@foo bar/baz" becomes "@foo%20bar%252Fbaz".
401 """
398 """
402 return urlreq.quote(text, safe=b'/@').replace(b'/', b'%252F')
399 return urlreq.quote(text, safe=b'/@').replace(b'/', b'%252F')
403
400
404
401
405 @templatefilter(b'rfc3339date', intype=templateutil.date)
402 @templatefilter(b'rfc3339date', intype=templateutil.date)
406 def rfc3339date(text):
403 def rfc3339date(text):
407 """Date. Returns a date using the Internet date format
404 """Date. Returns a date using the Internet date format
408 specified in RFC 3339: "2009-08-18T13:00:13+02:00".
405 specified in RFC 3339: "2009-08-18T13:00:13+02:00".
409 """
406 """
410 return dateutil.datestr(text, b"%Y-%m-%dT%H:%M:%S%1:%2")
407 return dateutil.datestr(text, b"%Y-%m-%dT%H:%M:%S%1:%2")
411
408
412
409
413 @templatefilter(b'rfc822date', intype=templateutil.date)
410 @templatefilter(b'rfc822date', intype=templateutil.date)
414 def rfc822date(text):
411 def rfc822date(text):
415 """Date. Returns a date using the same format used in email
412 """Date. Returns a date using the same format used in email
416 headers: "Tue, 18 Aug 2009 13:00:13 +0200".
413 headers: "Tue, 18 Aug 2009 13:00:13 +0200".
417 """
414 """
418 return dateutil.datestr(text, b"%a, %d %b %Y %H:%M:%S %1%2")
415 return dateutil.datestr(text, b"%a, %d %b %Y %H:%M:%S %1%2")
419
416
420
417
421 @templatefilter(b'short', intype=bytes)
418 @templatefilter(b'short', intype=bytes)
422 def short(text):
419 def short(text):
423 """Changeset hash. Returns the short form of a changeset hash,
420 """Changeset hash. Returns the short form of a changeset hash,
424 i.e. a 12 hexadecimal digit string.
421 i.e. a 12 hexadecimal digit string.
425 """
422 """
426 return text[:12]
423 return text[:12]
427
424
428
425
429 @templatefilter(b'shortbisect', intype=bytes)
426 @templatefilter(b'shortbisect', intype=bytes)
430 def shortbisect(label):
427 def shortbisect(label):
431 """Any text. Treats `label` as a bisection status, and
428 """Any text. Treats `label` as a bisection status, and
432 returns a single-character representing the status (G: good, B: bad,
429 returns a single-character representing the status (G: good, B: bad,
433 S: skipped, U: untested, I: ignored). Returns single space if `text`
430 S: skipped, U: untested, I: ignored). Returns single space if `text`
434 is not a valid bisection status.
431 is not a valid bisection status.
435 """
432 """
436 if label:
433 if label:
437 return label[0:1].upper()
434 return label[0:1].upper()
438 return b' '
435 return b' '
439
436
440
437
441 @templatefilter(b'shortdate', intype=templateutil.date)
438 @templatefilter(b'shortdate', intype=templateutil.date)
442 def shortdate(text):
439 def shortdate(text):
443 """Date. Returns a date like "2006-09-18"."""
440 """Date. Returns a date like "2006-09-18"."""
444 return dateutil.shortdate(text)
441 return dateutil.shortdate(text)
445
442
446
443
447 @templatefilter(b'slashpath', intype=bytes)
444 @templatefilter(b'slashpath', intype=bytes)
448 def slashpath(path):
445 def slashpath(path):
449 """Any text. Replaces the native path separator with slash."""
446 """Any text. Replaces the native path separator with slash."""
450 return util.pconvert(path)
447 return util.pconvert(path)
451
448
452
449
453 @templatefilter(b'splitlines', intype=bytes)
450 @templatefilter(b'splitlines', intype=bytes)
454 def splitlines(text):
451 def splitlines(text):
455 """Any text. Split text into a list of lines."""
452 """Any text. Split text into a list of lines."""
456 return templateutil.hybridlist(text.splitlines(), name=b'line')
453 return templateutil.hybridlist(text.splitlines(), name=b'line')
457
454
458
455
459 @templatefilter(b'stringescape', intype=bytes)
456 @templatefilter(b'stringescape', intype=bytes)
460 def stringescape(text):
457 def stringescape(text):
461 return stringutil.escapestr(text)
458 return stringutil.escapestr(text)
462
459
463
460
464 @templatefilter(b'stringify', intype=bytes)
461 @templatefilter(b'stringify', intype=bytes)
465 def stringify(thing):
462 def stringify(thing):
466 """Any type. Turns the value into text by converting values into
463 """Any type. Turns the value into text by converting values into
467 text and concatenating them.
464 text and concatenating them.
468 """
465 """
469 return thing # coerced by the intype
466 return thing # coerced by the intype
470
467
471
468
472 @templatefilter(b'stripdir', intype=bytes)
469 @templatefilter(b'stripdir', intype=bytes)
473 def stripdir(text):
470 def stripdir(text):
474 """Treat the text as path and strip a directory level, if
471 """Treat the text as path and strip a directory level, if
475 possible. For example, "foo" and "foo/bar" becomes "foo".
472 possible. For example, "foo" and "foo/bar" becomes "foo".
476 """
473 """
477 dir = os.path.dirname(text)
474 dir = os.path.dirname(text)
478 if dir == b"":
475 if dir == b"":
479 return os.path.basename(text)
476 return os.path.basename(text)
480 else:
477 else:
481 return dir
478 return dir
482
479
483
480
484 @templatefilter(b'tabindent', intype=bytes)
481 @templatefilter(b'tabindent', intype=bytes)
485 def tabindent(text):
482 def tabindent(text):
486 """Any text. Returns the text, with every non-empty line
483 """Any text. Returns the text, with every non-empty line
487 except the first starting with a tab character.
484 except the first starting with a tab character.
488 """
485 """
489 return indent(text, b'\t')
486 return indent(text, b'\t')
490
487
491
488
492 @templatefilter(b'upper', intype=bytes)
489 @templatefilter(b'upper', intype=bytes)
493 def upper(text):
490 def upper(text):
494 """Any text. Converts the text to uppercase."""
491 """Any text. Converts the text to uppercase."""
495 return encoding.upper(text)
492 return encoding.upper(text)
496
493
497
494
498 @templatefilter(b'urlescape', intype=bytes)
495 @templatefilter(b'urlescape', intype=bytes)
499 def urlescape(text):
496 def urlescape(text):
500 """Any text. Escapes all "special" characters. For example,
497 """Any text. Escapes all "special" characters. For example,
501 "foo bar" becomes "foo%20bar".
498 "foo bar" becomes "foo%20bar".
502 """
499 """
503 return urlreq.quote(text)
500 return urlreq.quote(text)
504
501
505
502
506 @templatefilter(b'user', intype=bytes)
503 @templatefilter(b'user', intype=bytes)
507 def userfilter(text):
504 def userfilter(text):
508 """Any text. Returns a short representation of a user name or email
505 """Any text. Returns a short representation of a user name or email
509 address."""
506 address."""
510 return stringutil.shortuser(text)
507 return stringutil.shortuser(text)
511
508
512
509
513 @templatefilter(b'emailuser', intype=bytes)
510 @templatefilter(b'emailuser', intype=bytes)
514 def emailuser(text):
511 def emailuser(text):
515 """Any text. Returns the user portion of an email address."""
512 """Any text. Returns the user portion of an email address."""
516 return stringutil.emailuser(text)
513 return stringutil.emailuser(text)
517
514
518
515
519 @templatefilter(b'utf8', intype=bytes)
516 @templatefilter(b'utf8', intype=bytes)
520 def utf8(text):
517 def utf8(text):
521 """Any text. Converts from the local character encoding to UTF-8."""
518 """Any text. Converts from the local character encoding to UTF-8."""
522 return encoding.fromlocal(text)
519 return encoding.fromlocal(text)
523
520
524
521
525 @templatefilter(b'xmlescape', intype=bytes)
522 @templatefilter(b'xmlescape', intype=bytes)
526 def xmlescape(text):
523 def xmlescape(text):
527 text = (
524 text = (
528 text.replace(b'&', b'&amp;')
525 text.replace(b'&', b'&amp;')
529 .replace(b'<', b'&lt;')
526 .replace(b'<', b'&lt;')
530 .replace(b'>', b'&gt;')
527 .replace(b'>', b'&gt;')
531 .replace(b'"', b'&quot;')
528 .replace(b'"', b'&quot;')
532 .replace(b"'", b'&#39;')
529 .replace(b"'", b'&#39;')
533 ) # &apos; invalid in HTML
530 ) # &apos; invalid in HTML
534 return re.sub(b'[\x00-\x08\x0B\x0C\x0E-\x1F]', b' ', text)
531 return re.sub(b'[\x00-\x08\x0B\x0C\x0E-\x1F]', b' ', text)
535
532
536
533
537 def websub(text, websubtable):
534 def websub(text, websubtable):
538 """:websub: Any text. Only applies to hgweb. Applies the regular
535 """:websub: Any text. Only applies to hgweb. Applies the regular
539 expression replacements defined in the websub section.
536 expression replacements defined in the websub section.
540 """
537 """
541 if websubtable:
538 if websubtable:
542 for regexp, format in websubtable:
539 for regexp, format in websubtable:
543 text = regexp.sub(format, text)
540 text = regexp.sub(format, text)
544 return text
541 return text
545
542
546
543
547 def loadfilter(ui, extname, registrarobj):
544 def loadfilter(ui, extname, registrarobj):
548 """Load template filter from specified registrarobj"""
545 """Load template filter from specified registrarobj"""
549 for name, func in registrarobj._table.items():
546 for name, func in registrarobj._table.items():
550 filters[name] = func
547 filters[name] = func
551
548
552
549
553 # tell hggettext to extract docstrings from these functions:
550 # tell hggettext to extract docstrings from these functions:
554 i18nfunctions = filters.values()
551 i18nfunctions = filters.values()
@@ -1,967 +1,975 b''
1 # stringutil.py - utility for generic string formatting, parsing, etc.
1 # stringutil.py - utility for generic string formatting, parsing, etc.
2 #
2 #
3 # Copyright 2005 K. Thananchayan <thananck@yahoo.com>
3 # Copyright 2005 K. Thananchayan <thananck@yahoo.com>
4 # Copyright 2005-2007 Olivia Mackall <olivia@selenic.com>
4 # Copyright 2005-2007 Olivia Mackall <olivia@selenic.com>
5 # Copyright 2006 Vadim Gelfer <vadim.gelfer@gmail.com>
5 # Copyright 2006 Vadim Gelfer <vadim.gelfer@gmail.com>
6 #
6 #
7 # This software may be used and distributed according to the terms of the
7 # This software may be used and distributed according to the terms of the
8 # GNU General Public License version 2 or any later version.
8 # GNU General Public License version 2 or any later version.
9
9
10
10
11 import ast
11 import ast
12 import codecs
12 import codecs
13 import re as remod
13 import re as remod
14 import textwrap
14 import textwrap
15 import types
15 import types
16
16
17 from ..i18n import _
17 from ..i18n import _
18 from ..thirdparty import attr
18 from ..thirdparty import attr
19
19
20 from .. import (
20 from .. import (
21 encoding,
21 encoding,
22 error,
22 error,
23 pycompat,
23 pycompat,
24 )
24 )
25
25
26 # regex special chars pulled from https://bugs.python.org/issue29995
26 # regex special chars pulled from https://bugs.python.org/issue29995
27 # which was part of Python 3.7.
27 # which was part of Python 3.7.
28 _respecial = pycompat.bytestr(b'()[]{}?*+-|^$\\.&~# \t\n\r\v\f')
28 _respecial = pycompat.bytestr(b'()[]{}?*+-|^$\\.&~# \t\n\r\v\f')
29 _regexescapemap = {ord(i): (b'\\' + i).decode('latin1') for i in _respecial}
29 _regexescapemap = {ord(i): (b'\\' + i).decode('latin1') for i in _respecial}
30 regexbytesescapemap = {i: (b'\\' + i) for i in _respecial}
30 regexbytesescapemap = {i: (b'\\' + i) for i in _respecial}
31
31
32
32
33 def reescape(pat):
33 def reescape(pat):
34 """Drop-in replacement for re.escape."""
34 """Drop-in replacement for re.escape."""
35 # NOTE: it is intentional that this works on unicodes and not
35 # NOTE: it is intentional that this works on unicodes and not
36 # bytes, as it's only possible to do the escaping with
36 # bytes, as it's only possible to do the escaping with
37 # unicode.translate, not bytes.translate. Sigh.
37 # unicode.translate, not bytes.translate. Sigh.
38 wantuni = True
38 wantuni = True
39 if isinstance(pat, bytes):
39 if isinstance(pat, bytes):
40 wantuni = False
40 wantuni = False
41 pat = pat.decode('latin1')
41 pat = pat.decode('latin1')
42 pat = pat.translate(_regexescapemap)
42 pat = pat.translate(_regexescapemap)
43 if wantuni:
43 if wantuni:
44 return pat
44 return pat
45 return pat.encode('latin1')
45 return pat.encode('latin1')
46
46
47
47
48 def pprint(o, bprefix=False, indent=0, level=0):
48 def pprint(o, bprefix=False, indent=0, level=0):
49 """Pretty print an object."""
49 """Pretty print an object."""
50 return b''.join(pprintgen(o, bprefix=bprefix, indent=indent, level=level))
50 return b''.join(pprintgen(o, bprefix=bprefix, indent=indent, level=level))
51
51
52
52
53 def pprintgen(o, bprefix=False, indent=0, level=0):
53 def pprintgen(o, bprefix=False, indent=0, level=0):
54 """Pretty print an object to a generator of atoms.
54 """Pretty print an object to a generator of atoms.
55
55
56 ``bprefix`` is a flag influencing whether bytestrings are preferred with
56 ``bprefix`` is a flag influencing whether bytestrings are preferred with
57 a ``b''`` prefix.
57 a ``b''`` prefix.
58
58
59 ``indent`` controls whether collections and nested data structures
59 ``indent`` controls whether collections and nested data structures
60 span multiple lines via the indentation amount in spaces. By default,
60 span multiple lines via the indentation amount in spaces. By default,
61 no newlines are emitted.
61 no newlines are emitted.
62
62
63 ``level`` specifies the initial indent level. Used if ``indent > 0``.
63 ``level`` specifies the initial indent level. Used if ``indent > 0``.
64 """
64 """
65
65
66 if isinstance(o, bytes):
66 if isinstance(o, bytes):
67 if bprefix:
67 if bprefix:
68 yield b"b'%s'" % escapestr(o)
68 yield b"b'%s'" % escapestr(o)
69 else:
69 else:
70 yield b"'%s'" % escapestr(o)
70 yield b"'%s'" % escapestr(o)
71 elif isinstance(o, bytearray):
71 elif isinstance(o, bytearray):
72 # codecs.escape_encode() can't handle bytearray, so escapestr fails
72 # codecs.escape_encode() can't handle bytearray, so escapestr fails
73 # without coercion.
73 # without coercion.
74 yield b"bytearray['%s']" % escapestr(bytes(o))
74 yield b"bytearray['%s']" % escapestr(bytes(o))
75 elif isinstance(o, list):
75 elif isinstance(o, list):
76 if not o:
76 if not o:
77 yield b'[]'
77 yield b'[]'
78 return
78 return
79
79
80 yield b'['
80 yield b'['
81
81
82 if indent:
82 if indent:
83 level += 1
83 level += 1
84 yield b'\n'
84 yield b'\n'
85 yield b' ' * (level * indent)
85 yield b' ' * (level * indent)
86
86
87 for i, a in enumerate(o):
87 for i, a in enumerate(o):
88 for chunk in pprintgen(
88 for chunk in pprintgen(
89 a, bprefix=bprefix, indent=indent, level=level
89 a, bprefix=bprefix, indent=indent, level=level
90 ):
90 ):
91 yield chunk
91 yield chunk
92
92
93 if i + 1 < len(o):
93 if i + 1 < len(o):
94 if indent:
94 if indent:
95 yield b',\n'
95 yield b',\n'
96 yield b' ' * (level * indent)
96 yield b' ' * (level * indent)
97 else:
97 else:
98 yield b', '
98 yield b', '
99
99
100 if indent:
100 if indent:
101 level -= 1
101 level -= 1
102 yield b'\n'
102 yield b'\n'
103 yield b' ' * (level * indent)
103 yield b' ' * (level * indent)
104
104
105 yield b']'
105 yield b']'
106 elif isinstance(o, dict):
106 elif isinstance(o, dict):
107 if not o:
107 if not o:
108 yield b'{}'
108 yield b'{}'
109 return
109 return
110
110
111 yield b'{'
111 yield b'{'
112
112
113 if indent:
113 if indent:
114 level += 1
114 level += 1
115 yield b'\n'
115 yield b'\n'
116 yield b' ' * (level * indent)
116 yield b' ' * (level * indent)
117
117
118 for i, (k, v) in enumerate(sorted(o.items())):
118 for i, (k, v) in enumerate(sorted(o.items())):
119 for chunk in pprintgen(
119 for chunk in pprintgen(
120 k, bprefix=bprefix, indent=indent, level=level
120 k, bprefix=bprefix, indent=indent, level=level
121 ):
121 ):
122 yield chunk
122 yield chunk
123
123
124 yield b': '
124 yield b': '
125
125
126 for chunk in pprintgen(
126 for chunk in pprintgen(
127 v, bprefix=bprefix, indent=indent, level=level
127 v, bprefix=bprefix, indent=indent, level=level
128 ):
128 ):
129 yield chunk
129 yield chunk
130
130
131 if i + 1 < len(o):
131 if i + 1 < len(o):
132 if indent:
132 if indent:
133 yield b',\n'
133 yield b',\n'
134 yield b' ' * (level * indent)
134 yield b' ' * (level * indent)
135 else:
135 else:
136 yield b', '
136 yield b', '
137
137
138 if indent:
138 if indent:
139 level -= 1
139 level -= 1
140 yield b'\n'
140 yield b'\n'
141 yield b' ' * (level * indent)
141 yield b' ' * (level * indent)
142
142
143 yield b'}'
143 yield b'}'
144 elif isinstance(o, set):
144 elif isinstance(o, set):
145 if not o:
145 if not o:
146 yield b'set([])'
146 yield b'set([])'
147 return
147 return
148
148
149 yield b'set(['
149 yield b'set(['
150
150
151 if indent:
151 if indent:
152 level += 1
152 level += 1
153 yield b'\n'
153 yield b'\n'
154 yield b' ' * (level * indent)
154 yield b' ' * (level * indent)
155
155
156 for i, k in enumerate(sorted(o)):
156 for i, k in enumerate(sorted(o)):
157 for chunk in pprintgen(
157 for chunk in pprintgen(
158 k, bprefix=bprefix, indent=indent, level=level
158 k, bprefix=bprefix, indent=indent, level=level
159 ):
159 ):
160 yield chunk
160 yield chunk
161
161
162 if i + 1 < len(o):
162 if i + 1 < len(o):
163 if indent:
163 if indent:
164 yield b',\n'
164 yield b',\n'
165 yield b' ' * (level * indent)
165 yield b' ' * (level * indent)
166 else:
166 else:
167 yield b', '
167 yield b', '
168
168
169 if indent:
169 if indent:
170 level -= 1
170 level -= 1
171 yield b'\n'
171 yield b'\n'
172 yield b' ' * (level * indent)
172 yield b' ' * (level * indent)
173
173
174 yield b'])'
174 yield b'])'
175 elif isinstance(o, tuple):
175 elif isinstance(o, tuple):
176 if not o:
176 if not o:
177 yield b'()'
177 yield b'()'
178 return
178 return
179
179
180 yield b'('
180 yield b'('
181
181
182 if indent:
182 if indent:
183 level += 1
183 level += 1
184 yield b'\n'
184 yield b'\n'
185 yield b' ' * (level * indent)
185 yield b' ' * (level * indent)
186
186
187 for i, a in enumerate(o):
187 for i, a in enumerate(o):
188 for chunk in pprintgen(
188 for chunk in pprintgen(
189 a, bprefix=bprefix, indent=indent, level=level
189 a, bprefix=bprefix, indent=indent, level=level
190 ):
190 ):
191 yield chunk
191 yield chunk
192
192
193 if i + 1 < len(o):
193 if i + 1 < len(o):
194 if indent:
194 if indent:
195 yield b',\n'
195 yield b',\n'
196 yield b' ' * (level * indent)
196 yield b' ' * (level * indent)
197 else:
197 else:
198 yield b', '
198 yield b', '
199
199
200 if indent:
200 if indent:
201 level -= 1
201 level -= 1
202 yield b'\n'
202 yield b'\n'
203 yield b' ' * (level * indent)
203 yield b' ' * (level * indent)
204
204
205 yield b')'
205 yield b')'
206 elif isinstance(o, types.GeneratorType):
206 elif isinstance(o, types.GeneratorType):
207 # Special case of empty generator.
207 # Special case of empty generator.
208 try:
208 try:
209 nextitem = next(o)
209 nextitem = next(o)
210 except StopIteration:
210 except StopIteration:
211 yield b'gen[]'
211 yield b'gen[]'
212 return
212 return
213
213
214 yield b'gen['
214 yield b'gen['
215
215
216 if indent:
216 if indent:
217 level += 1
217 level += 1
218 yield b'\n'
218 yield b'\n'
219 yield b' ' * (level * indent)
219 yield b' ' * (level * indent)
220
220
221 last = False
221 last = False
222
222
223 while not last:
223 while not last:
224 current = nextitem
224 current = nextitem
225
225
226 try:
226 try:
227 nextitem = next(o)
227 nextitem = next(o)
228 except StopIteration:
228 except StopIteration:
229 last = True
229 last = True
230
230
231 for chunk in pprintgen(
231 for chunk in pprintgen(
232 current, bprefix=bprefix, indent=indent, level=level
232 current, bprefix=bprefix, indent=indent, level=level
233 ):
233 ):
234 yield chunk
234 yield chunk
235
235
236 if not last:
236 if not last:
237 if indent:
237 if indent:
238 yield b',\n'
238 yield b',\n'
239 yield b' ' * (level * indent)
239 yield b' ' * (level * indent)
240 else:
240 else:
241 yield b', '
241 yield b', '
242
242
243 if indent:
243 if indent:
244 level -= 1
244 level -= 1
245 yield b'\n'
245 yield b'\n'
246 yield b' ' * (level * indent)
246 yield b' ' * (level * indent)
247
247
248 yield b']'
248 yield b']'
249 else:
249 else:
250 yield pycompat.byterepr(o)
250 yield pycompat.byterepr(o)
251
251
252
252
253 def prettyrepr(o):
253 def prettyrepr(o):
254 """Pretty print a representation of a possibly-nested object"""
254 """Pretty print a representation of a possibly-nested object"""
255 lines = []
255 lines = []
256 rs = pycompat.byterepr(o)
256 rs = pycompat.byterepr(o)
257 p0 = p1 = 0
257 p0 = p1 = 0
258 while p0 < len(rs):
258 while p0 < len(rs):
259 # '... field=<type ... field=<type ...'
259 # '... field=<type ... field=<type ...'
260 # ~~~~~~~~~~~~~~~~
260 # ~~~~~~~~~~~~~~~~
261 # p0 p1 q0 q1
261 # p0 p1 q0 q1
262 q0 = -1
262 q0 = -1
263 q1 = rs.find(b'<', p1 + 1)
263 q1 = rs.find(b'<', p1 + 1)
264 if q1 < 0:
264 if q1 < 0:
265 q1 = len(rs)
265 q1 = len(rs)
266 # pytype: disable=wrong-arg-count
266 # pytype: disable=wrong-arg-count
267 # TODO: figure out why pytype doesn't recognize the optional start
267 # TODO: figure out why pytype doesn't recognize the optional start
268 # arg
268 # arg
269 elif q1 > p1 + 1 and rs.startswith(b'=', q1 - 1):
269 elif q1 > p1 + 1 and rs.startswith(b'=', q1 - 1):
270 # pytype: enable=wrong-arg-count
270 # pytype: enable=wrong-arg-count
271 # backtrack for ' field=<'
271 # backtrack for ' field=<'
272 q0 = rs.rfind(b' ', p1 + 1, q1 - 1)
272 q0 = rs.rfind(b' ', p1 + 1, q1 - 1)
273 if q0 < 0:
273 if q0 < 0:
274 q0 = q1
274 q0 = q1
275 else:
275 else:
276 q0 += 1 # skip ' '
276 q0 += 1 # skip ' '
277 l = rs.count(b'<', 0, p0) - rs.count(b'>', 0, p0)
277 l = rs.count(b'<', 0, p0) - rs.count(b'>', 0, p0)
278 assert l >= 0
278 assert l >= 0
279 lines.append((l, rs[p0:q0].rstrip()))
279 lines.append((l, rs[p0:q0].rstrip()))
280 p0, p1 = q0, q1
280 p0, p1 = q0, q1
281 return b'\n'.join(b' ' * l + s for l, s in lines)
281 return b'\n'.join(b' ' * l + s for l, s in lines)
282
282
283
283
284 def buildrepr(r):
284 def buildrepr(r):
285 """Format an optional printable representation from unexpanded bits
285 """Format an optional printable representation from unexpanded bits
286
286
287 ======== =================================
287 ======== =================================
288 type(r) example
288 type(r) example
289 ======== =================================
289 ======== =================================
290 tuple ('<not %r>', other)
290 tuple ('<not %r>', other)
291 bytes '<branch closed>'
291 bytes '<branch closed>'
292 callable lambda: '<branch %r>' % sorted(b)
292 callable lambda: '<branch %r>' % sorted(b)
293 object other
293 object other
294 ======== =================================
294 ======== =================================
295 """
295 """
296 if r is None:
296 if r is None:
297 return b''
297 return b''
298 elif isinstance(r, tuple):
298 elif isinstance(r, tuple):
299 return r[0] % pycompat.rapply(pycompat.maybebytestr, r[1:])
299 return r[0] % pycompat.rapply(pycompat.maybebytestr, r[1:])
300 elif isinstance(r, bytes):
300 elif isinstance(r, bytes):
301 return r
301 return r
302 elif callable(r):
302 elif callable(r):
303 return r()
303 return r()
304 else:
304 else:
305 return pprint(r)
305 return pprint(r)
306
306
307
307
308 def binary(s):
308 def binary(s):
309 """return true if a string is binary data"""
309 """return true if a string is binary data"""
310 return bool(s and b'\0' in s)
310 return bool(s and b'\0' in s)
311
311
312
312
313 def _splitpattern(pattern):
313 def _splitpattern(pattern):
314 if pattern.startswith(b're:'):
314 if pattern.startswith(b're:'):
315 return b're', pattern[3:]
315 return b're', pattern[3:]
316 elif pattern.startswith(b'literal:'):
316 elif pattern.startswith(b'literal:'):
317 return b'literal', pattern[8:]
317 return b'literal', pattern[8:]
318 return b'literal', pattern
318 return b'literal', pattern
319
319
320
320
321 def stringmatcher(pattern, casesensitive=True):
321 def stringmatcher(pattern, casesensitive=True):
322 """
322 """
323 accepts a string, possibly starting with 're:' or 'literal:' prefix.
323 accepts a string, possibly starting with 're:' or 'literal:' prefix.
324 returns the matcher name, pattern, and matcher function.
324 returns the matcher name, pattern, and matcher function.
325 missing or unknown prefixes are treated as literal matches.
325 missing or unknown prefixes are treated as literal matches.
326
326
327 helper for tests:
327 helper for tests:
328 >>> def test(pattern, *tests):
328 >>> def test(pattern, *tests):
329 ... kind, pattern, matcher = stringmatcher(pattern)
329 ... kind, pattern, matcher = stringmatcher(pattern)
330 ... return (kind, pattern, [bool(matcher(t)) for t in tests])
330 ... return (kind, pattern, [bool(matcher(t)) for t in tests])
331 >>> def itest(pattern, *tests):
331 >>> def itest(pattern, *tests):
332 ... kind, pattern, matcher = stringmatcher(pattern, casesensitive=False)
332 ... kind, pattern, matcher = stringmatcher(pattern, casesensitive=False)
333 ... return (kind, pattern, [bool(matcher(t)) for t in tests])
333 ... return (kind, pattern, [bool(matcher(t)) for t in tests])
334
334
335 exact matching (no prefix):
335 exact matching (no prefix):
336 >>> test(b'abcdefg', b'abc', b'def', b'abcdefg')
336 >>> test(b'abcdefg', b'abc', b'def', b'abcdefg')
337 ('literal', 'abcdefg', [False, False, True])
337 ('literal', 'abcdefg', [False, False, True])
338
338
339 regex matching ('re:' prefix)
339 regex matching ('re:' prefix)
340 >>> test(b're:a.+b', b'nomatch', b'fooadef', b'fooadefbar')
340 >>> test(b're:a.+b', b'nomatch', b'fooadef', b'fooadefbar')
341 ('re', 'a.+b', [False, False, True])
341 ('re', 'a.+b', [False, False, True])
342
342
343 force exact matches ('literal:' prefix)
343 force exact matches ('literal:' prefix)
344 >>> test(b'literal:re:foobar', b'foobar', b're:foobar')
344 >>> test(b'literal:re:foobar', b'foobar', b're:foobar')
345 ('literal', 're:foobar', [False, True])
345 ('literal', 're:foobar', [False, True])
346
346
347 unknown prefixes are ignored and treated as literals
347 unknown prefixes are ignored and treated as literals
348 >>> test(b'foo:bar', b'foo', b'bar', b'foo:bar')
348 >>> test(b'foo:bar', b'foo', b'bar', b'foo:bar')
349 ('literal', 'foo:bar', [False, False, True])
349 ('literal', 'foo:bar', [False, False, True])
350
350
351 case insensitive regex matches
351 case insensitive regex matches
352 >>> itest(b're:A.+b', b'nomatch', b'fooadef', b'fooadefBar')
352 >>> itest(b're:A.+b', b'nomatch', b'fooadef', b'fooadefBar')
353 ('re', 'A.+b', [False, False, True])
353 ('re', 'A.+b', [False, False, True])
354
354
355 case insensitive literal matches
355 case insensitive literal matches
356 >>> itest(b'ABCDEFG', b'abc', b'def', b'abcdefg')
356 >>> itest(b'ABCDEFG', b'abc', b'def', b'abcdefg')
357 ('literal', 'ABCDEFG', [False, False, True])
357 ('literal', 'ABCDEFG', [False, False, True])
358 """
358 """
359 kind, pattern = _splitpattern(pattern)
359 kind, pattern = _splitpattern(pattern)
360 if kind == b're':
360 if kind == b're':
361 try:
361 try:
362 flags = 0
362 flags = 0
363 if not casesensitive:
363 if not casesensitive:
364 flags = remod.I
364 flags = remod.I
365 regex = remod.compile(pattern, flags)
365 regex = remod.compile(pattern, flags)
366 except remod.error as e:
366 except remod.error as e:
367 raise error.ParseError(
367 raise error.ParseError(
368 _(b'invalid regular expression: %s') % forcebytestr(e)
368 _(b'invalid regular expression: %s') % forcebytestr(e)
369 )
369 )
370 return kind, pattern, regex.search
370 return kind, pattern, regex.search
371 elif kind == b'literal':
371 elif kind == b'literal':
372 if casesensitive:
372 if casesensitive:
373 match = pattern.__eq__
373 match = pattern.__eq__
374 else:
374 else:
375 ipat = encoding.lower(pattern)
375 ipat = encoding.lower(pattern)
376 match = lambda s: ipat == encoding.lower(s)
376 match = lambda s: ipat == encoding.lower(s)
377 return kind, pattern, match
377 return kind, pattern, match
378
378
379 raise error.ProgrammingError(b'unhandled pattern kind: %s' % kind)
379 raise error.ProgrammingError(b'unhandled pattern kind: %s' % kind)
380
380
381
381
382 def substringregexp(pattern, flags=0):
382 def substringregexp(pattern, flags=0):
383 """Build a regexp object from a string pattern possibly starting with
383 """Build a regexp object from a string pattern possibly starting with
384 're:' or 'literal:' prefix.
384 're:' or 'literal:' prefix.
385
385
386 helper for tests:
386 helper for tests:
387 >>> def test(pattern, *tests):
387 >>> def test(pattern, *tests):
388 ... regexp = substringregexp(pattern)
388 ... regexp = substringregexp(pattern)
389 ... return [bool(regexp.search(t)) for t in tests]
389 ... return [bool(regexp.search(t)) for t in tests]
390 >>> def itest(pattern, *tests):
390 >>> def itest(pattern, *tests):
391 ... regexp = substringregexp(pattern, remod.I)
391 ... regexp = substringregexp(pattern, remod.I)
392 ... return [bool(regexp.search(t)) for t in tests]
392 ... return [bool(regexp.search(t)) for t in tests]
393
393
394 substring matching (no prefix):
394 substring matching (no prefix):
395 >>> test(b'bcde', b'abc', b'def', b'abcdefg')
395 >>> test(b'bcde', b'abc', b'def', b'abcdefg')
396 [False, False, True]
396 [False, False, True]
397
397
398 substring pattern should be escaped:
398 substring pattern should be escaped:
399 >>> substringregexp(b'.bc').pattern
399 >>> substringregexp(b'.bc').pattern
400 '\\\\.bc'
400 '\\\\.bc'
401 >>> test(b'.bc', b'abc', b'def', b'abcdefg')
401 >>> test(b'.bc', b'abc', b'def', b'abcdefg')
402 [False, False, False]
402 [False, False, False]
403
403
404 regex matching ('re:' prefix)
404 regex matching ('re:' prefix)
405 >>> test(b're:a.+b', b'nomatch', b'fooadef', b'fooadefbar')
405 >>> test(b're:a.+b', b'nomatch', b'fooadef', b'fooadefbar')
406 [False, False, True]
406 [False, False, True]
407
407
408 force substring matches ('literal:' prefix)
408 force substring matches ('literal:' prefix)
409 >>> test(b'literal:re:foobar', b'foobar', b're:foobar')
409 >>> test(b'literal:re:foobar', b'foobar', b're:foobar')
410 [False, True]
410 [False, True]
411
411
412 case insensitive literal matches
412 case insensitive literal matches
413 >>> itest(b'BCDE', b'abc', b'def', b'abcdefg')
413 >>> itest(b'BCDE', b'abc', b'def', b'abcdefg')
414 [False, False, True]
414 [False, False, True]
415
415
416 case insensitive regex matches
416 case insensitive regex matches
417 >>> itest(b're:A.+b', b'nomatch', b'fooadef', b'fooadefBar')
417 >>> itest(b're:A.+b', b'nomatch', b'fooadef', b'fooadefBar')
418 [False, False, True]
418 [False, False, True]
419 """
419 """
420 kind, pattern = _splitpattern(pattern)
420 kind, pattern = _splitpattern(pattern)
421 if kind == b're':
421 if kind == b're':
422 try:
422 try:
423 return remod.compile(pattern, flags)
423 return remod.compile(pattern, flags)
424 except remod.error as e:
424 except remod.error as e:
425 raise error.ParseError(
425 raise error.ParseError(
426 _(b'invalid regular expression: %s') % forcebytestr(e)
426 _(b'invalid regular expression: %s') % forcebytestr(e)
427 )
427 )
428 elif kind == b'literal':
428 elif kind == b'literal':
429 return remod.compile(remod.escape(pattern), flags)
429 return remod.compile(remod.escape(pattern), flags)
430
430
431 raise error.ProgrammingError(b'unhandled pattern kind: %s' % kind)
431 raise error.ProgrammingError(b'unhandled pattern kind: %s' % kind)
432
432
433
433
434 def shortuser(user):
434 def shortuser(user):
435 """Return a short representation of a user name or email address."""
435 """Return a short representation of a user name or email address."""
436 f = user.find(b'@')
436 f = user.find(b'@')
437 if f >= 0:
437 if f >= 0:
438 user = user[:f]
438 user = user[:f]
439 f = user.find(b'<')
439 f = user.find(b'<')
440 if f >= 0:
440 if f >= 0:
441 user = user[f + 1 :]
441 user = user[f + 1 :]
442 f = user.find(b' ')
442 f = user.find(b' ')
443 if f >= 0:
443 if f >= 0:
444 user = user[:f]
444 user = user[:f]
445 f = user.find(b'.')
445 f = user.find(b'.')
446 if f >= 0:
446 if f >= 0:
447 user = user[:f]
447 user = user[:f]
448 return user
448 return user
449
449
450
450
451 def emailuser(user):
451 def emailuser(user):
452 """Return the user portion of an email address."""
452 """Return the user portion of an email address."""
453 f = user.find(b'@')
453 f = user.find(b'@')
454 if f >= 0:
454 if f >= 0:
455 user = user[:f]
455 user = user[:f]
456 f = user.find(b'<')
456 f = user.find(b'<')
457 if f >= 0:
457 if f >= 0:
458 user = user[f + 1 :]
458 user = user[f + 1 :]
459 return user
459 return user
460
460
461
461
462 def email(author):
462 def email(author):
463 '''get email of author.'''
463 '''get email of author.'''
464 r = author.find(b'>')
464 r = author.find(b'>')
465 if r == -1:
465 if r == -1:
466 r = None
466 r = None
467 return author[author.find(b'<') + 1 : r]
467 return author[author.find(b'<') + 1 : r]
468
468
469
469
470 def person(author):
470 def person(author):
471 """Returns the name before an email address,
471 """Returns the name before an email address,
472 interpreting it as per RFC 5322
472 interpreting it as per RFC 5322
473
473
474 >>> person(b'foo@bar')
474 >>> person(b'foo@bar')
475 'foo'
475 'foo'
476 >>> person(b'Foo Bar <foo@bar>')
476 >>> person(b'Foo Bar <foo@bar>')
477 'Foo Bar'
477 'Foo Bar'
478 >>> person(b'"Foo Bar" <foo@bar>')
478 >>> person(b'"Foo Bar" <foo@bar>')
479 'Foo Bar'
479 'Foo Bar'
480 >>> person(b'"Foo \"buz\" Bar" <foo@bar>')
480 >>> person(b'"Foo \"buz\" Bar" <foo@bar>')
481 'Foo "buz" Bar'
481 'Foo "buz" Bar'
482 >>> # The following are invalid, but do exist in real-life
482 >>> # The following are invalid, but do exist in real-life
483 ...
483 ...
484 >>> person(b'Foo "buz" Bar <foo@bar>')
484 >>> person(b'Foo "buz" Bar <foo@bar>')
485 'Foo "buz" Bar'
485 'Foo "buz" Bar'
486 >>> person(b'"Foo Bar <foo@bar>')
486 >>> person(b'"Foo Bar <foo@bar>')
487 'Foo Bar'
487 'Foo Bar'
488 """
488 """
489 if b'@' not in author:
489 if b'@' not in author:
490 return author
490 return author
491 f = author.find(b'<')
491 f = author.find(b'<')
492 if f != -1:
492 if f != -1:
493 return author[:f].strip(b' "').replace(b'\\"', b'"')
493 return author[:f].strip(b' "').replace(b'\\"', b'"')
494 f = author.find(b'@')
494 f = author.find(b'@')
495 return author[:f].replace(b'.', b' ')
495 return author[:f].replace(b'.', b' ')
496
496
497
497
498 @attr.s(hash=True)
498 @attr.s(hash=True)
499 class mailmapping:
499 class mailmapping:
500 """Represents a username/email key or value in
500 """Represents a username/email key or value in
501 a mailmap file"""
501 a mailmap file"""
502
502
503 email = attr.ib()
503 email = attr.ib()
504 name = attr.ib(default=None)
504 name = attr.ib(default=None)
505
505
506
506
507 def _ismailmaplineinvalid(names, emails):
507 def _ismailmaplineinvalid(names, emails):
508 """Returns True if the parsed names and emails
508 """Returns True if the parsed names and emails
509 in a mailmap entry are invalid.
509 in a mailmap entry are invalid.
510
510
511 >>> # No names or emails fails
511 >>> # No names or emails fails
512 >>> names, emails = [], []
512 >>> names, emails = [], []
513 >>> _ismailmaplineinvalid(names, emails)
513 >>> _ismailmaplineinvalid(names, emails)
514 True
514 True
515 >>> # Only one email fails
515 >>> # Only one email fails
516 >>> emails = [b'email@email.com']
516 >>> emails = [b'email@email.com']
517 >>> _ismailmaplineinvalid(names, emails)
517 >>> _ismailmaplineinvalid(names, emails)
518 True
518 True
519 >>> # One email and one name passes
519 >>> # One email and one name passes
520 >>> names = [b'Test Name']
520 >>> names = [b'Test Name']
521 >>> _ismailmaplineinvalid(names, emails)
521 >>> _ismailmaplineinvalid(names, emails)
522 False
522 False
523 >>> # No names but two emails passes
523 >>> # No names but two emails passes
524 >>> names = []
524 >>> names = []
525 >>> emails = [b'proper@email.com', b'commit@email.com']
525 >>> emails = [b'proper@email.com', b'commit@email.com']
526 >>> _ismailmaplineinvalid(names, emails)
526 >>> _ismailmaplineinvalid(names, emails)
527 False
527 False
528 """
528 """
529 return not emails or not names and len(emails) < 2
529 return not emails or not names and len(emails) < 2
530
530
531
531
532 def parsemailmap(mailmapcontent):
532 def parsemailmap(mailmapcontent):
533 """Parses data in the .mailmap format
533 """Parses data in the .mailmap format
534
534
535 >>> mmdata = b"\\n".join([
535 >>> mmdata = b"\\n".join([
536 ... b'# Comment',
536 ... b'# Comment',
537 ... b'Name <commit1@email.xx>',
537 ... b'Name <commit1@email.xx>',
538 ... b'<name@email.xx> <commit2@email.xx>',
538 ... b'<name@email.xx> <commit2@email.xx>',
539 ... b'Name <proper@email.xx> <commit3@email.xx>',
539 ... b'Name <proper@email.xx> <commit3@email.xx>',
540 ... b'Name <proper@email.xx> Commit <commit4@email.xx>',
540 ... b'Name <proper@email.xx> Commit <commit4@email.xx>',
541 ... ])
541 ... ])
542 >>> mm = parsemailmap(mmdata)
542 >>> mm = parsemailmap(mmdata)
543 >>> for key in sorted(mm.keys()):
543 >>> for key in sorted(mm.keys()):
544 ... print(key)
544 ... print(key)
545 mailmapping(email='commit1@email.xx', name=None)
545 mailmapping(email='commit1@email.xx', name=None)
546 mailmapping(email='commit2@email.xx', name=None)
546 mailmapping(email='commit2@email.xx', name=None)
547 mailmapping(email='commit3@email.xx', name=None)
547 mailmapping(email='commit3@email.xx', name=None)
548 mailmapping(email='commit4@email.xx', name='Commit')
548 mailmapping(email='commit4@email.xx', name='Commit')
549 >>> for val in sorted(mm.values()):
549 >>> for val in sorted(mm.values()):
550 ... print(val)
550 ... print(val)
551 mailmapping(email='commit1@email.xx', name='Name')
551 mailmapping(email='commit1@email.xx', name='Name')
552 mailmapping(email='name@email.xx', name=None)
552 mailmapping(email='name@email.xx', name=None)
553 mailmapping(email='proper@email.xx', name='Name')
553 mailmapping(email='proper@email.xx', name='Name')
554 mailmapping(email='proper@email.xx', name='Name')
554 mailmapping(email='proper@email.xx', name='Name')
555 """
555 """
556 mailmap = {}
556 mailmap = {}
557
557
558 if mailmapcontent is None:
558 if mailmapcontent is None:
559 return mailmap
559 return mailmap
560
560
561 for line in mailmapcontent.splitlines():
561 for line in mailmapcontent.splitlines():
562
562
563 # Don't bother checking the line if it is a comment or
563 # Don't bother checking the line if it is a comment or
564 # is an improperly formed author field
564 # is an improperly formed author field
565 if line.lstrip().startswith(b'#'):
565 if line.lstrip().startswith(b'#'):
566 continue
566 continue
567
567
568 # names, emails hold the parsed emails and names for each line
568 # names, emails hold the parsed emails and names for each line
569 # name_builder holds the words in a persons name
569 # name_builder holds the words in a persons name
570 names, emails = [], []
570 names, emails = [], []
571 namebuilder = []
571 namebuilder = []
572
572
573 for element in line.split():
573 for element in line.split():
574 if element.startswith(b'#'):
574 if element.startswith(b'#'):
575 # If we reach a comment in the mailmap file, move on
575 # If we reach a comment in the mailmap file, move on
576 break
576 break
577
577
578 elif element.startswith(b'<') and element.endswith(b'>'):
578 elif element.startswith(b'<') and element.endswith(b'>'):
579 # We have found an email.
579 # We have found an email.
580 # Parse it, and finalize any names from earlier
580 # Parse it, and finalize any names from earlier
581 emails.append(element[1:-1]) # Slice off the "<>"
581 emails.append(element[1:-1]) # Slice off the "<>"
582
582
583 if namebuilder:
583 if namebuilder:
584 names.append(b' '.join(namebuilder))
584 names.append(b' '.join(namebuilder))
585 namebuilder = []
585 namebuilder = []
586
586
587 # Break if we have found a second email, any other
587 # Break if we have found a second email, any other
588 # data does not fit the spec for .mailmap
588 # data does not fit the spec for .mailmap
589 if len(emails) > 1:
589 if len(emails) > 1:
590 break
590 break
591
591
592 else:
592 else:
593 # We have found another word in the committers name
593 # We have found another word in the committers name
594 namebuilder.append(element)
594 namebuilder.append(element)
595
595
596 # Check to see if we have parsed the line into a valid form
596 # Check to see if we have parsed the line into a valid form
597 # We require at least one email, and either at least one
597 # We require at least one email, and either at least one
598 # name or a second email
598 # name or a second email
599 if _ismailmaplineinvalid(names, emails):
599 if _ismailmaplineinvalid(names, emails):
600 continue
600 continue
601
601
602 mailmapkey = mailmapping(
602 mailmapkey = mailmapping(
603 email=emails[-1],
603 email=emails[-1],
604 name=names[-1] if len(names) == 2 else None,
604 name=names[-1] if len(names) == 2 else None,
605 )
605 )
606
606
607 mailmap[mailmapkey] = mailmapping(
607 mailmap[mailmapkey] = mailmapping(
608 email=emails[0],
608 email=emails[0],
609 name=names[0] if names else None,
609 name=names[0] if names else None,
610 )
610 )
611
611
612 return mailmap
612 return mailmap
613
613
614
614
615 def mapname(mailmap, author):
615 def mapname(mailmap, author):
616 """Returns the author field according to the mailmap cache, or
616 """Returns the author field according to the mailmap cache, or
617 the original author field.
617 the original author field.
618
618
619 >>> mmdata = b"\\n".join([
619 >>> mmdata = b"\\n".join([
620 ... b'# Comment',
620 ... b'# Comment',
621 ... b'Name <commit1@email.xx>',
621 ... b'Name <commit1@email.xx>',
622 ... b'<name@email.xx> <commit2@email.xx>',
622 ... b'<name@email.xx> <commit2@email.xx>',
623 ... b'Name <proper@email.xx> <commit3@email.xx>',
623 ... b'Name <proper@email.xx> <commit3@email.xx>',
624 ... b'Name <proper@email.xx> Commit <commit4@email.xx>',
624 ... b'Name <proper@email.xx> Commit <commit4@email.xx>',
625 ... ])
625 ... ])
626 >>> m = parsemailmap(mmdata)
626 >>> m = parsemailmap(mmdata)
627 >>> mapname(m, b'Commit <commit1@email.xx>')
627 >>> mapname(m, b'Commit <commit1@email.xx>')
628 'Name <commit1@email.xx>'
628 'Name <commit1@email.xx>'
629 >>> mapname(m, b'Name <commit2@email.xx>')
629 >>> mapname(m, b'Name <commit2@email.xx>')
630 'Name <name@email.xx>'
630 'Name <name@email.xx>'
631 >>> mapname(m, b'Commit <commit3@email.xx>')
631 >>> mapname(m, b'Commit <commit3@email.xx>')
632 'Name <proper@email.xx>'
632 'Name <proper@email.xx>'
633 >>> mapname(m, b'Commit <commit4@email.xx>')
633 >>> mapname(m, b'Commit <commit4@email.xx>')
634 'Name <proper@email.xx>'
634 'Name <proper@email.xx>'
635 >>> mapname(m, b'Unknown Name <unknown@email.com>')
635 >>> mapname(m, b'Unknown Name <unknown@email.com>')
636 'Unknown Name <unknown@email.com>'
636 'Unknown Name <unknown@email.com>'
637 """
637 """
638 # If the author field coming in isn't in the correct format,
638 # If the author field coming in isn't in the correct format,
639 # or the mailmap is empty just return the original author field
639 # or the mailmap is empty just return the original author field
640 if not isauthorwellformed(author) or not mailmap:
640 if not isauthorwellformed(author) or not mailmap:
641 return author
641 return author
642
642
643 # Turn the user name into a mailmapping
643 # Turn the user name into a mailmapping
644 commit = mailmapping(name=person(author), email=email(author))
644 commit = mailmapping(name=person(author), email=email(author))
645
645
646 try:
646 try:
647 # Try and use both the commit email and name as the key
647 # Try and use both the commit email and name as the key
648 proper = mailmap[commit]
648 proper = mailmap[commit]
649
649
650 except KeyError:
650 except KeyError:
651 # If the lookup fails, use just the email as the key instead
651 # If the lookup fails, use just the email as the key instead
652 # We call this commit2 as not to erase original commit fields
652 # We call this commit2 as not to erase original commit fields
653 commit2 = mailmapping(email=commit.email)
653 commit2 = mailmapping(email=commit.email)
654 proper = mailmap.get(commit2, mailmapping(None, None))
654 proper = mailmap.get(commit2, mailmapping(None, None))
655
655
656 # Return the author field with proper values filled in
656 # Return the author field with proper values filled in
657 return b'%s <%s>' % (
657 return b'%s <%s>' % (
658 proper.name if proper.name else commit.name,
658 proper.name if proper.name else commit.name,
659 proper.email if proper.email else commit.email,
659 proper.email if proper.email else commit.email,
660 )
660 )
661
661
662
662
663 _correctauthorformat = remod.compile(br'^[^<]+\s<[^<>]+@[^<>]+>$')
663 _correctauthorformat = remod.compile(br'^[^<]+\s<[^<>]+@[^<>]+>$')
664
664
665
665
666 def isauthorwellformed(author):
666 def isauthorwellformed(author):
667 """Return True if the author field is well formed
667 """Return True if the author field is well formed
668 (ie "Contributor Name <contrib@email.dom>")
668 (ie "Contributor Name <contrib@email.dom>")
669
669
670 >>> isauthorwellformed(b'Good Author <good@author.com>')
670 >>> isauthorwellformed(b'Good Author <good@author.com>')
671 True
671 True
672 >>> isauthorwellformed(b'Author <good@author.com>')
672 >>> isauthorwellformed(b'Author <good@author.com>')
673 True
673 True
674 >>> isauthorwellformed(b'Bad Author')
674 >>> isauthorwellformed(b'Bad Author')
675 False
675 False
676 >>> isauthorwellformed(b'Bad Author <author@author.com')
676 >>> isauthorwellformed(b'Bad Author <author@author.com')
677 False
677 False
678 >>> isauthorwellformed(b'Bad Author author@author.com')
678 >>> isauthorwellformed(b'Bad Author author@author.com')
679 False
679 False
680 >>> isauthorwellformed(b'<author@author.com>')
680 >>> isauthorwellformed(b'<author@author.com>')
681 False
681 False
682 >>> isauthorwellformed(b'Bad Author <author>')
682 >>> isauthorwellformed(b'Bad Author <author>')
683 False
683 False
684 """
684 """
685 return _correctauthorformat.match(author) is not None
685 return _correctauthorformat.match(author) is not None
686
686
687
687
688 def firstline(text):
689 """Return the first line of the input"""
690 try:
691 return text.splitlines()[0]
692 except IndexError:
693 return b''
694
695
688 def ellipsis(text, maxlength=400):
696 def ellipsis(text, maxlength=400):
689 """Trim string to at most maxlength (default: 400) columns in display."""
697 """Trim string to at most maxlength (default: 400) columns in display."""
690 return encoding.trim(text, maxlength, ellipsis=b'...')
698 return encoding.trim(text, maxlength, ellipsis=b'...')
691
699
692
700
693 def escapestr(s):
701 def escapestr(s):
694 if isinstance(s, memoryview):
702 if isinstance(s, memoryview):
695 s = bytes(s)
703 s = bytes(s)
696 # call underlying function of s.encode('string_escape') directly for
704 # call underlying function of s.encode('string_escape') directly for
697 # Python 3 compatibility
705 # Python 3 compatibility
698 return codecs.escape_encode(s)[0] # pytype: disable=module-attr
706 return codecs.escape_encode(s)[0] # pytype: disable=module-attr
699
707
700
708
701 def unescapestr(s):
709 def unescapestr(s):
702 return codecs.escape_decode(s)[0] # pytype: disable=module-attr
710 return codecs.escape_decode(s)[0] # pytype: disable=module-attr
703
711
704
712
705 def forcebytestr(obj):
713 def forcebytestr(obj):
706 """Portably format an arbitrary object (e.g. exception) into a byte
714 """Portably format an arbitrary object (e.g. exception) into a byte
707 string."""
715 string."""
708 try:
716 try:
709 return pycompat.bytestr(obj)
717 return pycompat.bytestr(obj)
710 except UnicodeEncodeError:
718 except UnicodeEncodeError:
711 # non-ascii string, may be lossy
719 # non-ascii string, may be lossy
712 return pycompat.bytestr(encoding.strtolocal(str(obj)))
720 return pycompat.bytestr(encoding.strtolocal(str(obj)))
713
721
714
722
715 def uirepr(s):
723 def uirepr(s):
716 # Avoid double backslash in Windows path repr()
724 # Avoid double backslash in Windows path repr()
717 return pycompat.byterepr(pycompat.bytestr(s)).replace(b'\\\\', b'\\')
725 return pycompat.byterepr(pycompat.bytestr(s)).replace(b'\\\\', b'\\')
718
726
719
727
720 # delay import of textwrap
728 # delay import of textwrap
721 def _MBTextWrapper(**kwargs):
729 def _MBTextWrapper(**kwargs):
722 class tw(textwrap.TextWrapper):
730 class tw(textwrap.TextWrapper):
723 """
731 """
724 Extend TextWrapper for width-awareness.
732 Extend TextWrapper for width-awareness.
725
733
726 Neither number of 'bytes' in any encoding nor 'characters' is
734 Neither number of 'bytes' in any encoding nor 'characters' is
727 appropriate to calculate terminal columns for specified string.
735 appropriate to calculate terminal columns for specified string.
728
736
729 Original TextWrapper implementation uses built-in 'len()' directly,
737 Original TextWrapper implementation uses built-in 'len()' directly,
730 so overriding is needed to use width information of each characters.
738 so overriding is needed to use width information of each characters.
731
739
732 In addition, characters classified into 'ambiguous' width are
740 In addition, characters classified into 'ambiguous' width are
733 treated as wide in East Asian area, but as narrow in other.
741 treated as wide in East Asian area, but as narrow in other.
734
742
735 This requires use decision to determine width of such characters.
743 This requires use decision to determine width of such characters.
736 """
744 """
737
745
738 def _cutdown(self, ucstr, space_left):
746 def _cutdown(self, ucstr, space_left):
739 l = 0
747 l = 0
740 colwidth = encoding.ucolwidth
748 colwidth = encoding.ucolwidth
741 for i in pycompat.xrange(len(ucstr)):
749 for i in pycompat.xrange(len(ucstr)):
742 l += colwidth(ucstr[i])
750 l += colwidth(ucstr[i])
743 if space_left < l:
751 if space_left < l:
744 return (ucstr[:i], ucstr[i:])
752 return (ucstr[:i], ucstr[i:])
745 return ucstr, b''
753 return ucstr, b''
746
754
747 # overriding of base class
755 # overriding of base class
748 def _handle_long_word(self, reversed_chunks, cur_line, cur_len, width):
756 def _handle_long_word(self, reversed_chunks, cur_line, cur_len, width):
749 space_left = max(width - cur_len, 1)
757 space_left = max(width - cur_len, 1)
750
758
751 if self.break_long_words:
759 if self.break_long_words:
752 cut, res = self._cutdown(reversed_chunks[-1], space_left)
760 cut, res = self._cutdown(reversed_chunks[-1], space_left)
753 cur_line.append(cut)
761 cur_line.append(cut)
754 reversed_chunks[-1] = res
762 reversed_chunks[-1] = res
755 elif not cur_line:
763 elif not cur_line:
756 cur_line.append(reversed_chunks.pop())
764 cur_line.append(reversed_chunks.pop())
757
765
758 # this overriding code is imported from TextWrapper of Python 2.6
766 # this overriding code is imported from TextWrapper of Python 2.6
759 # to calculate columns of string by 'encoding.ucolwidth()'
767 # to calculate columns of string by 'encoding.ucolwidth()'
760 def _wrap_chunks(self, chunks):
768 def _wrap_chunks(self, chunks):
761 colwidth = encoding.ucolwidth
769 colwidth = encoding.ucolwidth
762
770
763 lines = []
771 lines = []
764 if self.width <= 0:
772 if self.width <= 0:
765 raise ValueError(b"invalid width %r (must be > 0)" % self.width)
773 raise ValueError(b"invalid width %r (must be > 0)" % self.width)
766
774
767 # Arrange in reverse order so items can be efficiently popped
775 # Arrange in reverse order so items can be efficiently popped
768 # from a stack of chucks.
776 # from a stack of chucks.
769 chunks.reverse()
777 chunks.reverse()
770
778
771 while chunks:
779 while chunks:
772
780
773 # Start the list of chunks that will make up the current line.
781 # Start the list of chunks that will make up the current line.
774 # cur_len is just the length of all the chunks in cur_line.
782 # cur_len is just the length of all the chunks in cur_line.
775 cur_line = []
783 cur_line = []
776 cur_len = 0
784 cur_len = 0
777
785
778 # Figure out which static string will prefix this line.
786 # Figure out which static string will prefix this line.
779 if lines:
787 if lines:
780 indent = self.subsequent_indent
788 indent = self.subsequent_indent
781 else:
789 else:
782 indent = self.initial_indent
790 indent = self.initial_indent
783
791
784 # Maximum width for this line.
792 # Maximum width for this line.
785 width = self.width - len(indent)
793 width = self.width - len(indent)
786
794
787 # First chunk on line is whitespace -- drop it, unless this
795 # First chunk on line is whitespace -- drop it, unless this
788 # is the very beginning of the text (i.e. no lines started yet).
796 # is the very beginning of the text (i.e. no lines started yet).
789 if self.drop_whitespace and chunks[-1].strip() == '' and lines:
797 if self.drop_whitespace and chunks[-1].strip() == '' and lines:
790 del chunks[-1]
798 del chunks[-1]
791
799
792 while chunks:
800 while chunks:
793 l = colwidth(chunks[-1])
801 l = colwidth(chunks[-1])
794
802
795 # Can at least squeeze this chunk onto the current line.
803 # Can at least squeeze this chunk onto the current line.
796 if cur_len + l <= width:
804 if cur_len + l <= width:
797 cur_line.append(chunks.pop())
805 cur_line.append(chunks.pop())
798 cur_len += l
806 cur_len += l
799
807
800 # Nope, this line is full.
808 # Nope, this line is full.
801 else:
809 else:
802 break
810 break
803
811
804 # The current line is full, and the next chunk is too big to
812 # The current line is full, and the next chunk is too big to
805 # fit on *any* line (not just this one).
813 # fit on *any* line (not just this one).
806 if chunks and colwidth(chunks[-1]) > width:
814 if chunks and colwidth(chunks[-1]) > width:
807 self._handle_long_word(chunks, cur_line, cur_len, width)
815 self._handle_long_word(chunks, cur_line, cur_len, width)
808
816
809 # If the last chunk on this line is all whitespace, drop it.
817 # If the last chunk on this line is all whitespace, drop it.
810 if (
818 if (
811 self.drop_whitespace
819 self.drop_whitespace
812 and cur_line
820 and cur_line
813 and cur_line[-1].strip() == r''
821 and cur_line[-1].strip() == r''
814 ):
822 ):
815 del cur_line[-1]
823 del cur_line[-1]
816
824
817 # Convert current line back to a string and store it in list
825 # Convert current line back to a string and store it in list
818 # of all lines (return value).
826 # of all lines (return value).
819 if cur_line:
827 if cur_line:
820 lines.append(indent + ''.join(cur_line))
828 lines.append(indent + ''.join(cur_line))
821
829
822 return lines
830 return lines
823
831
824 global _MBTextWrapper
832 global _MBTextWrapper
825 _MBTextWrapper = tw
833 _MBTextWrapper = tw
826 return tw(**kwargs)
834 return tw(**kwargs)
827
835
828
836
829 def wrap(line, width, initindent=b'', hangindent=b''):
837 def wrap(line, width, initindent=b'', hangindent=b''):
830 maxindent = max(len(hangindent), len(initindent))
838 maxindent = max(len(hangindent), len(initindent))
831 if width <= maxindent:
839 if width <= maxindent:
832 # adjust for weird terminal size
840 # adjust for weird terminal size
833 width = max(78, maxindent + 1)
841 width = max(78, maxindent + 1)
834 line = line.decode(
842 line = line.decode(
835 pycompat.sysstr(encoding.encoding),
843 pycompat.sysstr(encoding.encoding),
836 pycompat.sysstr(encoding.encodingmode),
844 pycompat.sysstr(encoding.encodingmode),
837 )
845 )
838 initindent = initindent.decode(
846 initindent = initindent.decode(
839 pycompat.sysstr(encoding.encoding),
847 pycompat.sysstr(encoding.encoding),
840 pycompat.sysstr(encoding.encodingmode),
848 pycompat.sysstr(encoding.encodingmode),
841 )
849 )
842 hangindent = hangindent.decode(
850 hangindent = hangindent.decode(
843 pycompat.sysstr(encoding.encoding),
851 pycompat.sysstr(encoding.encoding),
844 pycompat.sysstr(encoding.encodingmode),
852 pycompat.sysstr(encoding.encodingmode),
845 )
853 )
846 wrapper = _MBTextWrapper(
854 wrapper = _MBTextWrapper(
847 width=width, initial_indent=initindent, subsequent_indent=hangindent
855 width=width, initial_indent=initindent, subsequent_indent=hangindent
848 )
856 )
849 return wrapper.fill(line).encode(pycompat.sysstr(encoding.encoding))
857 return wrapper.fill(line).encode(pycompat.sysstr(encoding.encoding))
850
858
851
859
852 _booleans = {
860 _booleans = {
853 b'1': True,
861 b'1': True,
854 b'yes': True,
862 b'yes': True,
855 b'true': True,
863 b'true': True,
856 b'on': True,
864 b'on': True,
857 b'always': True,
865 b'always': True,
858 b'0': False,
866 b'0': False,
859 b'no': False,
867 b'no': False,
860 b'false': False,
868 b'false': False,
861 b'off': False,
869 b'off': False,
862 b'never': False,
870 b'never': False,
863 }
871 }
864
872
865
873
866 def parsebool(s):
874 def parsebool(s):
867 """Parse s into a boolean.
875 """Parse s into a boolean.
868
876
869 If s is not a valid boolean, returns None.
877 If s is not a valid boolean, returns None.
870 """
878 """
871 return _booleans.get(s.lower(), None)
879 return _booleans.get(s.lower(), None)
872
880
873
881
874 def parselist(value):
882 def parselist(value):
875 """parse a configuration value as a list of comma/space separated strings
883 """parse a configuration value as a list of comma/space separated strings
876
884
877 >>> parselist(b'this,is "a small" ,test')
885 >>> parselist(b'this,is "a small" ,test')
878 ['this', 'is', 'a small', 'test']
886 ['this', 'is', 'a small', 'test']
879 """
887 """
880
888
881 def _parse_plain(parts, s, offset):
889 def _parse_plain(parts, s, offset):
882 whitespace = False
890 whitespace = False
883 while offset < len(s) and (
891 while offset < len(s) and (
884 s[offset : offset + 1].isspace() or s[offset : offset + 1] == b','
892 s[offset : offset + 1].isspace() or s[offset : offset + 1] == b','
885 ):
893 ):
886 whitespace = True
894 whitespace = True
887 offset += 1
895 offset += 1
888 if offset >= len(s):
896 if offset >= len(s):
889 return None, parts, offset
897 return None, parts, offset
890 if whitespace:
898 if whitespace:
891 parts.append(b'')
899 parts.append(b'')
892 if s[offset : offset + 1] == b'"' and not parts[-1]:
900 if s[offset : offset + 1] == b'"' and not parts[-1]:
893 return _parse_quote, parts, offset + 1
901 return _parse_quote, parts, offset + 1
894 elif s[offset : offset + 1] == b'"' and parts[-1][-1:] == b'\\':
902 elif s[offset : offset + 1] == b'"' and parts[-1][-1:] == b'\\':
895 parts[-1] = parts[-1][:-1] + s[offset : offset + 1]
903 parts[-1] = parts[-1][:-1] + s[offset : offset + 1]
896 return _parse_plain, parts, offset + 1
904 return _parse_plain, parts, offset + 1
897 parts[-1] += s[offset : offset + 1]
905 parts[-1] += s[offset : offset + 1]
898 return _parse_plain, parts, offset + 1
906 return _parse_plain, parts, offset + 1
899
907
900 def _parse_quote(parts, s, offset):
908 def _parse_quote(parts, s, offset):
901 if offset < len(s) and s[offset : offset + 1] == b'"': # ""
909 if offset < len(s) and s[offset : offset + 1] == b'"': # ""
902 parts.append(b'')
910 parts.append(b'')
903 offset += 1
911 offset += 1
904 while offset < len(s) and (
912 while offset < len(s) and (
905 s[offset : offset + 1].isspace()
913 s[offset : offset + 1].isspace()
906 or s[offset : offset + 1] == b','
914 or s[offset : offset + 1] == b','
907 ):
915 ):
908 offset += 1
916 offset += 1
909 return _parse_plain, parts, offset
917 return _parse_plain, parts, offset
910
918
911 while offset < len(s) and s[offset : offset + 1] != b'"':
919 while offset < len(s) and s[offset : offset + 1] != b'"':
912 if (
920 if (
913 s[offset : offset + 1] == b'\\'
921 s[offset : offset + 1] == b'\\'
914 and offset + 1 < len(s)
922 and offset + 1 < len(s)
915 and s[offset + 1 : offset + 2] == b'"'
923 and s[offset + 1 : offset + 2] == b'"'
916 ):
924 ):
917 offset += 1
925 offset += 1
918 parts[-1] += b'"'
926 parts[-1] += b'"'
919 else:
927 else:
920 parts[-1] += s[offset : offset + 1]
928 parts[-1] += s[offset : offset + 1]
921 offset += 1
929 offset += 1
922
930
923 if offset >= len(s):
931 if offset >= len(s):
924 real_parts = _configlist(parts[-1])
932 real_parts = _configlist(parts[-1])
925 if not real_parts:
933 if not real_parts:
926 parts[-1] = b'"'
934 parts[-1] = b'"'
927 else:
935 else:
928 real_parts[0] = b'"' + real_parts[0]
936 real_parts[0] = b'"' + real_parts[0]
929 parts = parts[:-1]
937 parts = parts[:-1]
930 parts.extend(real_parts)
938 parts.extend(real_parts)
931 return None, parts, offset
939 return None, parts, offset
932
940
933 offset += 1
941 offset += 1
934 while offset < len(s) and s[offset : offset + 1] in [b' ', b',']:
942 while offset < len(s) and s[offset : offset + 1] in [b' ', b',']:
935 offset += 1
943 offset += 1
936
944
937 if offset < len(s):
945 if offset < len(s):
938 if offset + 1 == len(s) and s[offset : offset + 1] == b'"':
946 if offset + 1 == len(s) and s[offset : offset + 1] == b'"':
939 parts[-1] += b'"'
947 parts[-1] += b'"'
940 offset += 1
948 offset += 1
941 else:
949 else:
942 parts.append(b'')
950 parts.append(b'')
943 else:
951 else:
944 return None, parts, offset
952 return None, parts, offset
945
953
946 return _parse_plain, parts, offset
954 return _parse_plain, parts, offset
947
955
948 def _configlist(s):
956 def _configlist(s):
949 s = s.rstrip(b' ,')
957 s = s.rstrip(b' ,')
950 if not s:
958 if not s:
951 return []
959 return []
952 parser, parts, offset = _parse_plain, [b''], 0
960 parser, parts, offset = _parse_plain, [b''], 0
953 while parser:
961 while parser:
954 parser, parts, offset = parser(parts, s, offset)
962 parser, parts, offset = parser(parts, s, offset)
955 return parts
963 return parts
956
964
957 if value is not None and isinstance(value, bytes):
965 if value is not None and isinstance(value, bytes):
958 result = _configlist(value.lstrip(b' ,\n'))
966 result = _configlist(value.lstrip(b' ,\n'))
959 else:
967 else:
960 result = value
968 result = value
961 return result or []
969 return result or []
962
970
963
971
964 def evalpythonliteral(s):
972 def evalpythonliteral(s):
965 """Evaluate a string containing a Python literal expression"""
973 """Evaluate a string containing a Python literal expression"""
966 # We could backport our tokenizer hack to rewrite '' to u'' if we want
974 # We could backport our tokenizer hack to rewrite '' to u'' if we want
967 return ast.literal_eval(s.decode('latin1'))
975 return ast.literal_eval(s.decode('latin1'))
General Comments 0
You need to be logged in to leave comments. Login now