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