Skip to content
Open
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
22 changes: 19 additions & 3 deletions src/reposteward/github.py
Original file line number Diff line number Diff line change
Expand Up @@ -757,7 +757,11 @@ def open_pull_request_references(
) -> dict[int, tuple[CompetingWork, ...]]:
references: dict[int, list[CompetingWork]] = {}
seen_pulls: set[int] = set()
issue_reference = re.compile(r"(?<![\w/])#(\d+)(?!\d)")
bare_issue_reference = re.compile(r"(?<![\w/])#(\d+)(?!\d)")
qualified_issue_reference = re.compile(
r"(?<![\w/])(?P<owner>[\w.-]+)/(?P<name>[\w.-]+)#(?P<num>\d+)(?!\d)"
)
target_full_name = full_name.casefold()
for page in range(1, 3):
pulls, _ = self._request(
"GET",
Expand Down Expand Up @@ -785,8 +789,20 @@ def open_pull_request_references(
url=str(pull.get("html_url") or ""),
detail=f"#{pull_number}: {pull.get('title', '')}",
)
for match in issue_reference.finditer(body):
number = int(match.group(1))
# GitHub auto-links both bare "#N" and qualified "owner/name#N"
# references. A qualified reference only targets this repository
# when its qualifier equals full_name (case-insensitively);
# qualifiers pointing at other repositories, and URL fragments
# such as "#issuecomment-1", must never count as competing work.
referenced: set[int] = set()
for match in qualified_issue_reference.finditer(body):
qualifier = f"{match.group('owner')}/{match.group('name')}"
if qualifier.casefold() != target_full_name:
continue
referenced.add(int(match.group("num")))
for match in bare_issue_reference.finditer(body):
referenced.add(int(match.group(1)))
for number in sorted(referenced):
references.setdefault(number, []).append(conflict)
if len(pulls) < 100:
break
Expand Down
68 changes: 68 additions & 0 deletions tests/test_github.py
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,74 @@ def request(method: str, path: str, **kwargs: Any) -> tuple[Any, Any]:
{"claim_comment", "open_pull_request"},
)

@staticmethod
def _pull_request_client(body: str) -> tuple[GitHubClient, Any]:
client = GitHubClient(GitHubConfig(), token="test-token")

def request(method: str, path: str, **kwargs: Any) -> tuple[Any, Any]:
if path.endswith("/comments"):
return [], None
return [
{
"number": 5045,
"title": "fix(frontend): keep renamed thread titles in sync",
"body": body,
"html_url": "https://example.test/pr/5045",
"user": {"login": "jiaqiang000"},
"head": {"repo": {"owner": {"login": "jiaqiang000"}}},
}
], None

return client, request

def test_qualified_cross_repository_reference_is_blocker(self) -> None:
# Regression for the deer-flow #5043/#5045 miss: the open PR referenced
# the issue as "Fixes bytedance/deer-flow#5043" and the gate saw nothing.
client, request = self._pull_request_client("Fixes bytedance/deer-flow#5043")
with patch.object(client, "_request", side_effect=request):
conflicts = client.competing_work(
"bytedance/deer-flow", 5043, own_login="betterkite"
)
self.assertEqual([value.kind for value in conflicts], ["open_pull_request"])
self.assertEqual(conflicts[0].actor, "jiaqiang000")

def test_qualified_reference_to_other_repository_is_ignored(self) -> None:
client, request = self._pull_request_client("Fixes other/repo#5043")
with patch.object(client, "_request", side_effect=request):
conflicts = client.competing_work(
"bytedance/deer-flow", 5043, own_login="betterkite"
)
self.assertEqual(conflicts, ())

def test_qualified_reference_matches_case_insensitively(self) -> None:
client, request = self._pull_request_client("Fixes Bytedance/Deer-Flow#5043")
with patch.object(client, "_request", side_effect=request):
conflicts = client.competing_work(
"bytedance/deer-flow", 5043, own_login="betterkite"
)
self.assertEqual([value.kind for value in conflicts], ["open_pull_request"])

def test_bare_and_qualified_references_deduplicate(self) -> None:
client, request = self._pull_request_client(
"Fixes #5043 and bytedance/deer-flow#5043"
)
with patch.object(client, "_request", side_effect=request):
references = client.open_pull_request_references(
"bytedance/deer-flow", own_login="betterkite"
)
self.assertEqual(list(references), [5043])
self.assertEqual(len(references[5043]), 1)

def test_url_fragments_are_not_issue_references(self) -> None:
client, request = self._pull_request_client(
"Context: https://github.com/bytedance/deer-flow/pull/5045#issuecomment-1"
)
with patch.object(client, "_request", side_effect=request):
conflicts = client.competing_work(
"bytedance/deer-flow", 5043, own_login="betterkite"
)
self.assertEqual(conflicts, ())


class GitHubPullRequestPaginationTests(unittest.TestCase):
def test_open_pull_requests_follows_every_rest_page(self) -> None:
Expand Down