2
0
Эх сурвалжийг харах

Add basic support for reading mailmap files.

Jelmer Vernooij 7 жил өмнө
parent
commit
8ce88629a9

+ 5 - 0
NEWS

@@ -1,5 +1,10 @@
 0.19.1	UNRELEASED
 
+ IMPROVEMENTS
+
+  * Add 'dulwich.mailmap' file for reading mailmap files.
+    (Jelmer Vernooij)
+
 0.19.0	2018-03-10
 
  BUG FIXES

+ 111 - 0
dulwich/mailmap.py

@@ -0,0 +1,111 @@
+# mailmap.py -- Mailmap reader
+# Copyright (C) 2018 Jelmer Vernooij <jelmer@samba.org>
+#
+# Dulwich is dual-licensed under the Apache License, Version 2.0 and the GNU
+# General Public License as public by the Free Software Foundation; version 2.0
+# or (at your option) any later version. You can redistribute it and/or
+# modify it under the terms of either of these two licenses.
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+# You should have received a copy of the licenses; if not, see
+# <http://www.gnu.org/licenses/> for a copy of the GNU General Public License
+# and <http://www.apache.org/licenses/LICENSE-2.0> for a copy of the Apache
+# License, Version 2.0.
+#
+
+"""Mailmap file reader."""
+
+
+def parse_identity(text):
+    # TODO(jelmer): Integrate this with dulwich.fastexport.split_email and
+    # dulwich.repo.check_user_identity
+    (name, email) = text.rsplit(b"<", 1)
+    name = name.strip()
+    email = email.rstrip(b">").strip()
+    if not name:
+        name = None
+    if not email:
+        email = None
+    return (name, email)
+
+
+def read_mailmap(f):
+    """Read a mailmap.
+
+    :param f: File-like object to read from
+    :return: Iterator over
+        ((canonical_name, canonical_email), (from_name, from_email)) tuples
+    """
+    for line in f:
+        # Remove comments
+        line = line.split('#')[0]
+        line = line.strip()
+        if not line:
+            continue
+        (canonical_identity, from_identity) = line.split('>', 1)
+        canonical_identity += ">"
+        if from_identity.strip():
+            parsed_from_identity = parse_identity(from_identity)
+        else:
+            parsed_from_identity = None
+        parsed_canonical_identity = parse_identity(canonical_identity)
+        yield parsed_canonical_identity, parsed_from_identity
+
+
+class Mailmap(object):
+    """Class for accessing a mailmap file."""
+
+    def __init__(self, map=None):
+        self._table = {}
+        if map:
+            for (canonical_identity, from_identity) in map:
+                self.add_entry(canonical_identity, from_identity)
+
+    def add_entry(self, canonical_identity, from_identity=None):
+        """Add an entry to the mail mail.
+
+        Any of the fields can be None, but at least one of them needs to be
+        set.
+
+        :param canonical_identity: The canonical identity (tuple)
+        :param from_identity: The from identity (tuple)
+        """
+        if from_identity is None:
+            from_name, from_email = None, None
+        else:
+            (from_name, from_email) = from_identity
+        (canonical_name, canonical_email) = canonical_identity
+        if from_name is None and from_email is None:
+            self._table[canonical_name, None] = canonical_identity
+            self._table[None, canonical_email] = canonical_identity
+        else:
+            self._table[from_name, from_email] = canonical_identity
+
+    def lookup(self, identity):
+        """Lookup an identity in this mailmail."""
+        if not isinstance(identity, tuple):
+            was_tuple = False
+            identity = parse_identity(identity)
+        else:
+            was_tuple = True
+        for query in [identity, (None, identity[1]), (identity[0], None)]:
+            canonical_identity = self._table.get(query)
+            if canonical_identity is not None:
+                identity = (
+                        canonical_identity[0] or identity[0],
+                        canonical_identity[1] or identity[1])
+                break
+        if was_tuple:
+            return identity
+        else:
+            return "%s <%s>" % identity
+
+    @classmethod
+    def from_path(cls, path):
+        with open(path, 'r') as f:
+            return cls(read_mailmap(f))

+ 19 - 0
dulwich/porcelain.py

@@ -1220,3 +1220,22 @@ def update_head(repo, target, detached=False, new_branch=None):
             r.refs.set_symbolic_ref(to_set, parse_ref(r, target))
         if new_branch is not None:
             r.refs.set_symbolic_ref(b"HEAD", to_set)
+
+
+def check_mailmap(repo, contact):
+    """Check canonical name and email of contact.
+
+    :param repo: Path to the repository
+    :param contact: Contact name and/or email
+    :return: Canonical contact data
+    """
+    with open_repo_closing(repo) as r:
+        from dulwich.mailmap import Mailmap
+        import errno
+        try:
+            mailmap = Mailmap.from_path(os.path.join(r.path, '.mailmap'))
+        except IOError as e:
+            if e.errno != errno.ENOENT:
+                raise
+            mailmap = Mailmap()
+        return mailmap.lookup(contact)

+ 1 - 0
dulwich/tests/__init__.py

@@ -111,6 +111,7 @@ def self_test_suite():
         'ignore',
         'index',
         'lru_cache',
+        'mailmap',
         'objects',
         'objectspec',
         'object_store',

+ 90 - 0
dulwich/tests/test_mailmap.py

@@ -0,0 +1,90 @@
+# test_mailmap.py -- Tests for dulwich.mailmap
+# Copyright (C) 2018 Jelmer Vernooij <jelmer@jelmer.uk>
+#
+# Dulwich is dual-licensed under the Apache License, Version 2.0 and the GNU
+# General Public License as public by the Free Software Foundation; version 2.0
+# or (at your option) any later version. You can redistribute it and/or
+# modify it under the terms of either of these two licenses.
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+# You should have received a copy of the licenses; if not, see
+# <http://www.gnu.org/licenses/> for a copy of the GNU General Public License
+# and <http://www.apache.org/licenses/LICENSE-2.0> for a copy of the Apache
+# License, Version 2.0.
+#
+
+"""Tests for dulwich.mailmap."""
+
+from io import BytesIO
+
+from unittest import TestCase
+
+from dulwich.mailmap import Mailmap, read_mailmap
+
+
+class ReadMailmapTests(TestCase):
+
+    def test_read(self):
+        b = BytesIO("""\
+Jane Doe         <jane@desktop.(none)>
+Joe R. Developer <joe@example.com>
+# A comment
+<cto@company.xx>                       <cto@coompany.xx> # Comment
+Some Dude <some@dude.xx>         nick1 <bugs@company.xx>
+Other Author <other@author.xx>   nick2 <bugs@company.xx>
+Other Author <other@author.xx>         <nick2@company.xx>
+Santa Claus <santa.claus@northpole.xx> <me@company.xx>
+""")
+        self.assertEqual([
+            (('Jane Doe', 'jane@desktop.(none)'), None),
+            (('Joe R. Developer', 'joe@example.com'), None),
+            ((None, 'cto@company.xx'), (None, 'cto@coompany.xx')),
+            (('Some Dude', 'some@dude.xx'), ('nick1', 'bugs@company.xx')),
+            (('Other Author', 'other@author.xx'),
+                ('nick2', 'bugs@company.xx')),
+            (('Other Author', 'other@author.xx'),
+                (None, 'nick2@company.xx')),
+            (('Santa Claus', 'santa.claus@northpole.xx'),
+                (None, 'me@company.xx'))],
+            list(read_mailmap(b)))
+
+
+class MailmapTests(TestCase):
+
+    def test_lookup(self):
+        m = Mailmap()
+        m.add_entry(('Jane Doe', 'jane@desktop.(none)'), (None, None))
+        m.add_entry(('Joe R. Developer', 'joe@example.com'), None)
+        m.add_entry((None, 'cto@company.xx'), (None, 'cto@coompany.xx'))
+        m.add_entry(
+                ('Some Dude', 'some@dude.xx'),
+                ('nick1', 'bugs@company.xx'))
+        m.add_entry(
+                ('Other Author', 'other@author.xx'),
+                ('nick2', 'bugs@company.xx'))
+        m.add_entry(
+                ('Other Author', 'other@author.xx'),
+                (None, 'nick2@company.xx'))
+        m.add_entry(
+                ('Santa Claus', 'santa.claus@northpole.xx'),
+                (None, 'me@company.xx'))
+        self.assertEqual(
+            'Jane Doe <jane@desktop.(none)>',
+            m.lookup('Jane Doe <jane@desktop.(none)>'))
+        self.assertEqual(
+            'Jane Doe <jane@desktop.(none)>',
+            m.lookup('Jane Doe <jane@example.com>'))
+        self.assertEqual(
+            'Jane Doe <jane@desktop.(none)>',
+            m.lookup('Jane D. <jane@desktop.(none)>'))
+        self.assertEqual(
+            'Some Dude <some@dude.xx>',
+            m.lookup('nick1 <bugs@company.xx>'))
+        self.assertEqual(
+            'CTO <cto@company.xx>',
+            m.lookup('CTO <cto@coompany.xx>'))

+ 19 - 0
dulwich/tests/test_porcelain.py

@@ -1273,3 +1273,22 @@ class UpdateHeadTests(PorcelainTestCase):
         self.assertEqual(c1.id, self.repo.head())
         self.assertEqual(b'ref: refs/heads/bar',
                          self.repo.refs.read_ref(b'HEAD'))
+
+
+class MailmapTests(PorcelainTestCase):
+
+    def test_no_mailmap(self):
+        self.assertEqual(
+            'Jelmer Vernooij <jelmer@samba.org>',
+            porcelain.check_mailmap(
+                self.repo, 'Jelmer Vernooij <jelmer@samba.org>'))
+
+    def test_mailmap_lookup(self):
+        with open(os.path.join(self.repo.path, '.mailmap'), 'w') as f:
+            f.write("""\
+Jelmer Vernooij <jelmer@debian.org>
+""")
+        self.assertEqual(
+            'Jelmer Vernooij <jelmer@debian.org>',
+            porcelain.check_mailmap(
+                self.repo, 'Jelmer Vernooij <jelmer@samba.org>'))