Mercurial > public > mercurial-scm > hg-stable
comparison mercurial/utils/stringutil.py @ 37212:2a2ce93e12f4
templatefuncs: add mailmap template function
This commit adds a template function to support the .mailmap file
in Mercurial repositories. The .mailmap file comes from git, and
can be used to map new emails and names for old commits. The general
use case is that someone may change their name or author commits
under different emails and aliases, which would make these
commits appear as though they came from different persons. The
file allows you to specify the correct name that should be used
in place of the author field specified in the commit.
The mailmap file has 4 possible formats used to map old "commit"
names to new "proper" names:
1. <proper@email.com> <commit@email.com>
2. Proper Name <commit@email.com>
3. Proper Name <proper@email.com> <commit@email.com>
4. Proper Name <proper@email.com> Commit Name <commit@email.com>
Essentially there is a commit email present in each mailmap entry,
that maps to either an updated name, email, or both. The final
possible format allows commits authored by a person who used
both an old name and an old email to map to a new name and email.
To parse the file, we split by spaces and build a name out
of every element that does not start with "<". Once we find an element
that does start with "<" we concatenate all the name elements that preceded
and add that as a parsed name. We then add the email as the first
parsed email. We repeat the process until the end of the line, or
a comment is found. We will be left with all parsed names in a list,
and all parsed emails in a list, with the 0 index being the proper
values and the 1 index being the commit values (if they were specified
in the entry).
The commit values are added as the keys to a dict, and with the proper
fields as the values. The mapname function takes the mapping object and
the commit author field and attempts to look for a corresponding entry.
To do so we try (commit name, commit email) first, and if no results are
returned then (None, commit email) is also looked up. This is due to
format 4 from above, where someone may have a mailmap entry with both
name and email, and if they don't it is possible they have an entry that
uses only the commit email.
Differential Revision: https://phab.mercurial-scm.org/D2904
author | Connor Sheehan <sheehan@mozilla.com> |
---|---|
date | Mon, 19 Mar 2018 11:16:21 -0400 |
parents | fb7140f1d09d |
children | 54b896f195d1 |
comparison
equal
deleted
inserted
replaced
37211:2208149c4b8e | 37212:2a2ce93e12f4 |
---|---|
12 import codecs | 12 import codecs |
13 import re as remod | 13 import re as remod |
14 import textwrap | 14 import textwrap |
15 | 15 |
16 from ..i18n import _ | 16 from ..i18n import _ |
17 from ..thirdparty import attr | |
17 | 18 |
18 from .. import ( | 19 from .. import ( |
19 encoding, | 20 encoding, |
20 error, | 21 error, |
21 pycompat, | 22 pycompat, |
155 f = author.find('<') | 156 f = author.find('<') |
156 if f != -1: | 157 if f != -1: |
157 return author[:f].strip(' "').replace('\\"', '"') | 158 return author[:f].strip(' "').replace('\\"', '"') |
158 f = author.find('@') | 159 f = author.find('@') |
159 return author[:f].replace('.', ' ') | 160 return author[:f].replace('.', ' ') |
161 | |
162 @attr.s(hash=True) | |
163 class mailmapping(object): | |
164 '''Represents a username/email key or value in | |
165 a mailmap file''' | |
166 email = attr.ib() | |
167 name = attr.ib(default=None) | |
168 | |
169 def parsemailmap(mailmapcontent): | |
170 """Parses data in the .mailmap format | |
171 | |
172 >>> mmdata = b"\\n".join([ | |
173 ... b'# Comment', | |
174 ... b'Name <commit1@email.xx>', | |
175 ... b'<name@email.xx> <commit2@email.xx>', | |
176 ... b'Name <proper@email.xx> <commit3@email.xx>', | |
177 ... b'Name <proper@email.xx> Commit <commit4@email.xx>', | |
178 ... ]) | |
179 >>> mm = parsemailmap(mmdata) | |
180 >>> for key in sorted(mm.keys()): | |
181 ... print(key) | |
182 mailmapping(email='commit1@email.xx', name=None) | |
183 mailmapping(email='commit2@email.xx', name=None) | |
184 mailmapping(email='commit3@email.xx', name=None) | |
185 mailmapping(email='commit4@email.xx', name='Commit') | |
186 >>> for val in sorted(mm.values()): | |
187 ... print(val) | |
188 mailmapping(email='commit1@email.xx', name='Name') | |
189 mailmapping(email='name@email.xx', name=None) | |
190 mailmapping(email='proper@email.xx', name='Name') | |
191 mailmapping(email='proper@email.xx', name='Name') | |
192 """ | |
193 mailmap = {} | |
194 | |
195 if mailmapcontent is None: | |
196 return mailmap | |
197 | |
198 for line in mailmapcontent.splitlines(): | |
199 | |
200 # Don't bother checking the line if it is a comment or | |
201 # is an improperly formed author field | |
202 if line.lstrip().startswith('#') or any(c not in line for c in '<>@'): | |
203 continue | |
204 | |
205 # name, email hold the parsed emails and names for each line | |
206 # name_builder holds the words in a persons name | |
207 name, email = [], [] | |
208 namebuilder = [] | |
209 | |
210 for element in line.split(): | |
211 if element.startswith('#'): | |
212 # If we reach a comment in the mailmap file, move on | |
213 break | |
214 | |
215 elif element.startswith('<') and element.endswith('>'): | |
216 # We have found an email. | |
217 # Parse it, and finalize any names from earlier | |
218 email.append(element[1:-1]) # Slice off the "<>" | |
219 | |
220 if namebuilder: | |
221 name.append(' '.join(namebuilder)) | |
222 namebuilder = [] | |
223 | |
224 # Break if we have found a second email, any other | |
225 # data does not fit the spec for .mailmap | |
226 if len(email) > 1: | |
227 break | |
228 | |
229 else: | |
230 # We have found another word in the committers name | |
231 namebuilder.append(element) | |
232 | |
233 mailmapkey = mailmapping( | |
234 email=email[-1], | |
235 name=name[-1] if len(name) == 2 else None, | |
236 ) | |
237 | |
238 mailmap[mailmapkey] = mailmapping( | |
239 email=email[0], | |
240 name=name[0] if name else None, | |
241 ) | |
242 | |
243 return mailmap | |
244 | |
245 def mapname(mailmap, author): | |
246 """Returns the author field according to the mailmap cache, or | |
247 the original author field. | |
248 | |
249 >>> mmdata = b"\\n".join([ | |
250 ... b'# Comment', | |
251 ... b'Name <commit1@email.xx>', | |
252 ... b'<name@email.xx> <commit2@email.xx>', | |
253 ... b'Name <proper@email.xx> <commit3@email.xx>', | |
254 ... b'Name <proper@email.xx> Commit <commit4@email.xx>', | |
255 ... ]) | |
256 >>> m = parsemailmap(mmdata) | |
257 >>> mapname(m, b'Commit <commit1@email.xx>') | |
258 'Name <commit1@email.xx>' | |
259 >>> mapname(m, b'Name <commit2@email.xx>') | |
260 'Name <name@email.xx>' | |
261 >>> mapname(m, b'Commit <commit3@email.xx>') | |
262 'Name <proper@email.xx>' | |
263 >>> mapname(m, b'Commit <commit4@email.xx>') | |
264 'Name <proper@email.xx>' | |
265 >>> mapname(m, b'Unknown Name <unknown@email.com>') | |
266 'Unknown Name <unknown@email.com>' | |
267 """ | |
268 # If the author field coming in isn't in the correct format, | |
269 # or the mailmap is empty just return the original author field | |
270 if not isauthorwellformed(author) or not mailmap: | |
271 return author | |
272 | |
273 # Turn the user name into a mailmaptup | |
274 commit = mailmapping(name=person(author), email=email(author)) | |
275 | |
276 try: | |
277 # Try and use both the commit email and name as the key | |
278 proper = mailmap[commit] | |
279 | |
280 except KeyError: | |
281 # If the lookup fails, use just the email as the key instead | |
282 # We call this commit2 as not to erase original commit fields | |
283 commit2 = mailmapping(email=commit.email) | |
284 proper = mailmap.get(commit2, mailmapping(None, None)) | |
285 | |
286 # Return the author field with proper values filled in | |
287 return '%s <%s>' % ( | |
288 proper.name if proper.name else commit.name, | |
289 proper.email if proper.email else commit.email, | |
290 ) | |
160 | 291 |
161 _correctauthorformat = remod.compile(br'^[^<]+\s\<[^<>]+@[^<>]+\>$') | 292 _correctauthorformat = remod.compile(br'^[^<]+\s\<[^<>]+@[^<>]+\>$') |
162 | 293 |
163 def isauthorwellformed(author): | 294 def isauthorwellformed(author): |
164 '''Return True if the author field is well formed | 295 '''Return True if the author field is well formed |