Skip to content

Commit 5481be0

Browse files
ethantkoeniglunny
authored andcommitted
Fix issue link rendering in commit messages (#2897)
* Fix issue link rendering in commit messages * Update page.tmpl * No links for parens * remove comment
1 parent 47f40cc commit 5481be0

File tree

7 files changed

+126
-52
lines changed

7 files changed

+126
-52
lines changed

modules/markup/html.go

Lines changed: 67 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -126,35 +126,82 @@ func URLJoin(base string, elems ...string) string {
126126
return u.String()
127127
}
128128

129+
// RenderIssueIndexPatternOptions options for RenderIssueIndexPattern function
130+
type RenderIssueIndexPatternOptions struct {
131+
// url to which non-special formatting should be linked. If empty,
132+
// no such links will be added
133+
DefaultURL string
134+
URLPrefix string
135+
Metas map[string]string
136+
}
137+
138+
// addText add text to the given buffer, adding a link to the default url
139+
// if appropriate
140+
func (opts RenderIssueIndexPatternOptions) addText(text []byte, buf *bytes.Buffer) {
141+
if len(text) == 0 {
142+
return
143+
} else if len(opts.DefaultURL) == 0 {
144+
buf.Write(text)
145+
return
146+
}
147+
buf.WriteString(`<a rel="nofollow" href="`)
148+
buf.WriteString(opts.DefaultURL)
149+
buf.WriteString(`">`)
150+
buf.Write(text)
151+
buf.WriteString(`</a>`)
152+
}
153+
129154
// RenderIssueIndexPattern renders issue indexes to corresponding links.
130-
func RenderIssueIndexPattern(rawBytes []byte, urlPrefix string, metas map[string]string) []byte {
131-
urlPrefix = cutoutVerbosePrefix(urlPrefix)
155+
func RenderIssueIndexPattern(rawBytes []byte, opts RenderIssueIndexPatternOptions) []byte {
156+
opts.URLPrefix = cutoutVerbosePrefix(opts.URLPrefix)
132157

133158
pattern := IssueNumericPattern
134-
if metas["style"] == IssueNameStyleAlphanumeric {
159+
if opts.Metas["style"] == IssueNameStyleAlphanumeric {
135160
pattern = IssueAlphanumericPattern
136161
}
137162

138-
ms := pattern.FindAll(rawBytes, -1)
139-
for _, m := range ms {
140-
if m[0] == ' ' || m[0] == '(' {
141-
m = m[1:] // ignore leading space or opening parentheses
163+
var buf bytes.Buffer
164+
remainder := rawBytes
165+
for {
166+
indices := pattern.FindIndex(remainder)
167+
if indices == nil || len(indices) < 2 {
168+
opts.addText(remainder, &buf)
169+
return buf.Bytes()
142170
}
143-
var link string
144-
if metas == nil {
145-
link = fmt.Sprintf(`<a href="%s">%s</a>`, URLJoin(urlPrefix, "issues", string(m[1:])), m)
171+
startIndex := indices[0]
172+
endIndex := indices[1]
173+
opts.addText(remainder[:startIndex], &buf)
174+
if remainder[startIndex] == '(' || remainder[startIndex] == ' ' {
175+
buf.WriteByte(remainder[startIndex])
176+
startIndex++
177+
}
178+
if opts.Metas == nil {
179+
buf.WriteString(`<a href="`)
180+
buf.WriteString(URLJoin(
181+
opts.URLPrefix, "issues", string(remainder[startIndex+1:endIndex])))
182+
buf.WriteString(`">`)
183+
buf.Write(remainder[startIndex:endIndex])
184+
buf.WriteString(`</a>`)
146185
} else {
147186
// Support for external issue tracker
148-
if metas["style"] == IssueNameStyleAlphanumeric {
149-
metas["index"] = string(m)
187+
buf.WriteString(`<a href="`)
188+
if opts.Metas["style"] == IssueNameStyleAlphanumeric {
189+
opts.Metas["index"] = string(remainder[startIndex:endIndex])
150190
} else {
151-
metas["index"] = string(m[1:])
191+
opts.Metas["index"] = string(remainder[startIndex+1 : endIndex])
152192
}
153-
link = fmt.Sprintf(`<a href="%s">%s</a>`, com.Expand(metas["format"], metas), m)
193+
buf.WriteString(com.Expand(opts.Metas["format"], opts.Metas))
194+
buf.WriteString(`">`)
195+
buf.Write(remainder[startIndex:endIndex])
196+
buf.WriteString(`</a>`)
154197
}
155-
rawBytes = bytes.Replace(rawBytes, m, []byte(link), 1)
198+
if endIndex < len(remainder) &&
199+
(remainder[endIndex] == ')' || remainder[endIndex] == ' ') {
200+
buf.WriteByte(remainder[endIndex])
201+
endIndex++
202+
}
203+
remainder = remainder[endIndex:]
156204
}
157-
return rawBytes
158205
}
159206

160207
// IsSameDomain checks if given url string has the same hostname as current Gitea instance
@@ -432,7 +479,10 @@ func RenderSpecialLink(rawBytes []byte, urlPrefix string, metas map[string]strin
432479

433480
rawBytes = RenderFullIssuePattern(rawBytes)
434481
rawBytes = RenderShortLinks(rawBytes, urlPrefix, false, isWikiMarkdown)
435-
rawBytes = RenderIssueIndexPattern(rawBytes, urlPrefix, metas)
482+
rawBytes = RenderIssueIndexPattern(rawBytes, RenderIssueIndexPatternOptions{
483+
URLPrefix: urlPrefix,
484+
Metas: metas,
485+
})
436486
rawBytes = RenderCrossReferenceIssueIndexPattern(rawBytes, urlPrefix, metas)
437487
rawBytes = renderFullSha1Pattern(rawBytes, urlPrefix)
438488
rawBytes = renderSha1CurrentPattern(rawBytes, urlPrefix)

modules/markup/html_test.go

Lines changed: 31 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -55,9 +55,12 @@ func link(href, contents string) string {
5555
return fmt.Sprintf("<a href=\"%s\">%s</a>", href, contents)
5656
}
5757

58-
func testRenderIssueIndexPattern(t *testing.T, input, expected string, metas map[string]string) {
59-
assert.Equal(t, expected,
60-
string(RenderIssueIndexPattern([]byte(input), AppSubURL, metas)))
58+
func testRenderIssueIndexPattern(t *testing.T, input, expected string, opts RenderIssueIndexPatternOptions) {
59+
if len(opts.URLPrefix) == 0 {
60+
opts.URLPrefix = AppSubURL
61+
}
62+
actual := string(RenderIssueIndexPattern([]byte(input), opts))
63+
assert.Equal(t, expected, actual)
6164
}
6265

6366
func TestURLJoin(t *testing.T) {
@@ -88,8 +91,8 @@ func TestURLJoin(t *testing.T) {
8891
func TestRender_IssueIndexPattern(t *testing.T) {
8992
// numeric: render inputs without valid mentions
9093
test := func(s string) {
91-
testRenderIssueIndexPattern(t, s, s, nil)
92-
testRenderIssueIndexPattern(t, s, s, numericMetas)
94+
testRenderIssueIndexPattern(t, s, s, RenderIssueIndexPatternOptions{})
95+
testRenderIssueIndexPattern(t, s, s, RenderIssueIndexPatternOptions{Metas: numericMetas})
9396
}
9497

9598
// should not render anything when there are no mentions
@@ -123,13 +126,13 @@ func TestRender_IssueIndexPattern2(t *testing.T) {
123126
links[i] = numericIssueLink(URLJoin(setting.AppSubURL, "issues"), index)
124127
}
125128
expectedNil := fmt.Sprintf(expectedFmt, links...)
126-
testRenderIssueIndexPattern(t, s, expectedNil, nil)
129+
testRenderIssueIndexPattern(t, s, expectedNil, RenderIssueIndexPatternOptions{})
127130

128131
for i, index := range indices {
129132
links[i] = numericIssueLink("https://someurl.com/someUser/someRepo/", index)
130133
}
131134
expectedNum := fmt.Sprintf(expectedFmt, links...)
132-
testRenderIssueIndexPattern(t, s, expectedNum, numericMetas)
135+
testRenderIssueIndexPattern(t, s, expectedNum, RenderIssueIndexPatternOptions{Metas: numericMetas})
133136
}
134137

135138
// should render freestanding mentions
@@ -155,7 +158,7 @@ func TestRender_IssueIndexPattern3(t *testing.T) {
155158

156159
// alphanumeric: render inputs without valid mentions
157160
test := func(s string) {
158-
testRenderIssueIndexPattern(t, s, s, alphanumericMetas)
161+
testRenderIssueIndexPattern(t, s, s, RenderIssueIndexPatternOptions{Metas: alphanumericMetas})
159162
}
160163
test("")
161164
test("this is a test")
@@ -187,13 +190,32 @@ func TestRender_IssueIndexPattern4(t *testing.T) {
187190
links[i] = alphanumIssueLink("https://someurl.com/someUser/someRepo/", name)
188191
}
189192
expected := fmt.Sprintf(expectedFmt, links...)
190-
testRenderIssueIndexPattern(t, s, expected, alphanumericMetas)
193+
testRenderIssueIndexPattern(t, s, expected, RenderIssueIndexPatternOptions{Metas: alphanumericMetas})
191194
}
192195
test("OTT-1234 test", "%s test", "OTT-1234")
193196
test("test T-12 issue", "test %s issue", "T-12")
194197
test("test issue ABCDEFGHIJ-1234567890", "test issue %s", "ABCDEFGHIJ-1234567890")
195198
}
196199

200+
func TestRenderIssueIndexPatternWithDefaultURL(t *testing.T) {
201+
setting.AppURL = AppURL
202+
setting.AppSubURL = AppSubURL
203+
204+
test := func(input string, expected string) {
205+
testRenderIssueIndexPattern(t, input, expected, RenderIssueIndexPatternOptions{
206+
DefaultURL: AppURL,
207+
})
208+
}
209+
test("hello #123 world",
210+
fmt.Sprintf(`<a rel="nofollow" href="%s">hello</a> `, AppURL)+
211+
fmt.Sprintf(`<a href="%sissues/123">#123</a> `, AppSubURL)+
212+
fmt.Sprintf(`<a rel="nofollow" href="%s">world</a>`, AppURL))
213+
test("hello (#123) world",
214+
fmt.Sprintf(`<a rel="nofollow" href="%s">hello </a>`, AppURL)+
215+
fmt.Sprintf(`(<a href="%sissues/123">#123</a>)`, AppSubURL)+
216+
fmt.Sprintf(`<a rel="nofollow" href="%s"> world</a>`, AppURL))
217+
}
218+
197219
func TestRender_AutoLink(t *testing.T) {
198220
setting.AppURL = AppURL
199221
setting.AppSubURL = AppSubURL

modules/templates/helper.go

Lines changed: 23 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,8 @@ func NewFuncMap() []template.FuncMap {
110110
"EscapePound": func(str string) string {
111111
return strings.NewReplacer("%", "%25", "#", "%23", " ", "%20", "?", "%3F").Replace(str)
112112
},
113-
"RenderCommitMessage": RenderCommitMessage,
113+
"RenderCommitMessage": RenderCommitMessage,
114+
"RenderCommitMessageLink": RenderCommitMessageLink,
114115
"ThemeColorMetaTag": func() string {
115116
return setting.UI.ThemeColorMetaTag
116117
},
@@ -252,28 +253,31 @@ func ReplaceLeft(s, old, new string) string {
252253
}
253254

254255
// RenderCommitMessage renders commit message with XSS-safe and special links.
255-
func RenderCommitMessage(full bool, msg, urlPrefix string, metas map[string]string) template.HTML {
256+
func RenderCommitMessage(msg, urlPrefix string, metas map[string]string) template.HTML {
257+
return renderCommitMessage(msg, markup.RenderIssueIndexPatternOptions{
258+
URLPrefix: urlPrefix,
259+
Metas: metas,
260+
})
261+
}
262+
263+
// RenderCommitMessageLink renders commit message as a XXS-safe link to the provided
264+
// default url, handling for special links.
265+
func RenderCommitMessageLink(msg, urlPrefix string, urlDefault string, metas map[string]string) template.HTML {
266+
return renderCommitMessage(msg, markup.RenderIssueIndexPatternOptions{
267+
DefaultURL: urlDefault,
268+
URLPrefix: urlPrefix,
269+
Metas: metas,
270+
})
271+
}
272+
273+
func renderCommitMessage(msg string, opts markup.RenderIssueIndexPatternOptions) template.HTML {
256274
cleanMsg := template.HTMLEscapeString(msg)
257-
fullMessage := string(markup.RenderIssueIndexPattern([]byte(cleanMsg), urlPrefix, metas))
275+
fullMessage := string(markup.RenderIssueIndexPattern([]byte(cleanMsg), opts))
258276
msgLines := strings.Split(strings.TrimSpace(fullMessage), "\n")
259-
numLines := len(msgLines)
260-
if numLines == 0 {
277+
if len(msgLines) == 0 {
261278
return template.HTML("")
262-
} else if !full {
263-
return template.HTML(msgLines[0])
264-
} else if numLines == 1 || (numLines >= 2 && len(msgLines[1]) == 0) {
265-
// First line is a header, standalone or followed by empty line
266-
header := fmt.Sprintf("<h3>%s</h3>", msgLines[0])
267-
if numLines >= 2 {
268-
fullMessage = header + fmt.Sprintf("\n<pre>%s</pre>", strings.Join(msgLines[2:], "\n"))
269-
} else {
270-
fullMessage = header
271-
}
272-
} else {
273-
// Non-standard git message, there is no header line
274-
fullMessage = fmt.Sprintf("<h4>%s</h4>", strings.Join(msgLines, "<br>"))
275279
}
276-
return template.HTML(fullMessage)
280+
return template.HTML(msgLines[0])
277281
}
278282

279283
// Actioner describes an action

templates/repo/commits_table.tmpl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@
6060
</a>
6161
</td>
6262
<td class="message collapsing">
63-
<span class="has-emoji{{if gt .ParentCount 1}} grey text{{end}}">{{RenderCommitMessage false .Summary $.RepoLink $.Repository.ComposeMetas}}</span>
63+
<span class="has-emoji{{if gt .ParentCount 1}} grey text{{end}}">{{RenderCommitMessage .Summary $.RepoLink $.Repository.ComposeMetas}}</span>
6464
{{template "repo/commit_status" .Status}}
6565
</td>
6666
<td class="grey text right aligned">{{TimeSince .Author.When $.Lang}}</td>

templates/repo/diff/page.tmpl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
<a class="ui floated right blue tiny button" href="{{EscapePound .SourcePath}}">
1010
{{.i18n.Tr "repo.diff.browse_source"}}
1111
</a>
12-
<h3>{{RenderCommitMessage false .Commit.Message $.RepoLink $.Repository.ComposeMetas}}{{template "repo/commit_status" .CommitStatus}}</h3>
12+
<h3>{{RenderCommitMessage .Commit.Message $.RepoLink $.Repository.ComposeMetas}}{{template "repo/commit_status" .CommitStatus}}</h3>
1313
</div>
1414
<div class="ui attached info segment {{if .Commit.Signature}} isSigned {{if .Verification.Verified }} isVerified {{end}}{{end}}">
1515
{{if .Author}}

templates/repo/graph.tmpl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
<a href="{{AppSubUrl}}/{{$.Username}}/{{$.Reponame}}/commit/{{.Rev}}">{{ .ShortRev}}</a>
2727
</code>
2828
<strong> {{.Branch}}</strong>
29-
<em>{{RenderCommitMessage false .Subject $.RepoLink $.Repository.ComposeMetas}}</em> by
29+
<em>{{RenderCommitMessage .Subject $.RepoLink $.Repository.ComposeMetas}}</em> by
3030
<span class="author">
3131
{{.Author}}
3232
</span>

templates/repo/view_list.tmpl

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727
</div>
2828
{{end}}
2929
</a>
30-
<span class="grey has-emoji">{{RenderCommitMessage false .LatestCommit.Summary .RepoLink $.Repository.ComposeMetas}}
30+
<span class="grey has-emoji">{{RenderCommitMessage .LatestCommit.Summary .RepoLink $.Repository.ComposeMetas}}
3131
{{template "repo/commit_status" .LatestCommitStatus}}</span>
3232
</th>
3333
<th class="nine wide">
@@ -75,9 +75,7 @@
7575
</td>
7676
{{end}}
7777
<td class="message collapsing has-emoji">
78-
<a rel="nofollow" href="{{$.RepoLink}}/commit/{{$commit.ID}}">
79-
{{RenderCommitMessage false $commit.Summary $.RepoLink $.Repository.ComposeMetas}}
80-
</a>
78+
{{RenderCommitMessageLink $commit.Summary $.RepoLink (print $.RepoLink "/commit/" $commit.ID) $.Repository.ComposeMetas}}
8179
</td>
8280
<td class="text grey right age">{{TimeSince $commit.Committer.When $.Lang}}</td>
8381
</tr>

0 commit comments

Comments
 (0)