D2904: templatefuncs: add mailmap template function

sheehan (Connor Sheehan) phabricator at mercurial-scm.org
Fri Mar 30 11:11:25 EDT 2018


sheehan updated this revision to Diff 7381.

REPOSITORY
  rHG Mercurial

CHANGES SINCE LAST UPDATE
  https://phab.mercurial-scm.org/D2904?vs=7361&id=7381

REVISION DETAIL
  https://phab.mercurial-scm.org/D2904

AFFECTED FILES
  mercurial/templatefuncs.py
  mercurial/utils/stringutil.py
  tests/test-mailmap.t

CHANGE DETAILS

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
+  $ hg commit -m "First commit" -u "Proper <commit at m.c>"
+  $ echo "Test content 2" > testfile2
+  $ hg add testfile2
+  $ hg commit -m "Second commit" -u "Commit Name 2 <commit2 at m.c>"
+  $ echo "Test content 3" > testfile3
+  $ hg add testfile3
+  $ hg commit -m "Third commit" -u "Commit Name 3 <commit3 at m.c>"
+  $ echo "Test content 4" > testfile4
+  $ hg add testfile4
+  $ hg commit -m "Fourth commit" -u "Commit Name 4 <commit4 at m.c>"
+
+Add a .mailmap file with each possible entry type plus comments
+  $ cat > .mailmap << EOF
+  > # Comment shouldn't break anything
+  > <proper at m.c> <commit at m.c> # Should update email only
+  > Proper Name 2 <commit2 at m.c> # Should update name only
+  > Proper Name 3 <proper at m.c> <commit3 at m.c> # Should update name, email due to email
+  > Proper Name 4 <proper at m.c> Commit Name 4 <commit4 at m.c> # Should update name, email due to name, email
+  > EOF
+  $ hg add .mailmap
+  $ hg commit -m "Add mailmap file" -u "Testuser <test123 at m.c>"
+
+Output of commits should be normal without filter
+  $ hg log -T "{author}\n" -r "all()"
+  Proper <commit at m.c>
+  Commit Name 2 <commit2 at m.c>
+  Commit Name 3 <commit3 at m.c>
+  Commit Name 4 <commit4 at m.c>
+  Testuser <test123 at m.c>
+
+Output of commits with filter shows their mailmap values
+  $ hg log -T "{mailmap(author)}\n" -r "all()"
+  Proper <proper at m.c>
+  Proper Name 2 <commit2 at m.c>
+  Proper Name 3 <proper at m.c>
+  Proper Name 4 <proper at m.c>
+  Testuser <test123 at m.c>
+
+Add new mailmap entry for testuser
+  $ cat >> .mailmap << EOF
+  > <newmmentry at m.c> <test123 at m.c>
+  > EOF
+
+Output of commits with filter shows their updated mailmap values
+  $ hg log -T "{mailmap(author)}\n" -r "all()"
+  Proper <proper at m.c>
+  Proper Name 2 <commit2 at m.c>
+  Proper Name 3 <proper at m.c>
+  Proper Name 4 <proper at m.c>
+  Testuser <newmmentry at m.c>
+
+A commit with improperly formatted user field should not break the filter
+  $ echo "some more test content" > testfile1
+  $ hg commit -m "Commit with improper user field" -u "Improper user"
+  $ hg log -T "{mailmap(author)}\n" -r "all()"
+  Proper <proper at m.c>
+  Proper Name 2 <commit2 at m.c>
+  Proper Name 3 <proper at m.c>
+  Proper Name 4 <proper at m.c>
+  Testuser <newmmentry at m.c>
+  Improper user
diff --git a/mercurial/utils/stringutil.py b/mercurial/utils/stringutil.py
--- a/mercurial/utils/stringutil.py
+++ b/mercurial/utils/stringutil.py
@@ -14,6 +14,7 @@
 import textwrap
 
 from ..i18n import _
+from ..thirdparty import attr
 
 from .. import (
     encoding,
@@ -335,3 +336,133 @@
         return author[:f].strip(' "').replace('\\"', '"')
     f = author.find('@')
     return author[:f].replace('.', ' ')
+
+ at attr.s(hash=True)
+class mailmapping(object):
+    '''Represents a username/email key or value in
+    a mailmap file'''
+    email = attr.ib()
+    name = attr.ib(default=None)
+
+def parsemailmap(mailmapcontent):
+    """Parses data in the .mailmap format
+
+    >>> mmdata = b"\\n".join([
+    ... b'# Comment',
+    ... b'Name <commit1 at email.xx>',
+    ... b'<name at email.xx> <commit2 at email.xx>',
+    ... b'Name <proper at email.xx> <commit3 at email.xx>',
+    ... b'Name <proper at email.xx> Commit <commit4 at email.xx>',
+    ... ])
+    >>> mm = parsemailmap(mmdata)
+    >>> for key in sorted(mm.keys()):
+    ...     print(key)
+    mailmapping(email='commit1 at email.xx', name=None)
+    mailmapping(email='commit2 at email.xx', name=None)
+    mailmapping(email='commit3 at email.xx', name=None)
+    mailmapping(email='commit4 at email.xx', name='Commit')
+    >>> for val in sorted(mm.values()):
+    ...     print(val)
+    mailmapping(email='commit1 at email.xx', name='Name')
+    mailmapping(email='name at email.xx', name=None)
+    mailmapping(email='proper at email.xx', name='Name')
+    mailmapping(email='proper at email.xx', name='Name')
+    """
+    mailmap = {}
+
+    if mailmapcontent is None:
+        return mailmap
+
+    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 = mailmapping(
+            email=email[-1],
+            name=name[-1] if len(name) == 2 else None,
+        )
+
+        mailmap[mailmapkey] = mailmapping(
+            email=email[0],
+            name=name[0] if name else None,
+        )
+
+    return mailmap
+
+def mapname(mailmap, author):
+    """Returns the author field according to the mailmap cache, or
+    the original author field.
+
+    >>> mmdata = b"\\n".join([
+    ...     b'# Comment',
+    ...     b'Name <commit1 at email.xx>',
+    ...     b'<name at email.xx> <commit2 at email.xx>',
+    ...     b'Name <proper at email.xx> <commit3 at email.xx>',
+    ...     b'Name <proper at email.xx> Commit <commit4 at email.xx>',
+    ... ])
+    >>> m = parsemailmap(mmdata)
+    >>> mapname(m, b'Commit <commit1 at email.xx>')
+    'Name <commit1 at email.xx>'
+    >>> mapname(m, b'Name <commit2 at email.xx>')
+    'Name <name at email.xx>'
+    >>> mapname(m, b'Commit <commit3 at email.xx>')
+    'Name <proper at email.xx>'
+    >>> mapname(m, b'Commit <commit4 at email.xx>')
+    'Name <proper at email.xx>'
+    >>> mapname(m, b'Unknown Name <unknown at email.com>')
+    'Unknown Name <unknown at email.com>'
+    """
+    # If the author field coming in isn't in the correct format,
+    # or the mailmap is empty just return the original author field
+    if not isauthorwellformed(author) or not mailmap:
+        return author
+
+    # Turn the user name into a mailmaptup
+    commit = mailmapping(name=person(author), email=email(author))
+
+    try:
+        # Try and use both the commit email and name as the key
+        proper = mailmap[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 = mailmapping(email=commit.email)
+        proper = mailmap.get(commit2, mailmapping(None, None))
+
+    # Return the author field with proper values filled in
+    return '%s <%s>' % (
+        proper.name if proper.name else commit.name,
+        proper.email if proper.email else commit.email,
+    )
diff --git a/mercurial/templatefuncs.py b/mercurial/templatefuncs.py
--- a/mercurial/templatefuncs.py
+++ b/mercurial/templatefuncs.py
@@ -26,7 +26,10 @@
     templateutil,
     util,
 )
-from .utils import dateutil
+from .utils import (
+    dateutil,
+    stringutil,
+)
 
 evalrawexp = templateutil.evalrawexp
 evalfuncarg = templateutil.evalfuncarg
@@ -167,6 +170,24 @@
         return node
     return templatefilters.short(node)
 
+ at templatefunc('mailmap(author)')
+def mailmap(context, mapping, args):
+    """Return the author, updated according to the value
+    set in the .mailmap file"""
+    if len(args) != 1:
+        raise error.ParseError(_("mailmap expects one argument"))
+
+    author = evalfuncarg(context, mapping, args[0])
+
+    cache = context.resource(mapping, 'cache')
+    repo = context.resource(mapping, 'repo')
+
+    if 'mailmap' not in cache:
+        data = repo.wvfs.tryread('.mailmap')
+        cache['mailmap'] = stringutil.parsemailmap(data)
+
+    return stringutil.mapname(cache['mailmap'], author) or author
+
 @templatefunc('pad(text, width[, fillchar=\' \'[, left=False]])',
               argspec='text width fillchar left')
 def pad(context, mapping, args):



To: sheehan, #hg-reviewers, yuja
Cc: yuja, mercurial-devel


More information about the Mercurial-devel mailing list