Skip to content

Commit 44822e6

Browse files
committed
feat: support parsing references with full issue URL
1 parent 141c92e commit 44822e6

File tree

4 files changed

+171
-58
lines changed

4 files changed

+171
-58
lines changed

README.md

+30
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,22 @@ Duplicate of #1
137137
{mentions: [{raw: '@user', prefix: '@', user: 'user'}]}
138138
```
139139

140+
### Parse references with full issue URL
141+
142+
```text
143+
https://github.com/owner/repo/pull/1
144+
145+
Fix https://github.com/owner/repo/issues/2
146+
```
147+
```js
148+
{
149+
refs: [{raw: 'https://github.com/owner/repo/pull/1', slug: 'owner/repo', prefix: undefined, issue: '1'},]
150+
actions: [
151+
{raw: 'Fix https://github.com/owner/repo/issues/2', action: 'Fix', slug: 'owner/repo', prefix: undefined, issue: '2'}
152+
]
153+
}
154+
```
155+
140156
### Ignore keywords case
141157

142158
```text
@@ -287,6 +303,20 @@ Default: `['#', 'gh-']`
287303

288304
List of keywords used to identify issues and pull requests.
289305

306+
##### hosts
307+
308+
Type: `Array<String>` `String`<br>
309+
Default: `['https://github.com', 'https://gitlab.com']`
310+
311+
List of base URL used to identify issues and pull requests with [full URL](#parse-references-with-full-issue-url).
312+
313+
##### issueURLSegments
314+
315+
Type: `Array<String>` `String`<br>
316+
Default: `['issues', 'pull', 'merge_requests']`
317+
318+
List of URL segment used to identify issues and pull requests with [full URL](#parse-references-with-full-issue-url).
319+
290320
### parse(text) => Result
291321

292322
Parse an issue description and returns a [Result](#result) object.

index.js

+44-16
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ const hostConfig = require('./lib/hosts-config');
99
const FENCE_BLOCK_REGEXP = /^(([ \t]*`{3,4})([^\n]*)([\s\S]+?)(^[ \t]*\2))/gm;
1010
const CODE_BLOCK_REGEXP = /(`(?![\\]))((?:.(?!\1(?![\\])))*.?)\1/g;
1111
const HTML_CODE_BLOCK_REGEXP = /(<code)+?((?!(<code|<\/code>)+?)[\S\s])*(<\/code>)+?/gim;
12+
const LEADING_TRAILING_SLASH_REGEXP = /^\/?([^/]+(?:\/[^/]+)*)\/?$/;
13+
const TRAILING_SLASH_REGEXP = /\/?$/;
1214

1315
function inverse(str) {
1416
return str
@@ -18,19 +20,34 @@ function inverse(str) {
1820
}
1921

2022
function join(keywords) {
21-
return keywords.map(escapeRegExp).join('|');
23+
return keywords
24+
.filter(Boolean)
25+
.map(escapeRegExp)
26+
.join('|');
2227
}
2328

24-
function buildMentionsRegexp(opts) {
25-
return `((?:(?:[^\\w\\n\\v\\r]|^)+(?:${join(opts.mentionsPrefixes)})[\\w-\\.]+[^\\W])+)`;
29+
function addLeadingAndTrailingSlash(value) {
30+
return value.replace(LEADING_TRAILING_SLASH_REGEXP, '/$1/');
2631
}
2732

28-
function buildRefRegexp(opts) {
29-
return `(?:(?:[^\\w\\n\\v\\r]|^)+(${join([].concat(opts.referenceActions, opts.duplicateActions))}))?(?:${[
30-
'[^\\w\\n\\v\\r]|^',
31-
]
32-
.concat(join(opts.issuePrefixes))
33-
.join('|')})+((?:(?:[\\w-\\.]+)\\/)+(?:[\\w-\\.]+))?(${join(opts.issuePrefixes)})(\\d+)(?!\\w)`;
33+
function addTrailingSlash(value) {
34+
return value.replace(TRAILING_SLASH_REGEXP, '/');
35+
}
36+
37+
function includesIgnoreCase(arr, value) {
38+
return arr.findIndex(val => val.toUpperCase() === value.toUpperCase()) > -1;
39+
}
40+
41+
function buildMentionsRegexp({mentionsPrefixes}) {
42+
return `((?:(?:[^\\w\\n\\v\\r]|^)+(?:${join(mentionsPrefixes)})[\\w-\\.]+[^\\W])+)`;
43+
}
44+
45+
function buildRefRegexp({referenceActions, duplicateActions, issuePrefixes, issueURLSegments, hosts}) {
46+
return `(?:(?:[^\\w\\n\\v\\r]|^)+(${join([].concat(referenceActions, duplicateActions))}))?(?:${['[^\\w\\n\\v\\r]|^']
47+
.concat(join(issuePrefixes.concat(issueURLSegments)))
48+
.join('|')})+${hosts.length > 0 ? `(?:${join(hosts)})?` : ''}((?:(?:[\\w-\\.]+)\\/)+(?:[\\w-\\.]+))?(${join(
49+
issuePrefixes.concat(issueURLSegments)
50+
)})(\\d+)(?!\\w)`;
3451
}
3552

3653
function buildRegexp(opts) {
@@ -42,11 +59,11 @@ function buildRegexp(opts) {
4259
);
4360
}
4461

45-
function buildMentionRegexp(opts) {
46-
return new RegExp(`(${join(opts.mentionsPrefixes)})([\\w-.]+)`, 'gim');
62+
function buildMentionRegexp({mentionsPrefixes}) {
63+
return new RegExp(`(${join(mentionsPrefixes)})([\\w-.]+)`, 'gim');
4764
}
4865

49-
function parse(text, regexp, mentionRegexp, opts) {
66+
function parse(text, regexp, mentionRegexp, {issuePrefixes, hosts, referenceActions, duplicateActions}) {
5067
let parsed;
5168
const results = {actions: [], refs: [], duplicates: [], mentions: []};
5269
let noCodeBlock = inverse(inverse(text.replace(FENCE_BLOCK_REGEXP, '')).replace(CODE_BLOCK_REGEXP, ''));
@@ -57,12 +74,20 @@ function parse(text, regexp, mentionRegexp, opts) {
5774

5875
while ((parsed = regexp.exec(noCodeBlock)) !== null) {
5976
let [raw, action, slug, prefix, issue, mentions] = parsed;
60-
raw = parsed[0].substring(parsed[0].indexOf(parsed[1] || parsed[2] || parsed[3]));
77+
prefix =
78+
prefix && issuePrefixes.some(issuePrefix => issuePrefix.toUpperCase() === prefix.toUpperCase())
79+
? prefix
80+
: undefined;
81+
raw = parsed[0].substring(
82+
parsed[0].indexOf(
83+
parsed[1] || hosts.find(host => parsed[0].toUpperCase().includes(host.toUpperCase())) || parsed[2] || parsed[3]
84+
)
85+
);
6186
action = capitalize(parsed[1]);
6287

63-
if (opts.referenceActions.findIndex(fix => fix.toUpperCase() === action.toUpperCase()) > -1) {
88+
if (includesIgnoreCase(referenceActions, action)) {
6489
results.actions.push({raw, action, slug, prefix, issue});
65-
} else if (opts.duplicateActions.findIndex(duplicate => duplicate.toUpperCase() === action.toUpperCase()) > -1) {
90+
} else if (includesIgnoreCase(duplicateActions, action)) {
6691
results.duplicates.push({raw, action, slug, prefix, issue});
6792
} else if (issue) {
6893
results.refs.push({raw, slug, prefix, issue});
@@ -79,7 +104,7 @@ function parse(text, regexp, mentionRegexp, opts) {
79104
}
80105

81106
module.exports = options => {
82-
if (!isUndefined(options) && !isString(options) && !isPlainObject(options)) {
107+
if (options !== undefined && !isString(options) && !isPlainObject(options)) {
83108
throw new TypeError('Options must be a String or an Object');
84109
}
85110

@@ -97,6 +122,9 @@ module.exports = options => {
97122
opts[opt] = opts[opt].filter(Boolean);
98123
}
99124

125+
opts.hosts = opts.hosts.map(addTrailingSlash);
126+
opts.issueURLSegments = opts.issueURLSegments.map(addLeadingAndTrailingSlash);
127+
100128
const regexp = buildRegexp(opts);
101129
const mentionRegexp = buildMentionRegexp(opts);
102130

lib/hosts-config.js

+8
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ module.exports = {
77
// https://guides.github.com/features/issues/#notifications
88
mentionsPrefixes: ['@'],
99
issuePrefixes: ['#', 'gh-'],
10+
hosts: ['https://github.com'],
11+
issueURLSegments: ['issues', 'pull'],
1012
},
1113
bitbucket: {
1214
// https://confluence.atlassian.com/bitbucket/resolve-issues-automatically-when-users-push-code-221451126.html
@@ -29,6 +31,8 @@ module.exports = {
2931
mentionsPrefixes: ['@'],
3032
// https://confluence.atlassian.com/bitbucket/mark-up-comments-issues-and-commit-messages-321859781.html
3133
issuePrefixes: ['#'],
34+
hosts: [],
35+
issueURLSegments: [],
3236
},
3337
gitlab: {
3438
// https://docs.gitlab.com/ee/user/project/issues/automatic_issue_closing.html
@@ -56,6 +60,8 @@ module.exports = {
5660
mentionsPrefixes: ['@'],
5761
// https://about.gitlab.com/2016/03/08/gitlab-tutorial-its-all-connected
5862
issuePrefixes: ['#', '!'],
63+
hosts: ['https://gitlab.com'],
64+
issueURLSegments: ['issues', 'merge_requests'],
5965
},
6066
default: {
6167
referenceActions: [
@@ -79,5 +85,7 @@ module.exports = {
7985
duplicateActions: ['Duplicate of', '/duplicate'],
8086
mentionsPrefixes: ['@'],
8187
issuePrefixes: ['#', 'gh-'],
88+
hosts: ['https://github.com', 'https://gitlab.com'],
89+
issueURLSegments: ['issues', 'pull', 'merge_requests'],
8290
},
8391
};

test/index.test.js

+89-42
Original file line numberDiff line numberDiff line change
@@ -2,21 +2,30 @@ import test from 'ava';
22
import m from '..';
33

44
test('Parse GitHub issue', t => {
5-
t.deepEqual(m('GitHub')('Fix #1 reSOLved gh-2 CLOSES Gh-3 fix o/r#4 #5 o/r#6 fixing #7 Duplicate OF #8 @user'), {
6-
actions: [
7-
{raw: 'Fix #1', action: 'Fix', slug: undefined, prefix: '#', issue: '1'},
8-
{raw: 'reSOLved gh-2', action: 'Resolved', slug: undefined, prefix: 'gh-', issue: '2'},
9-
{raw: 'CLOSES Gh-3', action: 'Closes', slug: undefined, prefix: 'Gh-', issue: '3'},
10-
{raw: 'fix o/r#4', action: 'Fix', slug: 'o/r', prefix: '#', issue: '4'},
11-
],
12-
refs: [
13-
{raw: '#5', slug: undefined, prefix: '#', issue: '5'},
14-
{raw: 'o/r#6', slug: 'o/r', prefix: '#', issue: '6'},
15-
{raw: '#7', slug: undefined, prefix: '#', issue: '7'},
16-
],
17-
duplicates: [{raw: 'Duplicate OF #8', action: 'Duplicate of', slug: undefined, prefix: '#', issue: '8'}],
18-
mentions: [{raw: '@user', prefix: '@', user: 'user'}],
19-
});
5+
t.deepEqual(
6+
m('GitHub')(
7+
'Fix #1 reSOLved gh-2 CLOSES Gh-3 fix o/r#4 #5 o/r#6 fix https://github.com/o/r/issues/7 https://github.com/o/r/issues/8 fix https://github.com/o/r/pull/9 https://github.com/o/r/pull/10 fixing #11 Duplicate OF #12 @user'
8+
),
9+
{
10+
actions: [
11+
{raw: 'Fix #1', action: 'Fix', slug: undefined, prefix: '#', issue: '1'},
12+
{raw: 'reSOLved gh-2', action: 'Resolved', slug: undefined, prefix: 'gh-', issue: '2'},
13+
{raw: 'CLOSES Gh-3', action: 'Closes', slug: undefined, prefix: 'Gh-', issue: '3'},
14+
{raw: 'fix o/r#4', action: 'Fix', slug: 'o/r', prefix: '#', issue: '4'},
15+
{raw: 'fix https://github.com/o/r/issues/7', action: 'Fix', slug: 'o/r', prefix: undefined, issue: '7'},
16+
{raw: 'fix https://github.com/o/r/pull/9', action: 'Fix', slug: 'o/r', prefix: undefined, issue: '9'},
17+
],
18+
refs: [
19+
{raw: '#5', slug: undefined, prefix: '#', issue: '5'},
20+
{raw: 'o/r#6', slug: 'o/r', prefix: '#', issue: '6'},
21+
{raw: 'https://github.com/o/r/issues/8', slug: 'o/r', prefix: undefined, issue: '8'},
22+
{raw: 'https://github.com/o/r/pull/10', slug: 'o/r', prefix: undefined, issue: '10'},
23+
{raw: '#11', slug: undefined, prefix: '#', issue: '11'},
24+
],
25+
duplicates: [{raw: 'Duplicate OF #12', action: 'Duplicate of', slug: undefined, prefix: '#', issue: '12'}],
26+
mentions: [{raw: '@user', prefix: '@', user: 'user'}],
27+
}
28+
);
2029
});
2130

2231
test('Parse Bitbucket issue', t => {
@@ -39,39 +48,75 @@ test('Parse Bitbucket issue', t => {
3948
});
4049

4150
test('Parse GitLab issue', t => {
42-
t.deepEqual(m('GitLab')('Fix #1 reSOLved #2 IMPLEMENT #3 fix g/sg/o/r#4 #5 o/r#6 fixing #7 /duplicate #8 @user'), {
43-
actions: [
44-
{raw: 'Fix #1', action: 'Fix', slug: undefined, prefix: '#', issue: '1'},
45-
{raw: 'reSOLved #2', action: 'Resolved', slug: undefined, prefix: '#', issue: '2'},
46-
{raw: 'IMPLEMENT #3', action: 'Implement', slug: undefined, prefix: '#', issue: '3'},
47-
{raw: 'fix g/sg/o/r#4', action: 'Fix', slug: 'g/sg/o/r', prefix: '#', issue: '4'},
48-
{raw: 'fixing #7', action: 'Fixing', slug: undefined, prefix: '#', issue: '7'},
49-
],
50-
refs: [{raw: '#5', slug: undefined, prefix: '#', issue: '5'}, {raw: 'o/r#6', slug: 'o/r', prefix: '#', issue: '6'}],
51-
duplicates: [{raw: '/duplicate #8', action: '/duplicate', slug: undefined, prefix: '#', issue: '8'}],
52-
mentions: [{raw: '@user', prefix: '@', user: 'user'}],
53-
});
51+
t.deepEqual(
52+
m('GitLab')(
53+
'Fix #1 reSOLved #2 IMPLEMENT #3 fix g/sg/o/r#4 #5 o/r#6 fix https://gitlab.com/o/r/issues/7 https://gitlab.com/o/r/issues/8 fix https://gitlab.com/o/r/merge_requests/9 https://gitlab.com/o/r/merge_requests/10 fixing #11 fixing !12 /duplicate #13 @user'
54+
),
55+
{
56+
actions: [
57+
{raw: 'Fix #1', action: 'Fix', slug: undefined, prefix: '#', issue: '1'},
58+
{raw: 'reSOLved #2', action: 'Resolved', slug: undefined, prefix: '#', issue: '2'},
59+
{raw: 'IMPLEMENT #3', action: 'Implement', slug: undefined, prefix: '#', issue: '3'},
60+
{raw: 'fix g/sg/o/r#4', action: 'Fix', slug: 'g/sg/o/r', prefix: '#', issue: '4'},
61+
{raw: 'fix https://gitlab.com/o/r/issues/7', action: 'Fix', slug: 'o/r', prefix: undefined, issue: '7'},
62+
{raw: 'fix https://gitlab.com/o/r/merge_requests/9', action: 'Fix', slug: 'o/r', prefix: undefined, issue: '9'},
63+
{raw: 'fixing #11', action: 'Fixing', slug: undefined, prefix: '#', issue: '11'},
64+
{raw: 'fixing !12', action: 'Fixing', slug: undefined, prefix: '!', issue: '12'},
65+
],
66+
refs: [
67+
{raw: '#5', slug: undefined, prefix: '#', issue: '5'},
68+
{raw: 'o/r#6', slug: 'o/r', prefix: '#', issue: '6'},
69+
{raw: 'https://gitlab.com/o/r/issues/8', slug: 'o/r', prefix: undefined, issue: '8'},
70+
{raw: 'https://gitlab.com/o/r/merge_requests/10', slug: 'o/r', prefix: undefined, issue: '10'},
71+
],
72+
duplicates: [{raw: '/duplicate #13', action: '/duplicate', slug: undefined, prefix: '#', issue: '13'}],
73+
mentions: [{raw: '@user', prefix: '@', user: 'user'}],
74+
}
75+
);
5476
});
5577

5678
test('Parse with default options', t => {
57-
t.deepEqual(m()('Fix #1 reSOLved gh-2 CLOSES Gh-3 fix o/r#4 #5 o/r#6 implementing #7 Duplicate OF #8 @user'), {
58-
actions: [
59-
{raw: 'Fix #1', action: 'Fix', slug: undefined, prefix: '#', issue: '1'},
60-
{raw: 'reSOLved gh-2', action: 'Resolved', slug: undefined, prefix: 'gh-', issue: '2'},
61-
{raw: 'CLOSES Gh-3', action: 'Closes', slug: undefined, prefix: 'Gh-', issue: '3'},
62-
{raw: 'fix o/r#4', action: 'Fix', slug: 'o/r', prefix: '#', issue: '4'},
63-
{raw: 'implementing #7', action: 'Implementing', slug: undefined, prefix: '#', issue: '7'},
64-
],
65-
refs: [{raw: '#5', slug: undefined, prefix: '#', issue: '5'}, {raw: 'o/r#6', slug: 'o/r', prefix: '#', issue: '6'}],
66-
duplicates: [{raw: 'Duplicate OF #8', action: 'Duplicate of', slug: undefined, prefix: '#', issue: '8'}],
67-
mentions: [{raw: '@user', prefix: '@', user: 'user'}],
68-
});
79+
t.deepEqual(
80+
m()(
81+
'Fix #1 reSOLved gh-2 CLOSES Gh-3 fix o/r#4 #5 o/r#6 implementing #7 https://github.com/o/r/issues/8 implementing https://github.com/o/r/issues/9 Duplicate OF #10 @user'
82+
),
83+
{
84+
actions: [
85+
{raw: 'Fix #1', action: 'Fix', slug: undefined, prefix: '#', issue: '1'},
86+
{raw: 'reSOLved gh-2', action: 'Resolved', slug: undefined, prefix: 'gh-', issue: '2'},
87+
{raw: 'CLOSES Gh-3', action: 'Closes', slug: undefined, prefix: 'Gh-', issue: '3'},
88+
{raw: 'fix o/r#4', action: 'Fix', slug: 'o/r', prefix: '#', issue: '4'},
89+
{raw: 'implementing #7', action: 'Implementing', slug: undefined, prefix: '#', issue: '7'},
90+
{
91+
raw: 'implementing https://github.com/o/r/issues/9',
92+
action: 'Implementing',
93+
slug: 'o/r',
94+
prefix: undefined,
95+
issue: '9',
96+
},
97+
],
98+
refs: [
99+
{raw: '#5', slug: undefined, prefix: '#', issue: '5'},
100+
{raw: 'o/r#6', slug: 'o/r', prefix: '#', issue: '6'},
101+
{raw: 'https://github.com/o/r/issues/8', slug: 'o/r', prefix: undefined, issue: '8'},
102+
],
103+
duplicates: [{raw: 'Duplicate OF #10', action: 'Duplicate of', slug: undefined, prefix: '#', issue: '10'}],
104+
mentions: [{raw: '@user', prefix: '@', user: 'user'}],
105+
}
106+
);
69107
});
70108

71109
test('Parse with custom options', t => {
72110
t.deepEqual(
73-
m({referenceActions: ['fix'], duplicateActions: [], mentionsPrefixes: '!', issuePrefixes: ['#']})(
74-
'Fix #1 reSOLved gh-2 CLOSES Gh-3 fixed o/r#4 #5 o/r#6 fixing #7 Duplicate OF #8 !user @other'
111+
m({
112+
referenceActions: ['fix'],
113+
duplicateActions: [],
114+
mentionsPrefixes: '!',
115+
issuePrefixes: ['#'],
116+
hosts: ['http://host1.com/', 'http://host2.com'],
117+
issueURLSegments: ['bugs'],
118+
})(
119+
'Fix #1 reSOLved gh-2 CLOSES Gh-3 fixed o/r#4 #5 o/r#6 fixing #7 http://host1.com/o/r/bugs/8 http://host2.com/o/r/bugs/9 Duplicate OF #10 !user @other'
75120
),
76121
{
77122
actions: [{raw: 'Fix #1', action: 'Fix', slug: undefined, prefix: '#', issue: '1'}],
@@ -80,7 +125,9 @@ test('Parse with custom options', t => {
80125
{raw: '#5', slug: undefined, prefix: '#', issue: '5'},
81126
{raw: 'o/r#6', slug: 'o/r', prefix: '#', issue: '6'},
82127
{raw: '#7', slug: undefined, prefix: '#', issue: '7'},
83-
{raw: '#8', slug: undefined, prefix: '#', issue: '8'},
128+
{raw: 'http://host1.com/o/r/bugs/8', slug: 'o/r', prefix: undefined, issue: '8'},
129+
{raw: 'http://host2.com/o/r/bugs/9', slug: 'o/r', prefix: undefined, issue: '9'},
130+
{raw: '#10', slug: undefined, prefix: '#', issue: '10'},
84131
],
85132
duplicates: [],
86133
mentions: [{raw: '!user', prefix: '!', user: 'user'}],

0 commit comments

Comments
 (0)