Skip to content

Commit 131baa2

Browse files
authored
Accept punctuation after simple+cross repository issue references (#10091)
* Support references ending in , . and ; * Accept :;, in simple refs; fix 2+ consecutive refs * Include cross-repository references * Add ?!, fix spacing problem
1 parent f8f6adc commit 131baa2

File tree

2 files changed

+97
-13
lines changed

2 files changed

+97
-13
lines changed

modules/references/references.go

Lines changed: 54 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -29,12 +29,14 @@ var (
2929
// mentionPattern matches all mentions in the form of "@user"
3030
mentionPattern = regexp.MustCompile(`(?:\s|^|\(|\[)(@[0-9a-zA-Z-_]+|@[0-9a-zA-Z-_][0-9a-zA-Z-_.]+[0-9a-zA-Z-_])(?:\s|[:,;.?!]\s|[:,;.?!]?$|\)|\])`)
3131
// issueNumericPattern matches string that references to a numeric issue, e.g. #1287
32-
issueNumericPattern = regexp.MustCompile(`(?:\s|^|\(|\[)([#!][0-9]+)(?:\s|$|\)|\]|:|\.(\s|$))`)
32+
issueNumericPattern = regexp.MustCompile(`(?:\s|^|\(|\[)([#!][0-9]+)(?:\s|$|\)|\]|[:;,.?!]\s|[:;,.?!]$)`)
3333
// issueAlphanumericPattern matches string that references to an alphanumeric issue, e.g. ABC-1234
3434
issueAlphanumericPattern = regexp.MustCompile(`(?:\s|^|\(|\[)([A-Z]{1,10}-[1-9][0-9]*)(?:\s|$|\)|\]|:|\.(\s|$))`)
3535
// crossReferenceIssueNumericPattern matches string that references a numeric issue in a different repository
3636
// e.g. gogits/gogs#12345
37-
crossReferenceIssueNumericPattern = regexp.MustCompile(`(?:\s|^|\(|\[)([0-9a-zA-Z-_\.]+/[0-9a-zA-Z-_\.]+[#!][0-9]+)(?:\s|$|\)|\]|\.(\s|$))`)
37+
crossReferenceIssueNumericPattern = regexp.MustCompile(`(?:\s|^|\(|\[)([0-9a-zA-Z-_\.]+/[0-9a-zA-Z-_\.]+[#!][0-9]+)(?:\s|$|\)|\]|[:;,.?!]\s|[:;,.?!]$)`)
38+
// spaceTrimmedPattern let's us find the trailing space
39+
spaceTrimmedPattern = regexp.MustCompile(`(?:.*[0-9a-zA-Z-_])\s`)
3840

3941
issueCloseKeywordsPat, issueReopenKeywordsPat *regexp.Regexp
4042
issueKeywordsOnce sync.Once
@@ -172,10 +174,24 @@ func FindAllMentionsMarkdown(content string) []string {
172174
// FindAllMentionsBytes matches mention patterns in given content
173175
// and returns a list of locations for the unvalidated user names, including the @ prefix.
174176
func FindAllMentionsBytes(content []byte) []RefSpan {
175-
mentions := mentionPattern.FindAllSubmatchIndex(content, -1)
176-
ret := make([]RefSpan, len(mentions))
177-
for i, val := range mentions {
178-
ret[i] = RefSpan{Start: val[2], End: val[3]}
177+
// Sadly we can't use FindAllSubmatchIndex because our pattern checks for starting and
178+
// trailing spaces (\s@mention,\s), so if we get two consecutive references, the space
179+
// from the second reference will be "eaten" by the first one:
180+
// ...\s@mention1\s@mention2\s... --> ...`\s@mention1\s`, (not) `@mention2,\s...`
181+
ret := make([]RefSpan, 0, 5)
182+
pos := 0
183+
for {
184+
match := mentionPattern.FindSubmatchIndex(content[pos:])
185+
if match == nil {
186+
break
187+
}
188+
ret = append(ret, RefSpan{Start: match[2] + pos, End: match[3] + pos})
189+
notrail := spaceTrimmedPattern.FindSubmatchIndex(content[match[2]+pos : match[3]+pos])
190+
if notrail == nil {
191+
pos = match[3] + pos
192+
} else {
193+
pos = match[3] + pos + notrail[1] - notrail[3]
194+
}
179195
}
180196
return ret
181197
}
@@ -252,19 +268,44 @@ func FindRenderizableReferenceAlphanumeric(content string) (bool, *RenderizableR
252268
func findAllIssueReferencesBytes(content []byte, links []string) []*rawReference {
253269

254270
ret := make([]*rawReference, 0, 10)
255-
256-
matches := issueNumericPattern.FindAllSubmatchIndex(content, -1)
257-
for _, match := range matches {
258-
if ref := getCrossReference(content, match[2], match[3], false, false); ref != nil {
271+
pos := 0
272+
273+
// Sadly we can't use FindAllSubmatchIndex because our pattern checks for starting and
274+
// trailing spaces (\s#ref,\s), so if we get two consecutive references, the space
275+
// from the second reference will be "eaten" by the first one:
276+
// ...\s#ref1\s#ref2\s... --> ...`\s#ref1\s`, (not) `#ref2,\s...`
277+
for {
278+
match := issueNumericPattern.FindSubmatchIndex(content[pos:])
279+
if match == nil {
280+
break
281+
}
282+
if ref := getCrossReference(content, match[2]+pos, match[3]+pos, false, false); ref != nil {
259283
ret = append(ret, ref)
260284
}
285+
notrail := spaceTrimmedPattern.FindSubmatchIndex(content[match[2]+pos : match[3]+pos])
286+
if notrail == nil {
287+
pos = match[3] + pos
288+
} else {
289+
pos = match[3] + pos + notrail[1] - notrail[3]
290+
}
261291
}
262292

263-
matches = crossReferenceIssueNumericPattern.FindAllSubmatchIndex(content, -1)
264-
for _, match := range matches {
265-
if ref := getCrossReference(content, match[2], match[3], false, false); ref != nil {
293+
pos = 0
294+
295+
for {
296+
match := crossReferenceIssueNumericPattern.FindSubmatchIndex(content[pos:])
297+
if match == nil {
298+
break
299+
}
300+
if ref := getCrossReference(content, match[2]+pos, match[3]+pos, false, false); ref != nil {
266301
ret = append(ret, ref)
267302
}
303+
notrail := spaceTrimmedPattern.FindSubmatchIndex(content[match[2]+pos : match[3]+pos])
304+
if notrail == nil {
305+
pos = match[3] + pos
306+
} else {
307+
pos = match[3] + pos + notrail[1] - notrail[3]
308+
}
268309
}
269310

270311
localhost := getGiteaHostName()

modules/references/references_test.go

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,39 @@ func TestFindAllIssueReferences(t *testing.T) {
135135
{1235, "", "", "1235", false, XRefActionNone, &RefSpan{Start: 8, End: 13}, nil},
136136
},
137137
},
138+
{
139+
"For [!123] yes",
140+
[]testResult{
141+
{123, "", "", "123", true, XRefActionNone, &RefSpan{Start: 5, End: 9}, nil},
142+
},
143+
},
144+
{
145+
"For (#345) yes",
146+
[]testResult{
147+
{345, "", "", "345", false, XRefActionNone, &RefSpan{Start: 5, End: 9}, nil},
148+
},
149+
},
150+
{
151+
"For #22,#23 no, neither #28:#29 or !30!31#32;33 should",
152+
[]testResult{},
153+
},
154+
{
155+
"For #24, and #25. yes; also #26; #27? #28! and #29: should",
156+
[]testResult{
157+
{24, "", "", "24", false, XRefActionNone, &RefSpan{Start: 4, End: 7}, nil},
158+
{25, "", "", "25", false, XRefActionNone, &RefSpan{Start: 13, End: 16}, nil},
159+
{26, "", "", "26", false, XRefActionNone, &RefSpan{Start: 28, End: 31}, nil},
160+
{27, "", "", "27", false, XRefActionNone, &RefSpan{Start: 33, End: 36}, nil},
161+
{28, "", "", "28", false, XRefActionNone, &RefSpan{Start: 38, End: 41}, nil},
162+
{29, "", "", "29", false, XRefActionNone, &RefSpan{Start: 47, End: 50}, nil},
163+
},
164+
},
165+
{
166+
"This user3/repo4#200, yes.",
167+
[]testResult{
168+
{200, "user3", "repo4", "200", false, XRefActionNone, &RefSpan{Start: 5, End: 20}, nil},
169+
},
170+
},
138171
{
139172
"Which abc. #9434 same as above",
140173
[]testResult{
@@ -217,6 +250,16 @@ func testFixtures(t *testing.T, fixtures []testFixture, context string) {
217250
setting.AppURL = prevURL
218251
}
219252

253+
func TestFindAllMentions(t *testing.T) {
254+
res := FindAllMentionsBytes([]byte("@tasha, @mike; @lucy: @john"))
255+
assert.EqualValues(t, []RefSpan{
256+
{Start: 0, End: 6},
257+
{Start: 8, End: 13},
258+
{Start: 15, End: 20},
259+
{Start: 22, End: 27},
260+
}, res)
261+
}
262+
220263
func TestRegExp_mentionPattern(t *testing.T) {
221264
trueTestCases := []struct {
222265
pat string

0 commit comments

Comments
 (0)