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
1 change: 1 addition & 0 deletions changelog.d/2435-issue-ref-url-fragment.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Reading an issue number out of a line no longer turns a link's URL fragment into an issue reference: `https://example.com/page#123` used to name issue #123 and write a card for an issue nobody meant, while `owner/repo#12`, a bare `#12` and GitHub issue and pull-request links keep naming the issue they point at ([#2435](https://github.com/ThinkFlowLab/FlyBot/issues/2435)).
6 changes: 4 additions & 2 deletions core/src/shared/issue-ref.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,10 @@
// front makes it part of an identifier, a version, a hex colour or a URL fragment (`C#7`, `1.2#3`,
// `#3366ff`, `help/#123`), and a word character behind means the number keeps going (`#1360abc`).
// `owner/repo#12` is its own shape, tried separately, so this separation cannot swallow it: there
// the `#` follows a repo name rather than a space.
const ISSUE_REF = /(?<![\w./-])#(\d{1,7})(?![\w])|(?:github\.com\/)?([\w.-]+)\/([\w.-]+)#(\d{1,7})(?![\w])|github\.com\/([\w.-]+)\/([\w.-]+)\/(?:issues|pull)\/(\d{1,7})(?![\w])/gi
// the `#` follows a repo name rather than a space. It needs the same boundary in front, or a link
// whose path carries a segment before the `#` reads as that shape and names an issue nobody meant
// (`https://example.com/page#123`, #1392).
const ISSUE_REF = /(?<![\w./-])#(\d{1,7})(?![\w])|(?<![\w./-])(?:github\.com\/)?([\w.-]+)\/([\w.-]+)#(\d{1,7})(?![\w])|github\.com\/([\w.-]+)\/([\w.-]+)\/(?:issues|pull)\/(\d{1,7})(?![\w])/gi

// The id a match of `ISSUE_REF` names, normalised to `#1234`. The three shapes carry their number in
// different groups (1, 4, 7); every reader takes it from here, so no caller can pick the wrong one.
Expand Down
42 changes: 42 additions & 0 deletions core/test/issue-ref-url-fragment.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
// A URL fragment is not an issue reference (#1392). The repository-qualified shape `owner/repo#12`
// had no boundary in front of it, so a link whose path carries a segment before the `#` read as
// `<host>/<segment>#12`: `https://example.com/page#123` named issue #123 and the dispatch wrote a
// card for an issue nobody meant (#1360). The boundary and the number are asserted apart: a
// fragment must not hand over digits, and a real GitHub URL with a fragment must still yield exactly
// one number.
import assert from 'node:assert/strict'
import { test } from 'node:test'
import { issueRefs, namesIssue } from '../src/shared/issue-ref.js'

const fragments = [
['a path segment before the #', 'see https://example.com/page#123 for details'],
['a two-segment path', 'https://example.com/docs/guide#12'],
['a nested path', 'read https://docs.example.org/a/b#42 then'],
['a fragment of seven digits', 'see https://example.com/page#1234567 for details'],
['a fragment longer than an id', 'https://example.com/page#12345678'],
]

for (const [what, text] of fragments) {
test(`a URL fragment names no issue (${what})`, () => {
assert.deepEqual(issueRefs(text), [], text)
})
}

test('a fragment does not keep the digits of the link it follows', () => {
assert.equal(namesIssue('https://example.com/page#123', 123), false)
assert.equal(namesIssue('https://example.com/page#1234', 123), false)
})

test('one GitHub URL yields exactly one issue number, fragment and all', () => {
assert.deepEqual(issueRefs('https://github.com/o/r/issues/123#issuecomment-4567'), ['#123'])
assert.deepEqual(issueRefs('https://github.com/o/r/pull/9#discussion_r123456'), ['#9'])
})

test('the shapes that do name an issue keep working beside a URL', () => {
assert.deepEqual(issueRefs('owner/repo#1360'), ['#1360'])
assert.deepEqual(issueRefs('see owner/repo#1360 for the fix'), ['#1360'])
assert.deepEqual(issueRefs('please rebase #1360 onto main'), ['#1360'])
assert.deepEqual(issueRefs('https://github.com/o/r/issues/1360'), ['#1360'])
assert.deepEqual(issueRefs('@Dev-2 rebase owner/repo#1360'), ['#1360'])
assert.equal(namesIssue('owner/repo#1360', 1360), true)
})
6 changes: 4 additions & 2 deletions patches/ds-bot/files/src/shared/issue-ref.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,10 @@
// front makes it part of an identifier, a version, a hex colour or a URL fragment (`C#7`, `1.2#3`,
// `#3366ff`, `help/#123`), and a word character behind means the number keeps going (`#1360abc`).
// `owner/repo#12` is its own shape, tried separately, so this separation cannot swallow it: there
// the `#` follows a repo name rather than a space.
const ISSUE_REF = /(?<![\w./-])#(\d{1,7})(?![\w])|(?:github\.com\/)?([\w.-]+)\/([\w.-]+)#(\d{1,7})(?![\w])|github\.com\/([\w.-]+)\/([\w.-]+)\/(?:issues|pull)\/(\d{1,7})(?![\w])/gi
// the `#` follows a repo name rather than a space. It needs the same boundary in front, or a link
// whose path carries a segment before the `#` reads as that shape and names an issue nobody meant
// (`https://example.com/page#123`, #1392).
const ISSUE_REF = /(?<![\w./-])#(\d{1,7})(?![\w])|(?<![\w./-])(?:github\.com\/)?([\w.-]+)\/([\w.-]+)#(\d{1,7})(?![\w])|github\.com\/([\w.-]+)\/([\w.-]+)\/(?:issues|pull)\/(\d{1,7})(?![\w])/gi

// The id a match of `ISSUE_REF` names, normalised to `#1234`. The three shapes carry their number in
// different groups (1, 4, 7); every reader takes it from here, so no caller can pick the wrong one.
Expand Down
42 changes: 42 additions & 0 deletions patches/ds-bot/files/test/issue-ref-url-fragment.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
// A URL fragment is not an issue reference (#1392). The repository-qualified shape `owner/repo#12`
// had no boundary in front of it, so a link whose path carries a segment before the `#` read as
// `<host>/<segment>#12`: `https://example.com/page#123` named issue #123 and the dispatch wrote a
// card for an issue nobody meant (#1360). The boundary and the number are asserted apart: a
// fragment must not hand over digits, and a real GitHub URL with a fragment must still yield exactly
// one number.
import assert from 'node:assert/strict'
import { test } from 'node:test'
import { issueRefs, namesIssue } from '../src/shared/issue-ref.js'

const fragments = [
['a path segment before the #', 'see https://example.com/page#123 for details'],
['a two-segment path', 'https://example.com/docs/guide#12'],
['a nested path', 'read https://docs.example.org/a/b#42 then'],
['a fragment of seven digits', 'see https://example.com/page#1234567 for details'],
['a fragment longer than an id', 'https://example.com/page#12345678'],
]

for (const [what, text] of fragments) {
test(`a URL fragment names no issue (${what})`, () => {
assert.deepEqual(issueRefs(text), [], text)
})
}

test('a fragment does not keep the digits of the link it follows', () => {
assert.equal(namesIssue('https://example.com/page#123', 123), false)
assert.equal(namesIssue('https://example.com/page#1234', 123), false)
})

test('one GitHub URL yields exactly one issue number, fragment and all', () => {
assert.deepEqual(issueRefs('https://github.com/o/r/issues/123#issuecomment-4567'), ['#123'])
assert.deepEqual(issueRefs('https://github.com/o/r/pull/9#discussion_r123456'), ['#9'])
})

test('the shapes that do name an issue keep working beside a URL', () => {
assert.deepEqual(issueRefs('owner/repo#1360'), ['#1360'])
assert.deepEqual(issueRefs('see owner/repo#1360 for the fix'), ['#1360'])
assert.deepEqual(issueRefs('please rebase #1360 onto main'), ['#1360'])
assert.deepEqual(issueRefs('https://github.com/o/r/issues/1360'), ['#1360'])
assert.deepEqual(issueRefs('@Dev-2 rebase owner/repo#1360'), ['#1360'])
assert.equal(namesIssue('owner/repo#1360', 1360), true)
})
Loading