##// END OF EJS Templates
stringutil: emit multiple chunks when pretty printing...
Gregory Szorc -
r39390:5ed7c6ca default
parent child Browse files
Show More
@@ -1,597 +1,675 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 Matt Mackall <mpm@selenic.com>
4 # Copyright 2005-2007 Matt Mackall <mpm@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 from __future__ import absolute_import
10 from __future__ import absolute_import
11
11
12 import ast
12 import ast
13 import codecs
13 import codecs
14 import re as remod
14 import re as remod
15 import textwrap
15 import textwrap
16 import types
16 import types
17
17
18 from ..i18n import _
18 from ..i18n import _
19 from ..thirdparty import attr
19 from ..thirdparty import attr
20
20
21 from .. import (
21 from .. import (
22 encoding,
22 encoding,
23 error,
23 error,
24 pycompat,
24 pycompat,
25 )
25 )
26
26
27 # regex special chars pulled from https://bugs.python.org/issue29995
27 # regex special chars pulled from https://bugs.python.org/issue29995
28 # which was part of Python 3.7.
28 # which was part of Python 3.7.
29 _respecial = pycompat.bytestr(b'()[]{}?*+-|^$\\.&~# \t\n\r\v\f')
29 _respecial = pycompat.bytestr(b'()[]{}?*+-|^$\\.&~# \t\n\r\v\f')
30 _regexescapemap = {ord(i): (b'\\' + i).decode('latin1') for i in _respecial}
30 _regexescapemap = {ord(i): (b'\\' + i).decode('latin1') for i in _respecial}
31
31
32 def reescape(pat):
32 def reescape(pat):
33 """Drop-in replacement for re.escape."""
33 """Drop-in replacement for re.escape."""
34 # NOTE: it is intentional that this works on unicodes and not
34 # NOTE: it is intentional that this works on unicodes and not
35 # bytes, as it's only possible to do the escaping with
35 # bytes, as it's only possible to do the escaping with
36 # unicode.translate, not bytes.translate. Sigh.
36 # unicode.translate, not bytes.translate. Sigh.
37 wantuni = True
37 wantuni = True
38 if isinstance(pat, bytes):
38 if isinstance(pat, bytes):
39 wantuni = False
39 wantuni = False
40 pat = pat.decode('latin1')
40 pat = pat.decode('latin1')
41 pat = pat.translate(_regexescapemap)
41 pat = pat.translate(_regexescapemap)
42 if wantuni:
42 if wantuni:
43 return pat
43 return pat
44 return pat.encode('latin1')
44 return pat.encode('latin1')
45
45
46 def pprint(o, bprefix=False):
46 def pprint(o, bprefix=False):
47 """Pretty print an object."""
47 """Pretty print an object."""
48 return b''.join(pprintgen(o, bprefix=bprefix))
48 return b''.join(pprintgen(o, bprefix=bprefix))
49
49
50 def pprintgen(o, bprefix=False):
50 def pprintgen(o, bprefix=False):
51 """Pretty print an object to a generator of atoms."""
51 """Pretty print an object to a generator of atoms."""
52
52
53 if isinstance(o, bytes):
53 if isinstance(o, bytes):
54 if bprefix:
54 if bprefix:
55 yield "b'%s'" % escapestr(o)
55 yield "b'%s'" % escapestr(o)
56 else:
56 else:
57 yield "'%s'" % escapestr(o)
57 yield "'%s'" % escapestr(o)
58 elif isinstance(o, bytearray):
58 elif isinstance(o, bytearray):
59 # codecs.escape_encode() can't handle bytearray, so escapestr fails
59 # codecs.escape_encode() can't handle bytearray, so escapestr fails
60 # without coercion.
60 # without coercion.
61 yield "bytearray['%s']" % escapestr(bytes(o))
61 yield "bytearray['%s']" % escapestr(bytes(o))
62 elif isinstance(o, list):
62 elif isinstance(o, list):
63 yield '[%s]' % (b', '.join(pprint(a, bprefix=bprefix) for a in o))
63 if not o:
64 yield '[]'
65 return
66
67 yield '['
68
69 for i, a in enumerate(o):
70 for chunk in pprintgen(a, bprefix=bprefix):
71 yield chunk
72
73 if i + 1 < len(o):
74 yield ', '
75
76 yield ']'
64 elif isinstance(o, dict):
77 elif isinstance(o, dict):
65 yield '{%s}' % (b', '.join(
78 if not o:
66 '%s: %s' % (pprint(k, bprefix=bprefix),
79 yield '{}'
67 pprint(v, bprefix=bprefix))
80 return
68 for k, v in sorted(o.items())))
81
82 yield '{'
83
84 for i, (k, v) in enumerate(sorted(o.items())):
85 for chunk in pprintgen(k, bprefix=bprefix):
86 yield chunk
87
88 yield ': '
89
90 for chunk in pprintgen(v, bprefix=bprefix):
91 yield chunk
92
93 if i + 1 < len(o):
94 yield ', '
95
96 yield '}'
69 elif isinstance(o, set):
97 elif isinstance(o, set):
70 yield 'set([%s])' % (b', '.join(
98 if not o:
71 pprint(k, bprefix=bprefix) for k in sorted(o)))
99 yield 'set([])'
100 return
101
102 yield 'set(['
103
104 for i, k in enumerate(sorted(o)):
105 for chunk in pprintgen(k, bprefix=bprefix):
106 yield chunk
107
108 if i + 1 < len(o):
109 yield ', '
110
111 yield '])'
72 elif isinstance(o, tuple):
112 elif isinstance(o, tuple):
73 yield '(%s)' % (b', '.join(pprint(a, bprefix=bprefix) for a in o))
113 if not o:
114 yield '()'
115 return
116
117 yield '('
118
119 for i, a in enumerate(o):
120 for chunk in pprintgen(a, bprefix=bprefix):
121 yield chunk
122
123 if i + 1 < len(o):
124 yield ', '
125
126 yield ')'
74 elif isinstance(o, types.GeneratorType):
127 elif isinstance(o, types.GeneratorType):
75 yield 'gen[%s]' % (b', '.join(pprint(a, bprefix=bprefix) for a in o))
128 # Special case of empty generator.
129 try:
130 nextitem = next(o)
131 except StopIteration:
132 yield 'gen[]'
133 return
134
135 yield 'gen['
136
137 last = False
138
139 while not last:
140 current = nextitem
141
142 try:
143 nextitem = next(o)
144 except StopIteration:
145 last = True
146
147 for chunk in pprintgen(current, bprefix=bprefix):
148 yield chunk
149
150 if not last:
151 yield ', '
152
153 yield ']'
76 else:
154 else:
77 yield pycompat.byterepr(o)
155 yield pycompat.byterepr(o)
78
156
79 def prettyrepr(o):
157 def prettyrepr(o):
80 """Pretty print a representation of a possibly-nested object"""
158 """Pretty print a representation of a possibly-nested object"""
81 lines = []
159 lines = []
82 rs = pycompat.byterepr(o)
160 rs = pycompat.byterepr(o)
83 p0 = p1 = 0
161 p0 = p1 = 0
84 while p0 < len(rs):
162 while p0 < len(rs):
85 # '... field=<type ... field=<type ...'
163 # '... field=<type ... field=<type ...'
86 # ~~~~~~~~~~~~~~~~
164 # ~~~~~~~~~~~~~~~~
87 # p0 p1 q0 q1
165 # p0 p1 q0 q1
88 q0 = -1
166 q0 = -1
89 q1 = rs.find('<', p1 + 1)
167 q1 = rs.find('<', p1 + 1)
90 if q1 < 0:
168 if q1 < 0:
91 q1 = len(rs)
169 q1 = len(rs)
92 elif q1 > p1 + 1 and rs.startswith('=', q1 - 1):
170 elif q1 > p1 + 1 and rs.startswith('=', q1 - 1):
93 # backtrack for ' field=<'
171 # backtrack for ' field=<'
94 q0 = rs.rfind(' ', p1 + 1, q1 - 1)
172 q0 = rs.rfind(' ', p1 + 1, q1 - 1)
95 if q0 < 0:
173 if q0 < 0:
96 q0 = q1
174 q0 = q1
97 else:
175 else:
98 q0 += 1 # skip ' '
176 q0 += 1 # skip ' '
99 l = rs.count('<', 0, p0) - rs.count('>', 0, p0)
177 l = rs.count('<', 0, p0) - rs.count('>', 0, p0)
100 assert l >= 0
178 assert l >= 0
101 lines.append((l, rs[p0:q0].rstrip()))
179 lines.append((l, rs[p0:q0].rstrip()))
102 p0, p1 = q0, q1
180 p0, p1 = q0, q1
103 return '\n'.join(' ' * l + s for l, s in lines)
181 return '\n'.join(' ' * l + s for l, s in lines)
104
182
105 def buildrepr(r):
183 def buildrepr(r):
106 """Format an optional printable representation from unexpanded bits
184 """Format an optional printable representation from unexpanded bits
107
185
108 ======== =================================
186 ======== =================================
109 type(r) example
187 type(r) example
110 ======== =================================
188 ======== =================================
111 tuple ('<not %r>', other)
189 tuple ('<not %r>', other)
112 bytes '<branch closed>'
190 bytes '<branch closed>'
113 callable lambda: '<branch %r>' % sorted(b)
191 callable lambda: '<branch %r>' % sorted(b)
114 object other
192 object other
115 ======== =================================
193 ======== =================================
116 """
194 """
117 if r is None:
195 if r is None:
118 return ''
196 return ''
119 elif isinstance(r, tuple):
197 elif isinstance(r, tuple):
120 return r[0] % pycompat.rapply(pycompat.maybebytestr, r[1:])
198 return r[0] % pycompat.rapply(pycompat.maybebytestr, r[1:])
121 elif isinstance(r, bytes):
199 elif isinstance(r, bytes):
122 return r
200 return r
123 elif callable(r):
201 elif callable(r):
124 return r()
202 return r()
125 else:
203 else:
126 return pprint(r)
204 return pprint(r)
127
205
128 def binary(s):
206 def binary(s):
129 """return true if a string is binary data"""
207 """return true if a string is binary data"""
130 return bool(s and '\0' in s)
208 return bool(s and '\0' in s)
131
209
132 def stringmatcher(pattern, casesensitive=True):
210 def stringmatcher(pattern, casesensitive=True):
133 """
211 """
134 accepts a string, possibly starting with 're:' or 'literal:' prefix.
212 accepts a string, possibly starting with 're:' or 'literal:' prefix.
135 returns the matcher name, pattern, and matcher function.
213 returns the matcher name, pattern, and matcher function.
136 missing or unknown prefixes are treated as literal matches.
214 missing or unknown prefixes are treated as literal matches.
137
215
138 helper for tests:
216 helper for tests:
139 >>> def test(pattern, *tests):
217 >>> def test(pattern, *tests):
140 ... kind, pattern, matcher = stringmatcher(pattern)
218 ... kind, pattern, matcher = stringmatcher(pattern)
141 ... return (kind, pattern, [bool(matcher(t)) for t in tests])
219 ... return (kind, pattern, [bool(matcher(t)) for t in tests])
142 >>> def itest(pattern, *tests):
220 >>> def itest(pattern, *tests):
143 ... kind, pattern, matcher = stringmatcher(pattern, casesensitive=False)
221 ... kind, pattern, matcher = stringmatcher(pattern, casesensitive=False)
144 ... return (kind, pattern, [bool(matcher(t)) for t in tests])
222 ... return (kind, pattern, [bool(matcher(t)) for t in tests])
145
223
146 exact matching (no prefix):
224 exact matching (no prefix):
147 >>> test(b'abcdefg', b'abc', b'def', b'abcdefg')
225 >>> test(b'abcdefg', b'abc', b'def', b'abcdefg')
148 ('literal', 'abcdefg', [False, False, True])
226 ('literal', 'abcdefg', [False, False, True])
149
227
150 regex matching ('re:' prefix)
228 regex matching ('re:' prefix)
151 >>> test(b're:a.+b', b'nomatch', b'fooadef', b'fooadefbar')
229 >>> test(b're:a.+b', b'nomatch', b'fooadef', b'fooadefbar')
152 ('re', 'a.+b', [False, False, True])
230 ('re', 'a.+b', [False, False, True])
153
231
154 force exact matches ('literal:' prefix)
232 force exact matches ('literal:' prefix)
155 >>> test(b'literal:re:foobar', b'foobar', b're:foobar')
233 >>> test(b'literal:re:foobar', b'foobar', b're:foobar')
156 ('literal', 're:foobar', [False, True])
234 ('literal', 're:foobar', [False, True])
157
235
158 unknown prefixes are ignored and treated as literals
236 unknown prefixes are ignored and treated as literals
159 >>> test(b'foo:bar', b'foo', b'bar', b'foo:bar')
237 >>> test(b'foo:bar', b'foo', b'bar', b'foo:bar')
160 ('literal', 'foo:bar', [False, False, True])
238 ('literal', 'foo:bar', [False, False, True])
161
239
162 case insensitive regex matches
240 case insensitive regex matches
163 >>> itest(b're:A.+b', b'nomatch', b'fooadef', b'fooadefBar')
241 >>> itest(b're:A.+b', b'nomatch', b'fooadef', b'fooadefBar')
164 ('re', 'A.+b', [False, False, True])
242 ('re', 'A.+b', [False, False, True])
165
243
166 case insensitive literal matches
244 case insensitive literal matches
167 >>> itest(b'ABCDEFG', b'abc', b'def', b'abcdefg')
245 >>> itest(b'ABCDEFG', b'abc', b'def', b'abcdefg')
168 ('literal', 'ABCDEFG', [False, False, True])
246 ('literal', 'ABCDEFG', [False, False, True])
169 """
247 """
170 if pattern.startswith('re:'):
248 if pattern.startswith('re:'):
171 pattern = pattern[3:]
249 pattern = pattern[3:]
172 try:
250 try:
173 flags = 0
251 flags = 0
174 if not casesensitive:
252 if not casesensitive:
175 flags = remod.I
253 flags = remod.I
176 regex = remod.compile(pattern, flags)
254 regex = remod.compile(pattern, flags)
177 except remod.error as e:
255 except remod.error as e:
178 raise error.ParseError(_('invalid regular expression: %s')
256 raise error.ParseError(_('invalid regular expression: %s')
179 % e)
257 % e)
180 return 're', pattern, regex.search
258 return 're', pattern, regex.search
181 elif pattern.startswith('literal:'):
259 elif pattern.startswith('literal:'):
182 pattern = pattern[8:]
260 pattern = pattern[8:]
183
261
184 match = pattern.__eq__
262 match = pattern.__eq__
185
263
186 if not casesensitive:
264 if not casesensitive:
187 ipat = encoding.lower(pattern)
265 ipat = encoding.lower(pattern)
188 match = lambda s: ipat == encoding.lower(s)
266 match = lambda s: ipat == encoding.lower(s)
189 return 'literal', pattern, match
267 return 'literal', pattern, match
190
268
191 def shortuser(user):
269 def shortuser(user):
192 """Return a short representation of a user name or email address."""
270 """Return a short representation of a user name or email address."""
193 f = user.find('@')
271 f = user.find('@')
194 if f >= 0:
272 if f >= 0:
195 user = user[:f]
273 user = user[:f]
196 f = user.find('<')
274 f = user.find('<')
197 if f >= 0:
275 if f >= 0:
198 user = user[f + 1:]
276 user = user[f + 1:]
199 f = user.find(' ')
277 f = user.find(' ')
200 if f >= 0:
278 if f >= 0:
201 user = user[:f]
279 user = user[:f]
202 f = user.find('.')
280 f = user.find('.')
203 if f >= 0:
281 if f >= 0:
204 user = user[:f]
282 user = user[:f]
205 return user
283 return user
206
284
207 def emailuser(user):
285 def emailuser(user):
208 """Return the user portion of an email address."""
286 """Return the user portion of an email address."""
209 f = user.find('@')
287 f = user.find('@')
210 if f >= 0:
288 if f >= 0:
211 user = user[:f]
289 user = user[:f]
212 f = user.find('<')
290 f = user.find('<')
213 if f >= 0:
291 if f >= 0:
214 user = user[f + 1:]
292 user = user[f + 1:]
215 return user
293 return user
216
294
217 def email(author):
295 def email(author):
218 '''get email of author.'''
296 '''get email of author.'''
219 r = author.find('>')
297 r = author.find('>')
220 if r == -1:
298 if r == -1:
221 r = None
299 r = None
222 return author[author.find('<') + 1:r]
300 return author[author.find('<') + 1:r]
223
301
224 def person(author):
302 def person(author):
225 """Returns the name before an email address,
303 """Returns the name before an email address,
226 interpreting it as per RFC 5322
304 interpreting it as per RFC 5322
227
305
228 >>> person(b'foo@bar')
306 >>> person(b'foo@bar')
229 'foo'
307 'foo'
230 >>> person(b'Foo Bar <foo@bar>')
308 >>> person(b'Foo Bar <foo@bar>')
231 'Foo Bar'
309 'Foo Bar'
232 >>> person(b'"Foo Bar" <foo@bar>')
310 >>> person(b'"Foo Bar" <foo@bar>')
233 'Foo Bar'
311 'Foo Bar'
234 >>> person(b'"Foo \"buz\" Bar" <foo@bar>')
312 >>> person(b'"Foo \"buz\" Bar" <foo@bar>')
235 'Foo "buz" Bar'
313 'Foo "buz" Bar'
236 >>> # The following are invalid, but do exist in real-life
314 >>> # The following are invalid, but do exist in real-life
237 ...
315 ...
238 >>> person(b'Foo "buz" Bar <foo@bar>')
316 >>> person(b'Foo "buz" Bar <foo@bar>')
239 'Foo "buz" Bar'
317 'Foo "buz" Bar'
240 >>> person(b'"Foo Bar <foo@bar>')
318 >>> person(b'"Foo Bar <foo@bar>')
241 'Foo Bar'
319 'Foo Bar'
242 """
320 """
243 if '@' not in author:
321 if '@' not in author:
244 return author
322 return author
245 f = author.find('<')
323 f = author.find('<')
246 if f != -1:
324 if f != -1:
247 return author[:f].strip(' "').replace('\\"', '"')
325 return author[:f].strip(' "').replace('\\"', '"')
248 f = author.find('@')
326 f = author.find('@')
249 return author[:f].replace('.', ' ')
327 return author[:f].replace('.', ' ')
250
328
251 @attr.s(hash=True)
329 @attr.s(hash=True)
252 class mailmapping(object):
330 class mailmapping(object):
253 '''Represents a username/email key or value in
331 '''Represents a username/email key or value in
254 a mailmap file'''
332 a mailmap file'''
255 email = attr.ib()
333 email = attr.ib()
256 name = attr.ib(default=None)
334 name = attr.ib(default=None)
257
335
258 def _ismailmaplineinvalid(names, emails):
336 def _ismailmaplineinvalid(names, emails):
259 '''Returns True if the parsed names and emails
337 '''Returns True if the parsed names and emails
260 in a mailmap entry are invalid.
338 in a mailmap entry are invalid.
261
339
262 >>> # No names or emails fails
340 >>> # No names or emails fails
263 >>> names, emails = [], []
341 >>> names, emails = [], []
264 >>> _ismailmaplineinvalid(names, emails)
342 >>> _ismailmaplineinvalid(names, emails)
265 True
343 True
266 >>> # Only one email fails
344 >>> # Only one email fails
267 >>> emails = [b'email@email.com']
345 >>> emails = [b'email@email.com']
268 >>> _ismailmaplineinvalid(names, emails)
346 >>> _ismailmaplineinvalid(names, emails)
269 True
347 True
270 >>> # One email and one name passes
348 >>> # One email and one name passes
271 >>> names = [b'Test Name']
349 >>> names = [b'Test Name']
272 >>> _ismailmaplineinvalid(names, emails)
350 >>> _ismailmaplineinvalid(names, emails)
273 False
351 False
274 >>> # No names but two emails passes
352 >>> # No names but two emails passes
275 >>> names = []
353 >>> names = []
276 >>> emails = [b'proper@email.com', b'commit@email.com']
354 >>> emails = [b'proper@email.com', b'commit@email.com']
277 >>> _ismailmaplineinvalid(names, emails)
355 >>> _ismailmaplineinvalid(names, emails)
278 False
356 False
279 '''
357 '''
280 return not emails or not names and len(emails) < 2
358 return not emails or not names and len(emails) < 2
281
359
282 def parsemailmap(mailmapcontent):
360 def parsemailmap(mailmapcontent):
283 """Parses data in the .mailmap format
361 """Parses data in the .mailmap format
284
362
285 >>> mmdata = b"\\n".join([
363 >>> mmdata = b"\\n".join([
286 ... b'# Comment',
364 ... b'# Comment',
287 ... b'Name <commit1@email.xx>',
365 ... b'Name <commit1@email.xx>',
288 ... b'<name@email.xx> <commit2@email.xx>',
366 ... b'<name@email.xx> <commit2@email.xx>',
289 ... b'Name <proper@email.xx> <commit3@email.xx>',
367 ... b'Name <proper@email.xx> <commit3@email.xx>',
290 ... b'Name <proper@email.xx> Commit <commit4@email.xx>',
368 ... b'Name <proper@email.xx> Commit <commit4@email.xx>',
291 ... ])
369 ... ])
292 >>> mm = parsemailmap(mmdata)
370 >>> mm = parsemailmap(mmdata)
293 >>> for key in sorted(mm.keys()):
371 >>> for key in sorted(mm.keys()):
294 ... print(key)
372 ... print(key)
295 mailmapping(email='commit1@email.xx', name=None)
373 mailmapping(email='commit1@email.xx', name=None)
296 mailmapping(email='commit2@email.xx', name=None)
374 mailmapping(email='commit2@email.xx', name=None)
297 mailmapping(email='commit3@email.xx', name=None)
375 mailmapping(email='commit3@email.xx', name=None)
298 mailmapping(email='commit4@email.xx', name='Commit')
376 mailmapping(email='commit4@email.xx', name='Commit')
299 >>> for val in sorted(mm.values()):
377 >>> for val in sorted(mm.values()):
300 ... print(val)
378 ... print(val)
301 mailmapping(email='commit1@email.xx', name='Name')
379 mailmapping(email='commit1@email.xx', name='Name')
302 mailmapping(email='name@email.xx', name=None)
380 mailmapping(email='name@email.xx', name=None)
303 mailmapping(email='proper@email.xx', name='Name')
381 mailmapping(email='proper@email.xx', name='Name')
304 mailmapping(email='proper@email.xx', name='Name')
382 mailmapping(email='proper@email.xx', name='Name')
305 """
383 """
306 mailmap = {}
384 mailmap = {}
307
385
308 if mailmapcontent is None:
386 if mailmapcontent is None:
309 return mailmap
387 return mailmap
310
388
311 for line in mailmapcontent.splitlines():
389 for line in mailmapcontent.splitlines():
312
390
313 # Don't bother checking the line if it is a comment or
391 # Don't bother checking the line if it is a comment or
314 # is an improperly formed author field
392 # is an improperly formed author field
315 if line.lstrip().startswith('#'):
393 if line.lstrip().startswith('#'):
316 continue
394 continue
317
395
318 # names, emails hold the parsed emails and names for each line
396 # names, emails hold the parsed emails and names for each line
319 # name_builder holds the words in a persons name
397 # name_builder holds the words in a persons name
320 names, emails = [], []
398 names, emails = [], []
321 namebuilder = []
399 namebuilder = []
322
400
323 for element in line.split():
401 for element in line.split():
324 if element.startswith('#'):
402 if element.startswith('#'):
325 # If we reach a comment in the mailmap file, move on
403 # If we reach a comment in the mailmap file, move on
326 break
404 break
327
405
328 elif element.startswith('<') and element.endswith('>'):
406 elif element.startswith('<') and element.endswith('>'):
329 # We have found an email.
407 # We have found an email.
330 # Parse it, and finalize any names from earlier
408 # Parse it, and finalize any names from earlier
331 emails.append(element[1:-1]) # Slice off the "<>"
409 emails.append(element[1:-1]) # Slice off the "<>"
332
410
333 if namebuilder:
411 if namebuilder:
334 names.append(' '.join(namebuilder))
412 names.append(' '.join(namebuilder))
335 namebuilder = []
413 namebuilder = []
336
414
337 # Break if we have found a second email, any other
415 # Break if we have found a second email, any other
338 # data does not fit the spec for .mailmap
416 # data does not fit the spec for .mailmap
339 if len(emails) > 1:
417 if len(emails) > 1:
340 break
418 break
341
419
342 else:
420 else:
343 # We have found another word in the committers name
421 # We have found another word in the committers name
344 namebuilder.append(element)
422 namebuilder.append(element)
345
423
346 # Check to see if we have parsed the line into a valid form
424 # Check to see if we have parsed the line into a valid form
347 # We require at least one email, and either at least one
425 # We require at least one email, and either at least one
348 # name or a second email
426 # name or a second email
349 if _ismailmaplineinvalid(names, emails):
427 if _ismailmaplineinvalid(names, emails):
350 continue
428 continue
351
429
352 mailmapkey = mailmapping(
430 mailmapkey = mailmapping(
353 email=emails[-1],
431 email=emails[-1],
354 name=names[-1] if len(names) == 2 else None,
432 name=names[-1] if len(names) == 2 else None,
355 )
433 )
356
434
357 mailmap[mailmapkey] = mailmapping(
435 mailmap[mailmapkey] = mailmapping(
358 email=emails[0],
436 email=emails[0],
359 name=names[0] if names else None,
437 name=names[0] if names else None,
360 )
438 )
361
439
362 return mailmap
440 return mailmap
363
441
364 def mapname(mailmap, author):
442 def mapname(mailmap, author):
365 """Returns the author field according to the mailmap cache, or
443 """Returns the author field according to the mailmap cache, or
366 the original author field.
444 the original author field.
367
445
368 >>> mmdata = b"\\n".join([
446 >>> mmdata = b"\\n".join([
369 ... b'# Comment',
447 ... b'# Comment',
370 ... b'Name <commit1@email.xx>',
448 ... b'Name <commit1@email.xx>',
371 ... b'<name@email.xx> <commit2@email.xx>',
449 ... b'<name@email.xx> <commit2@email.xx>',
372 ... b'Name <proper@email.xx> <commit3@email.xx>',
450 ... b'Name <proper@email.xx> <commit3@email.xx>',
373 ... b'Name <proper@email.xx> Commit <commit4@email.xx>',
451 ... b'Name <proper@email.xx> Commit <commit4@email.xx>',
374 ... ])
452 ... ])
375 >>> m = parsemailmap(mmdata)
453 >>> m = parsemailmap(mmdata)
376 >>> mapname(m, b'Commit <commit1@email.xx>')
454 >>> mapname(m, b'Commit <commit1@email.xx>')
377 'Name <commit1@email.xx>'
455 'Name <commit1@email.xx>'
378 >>> mapname(m, b'Name <commit2@email.xx>')
456 >>> mapname(m, b'Name <commit2@email.xx>')
379 'Name <name@email.xx>'
457 'Name <name@email.xx>'
380 >>> mapname(m, b'Commit <commit3@email.xx>')
458 >>> mapname(m, b'Commit <commit3@email.xx>')
381 'Name <proper@email.xx>'
459 'Name <proper@email.xx>'
382 >>> mapname(m, b'Commit <commit4@email.xx>')
460 >>> mapname(m, b'Commit <commit4@email.xx>')
383 'Name <proper@email.xx>'
461 'Name <proper@email.xx>'
384 >>> mapname(m, b'Unknown Name <unknown@email.com>')
462 >>> mapname(m, b'Unknown Name <unknown@email.com>')
385 'Unknown Name <unknown@email.com>'
463 'Unknown Name <unknown@email.com>'
386 """
464 """
387 # If the author field coming in isn't in the correct format,
465 # If the author field coming in isn't in the correct format,
388 # or the mailmap is empty just return the original author field
466 # or the mailmap is empty just return the original author field
389 if not isauthorwellformed(author) or not mailmap:
467 if not isauthorwellformed(author) or not mailmap:
390 return author
468 return author
391
469
392 # Turn the user name into a mailmapping
470 # Turn the user name into a mailmapping
393 commit = mailmapping(name=person(author), email=email(author))
471 commit = mailmapping(name=person(author), email=email(author))
394
472
395 try:
473 try:
396 # Try and use both the commit email and name as the key
474 # Try and use both the commit email and name as the key
397 proper = mailmap[commit]
475 proper = mailmap[commit]
398
476
399 except KeyError:
477 except KeyError:
400 # If the lookup fails, use just the email as the key instead
478 # If the lookup fails, use just the email as the key instead
401 # We call this commit2 as not to erase original commit fields
479 # We call this commit2 as not to erase original commit fields
402 commit2 = mailmapping(email=commit.email)
480 commit2 = mailmapping(email=commit.email)
403 proper = mailmap.get(commit2, mailmapping(None, None))
481 proper = mailmap.get(commit2, mailmapping(None, None))
404
482
405 # Return the author field with proper values filled in
483 # Return the author field with proper values filled in
406 return '%s <%s>' % (
484 return '%s <%s>' % (
407 proper.name if proper.name else commit.name,
485 proper.name if proper.name else commit.name,
408 proper.email if proper.email else commit.email,
486 proper.email if proper.email else commit.email,
409 )
487 )
410
488
411 _correctauthorformat = remod.compile(br'^[^<]+\s\<[^<>]+@[^<>]+\>$')
489 _correctauthorformat = remod.compile(br'^[^<]+\s\<[^<>]+@[^<>]+\>$')
412
490
413 def isauthorwellformed(author):
491 def isauthorwellformed(author):
414 '''Return True if the author field is well formed
492 '''Return True if the author field is well formed
415 (ie "Contributor Name <contrib@email.dom>")
493 (ie "Contributor Name <contrib@email.dom>")
416
494
417 >>> isauthorwellformed(b'Good Author <good@author.com>')
495 >>> isauthorwellformed(b'Good Author <good@author.com>')
418 True
496 True
419 >>> isauthorwellformed(b'Author <good@author.com>')
497 >>> isauthorwellformed(b'Author <good@author.com>')
420 True
498 True
421 >>> isauthorwellformed(b'Bad Author')
499 >>> isauthorwellformed(b'Bad Author')
422 False
500 False
423 >>> isauthorwellformed(b'Bad Author <author@author.com')
501 >>> isauthorwellformed(b'Bad Author <author@author.com')
424 False
502 False
425 >>> isauthorwellformed(b'Bad Author author@author.com')
503 >>> isauthorwellformed(b'Bad Author author@author.com')
426 False
504 False
427 >>> isauthorwellformed(b'<author@author.com>')
505 >>> isauthorwellformed(b'<author@author.com>')
428 False
506 False
429 >>> isauthorwellformed(b'Bad Author <author>')
507 >>> isauthorwellformed(b'Bad Author <author>')
430 False
508 False
431 '''
509 '''
432 return _correctauthorformat.match(author) is not None
510 return _correctauthorformat.match(author) is not None
433
511
434 def ellipsis(text, maxlength=400):
512 def ellipsis(text, maxlength=400):
435 """Trim string to at most maxlength (default: 400) columns in display."""
513 """Trim string to at most maxlength (default: 400) columns in display."""
436 return encoding.trim(text, maxlength, ellipsis='...')
514 return encoding.trim(text, maxlength, ellipsis='...')
437
515
438 def escapestr(s):
516 def escapestr(s):
439 if isinstance(s, memoryview):
517 if isinstance(s, memoryview):
440 s = bytes(s)
518 s = bytes(s)
441 # call underlying function of s.encode('string_escape') directly for
519 # call underlying function of s.encode('string_escape') directly for
442 # Python 3 compatibility
520 # Python 3 compatibility
443 return codecs.escape_encode(s)[0]
521 return codecs.escape_encode(s)[0]
444
522
445 def unescapestr(s):
523 def unescapestr(s):
446 return codecs.escape_decode(s)[0]
524 return codecs.escape_decode(s)[0]
447
525
448 def forcebytestr(obj):
526 def forcebytestr(obj):
449 """Portably format an arbitrary object (e.g. exception) into a byte
527 """Portably format an arbitrary object (e.g. exception) into a byte
450 string."""
528 string."""
451 try:
529 try:
452 return pycompat.bytestr(obj)
530 return pycompat.bytestr(obj)
453 except UnicodeEncodeError:
531 except UnicodeEncodeError:
454 # non-ascii string, may be lossy
532 # non-ascii string, may be lossy
455 return pycompat.bytestr(encoding.strtolocal(str(obj)))
533 return pycompat.bytestr(encoding.strtolocal(str(obj)))
456
534
457 def uirepr(s):
535 def uirepr(s):
458 # Avoid double backslash in Windows path repr()
536 # Avoid double backslash in Windows path repr()
459 return pycompat.byterepr(pycompat.bytestr(s)).replace(b'\\\\', b'\\')
537 return pycompat.byterepr(pycompat.bytestr(s)).replace(b'\\\\', b'\\')
460
538
461 # delay import of textwrap
539 # delay import of textwrap
462 def _MBTextWrapper(**kwargs):
540 def _MBTextWrapper(**kwargs):
463 class tw(textwrap.TextWrapper):
541 class tw(textwrap.TextWrapper):
464 """
542 """
465 Extend TextWrapper for width-awareness.
543 Extend TextWrapper for width-awareness.
466
544
467 Neither number of 'bytes' in any encoding nor 'characters' is
545 Neither number of 'bytes' in any encoding nor 'characters' is
468 appropriate to calculate terminal columns for specified string.
546 appropriate to calculate terminal columns for specified string.
469
547
470 Original TextWrapper implementation uses built-in 'len()' directly,
548 Original TextWrapper implementation uses built-in 'len()' directly,
471 so overriding is needed to use width information of each characters.
549 so overriding is needed to use width information of each characters.
472
550
473 In addition, characters classified into 'ambiguous' width are
551 In addition, characters classified into 'ambiguous' width are
474 treated as wide in East Asian area, but as narrow in other.
552 treated as wide in East Asian area, but as narrow in other.
475
553
476 This requires use decision to determine width of such characters.
554 This requires use decision to determine width of such characters.
477 """
555 """
478 def _cutdown(self, ucstr, space_left):
556 def _cutdown(self, ucstr, space_left):
479 l = 0
557 l = 0
480 colwidth = encoding.ucolwidth
558 colwidth = encoding.ucolwidth
481 for i in pycompat.xrange(len(ucstr)):
559 for i in pycompat.xrange(len(ucstr)):
482 l += colwidth(ucstr[i])
560 l += colwidth(ucstr[i])
483 if space_left < l:
561 if space_left < l:
484 return (ucstr[:i], ucstr[i:])
562 return (ucstr[:i], ucstr[i:])
485 return ucstr, ''
563 return ucstr, ''
486
564
487 # overriding of base class
565 # overriding of base class
488 def _handle_long_word(self, reversed_chunks, cur_line, cur_len, width):
566 def _handle_long_word(self, reversed_chunks, cur_line, cur_len, width):
489 space_left = max(width - cur_len, 1)
567 space_left = max(width - cur_len, 1)
490
568
491 if self.break_long_words:
569 if self.break_long_words:
492 cut, res = self._cutdown(reversed_chunks[-1], space_left)
570 cut, res = self._cutdown(reversed_chunks[-1], space_left)
493 cur_line.append(cut)
571 cur_line.append(cut)
494 reversed_chunks[-1] = res
572 reversed_chunks[-1] = res
495 elif not cur_line:
573 elif not cur_line:
496 cur_line.append(reversed_chunks.pop())
574 cur_line.append(reversed_chunks.pop())
497
575
498 # this overriding code is imported from TextWrapper of Python 2.6
576 # this overriding code is imported from TextWrapper of Python 2.6
499 # to calculate columns of string by 'encoding.ucolwidth()'
577 # to calculate columns of string by 'encoding.ucolwidth()'
500 def _wrap_chunks(self, chunks):
578 def _wrap_chunks(self, chunks):
501 colwidth = encoding.ucolwidth
579 colwidth = encoding.ucolwidth
502
580
503 lines = []
581 lines = []
504 if self.width <= 0:
582 if self.width <= 0:
505 raise ValueError("invalid width %r (must be > 0)" % self.width)
583 raise ValueError("invalid width %r (must be > 0)" % self.width)
506
584
507 # Arrange in reverse order so items can be efficiently popped
585 # Arrange in reverse order so items can be efficiently popped
508 # from a stack of chucks.
586 # from a stack of chucks.
509 chunks.reverse()
587 chunks.reverse()
510
588
511 while chunks:
589 while chunks:
512
590
513 # Start the list of chunks that will make up the current line.
591 # Start the list of chunks that will make up the current line.
514 # cur_len is just the length of all the chunks in cur_line.
592 # cur_len is just the length of all the chunks in cur_line.
515 cur_line = []
593 cur_line = []
516 cur_len = 0
594 cur_len = 0
517
595
518 # Figure out which static string will prefix this line.
596 # Figure out which static string will prefix this line.
519 if lines:
597 if lines:
520 indent = self.subsequent_indent
598 indent = self.subsequent_indent
521 else:
599 else:
522 indent = self.initial_indent
600 indent = self.initial_indent
523
601
524 # Maximum width for this line.
602 # Maximum width for this line.
525 width = self.width - len(indent)
603 width = self.width - len(indent)
526
604
527 # First chunk on line is whitespace -- drop it, unless this
605 # First chunk on line is whitespace -- drop it, unless this
528 # is the very beginning of the text (i.e. no lines started yet).
606 # is the very beginning of the text (i.e. no lines started yet).
529 if self.drop_whitespace and chunks[-1].strip() == r'' and lines:
607 if self.drop_whitespace and chunks[-1].strip() == r'' and lines:
530 del chunks[-1]
608 del chunks[-1]
531
609
532 while chunks:
610 while chunks:
533 l = colwidth(chunks[-1])
611 l = colwidth(chunks[-1])
534
612
535 # Can at least squeeze this chunk onto the current line.
613 # Can at least squeeze this chunk onto the current line.
536 if cur_len + l <= width:
614 if cur_len + l <= width:
537 cur_line.append(chunks.pop())
615 cur_line.append(chunks.pop())
538 cur_len += l
616 cur_len += l
539
617
540 # Nope, this line is full.
618 # Nope, this line is full.
541 else:
619 else:
542 break
620 break
543
621
544 # The current line is full, and the next chunk is too big to
622 # The current line is full, and the next chunk is too big to
545 # fit on *any* line (not just this one).
623 # fit on *any* line (not just this one).
546 if chunks and colwidth(chunks[-1]) > width:
624 if chunks and colwidth(chunks[-1]) > width:
547 self._handle_long_word(chunks, cur_line, cur_len, width)
625 self._handle_long_word(chunks, cur_line, cur_len, width)
548
626
549 # If the last chunk on this line is all whitespace, drop it.
627 # If the last chunk on this line is all whitespace, drop it.
550 if (self.drop_whitespace and
628 if (self.drop_whitespace and
551 cur_line and cur_line[-1].strip() == r''):
629 cur_line and cur_line[-1].strip() == r''):
552 del cur_line[-1]
630 del cur_line[-1]
553
631
554 # Convert current line back to a string and store it in list
632 # Convert current line back to a string and store it in list
555 # of all lines (return value).
633 # of all lines (return value).
556 if cur_line:
634 if cur_line:
557 lines.append(indent + r''.join(cur_line))
635 lines.append(indent + r''.join(cur_line))
558
636
559 return lines
637 return lines
560
638
561 global _MBTextWrapper
639 global _MBTextWrapper
562 _MBTextWrapper = tw
640 _MBTextWrapper = tw
563 return tw(**kwargs)
641 return tw(**kwargs)
564
642
565 def wrap(line, width, initindent='', hangindent=''):
643 def wrap(line, width, initindent='', hangindent=''):
566 maxindent = max(len(hangindent), len(initindent))
644 maxindent = max(len(hangindent), len(initindent))
567 if width <= maxindent:
645 if width <= maxindent:
568 # adjust for weird terminal size
646 # adjust for weird terminal size
569 width = max(78, maxindent + 1)
647 width = max(78, maxindent + 1)
570 line = line.decode(pycompat.sysstr(encoding.encoding),
648 line = line.decode(pycompat.sysstr(encoding.encoding),
571 pycompat.sysstr(encoding.encodingmode))
649 pycompat.sysstr(encoding.encodingmode))
572 initindent = initindent.decode(pycompat.sysstr(encoding.encoding),
650 initindent = initindent.decode(pycompat.sysstr(encoding.encoding),
573 pycompat.sysstr(encoding.encodingmode))
651 pycompat.sysstr(encoding.encodingmode))
574 hangindent = hangindent.decode(pycompat.sysstr(encoding.encoding),
652 hangindent = hangindent.decode(pycompat.sysstr(encoding.encoding),
575 pycompat.sysstr(encoding.encodingmode))
653 pycompat.sysstr(encoding.encodingmode))
576 wrapper = _MBTextWrapper(width=width,
654 wrapper = _MBTextWrapper(width=width,
577 initial_indent=initindent,
655 initial_indent=initindent,
578 subsequent_indent=hangindent)
656 subsequent_indent=hangindent)
579 return wrapper.fill(line).encode(pycompat.sysstr(encoding.encoding))
657 return wrapper.fill(line).encode(pycompat.sysstr(encoding.encoding))
580
658
581 _booleans = {'1': True, 'yes': True, 'true': True, 'on': True, 'always': True,
659 _booleans = {'1': True, 'yes': True, 'true': True, 'on': True, 'always': True,
582 '0': False, 'no': False, 'false': False, 'off': False,
660 '0': False, 'no': False, 'false': False, 'off': False,
583 'never': False}
661 'never': False}
584
662
585 def parsebool(s):
663 def parsebool(s):
586 """Parse s into a boolean.
664 """Parse s into a boolean.
587
665
588 If s is not a valid boolean, returns None.
666 If s is not a valid boolean, returns None.
589 """
667 """
590 return _booleans.get(s.lower(), None)
668 return _booleans.get(s.lower(), None)
591
669
592 def evalpythonliteral(s):
670 def evalpythonliteral(s):
593 """Evaluate a string containing a Python literal expression"""
671 """Evaluate a string containing a Python literal expression"""
594 # We could backport our tokenizer hack to rewrite '' to u'' if we want
672 # We could backport our tokenizer hack to rewrite '' to u'' if we want
595 if pycompat.ispy3:
673 if pycompat.ispy3:
596 return ast.literal_eval(s.decode('latin1'))
674 return ast.literal_eval(s.decode('latin1'))
597 return ast.literal_eval(s)
675 return ast.literal_eval(s)
General Comments 0
You need to be logged in to leave comments. Login now