diff --git a/modules/markup/html.go b/modules/markup/html.go
index a635ce219b..4943bdf4a5 100644
--- a/modules/markup/html.go
+++ b/modules/markup/html.go
@@ -175,16 +175,25 @@ var emojiProcessors = []processor{
emojiProcessor,
}
+// isBareURLSubject reports whether the (HTML-escaped) commit subject content
+// is entirely a single URL, ignoring leading/trailing whitespace.
+func isBareURLSubject(content string) bool {
+ s := strings.TrimSpace(html.UnescapeString(content))
+ if s == "" {
+ return false
+ }
+ m := common.GlobalVars().LinkRegex.FindStringIndex(s)
+ return m != nil && m[0] == 0 && m[1] == len(s)
+}
+
// PostProcessCommitMessageSubject will use the same logic as PostProcess and
// PostProcessCommitMessage, but will disable the shortLinkProcessor and
-// emailAddressProcessor, will add a defaultLinkProcessor if defaultLink is set,
-// which changes every text node into a link to the passed default link.
+// emailAddressProcessor, and wraps the whole subject in defaultLink.
func PostProcessCommitMessageSubject(ctx *RenderContext, defaultLink, content string) (string, error) {
procs := []processor{
fullIssuePatternProcessor,
comparePatternProcessor,
fullHashPatternProcessor,
- linkProcessor,
mentionProcessor,
issueIndexPatternProcessor,
commitCrossReferencePatternProcessor,
@@ -192,6 +201,15 @@ func PostProcessCommitMessageSubject(ctx *RenderContext, defaultLink, content st
emojiShortCodeProcessor,
emojiProcessor,
}
+ // When the whole subject is a bare URL, linkProcessor would turn it into
+ // a competing anchor and hijack the surrounding defaultLink wrapper, leaving
+ // the subject visually unclickable. Match GitHub: render such subjects as
+ // plain text inside defaultLink. Partial URLs inside larger text still become
+ // their own links (nested anchors aren't legal HTML, so the outer defaultLink
+ // naturally breaks on that span, same as on GitHub).
+ if !isBareURLSubject(content) {
+ procs = append(procs, linkProcessor)
+ }
procs = append(procs, func(ctx *RenderContext, node *html.Node) {
ch := &html.Node{Parent: node, Type: html.TextNode, Data: node.Data}
node.Type = html.ElementNode
diff --git a/modules/templates/util_render_test.go b/modules/templates/util_render_test.go
index f732be014a..be1190cc49 100644
--- a/modules/templates/util_render_test.go
+++ b/modules/templates/util_render_test.go
@@ -140,6 +140,18 @@ com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb mit
assert.EqualValues(t, expected, newTestRenderUtils(t).RenderCommitMessageLinkSubject(testInput(), "https://example.com/link", mockRepo))
})
+ t.Run("RenderCommitMessageLinkSubjectURLOnly", func(t *testing.T) {
+ // a bare URL in the subject must not hijack the default link
+ expected := `https://example.com/file.bin`
+ assert.EqualValues(t, expected, newTestRenderUtils(t).RenderCommitMessageLinkSubject("https://example.com/file.bin", "https://example.com/link", mockRepo))
+ })
+
+ t.Run("RenderCommitMessageLinkSubjectPartialURL", func(t *testing.T) {
+ // a URL embedded in larger subject text still becomes its own link
+ expected := `see https://example.com/x here`
+ assert.EqualValues(t, expected, newTestRenderUtils(t).RenderCommitMessageLinkSubject("see https://example.com/x here", "https://example.com/link", mockRepo))
+ })
+
t.Run("RenderIssueTitle", func(t *testing.T) {
defer test.MockVariableValue(&markup.RenderBehaviorForTesting.DisableAdditionalAttributes, true)()
expected := ` space @mention-user