diff --git a/mercurial/templatefuncs.py b/mercurial/templatefuncs.py --- a/mercurial/templatefuncs.py +++ b/mercurial/templatefuncs.py @@ -7,6 +7,7 @@ from __future__ import absolute_import +import collections import re from .i18n import _ @@ -167,6 +168,119 @@ return node return templatefilters.short(node) +# Represents mailmap keys/values +mailmaptup = collections.namedtuple('mailmaptup', ['name', 'email']) + +def parsemailmap(mailmapcontent): + """Parses data in the .mailmap format""" + map = {} + for line in mailmapcontent.splitlines(): + + # Don't bother checking the line if it is a comment or + # is an improperly formed author field + if line.lstrip().startswith('#') or any(c not in line for c in '<>@'): + continue + + # name, email hold the parsed emails and names for each line + # name_builder holds the words in a persons name + name, email = [], [] + namebuilder = [] + + for element in line.split(): + if element.startswith('#'): + # If we reach a comment in the mailmap file, move on + break + + elif element.startswith('<') and element.endswith('>'): + # We have found an email. + # Parse it, and finalize any names from earlier + email.append(element[1:-1]) # Slice off the "<>" + + if namebuilder: + name.append(' '.join(namebuilder)) + namebuilder = [] + + # Break if we have found a second email, any other + # data does not fit the spec for .mailmap + if len(email) > 1: + break + + else: + # We have found another word in the committers name + namebuilder.append(element) + + mailmapkey = mailmaptup( + name=name[-1] if len(name) == 2 else None, + email=email[-1], + ) + + map[mailmapkey] = mailmaptup( + name=name[0] if name else None, + email=email[0], + ) + + return map + +def mapname(map, author): + """Returns the author field according to the mailmap cache, or + the original author field.""" + # If the author field coming in isn't in the correct format, + # just return the original author field + if not util.isauthorwellformed(author): + return author + + # Turn the user name into a mailmaptup + commit = mailmaptup(*(l.strip('> ') for l in author.split('<'))) + + try: + # Try and use both the commit email and name as the key + proper = map[commit] + + except KeyError: + # If the lookup fails, use just the email as the key instead + # We call this commit2 as not to erase original commit fields + commit2 = mailmaptup(name=None, email=commit.email) + proper = map.get(commit2, mailmaptup(None, None)) + + # Return the author field with proper values filled in + return '{name} <{email}>'.format( + name=proper.name if proper.name else commit.name, + email=proper.email if proper.email else commit.email, + ) + +@templatefunc('mailmap(author)') +def mailmap(context, mapping, args, **kwargs): + """Return the author, updated according to the value + set in the mailmap""" + if len(args) != 1: + raise error.ParseError(_("mailmap expects one argument")) + + author = evalfuncarg(context, mapping, args[0]) + + try: + cache = context.resource(mapping, 'cache') + repo = context.resource(mapping, 'repo') + + wctx = repo[None] + wfctx = wctx.filectx('.mailmap') + + if not wfctx.exists(): + return author + + filedate = wfctx.date() + + if 'mailmap' not in cache or cache['mailmap']['date'] < filedate: + cache['mailmap'] = { + 'mapping': parsemailmap(wfctx.data()), + 'date': filedate, + } + + except (error.ManifestLookupError, IOError): + # Return the plain author if no mailmap file is found + return author + + return mapname(cache['mailmap']['mapping'], author) or author + @templatefunc('pad(text, width[, fillchar=\' \'[, left=False]])', argspec='text width fillchar left') def pad(context, mapping, args): diff --git a/tests/test-mailmap.t b/tests/test-mailmap.t new file mode 100644 --- /dev/null +++ b/tests/test-mailmap.t @@ -0,0 +1,67 @@ +Create a repo and add some commits + + $ hg init mm + $ cd mm + $ echo "Test content" > testfile1 + $ hg add testfile1 + $ HGUSER="Proper " hg commit -m "First commit" + $ echo "Test content 2" > testfile2 + $ hg add testfile2 + $ HGUSER="Commit Name 2 " hg commit -m "Second commit" + $ echo "Test content 3" > testfile3 + $ hg add testfile3 + $ HGUSER="Commit Name 3 " hg commit -m "Third commit" + $ echo "Test content 4" > testfile4 + $ hg add testfile4 + $ HGUSER="Commit Name 4 " hg commit -m "Fourth commit" + +Add a .mailmap file with each possible entry type plus comments + $ cat > .mailmap << EOF + > # Comment shouldn't break anything + > # Should update email only + > Proper Name 2 # Should update name only + > Proper Name 3 # Should update name, email due to email + > Proper Name 4 Commit Name 4 # Should update name, email due to name, email + > EOF + $ hg add .mailmap + $ HGUSER="Testuser " hg commit -m "Add mailmap file" + +Output of commits should be normal without filter + $ hg log -T "{author}\n" -r "all()" + Proper + Commit Name 2 + Commit Name 3 + Commit Name 4 + Testuser + +Output of commits with filter shows their mailmap values + $ hg log -T "{mailmap(author)}\n" -r "all()" + Proper + Proper Name 2 + Proper Name 3 + Proper Name 4 + Testuser + +Add new mailmap entry for testuser + $ cat >> .mailmap << EOF + > + > EOF + +Output of commits with filter shows their updated mailmap values + $ hg log -T "{mailmap(author)}\n" -r "all()" + Proper + Proper Name 2 + Proper Name 3 + Proper Name 4 + Testuser + +A commit with improperly formatted user field should not break the filter + $ echo "some more test content" > testfile1 + $ HGUSER="Improper user" hg commit -m "Commit with improper user field" + $ hg log -T "{mailmap(author)}\n" -r "all()" + Proper + Proper Name 2 + Proper Name 3 + Proper Name 4 + Testuser + Improper user