Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 14 additions & 16 deletions packages/darnit/src/darnit/context/sieve.py
Original file line number Diff line number Diff line change
Expand Up @@ -518,25 +518,23 @@ def _detect_governance_deterministic(self, local_path: str) -> list[ContextSigna
# =========================================================================

def _parse_maintainers_file(self, content: str) -> list[str]:
"""Parse maintainers from MAINTAINERS.md content.
"""Parse maintainer handles from MAINTAINERS.md content.

Looks for:
- @username mentions
- GitHub usernames in lists
- Email addresses with names
Returns only ``@username`` handles. Email addresses are stripped
before matching so a domain (``realcorp`` in ``alice@realcorp.io``)
is never read as a handle, and display names are left out: a bare
name is not a GitHub handle, and GitHub rejects it as a code owner.
"""
maintainers = []

# Pattern for @username
at_mentions = re.findall(r"@([a-zA-Z0-9][-a-zA-Z0-9]*)", content)
maintainers.extend([f"@{m}" for m in at_mentions])

# Pattern for "Name (email)" or "Name <email>"
name_email = re.findall(
r"[-*]\s*([A-Z][a-zA-Z]+(?:\s+[A-Z][a-zA-Z]+)*)\s*[(<]",
content,
# Strip email addresses first: the @ in alice@example.com is never a
# handle, and without this its domain would match the handle pattern.
no_emails = re.sub(
r"[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}", "", content
)
maintainers.extend(name_email)

# Pattern for @username. The lookbehind keeps the @ of any surviving
# address-like text (e.g. user@localhost) from matching as a handle.
at_mentions = re.findall(r"(?<![\w.])@([a-zA-Z0-9][-a-zA-Z0-9]*)", no_emails)
maintainers = [f"@{m}" for m in at_mentions]

# Dedupe while preserving order
seen = set()
Expand Down
52 changes: 52 additions & 0 deletions tests/darnit/context/test_context_sieve.py
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,58 @@ def test_detect_maintainers_from_file(self, temp_repo):
# High confidence from explicit file
assert result.confidence >= 0.8

@pytest.mark.unit
def test_detect_maintainers_from_file_ignores_email_domain(self, temp_repo):
"""An email domain must not become a maintainer handle (issue #464)."""
(Path(temp_repo) / "MAINTAINERS.md").write_text(
"# Maintainers\n\n- Alice Example <alice@realcorp.io> @alice\n"
)

sieve = ContextSieve()
result = sieve.detect("maintainers", temp_repo, "owner", "repo")

assert result.key == "maintainers"
assert "@alice" in result.value
assert not any("realcorp" in str(v).lower() for v in result.value)

@pytest.mark.unit
def test_parse_maintainers_file_email_domain_not_a_handle(self):
"""The issue's exact example: only the real handle is returned."""
sieve = ContextSieve()
assert (
sieve._parse_maintainers_file("- Alice Example <alice@realcorp.io> @alice\n")
== ["@alice"]
)

@pytest.mark.unit
def test_parse_maintainers_file_name_and_email_without_handle(self):
"""A display name plus email, with no @handle, yields no maintainers."""
sieve = ContextSieve()
assert sieve._parse_maintainers_file("- Alice Example <alice@realcorp.io>\n") == []

@pytest.mark.unit
def test_parse_maintainers_file_plain_email_yields_nothing(self):
"""A bare email address contributes no handle."""
sieve = ContextSieve()
assert sieve._parse_maintainers_file("Contact: bob@example.com\n") == []

@pytest.mark.unit
def test_parse_maintainers_file_keeps_plain_handles(self):
"""Ordinary @mentions still parse as before."""
sieve = ContextSieve()
assert sieve._parse_maintainers_file(
"- @alice - Lead maintainer\n- @bob - Core contributor\n"
) == ["@alice", "@bob"]

@pytest.mark.unit
def test_parse_maintainers_file_handle_glued_to_email(self):
"""A handle written directly after an email still parses."""
sieve = ContextSieve()
assert (
sieve._parse_maintainers_file("- Alice Example <alice@realcorp.io>@alice\n")
== ["@alice"]
)

@pytest.mark.unit
def test_detect_maintainers_from_codeowners(self, temp_repo):
"""Detects maintainers from CODEOWNERS file."""
Expand Down
Loading