Fix issue link rendering in commit messages (#2897)
* Fix issue link rendering in commit messages * Update page.tmpl * No links for parens * remove comment
This commit is contained in:
parent
47f40ccd5e
commit
5481be0ac5
7 changed files with 126 additions and 52 deletions
|
@ -126,35 +126,82 @@ func URLJoin(base string, elems ...string) string {
|
||||||
return u.String()
|
return u.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RenderIssueIndexPatternOptions options for RenderIssueIndexPattern function
|
||||||
|
type RenderIssueIndexPatternOptions struct {
|
||||||
|
// url to which non-special formatting should be linked. If empty,
|
||||||
|
// no such links will be added
|
||||||
|
DefaultURL string
|
||||||
|
URLPrefix string
|
||||||
|
Metas map[string]string
|
||||||
|
}
|
||||||
|
|
||||||
|
// addText add text to the given buffer, adding a link to the default url
|
||||||
|
// if appropriate
|
||||||
|
func (opts RenderIssueIndexPatternOptions) addText(text []byte, buf *bytes.Buffer) {
|
||||||
|
if len(text) == 0 {
|
||||||
|
return
|
||||||
|
} else if len(opts.DefaultURL) == 0 {
|
||||||
|
buf.Write(text)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
buf.WriteString(`<a rel="nofollow" href="`)
|
||||||
|
buf.WriteString(opts.DefaultURL)
|
||||||
|
buf.WriteString(`">`)
|
||||||
|
buf.Write(text)
|
||||||
|
buf.WriteString(`</a>`)
|
||||||
|
}
|
||||||
|
|
||||||
// RenderIssueIndexPattern renders issue indexes to corresponding links.
|
// RenderIssueIndexPattern renders issue indexes to corresponding links.
|
||||||
func RenderIssueIndexPattern(rawBytes []byte, urlPrefix string, metas map[string]string) []byte {
|
func RenderIssueIndexPattern(rawBytes []byte, opts RenderIssueIndexPatternOptions) []byte {
|
||||||
urlPrefix = cutoutVerbosePrefix(urlPrefix)
|
opts.URLPrefix = cutoutVerbosePrefix(opts.URLPrefix)
|
||||||
|
|
||||||
pattern := IssueNumericPattern
|
pattern := IssueNumericPattern
|
||||||
if metas["style"] == IssueNameStyleAlphanumeric {
|
if opts.Metas["style"] == IssueNameStyleAlphanumeric {
|
||||||
pattern = IssueAlphanumericPattern
|
pattern = IssueAlphanumericPattern
|
||||||
}
|
}
|
||||||
|
|
||||||
ms := pattern.FindAll(rawBytes, -1)
|
var buf bytes.Buffer
|
||||||
for _, m := range ms {
|
remainder := rawBytes
|
||||||
if m[0] == ' ' || m[0] == '(' {
|
for {
|
||||||
m = m[1:] // ignore leading space or opening parentheses
|
indices := pattern.FindIndex(remainder)
|
||||||
|
if indices == nil || len(indices) < 2 {
|
||||||
|
opts.addText(remainder, &buf)
|
||||||
|
return buf.Bytes()
|
||||||
}
|
}
|
||||||
var link string
|
startIndex := indices[0]
|
||||||
if metas == nil {
|
endIndex := indices[1]
|
||||||
link = fmt.Sprintf(`<a href="%s">%s</a>`, URLJoin(urlPrefix, "issues", string(m[1:])), m)
|
opts.addText(remainder[:startIndex], &buf)
|
||||||
|
if remainder[startIndex] == '(' || remainder[startIndex] == ' ' {
|
||||||
|
buf.WriteByte(remainder[startIndex])
|
||||||
|
startIndex++
|
||||||
|
}
|
||||||
|
if opts.Metas == nil {
|
||||||
|
buf.WriteString(`<a href="`)
|
||||||
|
buf.WriteString(URLJoin(
|
||||||
|
opts.URLPrefix, "issues", string(remainder[startIndex+1:endIndex])))
|
||||||
|
buf.WriteString(`">`)
|
||||||
|
buf.Write(remainder[startIndex:endIndex])
|
||||||
|
buf.WriteString(`</a>`)
|
||||||
} else {
|
} else {
|
||||||
// Support for external issue tracker
|
// Support for external issue tracker
|
||||||
if metas["style"] == IssueNameStyleAlphanumeric {
|
buf.WriteString(`<a href="`)
|
||||||
metas["index"] = string(m)
|
if opts.Metas["style"] == IssueNameStyleAlphanumeric {
|
||||||
|
opts.Metas["index"] = string(remainder[startIndex:endIndex])
|
||||||
} else {
|
} else {
|
||||||
metas["index"] = string(m[1:])
|
opts.Metas["index"] = string(remainder[startIndex+1 : endIndex])
|
||||||
}
|
}
|
||||||
link = fmt.Sprintf(`<a href="%s">%s</a>`, com.Expand(metas["format"], metas), m)
|
buf.WriteString(com.Expand(opts.Metas["format"], opts.Metas))
|
||||||
|
buf.WriteString(`">`)
|
||||||
|
buf.Write(remainder[startIndex:endIndex])
|
||||||
|
buf.WriteString(`</a>`)
|
||||||
}
|
}
|
||||||
rawBytes = bytes.Replace(rawBytes, m, []byte(link), 1)
|
if endIndex < len(remainder) &&
|
||||||
|
(remainder[endIndex] == ')' || remainder[endIndex] == ' ') {
|
||||||
|
buf.WriteByte(remainder[endIndex])
|
||||||
|
endIndex++
|
||||||
|
}
|
||||||
|
remainder = remainder[endIndex:]
|
||||||
}
|
}
|
||||||
return rawBytes
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsSameDomain checks if given url string has the same hostname as current Gitea instance
|
// 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
|
||||||
|
|
||||||
rawBytes = RenderFullIssuePattern(rawBytes)
|
rawBytes = RenderFullIssuePattern(rawBytes)
|
||||||
rawBytes = RenderShortLinks(rawBytes, urlPrefix, false, isWikiMarkdown)
|
rawBytes = RenderShortLinks(rawBytes, urlPrefix, false, isWikiMarkdown)
|
||||||
rawBytes = RenderIssueIndexPattern(rawBytes, urlPrefix, metas)
|
rawBytes = RenderIssueIndexPattern(rawBytes, RenderIssueIndexPatternOptions{
|
||||||
|
URLPrefix: urlPrefix,
|
||||||
|
Metas: metas,
|
||||||
|
})
|
||||||
rawBytes = RenderCrossReferenceIssueIndexPattern(rawBytes, urlPrefix, metas)
|
rawBytes = RenderCrossReferenceIssueIndexPattern(rawBytes, urlPrefix, metas)
|
||||||
rawBytes = renderFullSha1Pattern(rawBytes, urlPrefix)
|
rawBytes = renderFullSha1Pattern(rawBytes, urlPrefix)
|
||||||
rawBytes = renderSha1CurrentPattern(rawBytes, urlPrefix)
|
rawBytes = renderSha1CurrentPattern(rawBytes, urlPrefix)
|
||||||
|
|
|
@ -55,9 +55,12 @@ func link(href, contents string) string {
|
||||||
return fmt.Sprintf("<a href=\"%s\">%s</a>", href, contents)
|
return fmt.Sprintf("<a href=\"%s\">%s</a>", href, contents)
|
||||||
}
|
}
|
||||||
|
|
||||||
func testRenderIssueIndexPattern(t *testing.T, input, expected string, metas map[string]string) {
|
func testRenderIssueIndexPattern(t *testing.T, input, expected string, opts RenderIssueIndexPatternOptions) {
|
||||||
assert.Equal(t, expected,
|
if len(opts.URLPrefix) == 0 {
|
||||||
string(RenderIssueIndexPattern([]byte(input), AppSubURL, metas)))
|
opts.URLPrefix = AppSubURL
|
||||||
|
}
|
||||||
|
actual := string(RenderIssueIndexPattern([]byte(input), opts))
|
||||||
|
assert.Equal(t, expected, actual)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestURLJoin(t *testing.T) {
|
func TestURLJoin(t *testing.T) {
|
||||||
|
@ -88,8 +91,8 @@ func TestURLJoin(t *testing.T) {
|
||||||
func TestRender_IssueIndexPattern(t *testing.T) {
|
func TestRender_IssueIndexPattern(t *testing.T) {
|
||||||
// numeric: render inputs without valid mentions
|
// numeric: render inputs without valid mentions
|
||||||
test := func(s string) {
|
test := func(s string) {
|
||||||
testRenderIssueIndexPattern(t, s, s, nil)
|
testRenderIssueIndexPattern(t, s, s, RenderIssueIndexPatternOptions{})
|
||||||
testRenderIssueIndexPattern(t, s, s, numericMetas)
|
testRenderIssueIndexPattern(t, s, s, RenderIssueIndexPatternOptions{Metas: numericMetas})
|
||||||
}
|
}
|
||||||
|
|
||||||
// should not render anything when there are no mentions
|
// should not render anything when there are no mentions
|
||||||
|
@ -123,13 +126,13 @@ func TestRender_IssueIndexPattern2(t *testing.T) {
|
||||||
links[i] = numericIssueLink(URLJoin(setting.AppSubURL, "issues"), index)
|
links[i] = numericIssueLink(URLJoin(setting.AppSubURL, "issues"), index)
|
||||||
}
|
}
|
||||||
expectedNil := fmt.Sprintf(expectedFmt, links...)
|
expectedNil := fmt.Sprintf(expectedFmt, links...)
|
||||||
testRenderIssueIndexPattern(t, s, expectedNil, nil)
|
testRenderIssueIndexPattern(t, s, expectedNil, RenderIssueIndexPatternOptions{})
|
||||||
|
|
||||||
for i, index := range indices {
|
for i, index := range indices {
|
||||||
links[i] = numericIssueLink("https://someurl.com/someUser/someRepo/", index)
|
links[i] = numericIssueLink("https://someurl.com/someUser/someRepo/", index)
|
||||||
}
|
}
|
||||||
expectedNum := fmt.Sprintf(expectedFmt, links...)
|
expectedNum := fmt.Sprintf(expectedFmt, links...)
|
||||||
testRenderIssueIndexPattern(t, s, expectedNum, numericMetas)
|
testRenderIssueIndexPattern(t, s, expectedNum, RenderIssueIndexPatternOptions{Metas: numericMetas})
|
||||||
}
|
}
|
||||||
|
|
||||||
// should render freestanding mentions
|
// should render freestanding mentions
|
||||||
|
@ -155,7 +158,7 @@ func TestRender_IssueIndexPattern3(t *testing.T) {
|
||||||
|
|
||||||
// alphanumeric: render inputs without valid mentions
|
// alphanumeric: render inputs without valid mentions
|
||||||
test := func(s string) {
|
test := func(s string) {
|
||||||
testRenderIssueIndexPattern(t, s, s, alphanumericMetas)
|
testRenderIssueIndexPattern(t, s, s, RenderIssueIndexPatternOptions{Metas: alphanumericMetas})
|
||||||
}
|
}
|
||||||
test("")
|
test("")
|
||||||
test("this is a test")
|
test("this is a test")
|
||||||
|
@ -187,13 +190,32 @@ func TestRender_IssueIndexPattern4(t *testing.T) {
|
||||||
links[i] = alphanumIssueLink("https://someurl.com/someUser/someRepo/", name)
|
links[i] = alphanumIssueLink("https://someurl.com/someUser/someRepo/", name)
|
||||||
}
|
}
|
||||||
expected := fmt.Sprintf(expectedFmt, links...)
|
expected := fmt.Sprintf(expectedFmt, links...)
|
||||||
testRenderIssueIndexPattern(t, s, expected, alphanumericMetas)
|
testRenderIssueIndexPattern(t, s, expected, RenderIssueIndexPatternOptions{Metas: alphanumericMetas})
|
||||||
}
|
}
|
||||||
test("OTT-1234 test", "%s test", "OTT-1234")
|
test("OTT-1234 test", "%s test", "OTT-1234")
|
||||||
test("test T-12 issue", "test %s issue", "T-12")
|
test("test T-12 issue", "test %s issue", "T-12")
|
||||||
test("test issue ABCDEFGHIJ-1234567890", "test issue %s", "ABCDEFGHIJ-1234567890")
|
test("test issue ABCDEFGHIJ-1234567890", "test issue %s", "ABCDEFGHIJ-1234567890")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestRenderIssueIndexPatternWithDefaultURL(t *testing.T) {
|
||||||
|
setting.AppURL = AppURL
|
||||||
|
setting.AppSubURL = AppSubURL
|
||||||
|
|
||||||
|
test := func(input string, expected string) {
|
||||||
|
testRenderIssueIndexPattern(t, input, expected, RenderIssueIndexPatternOptions{
|
||||||
|
DefaultURL: AppURL,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
test("hello #123 world",
|
||||||
|
fmt.Sprintf(`<a rel="nofollow" href="%s">hello</a> `, AppURL)+
|
||||||
|
fmt.Sprintf(`<a href="%sissues/123">#123</a> `, AppSubURL)+
|
||||||
|
fmt.Sprintf(`<a rel="nofollow" href="%s">world</a>`, AppURL))
|
||||||
|
test("hello (#123) world",
|
||||||
|
fmt.Sprintf(`<a rel="nofollow" href="%s">hello </a>`, AppURL)+
|
||||||
|
fmt.Sprintf(`(<a href="%sissues/123">#123</a>)`, AppSubURL)+
|
||||||
|
fmt.Sprintf(`<a rel="nofollow" href="%s"> world</a>`, AppURL))
|
||||||
|
}
|
||||||
|
|
||||||
func TestRender_AutoLink(t *testing.T) {
|
func TestRender_AutoLink(t *testing.T) {
|
||||||
setting.AppURL = AppURL
|
setting.AppURL = AppURL
|
||||||
setting.AppSubURL = AppSubURL
|
setting.AppSubURL = AppSubURL
|
||||||
|
|
|
@ -110,7 +110,8 @@ func NewFuncMap() []template.FuncMap {
|
||||||
"EscapePound": func(str string) string {
|
"EscapePound": func(str string) string {
|
||||||
return strings.NewReplacer("%", "%25", "#", "%23", " ", "%20", "?", "%3F").Replace(str)
|
return strings.NewReplacer("%", "%25", "#", "%23", " ", "%20", "?", "%3F").Replace(str)
|
||||||
},
|
},
|
||||||
"RenderCommitMessage": RenderCommitMessage,
|
"RenderCommitMessage": RenderCommitMessage,
|
||||||
|
"RenderCommitMessageLink": RenderCommitMessageLink,
|
||||||
"ThemeColorMetaTag": func() string {
|
"ThemeColorMetaTag": func() string {
|
||||||
return setting.UI.ThemeColorMetaTag
|
return setting.UI.ThemeColorMetaTag
|
||||||
},
|
},
|
||||||
|
@ -252,28 +253,31 @@ func ReplaceLeft(s, old, new string) string {
|
||||||
}
|
}
|
||||||
|
|
||||||
// RenderCommitMessage renders commit message with XSS-safe and special links.
|
// RenderCommitMessage renders commit message with XSS-safe and special links.
|
||||||
func RenderCommitMessage(full bool, msg, urlPrefix string, metas map[string]string) template.HTML {
|
func RenderCommitMessage(msg, urlPrefix string, metas map[string]string) template.HTML {
|
||||||
|
return renderCommitMessage(msg, markup.RenderIssueIndexPatternOptions{
|
||||||
|
URLPrefix: urlPrefix,
|
||||||
|
Metas: metas,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// RenderCommitMessageLink renders commit message as a XXS-safe link to the provided
|
||||||
|
// default url, handling for special links.
|
||||||
|
func RenderCommitMessageLink(msg, urlPrefix string, urlDefault string, metas map[string]string) template.HTML {
|
||||||
|
return renderCommitMessage(msg, markup.RenderIssueIndexPatternOptions{
|
||||||
|
DefaultURL: urlDefault,
|
||||||
|
URLPrefix: urlPrefix,
|
||||||
|
Metas: metas,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func renderCommitMessage(msg string, opts markup.RenderIssueIndexPatternOptions) template.HTML {
|
||||||
cleanMsg := template.HTMLEscapeString(msg)
|
cleanMsg := template.HTMLEscapeString(msg)
|
||||||
fullMessage := string(markup.RenderIssueIndexPattern([]byte(cleanMsg), urlPrefix, metas))
|
fullMessage := string(markup.RenderIssueIndexPattern([]byte(cleanMsg), opts))
|
||||||
msgLines := strings.Split(strings.TrimSpace(fullMessage), "\n")
|
msgLines := strings.Split(strings.TrimSpace(fullMessage), "\n")
|
||||||
numLines := len(msgLines)
|
if len(msgLines) == 0 {
|
||||||
if numLines == 0 {
|
|
||||||
return template.HTML("")
|
return template.HTML("")
|
||||||
} else if !full {
|
|
||||||
return template.HTML(msgLines[0])
|
|
||||||
} else if numLines == 1 || (numLines >= 2 && len(msgLines[1]) == 0) {
|
|
||||||
// First line is a header, standalone or followed by empty line
|
|
||||||
header := fmt.Sprintf("<h3>%s</h3>", msgLines[0])
|
|
||||||
if numLines >= 2 {
|
|
||||||
fullMessage = header + fmt.Sprintf("\n<pre>%s</pre>", strings.Join(msgLines[2:], "\n"))
|
|
||||||
} else {
|
|
||||||
fullMessage = header
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Non-standard git message, there is no header line
|
|
||||||
fullMessage = fmt.Sprintf("<h4>%s</h4>", strings.Join(msgLines, "<br>"))
|
|
||||||
}
|
}
|
||||||
return template.HTML(fullMessage)
|
return template.HTML(msgLines[0])
|
||||||
}
|
}
|
||||||
|
|
||||||
// Actioner describes an action
|
// Actioner describes an action
|
||||||
|
|
|
@ -60,7 +60,7 @@
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
<td class="message collapsing">
|
<td class="message collapsing">
|
||||||
<span class="has-emoji{{if gt .ParentCount 1}} grey text{{end}}">{{RenderCommitMessage false .Summary $.RepoLink $.Repository.ComposeMetas}}</span>
|
<span class="has-emoji{{if gt .ParentCount 1}} grey text{{end}}">{{RenderCommitMessage .Summary $.RepoLink $.Repository.ComposeMetas}}</span>
|
||||||
{{template "repo/commit_status" .Status}}
|
{{template "repo/commit_status" .Status}}
|
||||||
</td>
|
</td>
|
||||||
<td class="grey text right aligned">{{TimeSince .Author.When $.Lang}}</td>
|
<td class="grey text right aligned">{{TimeSince .Author.When $.Lang}}</td>
|
||||||
|
|
|
@ -9,7 +9,7 @@
|
||||||
<a class="ui floated right blue tiny button" href="{{EscapePound .SourcePath}}">
|
<a class="ui floated right blue tiny button" href="{{EscapePound .SourcePath}}">
|
||||||
{{.i18n.Tr "repo.diff.browse_source"}}
|
{{.i18n.Tr "repo.diff.browse_source"}}
|
||||||
</a>
|
</a>
|
||||||
<h3>{{RenderCommitMessage false .Commit.Message $.RepoLink $.Repository.ComposeMetas}}{{template "repo/commit_status" .CommitStatus}}</h3>
|
<h3>{{RenderCommitMessage .Commit.Message $.RepoLink $.Repository.ComposeMetas}}{{template "repo/commit_status" .CommitStatus}}</h3>
|
||||||
</div>
|
</div>
|
||||||
<div class="ui attached info segment {{if .Commit.Signature}} isSigned {{if .Verification.Verified }} isVerified {{end}}{{end}}">
|
<div class="ui attached info segment {{if .Commit.Signature}} isSigned {{if .Verification.Verified }} isVerified {{end}}{{end}}">
|
||||||
{{if .Author}}
|
{{if .Author}}
|
||||||
|
|
|
@ -26,7 +26,7 @@
|
||||||
<a href="{{AppSubUrl}}/{{$.Username}}/{{$.Reponame}}/commit/{{.Rev}}">{{ .ShortRev}}</a>
|
<a href="{{AppSubUrl}}/{{$.Username}}/{{$.Reponame}}/commit/{{.Rev}}">{{ .ShortRev}}</a>
|
||||||
</code>
|
</code>
|
||||||
<strong> {{.Branch}}</strong>
|
<strong> {{.Branch}}</strong>
|
||||||
<em>{{RenderCommitMessage false .Subject $.RepoLink $.Repository.ComposeMetas}}</em> by
|
<em>{{RenderCommitMessage .Subject $.RepoLink $.Repository.ComposeMetas}}</em> by
|
||||||
<span class="author">
|
<span class="author">
|
||||||
{{.Author}}
|
{{.Author}}
|
||||||
</span>
|
</span>
|
||||||
|
|
|
@ -27,7 +27,7 @@
|
||||||
</div>
|
</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
</a>
|
</a>
|
||||||
<span class="grey has-emoji">{{RenderCommitMessage false .LatestCommit.Summary .RepoLink $.Repository.ComposeMetas}}
|
<span class="grey has-emoji">{{RenderCommitMessage .LatestCommit.Summary .RepoLink $.Repository.ComposeMetas}}
|
||||||
{{template "repo/commit_status" .LatestCommitStatus}}</span>
|
{{template "repo/commit_status" .LatestCommitStatus}}</span>
|
||||||
</th>
|
</th>
|
||||||
<th class="nine wide">
|
<th class="nine wide">
|
||||||
|
@ -75,9 +75,7 @@
|
||||||
</td>
|
</td>
|
||||||
{{end}}
|
{{end}}
|
||||||
<td class="message collapsing has-emoji">
|
<td class="message collapsing has-emoji">
|
||||||
<a rel="nofollow" href="{{$.RepoLink}}/commit/{{$commit.ID}}">
|
{{RenderCommitMessageLink $commit.Summary $.RepoLink (print $.RepoLink "/commit/" $commit.ID) $.Repository.ComposeMetas}}
|
||||||
{{RenderCommitMessage false $commit.Summary $.RepoLink $.Repository.ComposeMetas}}
|
|
||||||
</a>
|
|
||||||
</td>
|
</td>
|
||||||
<td class="text grey right age">{{TimeSince $commit.Committer.When $.Lang}}</td>
|
<td class="text grey right age">{{TimeSince $commit.Committer.When $.Lang}}</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
Reference in a new issue