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
8 changes: 7 additions & 1 deletion src/db/repositories.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7273,12 +7273,18 @@ export function extractLinkedIssueNumbersWithOverflow(text: string, repoFullName
const normalizedLimit = Math.max(0, Math.floor(limit));
const target = repoFullName.toLowerCase();

// Strip inline code spans before scanning: GitHub's own native closing-keyword linker does not treat
// backtick-wrapped text as a real "Closes #N" directive, and this repo's own PR template checklist item
// contains the literal example text "(e.g. `Closes #123`)" -- without this, every PR that keeps the
// unmodified template checklist would spuriously link to issue #123.
const withoutCodeSpans = text.replace(/`[^`\n]*`/g, " ");

const linkedIssues: number[] = [];
const seen = new Set<number>();
// Matches both GitHub's bare `KEYWORD #N` and fully-qualified `KEYWORD owner/repo#N` closing syntax (#3862) --
// the qualified form only counts when owner/repo case-insensitively matches THIS repo; a reference to a
// different repo closes an issue there, not here, and must not spoof a same-repo linked-issue match.
for (const match of text.matchAll(/\b(?:close[sd]?|fix(?:e[sd])?|resolve[sd]?)\s+(?:([\w.-]+\/[\w.-]+)#|#)(\d+)\b/gi)) {
for (const match of withoutCodeSpans.matchAll(/\b(?:close[sd]?|fix(?:e[sd])?|resolve[sd]?)\s+(?:([\w.-]+\/[\w.-]+)#|#)(\d+)\b/gi)) {
const owner = match[1];
if (owner && owner.toLowerCase() !== target) continue;
const value = Number(match[2]);
Expand Down
9 changes: 9 additions & 0 deletions test/unit/db-parsers.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,15 @@ describe("database row parser hardening", () => {
expect(extractLinkedIssueNumbers("Fixes #1", "owner/repo", -5)).toEqual([]);
});

it("REGRESSION: ignores a closing keyword inside an inline code span, e.g. this repo's own PR template checklist example", () => {
// .github/pull_request_template.md literally contains "(e.g. `Closes #123`)" -- every PR that keeps the
// unmodified checklist item must NOT spuriously link to issue #123.
const templateLine = "- [ ] I linked a currently open issue this PR resolves (e.g. `Closes #123`) — a linked open issue is required for every contributor PR.";
expect(extractLinkedIssueNumbers(templateLine, "owner/repo")).toEqual([]);
// A real closing keyword elsewhere in the same body still counts.
expect(extractLinkedIssueNumbers(`Closes #42\n\n${templateLine}`, "owner/repo")).toEqual([42]);
});

it("recognizes the fully-qualified `Fixes owner/repo#N` closing syntax when owner/repo matches this repo (#3862)", () => {
expect(extractLinkedIssueNumbers("Closes owner/repo#42", "owner/repo")).toEqual([42]);
// Case-insensitive, matching GitHub's own repo-name matching.
Expand Down