DPDK patches and discussions
 help / color / mirror / Atom feed
From: Bruce Richardson <bruce.richardson@intel.com>
To: dev@dpdk.org
Cc: marat.khalili@huawei.com, Bruce Richardson <bruce.richardson@intel.com>
Subject: [PATCH v2 1/2] devtools/mailmap_ctl: script to work with mailmap
Date: Fri,  8 Aug 2025 22:08:33 +0100	[thread overview]
Message-ID: <20250808210837.518507-2-bruce.richardson@intel.com> (raw)
In-Reply-To: <20250808210837.518507-1-bruce.richardson@intel.com>

Add a script to easily add entries to, check and sort the mailmap file.

Signed-off-by: Bruce Richardson <bruce.richardson@intel.com>
---
 devtools/mailmap_ctl.py | 212 ++++++++++++++++++++++++++++++++++++++++
 1 file changed, 212 insertions(+)
 create mode 100755 devtools/mailmap_ctl.py

diff --git a/devtools/mailmap_ctl.py b/devtools/mailmap_ctl.py
new file mode 100755
index 0000000000..15548c54cd
--- /dev/null
+++ b/devtools/mailmap_ctl.py
@@ -0,0 +1,212 @@
+#!/usr/bin/env python3
+# SPDX-License-Identifier: BSD-3-Clause
+# Copyright(c) 2025 Intel Corporation
+
+"""
+A tool for manipulating the .mailmap file in DPDK repository.
+
+This script supports three operations:
+- add: adds a new entry to the mailmap file in the correct position
+- check: validates mailmap entries are sorted and correctly formatted
+- sort: sorts the mailmap entries alphabetically by name
+"""
+
+import sys
+import re
+import argparse
+import itertools
+import unicodedata
+from pathlib import Path
+from dataclasses import dataclass
+from typing import List, Optional
+
+
+@dataclass
+class MailmapEntry:
+    """Represents a single mailmap entry."""
+
+    name: str
+    name_for_sorting: str
+    email1: str
+    email2: Optional[str]
+    line_number: int
+
+    def __str__(self) -> str:
+        """Format the entry back to mailmap string format."""
+        return f"{self.name} <{self.email1}>" + (f" <{self.email2}>" if self.email2 else "")
+
+    @staticmethod
+    def _get_name_for_sorting(name) -> str:
+        """Normalize a name for sorting purposes."""
+        # Remove accents/diacritics. Separate accented chars into two - so accent is separate,
+        # then remove the accent.
+        normalized = unicodedata.normalize("NFD", name)
+        normalized = "".join(c for c in normalized if unicodedata.category(c) != "Mn")
+
+        return normalized.lower()
+
+    @classmethod
+    def parse(cls, line: str, line_number: int = 0):
+        """
+        Parse a mailmap line and create a MailmapEntry instance.
+
+        Valid formats:
+        - Name <email>
+        - Name <primary_email> <secondary_email>
+        """
+        # Pattern to match mailmap entries
+        # Group 1: Name, Group 2: first email, Group 3: optional second email
+        pattern = r"^([^<]+?)\s*<([^>]+)>(?:\s*<([^>]+)>)?$"
+        match = re.match(pattern, line.strip())
+        if not match:
+            raise argparse.ArgumentTypeError(f"Invalid entry format: '{line}'")
+
+        name = match.group(1).strip()
+        primary_email = match.group(2).strip()
+        secondary_email = match.group(3).strip() if match.group(3) else None
+
+        return cls(
+            name=name,
+            name_for_sorting=cls._get_name_for_sorting(name),
+            email1=primary_email,
+            email2=secondary_email,
+            line_number=line_number,
+        )
+
+
+def read_and_parse_mailmap(mailmap_path: Path, fail_on_err: bool) -> List[MailmapEntry]:
+    """Read and parse a mailmap file, returning entries."""
+    try:
+        with open(mailmap_path, "r", encoding="utf-8") as f:
+            lines = f.readlines()
+    except IOError as e:
+        print(f"Error reading {mailmap_path}: {e}", file=sys.stderr)
+        sys.exit(1)
+
+    entries = []
+    for line_num, line in enumerate(lines, 1):
+        stripped_line = line.strip()
+
+        # Skip empty lines and comments
+        if not stripped_line or stripped_line.startswith("#"):
+            continue
+
+        try:
+            entry = MailmapEntry.parse(stripped_line, line_num)
+        except argparse.ArgumentTypeError as e:
+            print(f"Line {line_num}: {e}", file=sys.stderr)
+            if fail_on_err:
+                sys.exit(1)
+            continue
+
+        entries.append(entry)
+    return entries
+
+
+def write_entries_to_file(mailmap_path: Path, entries: List[MailmapEntry]):
+    """Write entries to mailmap file."""
+    try:
+        with open(mailmap_path, "w", encoding="utf-8") as f:
+            for entry in entries:
+                f.write(str(entry) + "\n")
+    except IOError as e:
+        print(f"Error writing {mailmap_path}: {e}", file=sys.stderr)
+        sys.exit(1)
+
+
+def check_mailmap(mailmap_path, _):
+    """Check that mailmap entries are correctly sorted and formatted."""
+    entries = read_and_parse_mailmap(mailmap_path, False)
+
+    errors = 0
+    for e1, e2 in itertools.pairwise(entries):
+        if e1.name_for_sorting > e2.name_for_sorting:
+            print(
+                f"Line {e2.line_number}: '{e2.name}' should come before '{e1.name}'",
+                file=sys.stderr,
+            )
+            errors += 1
+
+    if errors:
+        sys.exit(1)
+
+
+def sort_mailmap(mailmap_path, _):
+    """Sort the mailmap entries alphabetically by name."""
+    entries = read_and_parse_mailmap(mailmap_path, True)
+
+    entries.sort(key=lambda x: x.name_for_sorting)
+    write_entries_to_file(mailmap_path, entries)
+
+
+def add_entry(mailmap_path, args):
+    """Add a new entry to the mailmap file in the correct alphabetical position."""
+    if not args.entry:
+        print("Error: 'add' operation requires an entry argument", file=sys.stderr)
+        sys.exit(1)
+
+    new_entry = args.entry
+    entries = read_and_parse_mailmap(mailmap_path, True)
+
+    # Check if entry already exists, checking email2 only if it's specified
+    if (
+        not new_entry.email2
+        and any(e.name == new_entry.name and e.email1 == new_entry.email1 for e in entries)
+    ) or any(
+        e.name == new_entry.name and e.email1 == new_entry.email1 and e.email2 == new_entry.email2
+        for e in entries
+    ):
+        print(
+            f"Error: Duplicate entry - '{new_entry.name} <{new_entry.email1}>' already exists",
+            file=sys.stderr,
+        )
+        sys.exit(1)
+
+    for n, entry in enumerate(entries):
+        if entry.name_for_sorting > new_entry.name_for_sorting:
+            entries.insert(n, new_entry)
+            break
+    else:
+        entries.append(new_entry)
+    write_entries_to_file(mailmap_path, entries)
+
+
+def main():
+    """Main function."""
+    # ops and functions implementing them
+    operations = {"add": add_entry, "check": check_mailmap, "sort": sort_mailmap}
+
+    parser = argparse.ArgumentParser(
+        description=__doc__,
+        formatter_class=argparse.RawDescriptionHelpFormatter,
+        epilog="NOTE:\n for operations which write .mailmap, any comments or blank lines in the file will be removed",
+    )
+    parser.add_argument("operation", choices=operations.keys(), help="Operation to perform")
+    parser.add_argument("--mailmap", help="Path to .mailmap file (default: search up tree)")
+    parser.add_argument(
+        "entry",
+        nargs="?",
+        type=MailmapEntry.parse,
+        help='Entry to add. Format: "Name <email@domain.com>"',
+    )
+
+    args = parser.parse_args()
+
+    if args.mailmap:
+        mailmap_path = Path(args.mailmap)
+    else:
+        # Find mailmap file
+        mailmap_path = Path(".").resolve()
+        while not (mailmap_path / ".mailmap").exists():
+            if mailmap_path == mailmap_path.parent:
+                print("Error: No .mailmap file found", file=sys.stderr)
+                sys.exit(1)
+            mailmap_path = mailmap_path.parent
+        mailmap_path = mailmap_path / ".mailmap"
+
+    # call appropriate operation
+    operations[args.operation](mailmap_path, args)
+
+
+if __name__ == "__main__":
+    main()
-- 
2.48.1


  reply	other threads:[~2025-08-08 21:08 UTC|newest]

Thread overview: 9+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2025-08-08 14:27 [PATCH 0/2] Improve mailmap file Bruce Richardson
2025-08-08 14:27 ` [PATCH 1/2] devtools/mailmap_ctl: script to work with mailmap Bruce Richardson
2025-08-08 16:44   ` Marat Khalili
2025-08-08 16:47     ` Bruce Richardson
2025-08-08 19:58     ` Bruce Richardson
2025-08-08 14:27 ` [PATCH 2/2] mailmap: sort mailmap Bruce Richardson
2025-08-08 21:08 ` [PATCH v2 0/2] Improve mailmap file Bruce Richardson
2025-08-08 21:08   ` Bruce Richardson [this message]
2025-08-08 21:08   ` [PATCH v2 2/2] mailmap: sort mailmap Bruce Richardson

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=20250808210837.518507-2-bruce.richardson@intel.com \
    --to=bruce.richardson@intel.com \
    --cc=dev@dpdk.org \
    --cc=marat.khalili@huawei.com \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox;
as well as URLs for NNTP newsgroup(s).