Files
gitea-MIRROR/modules/markup/html_commit.go
silverwind 51879941dc enhance: improve commit and compare link rendering (#40154)
Commit, compare and SHA links render the SHA in code with the rest of
the URL after it, and links to other repositories get an `owner/repo@`
prefix like references do.

- Abbreviated SHAs, branch compares and `<url>` autolinks now render as
commit links
- Pull request commit URLs no longer break into an issue reference plus
stray text
- Only routes that take a SHA are shortened, so file and API URLs stay
plain

Replaces https://github.com/go-gitea/gitea/pull/36725

<img width="620" alt="Commit links before and after"
src="https://github.com/user-attachments/assets/5919c8a0-94e3-4148-ad11-6944bf790572"
/>
2026-11-07 22:43:07 +00:00

135 lines
4.8 KiB
Go

// Copyright 2024 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package markup
import (
"fmt"
"slices"
"strings"
"gitea.dev/modules/base"
"gitea.dev/modules/git"
"gitea.dev/modules/httplib"
"gitea.dev/modules/references"
"golang.org/x/net/html"
"golang.org/x/net/html/atom"
)
func setCommitLinkContent(anchor *html.Node, prefix, refs, rest string) {
anchor.Attr = append(anchor.Attr, html.Attribute{Key: "class", Val: "commit"}) // plain, so markdown strips it and commit messages keep it for CSS
anchor.FirstChild.Data = prefix
code := &html.Node{Type: html.ElementNode, Data: atom.Code.String()}
code.AppendChild(&html.Node{Type: html.TextNode, Data: refs})
anchor.AppendChild(code)
if rest != "" {
anchor.AppendChild(&html.Node{Type: html.TextNode, Data: rest})
}
}
func createCommitLink(ctx *RenderContext, href, prefix, sha string) *html.Node {
anchor := createLink(ctx, href, "", "")
setCommitLinkContent(anchor, prefix, base.ShortSha(sha), "")
return anchor
}
func otherRepoPrefix(ctx *RenderContext, owner, repo string) string {
if strings.EqualFold(owner, ctx.RenderOptions.Metas["user"]) && strings.EqualFold(repo, ctx.RenderOptions.Metas["repo"]) {
return ""
}
return owner + "/" + repo + "@"
}
// compare refs can also be branch or tag names, so only full SHAs are shortened
func shortCompareRef(ref string) string {
if git.IsStringValidObjectID(nil, ref) {
return base.ShortSha(ref)
}
return ref
}
// linkified URLs and "<url>" autolinks reach here as links whose text is their own URL
func shortenCommitLink(ctx *RenderContext, anchor *html.Node) {
text := anchor.FirstChild
if ctx.RenderOptions.Metas == nil || text == nil || text != anchor.LastChild || text.Type != html.TextNode || !strings.Contains(text.Data, "://") {
return
}
if !slices.ContainsFunc(anchor.Attr, func(attr html.Attribute) bool { return attr.Key == "href" && attr.Val == text.Data }) {
return
}
siteURL := httplib.ParseGiteaSiteURL(ctx, text.Data)
if siteURL == nil {
return
}
var refs, rest string
if match := globalVars().compareURLPattern.FindStringSubmatch(siteURL.RepoSubPath); match != nil {
refs, rest = shortCompareRef(match[1])+match[2]+shortCompareRef(match[3]), match[4]
} else if match := globalVars().commitURLPattern.FindStringSubmatch(siteURL.RepoSubPath); match != nil {
refs, rest = base.ShortSha(match[1]), match[2]
} else {
return
}
if idx := strings.IndexAny(text.Data, "?#"); idx != -1 {
rest += text.Data[idx:]
}
// quoting the link keeps its URL instead of the short text
anchor.Attr = slices.DeleteFunc(anchor.Attr, func(attr html.Attribute) bool { return attr.Key == "data-markdown-generated-content" })
setCommitLinkContent(anchor, otherRepoPrefix(ctx, siteURL.OwnerName, siteURL.RepoName), refs, rest)
}
// hashCurrentPatternProcessor renders SHA1 strings to corresponding links that
// are assumed to be in the same repository.
func hashCurrentPatternProcessor(ctx *RenderContext, node *html.Node) {
if ctx.RenderOptions.Metas == nil || ctx.RenderOptions.Metas["user"] == "" || ctx.RenderOptions.Metas["repo"] == "" || ctx.RenderHelper == nil {
return
}
start := 0
next := node.NextSibling
for node != nil && node != next && start < len(node.Data) {
m := globalVars().hashCurrentPattern.FindStringSubmatchIndex(node.Data[start:])
if m == nil {
return
}
m[2] += start
m[3] += start
hash := node.Data[m[2]:m[3]]
// The regex does not lie, it matches the hash pattern.
// However, a regex cannot know if a hash actually exists or not.
// We could assume that a SHA1 hash should probably contain alphas AND numerics
// but that is not always the case.
// Although unlikely, deadbeef and 1234567 are valid short forms of SHA1 hash
// as used by git and github for linking and thus we have to do similar.
// Because of this, we check to make sure that a matched hash is actually
// a commit in the repository before making it a link.
if !ctx.RenderHelper.IsCommitIDExisting(hash) {
start = m[3]
continue
}
link := fmt.Sprintf("/:root/%s/%s/commit/%s", ctx.RenderOptions.Metas["user"], ctx.RenderOptions.Metas["repo"], hash)
replaceContent(node, m[2], m[3], createCommitLink(ctx, link, "", hash))
start = 0
node = node.NextSibling.NextSibling
}
}
func commitCrossReferencePatternProcessor(ctx *RenderContext, node *html.Node) {
next := node.NextSibling
for node != nil && node != next {
found, ref := references.FindRenderizableCommitCrossReference(node.Data)
if !found {
return
}
linkHref := fmt.Sprintf("/:root/%s/%s/commit/%s", ref.Owner, ref.Name, ref.CommitSha)
link := createCommitLink(ctx, linkHref, otherRepoPrefix(ctx, ref.Owner, ref.Name), ref.CommitSha)
replaceContent(node, ref.RefLocation.Start, ref.RefLocation.End, link)
node = node.NextSibling.NextSibling
}
}