# mailmap.py -- Mailmap reader # Copyright (C) 2018 Jelmer Vernooij # # SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later # Dulwich is dual-licensed under the Apache License, Version 2.0 and the GNU # General Public License as published 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 # for a copy of the GNU General Public License # and for a copy of the Apache # License, Version 2.0. # """Mailmap file reader.""" __all__ = [ "Mailmap", "parse_identity", "read_mailmap", ] from collections.abc import Iterator from typing import IO def parse_identity(text: bytes) -> tuple[bytes | None, bytes | None]: """Parse an identity string into name and email. Args: text: Identity string in format "Name " Returns: Tuple of (name, email) where either can be None """ # TODO(jelmer): Integrate this with dulwich.fastexport.split_email and # dulwich.repo.check_user_identity (name_str, email_str) = text.rsplit(b"<", 1) name_str = name_str.strip() email_str = email_str.rstrip(b">").strip() name: bytes | None = name_str if name_str else None email: bytes | None = email_str if email_str else None return (name, email) def read_mailmap( f: IO[bytes], ) -> Iterator[ tuple[ tuple[bytes | None, bytes | None], tuple[bytes | None, bytes | None] | None, ] ]: """Read a mailmap. Args: f: File-like object to read from Returns: Iterator over ((canonical_name, canonical_email), (from_name, from_email)) tuples """ for line in f: # Remove comments line = line.split(b"#")[0] line = line.strip() if not line: continue (canonical_identity, from_identity) = line.split(b">", 1) canonical_identity += b">" 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: """Class for accessing a mailmap file.""" def __init__( self, map: Iterator[ tuple[ tuple[bytes | None, bytes | None], tuple[bytes | None, bytes | None] | None, ] ] | None = None, ) -> None: """Initialize Mailmap. Args: map: Optional iterator of (canonical_identity, from_identity) tuples """ self._table: dict[ tuple[bytes | None, bytes | None], tuple[bytes | None, bytes | None], ] = {} if map: for canonical_identity, from_identity in map: self.add_entry(canonical_identity, from_identity) def add_entry( self, canonical_identity: tuple[bytes | None, bytes | None], from_identity: tuple[bytes | None, bytes | None] | None = None, ) -> 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. Args: canonical_identity: The canonical identity (tuple) 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: bytes | tuple[bytes | None, bytes | None] ) -> bytes | tuple[bytes | None, bytes | None]: """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: name, email = identity if name is None: name = b"" if email is None: email = b"" return name + b" <" + email + b">" @classmethod def from_path(cls, path: str) -> "Mailmap": """Create Mailmap from file path. Args: path: Path to mailmap file Returns: Mailmap instance """ with open(path, "rb") as f: return cls(read_mailmap(f))