mirror of
https://github.com/go-gitea/gitea.git
synced 2026-11-09 17:37:20 -06:00
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" />
This commit is contained in:
@@ -32,12 +32,11 @@ const (
|
|||||||
type globalVarsType struct {
|
type globalVarsType struct {
|
||||||
hashCurrentPattern *regexp.Regexp
|
hashCurrentPattern *regexp.Regexp
|
||||||
shortLinkPattern *regexp.Regexp
|
shortLinkPattern *regexp.Regexp
|
||||||
anyHashPattern *regexp.Regexp
|
compareURLPattern *regexp.Regexp
|
||||||
comparePattern *regexp.Regexp
|
commitURLPattern *regexp.Regexp
|
||||||
emailRegex *regexp.Regexp
|
emailRegex *regexp.Regexp
|
||||||
emojiShortCodeRegex *regexp.Regexp
|
emojiShortCodeRegex *regexp.Regexp
|
||||||
issueFullPattern *regexp.Regexp
|
issueFullPattern *regexp.Regexp
|
||||||
filesChangedFullPattern *regexp.Regexp
|
|
||||||
codePreviewPattern *regexp.Regexp
|
codePreviewPattern *regexp.Regexp
|
||||||
|
|
||||||
tagCleaner *regexp.Regexp
|
tagCleaner *regexp.Regexp
|
||||||
@@ -61,11 +60,9 @@ var globalVars = sync.OnceValue(func() *globalVarsType {
|
|||||||
// shortLinkPattern matches short but difficult to parse [[name|link|arg=test]] syntax
|
// shortLinkPattern matches short but difficult to parse [[name|link|arg=test]] syntax
|
||||||
v.shortLinkPattern = regexp.MustCompile(`\[\[(.*?)\]\](\w*)`)
|
v.shortLinkPattern = regexp.MustCompile(`\[\[(.*?)\]\](\w*)`)
|
||||||
|
|
||||||
// anyHashPattern splits url containing SHA into parts
|
// repo sub-paths of the compare page and of the routes that take a commit SHA
|
||||||
v.anyHashPattern = regexp.MustCompile(`https?://(?:\S+/){4,5}([0-9a-f]{40,64})((\.\w+)*)(/[-+~%./\w]+)?(\?[-+~%.\w&=]+)?(#[-+~%.\w]+)?`)
|
v.compareURLPattern = regexp.MustCompile(`^/compare/(.+?)(\.\.\.?)(.+?)((?:\.diff|\.patch)?)$`)
|
||||||
|
v.commitURLPattern = regexp.MustCompile(`^/(?:(?:[a-z-]+/)?commit|blob|archive|pulls/[0-9]+/commits)/([0-9a-f]{7,64})((?:[./].*)?)$`)
|
||||||
// comparePattern matches "http://domain/org/repo/compare/COMMIT1...COMMIT2#hash"
|
|
||||||
v.comparePattern = regexp.MustCompile(`https?://(?:\S+/){4,5}([0-9a-f]{7,64})(\.\.\.?)([0-9a-f]{7,64})?(#[-+~_%.a-zA-Z0-9]+)?`)
|
|
||||||
|
|
||||||
// emailRegex is definitely not perfect with edge cases,
|
// emailRegex is definitely not perfect with edge cases,
|
||||||
// it is still accepted by the CommonMark specification, as well as the HTML5 spec:
|
// it is still accepted by the CommonMark specification, as well as the HTML5 spec:
|
||||||
@@ -80,9 +77,6 @@ var globalVars = sync.OnceValue(func() *globalVarsType {
|
|||||||
// example: https://domain/org/repo/pulls/27#hash
|
// example: https://domain/org/repo/pulls/27#hash
|
||||||
v.issueFullPattern = regexp.MustCompile(`https?://(?:\S+/)[\w_.-]+/[\w_.-]+/(?:issues|pulls)/((?:\w{1,10}-)?[1-9][0-9]*)([\?|#](\S+)?)?\b`)
|
v.issueFullPattern = regexp.MustCompile(`https?://(?:\S+/)[\w_.-]+/[\w_.-]+/(?:issues|pulls)/((?:\w{1,10}-)?[1-9][0-9]*)([\?|#](\S+)?)?\b`)
|
||||||
|
|
||||||
// example: https://domain/org/repo/pulls/27/files#hash
|
|
||||||
v.filesChangedFullPattern = regexp.MustCompile(`https?://(?:\S+/)[\w_.-]+/[\w_.-]+/pulls/((?:\w{1,10}-)?[1-9][0-9]*)/files([\?|#](\S+)?)?\b`)
|
|
||||||
|
|
||||||
// codePreviewPattern matches "http://domain/.../{owner}/{repo}/src/commit/{commit}/{filepath}#L10-L20"
|
// codePreviewPattern matches "http://domain/.../{owner}/{repo}/src/commit/{commit}/{filepath}#L10-L20"
|
||||||
v.codePreviewPattern = regexp.MustCompile(`https?://\S+/([^\s/]+)/([^\s/]+)/src/commit/([0-9a-f]{7,64})(/\S+)#(L\d+(-L\d+)?)`)
|
v.codePreviewPattern = regexp.MustCompile(`https?://\S+/([^\s/]+)/([^\s/]+)/src/commit/([0-9a-f]{7,64})(/\S+)#(L\d+(-L\d+)?)`)
|
||||||
|
|
||||||
@@ -102,9 +96,7 @@ type processor func(ctx *RenderContext, node *html.Node)
|
|||||||
func PostProcessDefault(ctx *RenderContext, input io.Reader, output io.Writer) error {
|
func PostProcessDefault(ctx *RenderContext, input io.Reader, output io.Writer) error {
|
||||||
procs := []processor{
|
procs := []processor{
|
||||||
fullIssuePatternProcessor,
|
fullIssuePatternProcessor,
|
||||||
comparePatternProcessor,
|
|
||||||
codePreviewPatternProcessor,
|
codePreviewPatternProcessor,
|
||||||
fullHashPatternProcessor,
|
|
||||||
shortLinkProcessor,
|
shortLinkProcessor,
|
||||||
linkProcessor,
|
linkProcessor,
|
||||||
mentionProcessor,
|
mentionProcessor,
|
||||||
@@ -122,8 +114,6 @@ func PostProcessDefault(ctx *RenderContext, input io.Reader, output io.Writer) e
|
|||||||
func PostProcessCommitMessage(ctx *RenderContext, content template.HTML) template.HTML {
|
func PostProcessCommitMessage(ctx *RenderContext, content template.HTML) template.HTML {
|
||||||
procs := []processor{
|
procs := []processor{
|
||||||
fullIssuePatternProcessor,
|
fullIssuePatternProcessor,
|
||||||
comparePatternProcessor,
|
|
||||||
fullHashPatternProcessor,
|
|
||||||
linkProcessor,
|
linkProcessor,
|
||||||
mentionProcessor,
|
mentionProcessor,
|
||||||
issueIndexPatternProcessor,
|
issueIndexPatternProcessor,
|
||||||
@@ -147,8 +137,6 @@ var emojiProcessors = []processor{
|
|||||||
func PostProcessCommitMessageSubject(ctx *RenderContext, defaultLink, content string) template.HTML {
|
func PostProcessCommitMessageSubject(ctx *RenderContext, defaultLink, content string) template.HTML {
|
||||||
procs := []processor{
|
procs := []processor{
|
||||||
fullIssuePatternProcessor,
|
fullIssuePatternProcessor,
|
||||||
comparePatternProcessor,
|
|
||||||
fullHashPatternProcessor,
|
|
||||||
mentionProcessor,
|
mentionProcessor,
|
||||||
issueIndexPatternProcessor,
|
issueIndexPatternProcessor,
|
||||||
commitCrossReferencePatternProcessor,
|
commitCrossReferencePatternProcessor,
|
||||||
@@ -361,6 +349,7 @@ func visitNode(ctx *RenderContext, procs []processor, node *html.Node) *html.Nod
|
|||||||
}
|
}
|
||||||
|
|
||||||
if node.Data == "a" {
|
if node.Data == "a" {
|
||||||
|
shortenCommitLink(ctx, node)
|
||||||
processNodeA(ctx, node)
|
processNodeA(ctx, node)
|
||||||
// only use emoji processors for the content in the "A" tag,
|
// only use emoji processors for the content in the "A" tag,
|
||||||
// because the content there is not processable, for example: the content is a commit id or a full URL.
|
// because the content there is not processable, for example: the content is a commit id or a full URL.
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"gitea.dev/modules/base"
|
"gitea.dev/modules/base"
|
||||||
|
"gitea.dev/modules/git"
|
||||||
"gitea.dev/modules/httplib"
|
"gitea.dev/modules/httplib"
|
||||||
"gitea.dev/modules/references"
|
"gitea.dev/modules/references"
|
||||||
|
|
||||||
@@ -16,176 +17,65 @@ import (
|
|||||||
"golang.org/x/net/html/atom"
|
"golang.org/x/net/html/atom"
|
||||||
)
|
)
|
||||||
|
|
||||||
type anyHashPatternResult struct {
|
func setCommitLinkContent(anchor *html.Node, prefix, refs, rest string) {
|
||||||
PosStart int
|
anchor.Attr = append(anchor.Attr, html.Attribute{Key: "class", Val: "commit"}) // plain, so markdown strips it and commit messages keep it for CSS
|
||||||
PosEnd int
|
anchor.FirstChild.Data = prefix
|
||||||
FullURL string
|
code := &html.Node{Type: html.ElementNode, Data: atom.Code.String()}
|
||||||
CommitID string
|
code.AppendChild(&html.Node{Type: html.TextNode, Data: refs})
|
||||||
CommitExt string
|
anchor.AppendChild(code)
|
||||||
SubPath string
|
if rest != "" {
|
||||||
QueryParams string
|
anchor.AppendChild(&html.Node{Type: html.TextNode, Data: rest})
|
||||||
QueryHash string
|
|
||||||
}
|
|
||||||
|
|
||||||
func createCodeLink(href, content, class string) *html.Node {
|
|
||||||
a := &html.Node{
|
|
||||||
Type: html.ElementNode,
|
|
||||||
Data: atom.A.String(),
|
|
||||||
Attr: []html.Attribute{{Key: "href", Val: href}},
|
|
||||||
}
|
|
||||||
|
|
||||||
if class != "" {
|
|
||||||
a.Attr = append(a.Attr, html.Attribute{Key: "class", Val: class})
|
|
||||||
}
|
|
||||||
|
|
||||||
text := &html.Node{
|
|
||||||
Type: html.TextNode,
|
|
||||||
Data: content,
|
|
||||||
}
|
|
||||||
|
|
||||||
code := &html.Node{
|
|
||||||
Type: html.ElementNode,
|
|
||||||
Data: atom.Code.String(),
|
|
||||||
}
|
|
||||||
|
|
||||||
code.AppendChild(text)
|
|
||||||
a.AppendChild(code)
|
|
||||||
return a
|
|
||||||
}
|
|
||||||
|
|
||||||
func anyHashPatternExtract(s string) (ret anyHashPatternResult, ok bool) {
|
|
||||||
m := globalVars().anyHashPattern.FindStringSubmatchIndex(s)
|
|
||||||
if m == nil {
|
|
||||||
return ret, false
|
|
||||||
}
|
|
||||||
|
|
||||||
pos := 0
|
|
||||||
|
|
||||||
ret.PosStart, ret.PosEnd = m[pos], m[pos+1]
|
|
||||||
pos += 2
|
|
||||||
|
|
||||||
ret.FullURL = s[ret.PosStart:ret.PosEnd]
|
|
||||||
if strings.HasSuffix(ret.FullURL, ".") {
|
|
||||||
// if url ends in '.', it's very likely that it is not part of the actual url but used to finish a sentence.
|
|
||||||
ret.PosEnd--
|
|
||||||
ret.FullURL = ret.FullURL[:len(ret.FullURL)-1]
|
|
||||||
for i := range m {
|
|
||||||
m[i] = min(m[i], ret.PosEnd)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ret.CommitID = s[m[pos]:m[pos+1]]
|
func createCommitLink(ctx *RenderContext, href, prefix, sha string) *html.Node {
|
||||||
pos += 2
|
anchor := createLink(ctx, href, "", "")
|
||||||
|
setCommitLinkContent(anchor, prefix, base.ShortSha(sha), "")
|
||||||
ret.CommitExt = s[m[pos]:m[pos+1]]
|
return anchor
|
||||||
pos += 4
|
|
||||||
|
|
||||||
if m[pos] > 0 {
|
|
||||||
ret.SubPath = s[m[pos]:m[pos+1]]
|
|
||||||
}
|
|
||||||
pos += 2
|
|
||||||
|
|
||||||
if m[pos] > 0 {
|
|
||||||
ret.QueryParams = s[m[pos]:m[pos+1]]
|
|
||||||
}
|
|
||||||
pos += 2
|
|
||||||
|
|
||||||
if m[pos] > 0 {
|
|
||||||
ret.QueryHash = s[m[pos]:m[pos+1]][1:]
|
|
||||||
}
|
|
||||||
return ret, true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// fullHashPatternProcessor renders SHA containing URLs
|
func otherRepoPrefix(ctx *RenderContext, owner, repo string) string {
|
||||||
func fullHashPatternProcessor(ctx *RenderContext, node *html.Node) {
|
if strings.EqualFold(owner, ctx.RenderOptions.Metas["user"]) && strings.EqualFold(repo, ctx.RenderOptions.Metas["repo"]) {
|
||||||
if ctx.RenderOptions.Metas == nil {
|
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
|
return
|
||||||
}
|
}
|
||||||
nodeStop := node.NextSibling
|
if !slices.ContainsFunc(anchor.Attr, func(attr html.Attribute) bool { return attr.Key == "href" && attr.Val == text.Data }) {
|
||||||
for node != nodeStop {
|
|
||||||
if node.Type != html.TextNode {
|
|
||||||
node = node.NextSibling
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
ret, ok := anyHashPatternExtract(node.Data)
|
|
||||||
if !ok {
|
|
||||||
node = node.NextSibling
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
text := base.ShortSha(ret.CommitID)
|
|
||||||
if ret.CommitExt != "" {
|
|
||||||
text += ret.CommitExt
|
|
||||||
}
|
|
||||||
if ret.SubPath != "" {
|
|
||||||
text += ret.SubPath
|
|
||||||
}
|
|
||||||
if ret.QueryHash != "" {
|
|
||||||
text += " (" + ret.QueryHash + ")"
|
|
||||||
}
|
|
||||||
// only turn commit links to the current instance into hash link
|
|
||||||
if !httplib.IsCurrentGiteaSiteURL(ctx, ret.FullURL) {
|
|
||||||
node = node.NextSibling
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
replaceContent(node, ret.PosStart, ret.PosEnd, createCodeLink(ret.FullURL, text, "commit"))
|
|
||||||
node = node.NextSibling.NextSibling
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func comparePatternProcessor(ctx *RenderContext, node *html.Node) {
|
|
||||||
if ctx.RenderOptions.Metas == nil {
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
nodeStop := node.NextSibling
|
siteURL := httplib.ParseGiteaSiteURL(ctx, text.Data)
|
||||||
for node != nodeStop {
|
if siteURL == nil {
|
||||||
if node.Type != html.TextNode {
|
return
|
||||||
node = node.NextSibling
|
|
||||||
continue
|
|
||||||
}
|
}
|
||||||
m := globalVars().comparePattern.FindStringSubmatchIndex(node.Data)
|
var refs, rest string
|
||||||
if m == nil || slices.Contains(m[:8], -1) { // ensure that every group (m[0]...m[7]) has a match
|
if match := globalVars().compareURLPattern.FindStringSubmatch(siteURL.RepoSubPath); match != nil {
|
||||||
node = node.NextSibling
|
refs, rest = shortCompareRef(match[1])+match[2]+shortCompareRef(match[3]), match[4]
|
||||||
continue
|
} 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 {
|
||||||
urlFull := node.Data[m[0]:m[1]]
|
rest += text.Data[idx:]
|
||||||
text1 := base.ShortSha(node.Data[m[2]:m[3]])
|
|
||||||
textDots := base.ShortSha(node.Data[m[4]:m[5]])
|
|
||||||
text2 := base.ShortSha(node.Data[m[6]:m[7]])
|
|
||||||
|
|
||||||
hash := ""
|
|
||||||
if m[9] > 0 {
|
|
||||||
hash = node.Data[m[8]:m[9]][1:]
|
|
||||||
}
|
|
||||||
|
|
||||||
start := m[0]
|
|
||||||
end := m[1]
|
|
||||||
|
|
||||||
// If url ends in '.', it's very likely that it is not part of the
|
|
||||||
// actual url but used to finish a sentence.
|
|
||||||
if strings.HasSuffix(urlFull, ".") {
|
|
||||||
end--
|
|
||||||
urlFull = urlFull[:len(urlFull)-1]
|
|
||||||
if hash != "" {
|
|
||||||
hash = hash[:len(hash)-1]
|
|
||||||
} else if text2 != "" {
|
|
||||||
text2 = text2[:len(text2)-1]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// only turn compare links to the current instance into hash link
|
|
||||||
if !httplib.IsCurrentGiteaSiteURL(ctx, urlFull) {
|
|
||||||
node = node.NextSibling
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
text := text1 + textDots + text2
|
|
||||||
if hash != "" {
|
|
||||||
text += " (" + hash + ")"
|
|
||||||
}
|
|
||||||
replaceContent(node, start, end, createCodeLink(urlFull, text, "compare"))
|
|
||||||
node = node.NextSibling.NextSibling
|
|
||||||
}
|
}
|
||||||
|
// 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
|
// hashCurrentPatternProcessor renders SHA1 strings to corresponding links that
|
||||||
@@ -220,7 +110,7 @@ func hashCurrentPatternProcessor(ctx *RenderContext, node *html.Node) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
link := fmt.Sprintf("/:root/%s/%s/commit/%s", ctx.RenderOptions.Metas["user"], ctx.RenderOptions.Metas["repo"], hash)
|
link := fmt.Sprintf("/:root/%s/%s/commit/%s", ctx.RenderOptions.Metas["user"], ctx.RenderOptions.Metas["repo"], hash)
|
||||||
replaceContent(node, m[2], m[3], createCodeLink(link, base.ShortSha(hash), "commit"))
|
replaceContent(node, m[2], m[3], createCommitLink(ctx, link, "", hash))
|
||||||
start = 0
|
start = 0
|
||||||
node = node.NextSibling.NextSibling
|
node = node.NextSibling.NextSibling
|
||||||
}
|
}
|
||||||
@@ -235,9 +125,8 @@ func commitCrossReferencePatternProcessor(ctx *RenderContext, node *html.Node) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
refText := ref.Owner + "/" + ref.Name + "@" + base.ShortSha(ref.CommitSha)
|
|
||||||
linkHref := fmt.Sprintf("/:root/%s/%s/commit/%s", ref.Owner, ref.Name, ref.CommitSha)
|
linkHref := fmt.Sprintf("/:root/%s/%s/commit/%s", ref.Owner, ref.Name, ref.CommitSha)
|
||||||
link := createLink(ctx, linkHref, refText, "commit")
|
link := createCommitLink(ctx, linkHref, otherRepoPrefix(ctx, ref.Owner, ref.Name), ref.CommitSha)
|
||||||
|
|
||||||
replaceContent(node, ref.RefLocation.Start, ref.RefLocation.End, link)
|
replaceContent(node, ref.RefLocation.Start, ref.RefLocation.End, link)
|
||||||
node = node.NextSibling.NextSibling
|
node = node.NextSibling.NextSibling
|
||||||
|
|||||||
@@ -281,11 +281,6 @@ func TestRender_AutoLink(t *testing.T) {
|
|||||||
err := PostProcessDefault(NewTestRenderContext(localMetas), strings.NewReader(input), &buffer)
|
err := PostProcessDefault(NewTestRenderContext(localMetas), strings.NewReader(input), &buffer)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer.String()))
|
assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer.String()))
|
||||||
|
|
||||||
buffer.Reset()
|
|
||||||
err = PostProcessDefault(NewTestRenderContext(localMetas), strings.NewReader(input), &buffer)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer.String()))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// render valid issue URLs
|
// render valid issue URLs
|
||||||
@@ -296,7 +291,7 @@ func TestRender_AutoLink(t *testing.T) {
|
|||||||
tmp := TestRepoURL + "commit/d8a994ef243349f321568f9e36d5c3f444b99cae"
|
tmp := TestRepoURL + "commit/d8a994ef243349f321568f9e36d5c3f444b99cae"
|
||||||
test(tmp, "<a href=\""+tmp+"\" class=\"commit\"><code>d8a994ef24</code></a>")
|
test(tmp, "<a href=\""+tmp+"\" class=\"commit\"><code>d8a994ef24</code></a>")
|
||||||
tmp += "#diff-2"
|
tmp += "#diff-2"
|
||||||
test(tmp, "<a href=\""+tmp+"\" class=\"commit\"><code>d8a994ef24 (diff-2)</code></a>")
|
test(tmp, "<a href=\""+tmp+"\" class=\"commit\"><code>d8a994ef24</code>#diff-2</a>")
|
||||||
|
|
||||||
// render other commit URLs
|
// render other commit URLs
|
||||||
tmp = "https://external-link.gitea.io/go-gitea/gitea/commit/d8a994ef243349f321568f9e36d5c3f444b99cae#diff-2"
|
tmp = "https://external-link.gitea.io/go-gitea/gitea/commit/d8a994ef243349f321568f9e36d5c3f444b99cae#diff-2"
|
||||||
@@ -355,65 +350,6 @@ func TestRegExp_sha1CurrentPattern(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestRegExp_anySHA1Pattern(t *testing.T) {
|
|
||||||
testCases := map[string]anyHashPatternResult{
|
|
||||||
"https://github.com/jquery/jquery/blob/a644101ed04d0beacea864ce805e0c4f86ba1cd1/test/unit/event.js#L2703": {
|
|
||||||
CommitID: "a644101ed04d0beacea864ce805e0c4f86ba1cd1",
|
|
||||||
SubPath: "/test/unit/event.js",
|
|
||||||
QueryHash: "L2703",
|
|
||||||
},
|
|
||||||
"https://github.com/jquery/jquery/blob/a644101ed04d0beacea864ce805e0c4f86ba1cd1/test/unit/event.js": {
|
|
||||||
CommitID: "a644101ed04d0beacea864ce805e0c4f86ba1cd1",
|
|
||||||
SubPath: "/test/unit/event.js",
|
|
||||||
},
|
|
||||||
"https://github.com/jquery/jquery/commit/0705be475092aede1eddae01319ec931fb9c65fc": {
|
|
||||||
CommitID: "0705be475092aede1eddae01319ec931fb9c65fc",
|
|
||||||
},
|
|
||||||
"https://github.com/jquery/jquery/tree/0705be475092aede1eddae01319ec931fb9c65fc/src": {
|
|
||||||
CommitID: "0705be475092aede1eddae01319ec931fb9c65fc",
|
|
||||||
SubPath: "/src",
|
|
||||||
},
|
|
||||||
"https://try.gogs.io/gogs/gogs/commit/d8a994ef243349f321568f9e36d5c3f444b99cae#diff-2": {
|
|
||||||
CommitID: "d8a994ef243349f321568f9e36d5c3f444b99cae",
|
|
||||||
QueryHash: "diff-2",
|
|
||||||
},
|
|
||||||
"non-url": {},
|
|
||||||
"http://a/b/c/d/e/1234567812345678123456781234567812345678123456781234567812345678?a=b#L1-L2": {
|
|
||||||
CommitID: "1234567812345678123456781234567812345678123456781234567812345678",
|
|
||||||
QueryHash: "L1-L2",
|
|
||||||
},
|
|
||||||
"http://a/b/c/d/e/1234567812345678123456781234567812345678123456781234567812345678.": {
|
|
||||||
CommitID: "1234567812345678123456781234567812345678123456781234567812345678",
|
|
||||||
},
|
|
||||||
"http://a/b/c/d/e/1234567812345678123456781234567812345678123456781234567812345678/sub.": {
|
|
||||||
CommitID: "1234567812345678123456781234567812345678123456781234567812345678",
|
|
||||||
SubPath: "/sub",
|
|
||||||
},
|
|
||||||
"http://a/b/c/d/e/1234567812345678123456781234567812345678123456781234567812345678?a=b.": {
|
|
||||||
CommitID: "1234567812345678123456781234567812345678123456781234567812345678",
|
|
||||||
},
|
|
||||||
"http://a/b/c/d/e/1234567812345678123456781234567812345678123456781234567812345678?a=b&c=d": {
|
|
||||||
CommitID: "1234567812345678123456781234567812345678123456781234567812345678",
|
|
||||||
},
|
|
||||||
"http://a/b/c/d/e/1234567812345678123456781234567812345678123456781234567812345678#hash.": {
|
|
||||||
CommitID: "1234567812345678123456781234567812345678123456781234567812345678",
|
|
||||||
QueryHash: "hash",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for k, v := range testCases {
|
|
||||||
ret, ok := anyHashPatternExtract(k)
|
|
||||||
if v.CommitID == "" {
|
|
||||||
assert.False(t, ok)
|
|
||||||
} else {
|
|
||||||
assert.Equal(t, strings.TrimSuffix(k, "."), ret.FullURL)
|
|
||||||
assert.Equal(t, v.CommitID, ret.CommitID)
|
|
||||||
assert.Equal(t, v.SubPath, ret.SubPath)
|
|
||||||
assert.Equal(t, v.QueryHash, ret.QueryHash)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestRegExp_shortLinkPattern(t *testing.T) {
|
func TestRegExp_shortLinkPattern(t *testing.T) {
|
||||||
trueTestCases := []string{
|
trueTestCases := []string{
|
||||||
"[[stuff]]",
|
"[[stuff]]",
|
||||||
|
|||||||
@@ -31,17 +31,23 @@ func fullIssuePatternProcessor(ctx *RenderContext, node *html.Node) {
|
|||||||
if ctx.RenderOptions.Metas == nil {
|
if ctx.RenderOptions.Metas == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
start := 0
|
||||||
next := node.NextSibling
|
next := node.NextSibling
|
||||||
for node != nil && node != next {
|
for node != nil && node != next && start < len(node.Data) {
|
||||||
m := globalVars().issueFullPattern.FindStringSubmatchIndex(node.Data)
|
m := globalVars().issueFullPattern.FindStringSubmatchIndex(node.Data[start:])
|
||||||
if m == nil {
|
if m == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
for idx := range m {
|
||||||
|
if m[idx] != -1 {
|
||||||
|
m[idx] += start
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
mDiffView := globalVars().filesChangedFullPattern.FindStringSubmatchIndex(node.Data)
|
// leave links to sub-pages like https://domain/org/repo/pulls/27/files or .../pulls/27/commits/{sha} to the other processors
|
||||||
// leave it as it is if the link is from "Files Changed" tab in PR Diff View https://domain/org/repo/pulls/27/files
|
if m[1] < len(node.Data) && node.Data[m[1]] == '/' {
|
||||||
if mDiffView != nil {
|
start = m[1]
|
||||||
return
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
link := node.Data[m[0]:m[1]]
|
link := node.Data[m[0]:m[1]]
|
||||||
@@ -71,6 +77,7 @@ func fullIssuePatternProcessor(ctx *RenderContext, node *html.Node) {
|
|||||||
text = matchOrg + "/" + matchRepo + text
|
text = matchOrg + "/" + matchRepo + text
|
||||||
replaceContent(node, m[0], m[1], createLink(ctx, link, text, "ref-issue"))
|
replaceContent(node, m[0], m[1], createLink(ctx, link, text, "ref-issue"))
|
||||||
}
|
}
|
||||||
|
start = 0
|
||||||
node = node.NextSibling.NextSibling
|
node = node.NextSibling.NextSibling
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -60,14 +60,14 @@ func TestRender_Commits(t *testing.T) {
|
|||||||
test(sha[:7], `<p><a href="`+commitPath[:len(commitPath)-(40-7)]+`" rel="nofollow"><code>65f1bf2</code></a></p>`)
|
test(sha[:7], `<p><a href="`+commitPath[:len(commitPath)-(40-7)]+`" rel="nofollow"><code>65f1bf2</code></a></p>`)
|
||||||
test(sha[:39], `<p><a href="`+commitPath[:len(commitPath)-(40-39)]+`" rel="nofollow"><code>65f1bf27bc</code></a></p>`)
|
test(sha[:39], `<p><a href="`+commitPath[:len(commitPath)-(40-39)]+`" rel="nofollow"><code>65f1bf27bc</code></a></p>`)
|
||||||
test(commit, `<p><a href="`+commit+`" rel="nofollow"><code>65f1bf27bc</code></a></p>`)
|
test(commit, `<p><a href="`+commit+`" rel="nofollow"><code>65f1bf27bc</code></a></p>`)
|
||||||
test(tree, `<p><a href="`+tree+`" rel="nofollow"><code>65f1bf27bc/src</code></a></p>`)
|
test(tree, `<p><a href="`+tree+`" rel="nofollow">`+tree+`</a></p>`)
|
||||||
|
|
||||||
test(file, `<p><a href="`+file+`" rel="nofollow"><code>65f1bf27bc/example.txt</code></a></p>`)
|
test(file, `<p><a href="`+file+`" rel="nofollow"><code>65f1bf27bc</code>/example.txt</a></p>`)
|
||||||
test(fileWithExtra, `<p><a href="`+file+`" rel="nofollow"><code>65f1bf27bc/example.txt</code></a>:</p>`)
|
test(fileWithExtra, `<p><a href="`+file+`" rel="nofollow"><code>65f1bf27bc</code>/example.txt</a>:</p>`)
|
||||||
test(fileWithHash, `<p><a href="`+fileWithHash+`" rel="nofollow"><code>65f1bf27bc/example.txt (L2)</code></a></p>`)
|
test(fileWithHash, `<p><a href="`+fileWithHash+`" rel="nofollow"><code>65f1bf27bc</code>/example.txt#L2</a></p>`)
|
||||||
test(fileWithHasExtra, `<p><a href="`+fileWithHash+`" rel="nofollow"><code>65f1bf27bc/example.txt (L2)</code></a>:</p>`)
|
test(fileWithHasExtra, `<p><a href="`+fileWithHash+`" rel="nofollow"><code>65f1bf27bc</code>/example.txt#L2</a>:</p>`)
|
||||||
test(commitCompare, `<p><a href="`+commitCompare+`" rel="nofollow"><code>65f1bf27bc...65f1bf27bc</code></a></p>`)
|
test(commitCompare, `<p><a href="`+commitCompare+`" rel="nofollow"><code>65f1bf27bc...65f1bf27bc</code></a></p>`)
|
||||||
test(commitCompareWithHash, `<p><a href="`+commitCompareWithHash+`" rel="nofollow"><code>65f1bf27bc...65f1bf27bc (L2)</code></a></p>`)
|
test(commitCompareWithHash, `<p><a href="`+commitCompareWithHash+`" rel="nofollow"><code>65f1bf27bc...65f1bf27bc</code>#L2</a></p>`)
|
||||||
|
|
||||||
test("commit "+sha, `<p>commit <a href="`+commitPath+`" rel="nofollow"><code>65f1bf27bc</code></a></p>`)
|
test("commit "+sha, `<p>commit <a href="`+commitPath+`" rel="nofollow"><code>65f1bf27bc</code></a></p>`)
|
||||||
test("/home/gitea/"+sha, "<p>/home/gitea/"+sha+"</p>")
|
test("/home/gitea/"+sha, "<p>/home/gitea/"+sha+"</p>")
|
||||||
@@ -79,6 +79,32 @@ func TestRender_Commits(t *testing.T) {
|
|||||||
test(sha[:14]+".", `<p>`+expected14+`.</p>`)
|
test(sha[:14]+".", `<p>`+expected14+`.</p>`)
|
||||||
test(sha[:14]+",", `<p>`+expected14+`,</p>`)
|
test(sha[:14]+",", `<p>`+expected14+`,</p>`)
|
||||||
test("["+sha[:14]+"]", `<p>[`+expected14+`]</p>`)
|
test("["+sha[:14]+"]", `<p>[`+expected14+`]</p>`)
|
||||||
|
|
||||||
|
commitShort := repo + "commit/" + sha[:7]
|
||||||
|
pullCommit := repo + "pulls/1/commits/" + sha
|
||||||
|
otherCommit := markup.TestAppURL + "user2/repo1/commit/" + sha
|
||||||
|
branchCompare := repo + "compare/main...feature/x"
|
||||||
|
githubCommit := "https://github.com/a/b/commit/" + sha
|
||||||
|
test(pullCommit+" fixes "+repo+"issues/2", `<p><a href="`+pullCommit+`" rel="nofollow"><code>65f1bf27bc</code></a> fixes <a href="`+repo+`issues/2" class="ref-issue" rel="nofollow">#2</a></p>`)
|
||||||
|
test(commitShort+"#diff-2.", `<p><a href="`+commitShort+`#diff-2" rel="nofollow"><code>65f1bf2</code>#diff-2</a>.</p>`)
|
||||||
|
test(commitShort+"?w=1", `<p><a href="`+commitShort+`?w=1" rel="nofollow"><code>65f1bf2</code>?w=1</a></p>`)
|
||||||
|
test(otherCommit, `<p><a href="`+otherCommit+`" rel="nofollow">user2/repo1@<code>65f1bf27bc</code></a></p>`)
|
||||||
|
test(branchCompare, `<p><a href="`+branchCompare+`" rel="nofollow"><code>main...feature/x</code></a></p>`)
|
||||||
|
test(githubCommit+" "+commitShort+" "+commit, `<p><a href="`+githubCommit+`" rel="nofollow">`+githubCommit+`</a> <a href="`+commitShort+`" rel="nofollow"><code>65f1bf2</code></a> <a href="`+commit+`" rel="nofollow"><code>65f1bf27bc</code></a></p>`)
|
||||||
|
test("<"+commitShort+"> ["+commit+"]("+commit+")", `<p><a href="`+commitShort+`" rel="nofollow"><code>65f1bf2</code></a> <a href="`+commit+`" rel="nofollow"><code>65f1bf27bc</code></a></p>`)
|
||||||
|
test("[[label|"+commitShort+"]]", `<p><a href="`+commitShort+`" rel="nofollow">label</a></p>`)
|
||||||
|
for input, attrs := range map[string]string{
|
||||||
|
commitShort + ".patch": ` rel="nofollow"><code>65f1bf2</code>.patch`,
|
||||||
|
repo + "compare/" + sha + ".." + sha: ` rel="nofollow"><code>65f1bf27bc..65f1bf27bc</code>`,
|
||||||
|
commitCompare + ".diff?w=1": ` rel="nofollow"><code>65f1bf27bc...65f1bf27bc</code>.diff?w=1`,
|
||||||
|
repo + "compare/20240101...20240201": ` rel="nofollow"><code>20240101...20240201</code>`,
|
||||||
|
repo + "src/commit/" + sha + "/README.md": ` rel="nofollow"><code>65f1bf27bc</code>/README.md`,
|
||||||
|
repo + "commit/" + sha[:6]: ` rel="nofollow">` + repo + "commit/" + sha[:6],
|
||||||
|
repo + "src/branch/main/sub-dir/commit/" + sha[:7]: ` rel="nofollow">` + repo + "src/branch/main/sub-dir/commit/" + sha[:7],
|
||||||
|
repo + "src/branch/main/" + sha + ".txt": ` rel="nofollow">` + repo + "src/branch/main/" + sha + ".txt",
|
||||||
|
} {
|
||||||
|
test(input, `<p><a href="`+input+`"`+attrs+`</a></p>`)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestRender_CrossReferences(t *testing.T) {
|
func TestRender_CrossReferences(t *testing.T) {
|
||||||
@@ -113,17 +139,13 @@ func TestRender_CrossReferences(t *testing.T) {
|
|||||||
inputURL := setting.AppURL + "a/b/commit/0123456789012345678901234567890123456789/foo.txt?a=b#L2-L3"
|
inputURL := setting.AppURL + "a/b/commit/0123456789012345678901234567890123456789/foo.txt?a=b#L2-L3"
|
||||||
test(
|
test(
|
||||||
inputURL,
|
inputURL,
|
||||||
`<p><a href="`+inputURL+`" rel="nofollow"><code>0123456789/foo.txt (L2-L3)</code></a></p>`)
|
`<p><a href="`+inputURL+`" rel="nofollow">a/b@<code>0123456789</code>/foo.txt?a=b#L2-L3</a></p>`)
|
||||||
|
|
||||||
inputURL = setting.AppURL + "repo/owner/archive/0123456789012345678901234567890123456789.tar.gz"
|
inputURL = setting.AppURL + "owner/repo/archive/0123456789012345678901234567890123456789.tar.gz?key=val"
|
||||||
test(
|
test(inputURL, `<p><a href="`+inputURL+`" rel="nofollow">owner/repo@<code>0123456789</code>.tar.gz?key=val</a></p>`)
|
||||||
inputURL,
|
|
||||||
`<p><a href="`+inputURL+`" rel="nofollow"><code>0123456789.tar.gz</code></a></p>`)
|
|
||||||
|
|
||||||
inputURL = setting.AppURL + "owner/repo/commit/0123456789012345678901234567890123456789.patch?key=val"
|
test("test-owner/test-repo@"+"0123456789012345678901234567890123456789", `<p><a href="/test-owner/test-repo/commit/0123456789012345678901234567890123456789" rel="nofollow">test-owner/test-repo@<code>0123456789</code></a></p>`)
|
||||||
test(
|
test(testRepoOwnerName+"/"+testRepoName+"@"+"0123456789012345678901234567890123456789", `<p><a href="/user13/repo11/commit/0123456789012345678901234567890123456789" rel="nofollow"><code>0123456789</code></a></p>`)
|
||||||
inputURL,
|
|
||||||
`<p><a href="`+inputURL+`" rel="nofollow"><code>0123456789.patch</code></a></p>`)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestRender_links(t *testing.T) {
|
func TestRender_links(t *testing.T) {
|
||||||
@@ -178,6 +200,7 @@ func TestRender_links(t *testing.T) {
|
|||||||
{"://", `<p>://</p>`},
|
{"://", `<p>://</p>`},
|
||||||
{"www", `<p>www</p>`},
|
{"www", `<p>www</p>`},
|
||||||
{"ftps://gitea.com", `<p>ftps://gitea.com</p>`},
|
{"ftps://gitea.com", `<p>ftps://gitea.com</p>`},
|
||||||
|
{markup.TestAppURL + "user13/repo11/commit/65f1bf2", `<p><a href="` + markup.TestAppURL + `user13/repo11/commit/65f1bf2" rel="nofollow">` + markup.TestAppURL + `user13/repo11/commit/65f1bf2</a></p>`},
|
||||||
} {
|
} {
|
||||||
test(testCase.input, testCase.expected)
|
test(testCase.input, testCase.expected)
|
||||||
}
|
}
|
||||||
@@ -459,7 +482,7 @@ func TestIssue18471(t *testing.T) {
|
|||||||
err := markup.PostProcessDefault(markup.NewTestRenderContext(localMetas), strings.NewReader(data), &res)
|
err := markup.PostProcessDefault(markup.NewTestRenderContext(localMetas), strings.NewReader(data), &res)
|
||||||
|
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Equal(t, `<a href="`+markup.TestAppURL+`org/repo/compare/783b039...da951ce" class="compare"><code>783b039...da951ce</code></a>`, res.String())
|
assert.Equal(t, `<a href="`+markup.TestAppURL+`org/repo/compare/783b039...da951ce" class="commit">org/repo@<code>783b039...da951ce</code></a>`, res.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestRender_TeamMention(t *testing.T) {
|
func TestRender_TeamMention(t *testing.T) {
|
||||||
|
|||||||
@@ -555,8 +555,8 @@ space</p>
|
|||||||
input := `http://localhost:3000/user/repo/commit/88fc37a3c0a4dda553bdcfc80c178a58247f42fb
|
input := `http://localhost:3000/user/repo/commit/88fc37a3c0a4dda553bdcfc80c178a58247f42fb
|
||||||
http://localhost:3000/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash`
|
http://localhost:3000/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash`
|
||||||
|
|
||||||
expected := `<p><a href="http://localhost:3000/user/repo/commit/88fc37a3c0a4dda553bdcfc80c178a58247f42fb" rel="nofollow"><code>88fc37a3c0</code></a>
|
expected := `<p><a href="http://localhost:3000/user/repo/commit/88fc37a3c0a4dda553bdcfc80c178a58247f42fb" rel="nofollow">user/repo@<code>88fc37a3c0</code></a>
|
||||||
<a href="http://localhost:3000/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash" rel="nofollow"><code>88fc37a3c0...12fc37a3c0 (hash)</code></a></p>
|
<a href="http://localhost:3000/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash" rel="nofollow">user/repo@<code>88fc37a3c0...12fc37a3c0</code>#hash</a></p>
|
||||||
`
|
`
|
||||||
result, err := markdown.RenderString(markup.NewTestRenderContext(localMetas), input)
|
result, err := markdown.RenderString(markup.NewTestRenderContext(localMetas), input)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|||||||
@@ -102,9 +102,9 @@ func TestRenderRepoComment(t *testing.T) {
|
|||||||

|

|
||||||
[[local image|image.jpg]]
|
[[local image|image.jpg]]
|
||||||
[[remote link|<a href="https://example.com/image.jpg">https://example.com/image.jpg</a>]]
|
[[remote link|<a href="https://example.com/image.jpg">https://example.com/image.jpg</a>]]
|
||||||
<a href="http://localhost:3000/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash" class="compare"><code>88fc37a3c0...12fc37a3c0 (hash)</code></a>
|
<a href="http://localhost:3000/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash" class="commit">user/repo@<code>88fc37a3c0...12fc37a3c0</code>#hash</a>
|
||||||
com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb pare
|
com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb pare
|
||||||
<a href="http://localhost:3000/user/repo/commit/88fc37a3c0a4dda553bdcfc80c178a58247f42fb" class="commit"><code>88fc37a3c0</code></a>
|
<a href="http://localhost:3000/user/repo/commit/88fc37a3c0a4dda553bdcfc80c178a58247f42fb" class="commit">user/repo@<code>88fc37a3c0</code></a>
|
||||||
com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb mit
|
com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb mit
|
||||||
<span class="emoji" data-alias="+1">👍</span>
|
<span class="emoji" data-alias="+1">👍</span>
|
||||||
<a href="mailto:mail@domain.com">mail@domain.com</a>
|
<a href="mailto:mail@domain.com">mail@domain.com</a>
|
||||||
@@ -226,9 +226,9 @@ func TestRenderMarkdownToHtml(t *testing.T) {
|
|||||||
<a href="https://example.com/image.jpg" target="_blank" rel="nofollow noopener"><img src="https://example.com/image.jpg" alt="remote image"/></a>
|
<a href="https://example.com/image.jpg" target="_blank" rel="nofollow noopener"><img src="https://example.com/image.jpg" alt="remote image"/></a>
|
||||||
<a href="/image.jpg" rel="nofollow"><img src="/image.jpg" title="local image" alt="local image"/></a>
|
<a href="/image.jpg" rel="nofollow"><img src="/image.jpg" title="local image" alt="local image"/></a>
|
||||||
<a href="https://example.com/image.jpg" rel="nofollow"><img src="https://example.com/image.jpg" title="remote link" alt="remote link"/></a>
|
<a href="https://example.com/image.jpg" rel="nofollow"><img src="https://example.com/image.jpg" title="remote link" alt="remote link"/></a>
|
||||||
<a href="http://localhost:3000/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash" rel="nofollow"><code>88fc37a3c0...12fc37a3c0 (hash)</code></a>
|
<a href="http://localhost:3000/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash" rel="nofollow">user/repo@<code>88fc37a3c0...12fc37a3c0</code>#hash</a>
|
||||||
com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb pare
|
com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb pare
|
||||||
<a href="http://localhost:3000/user/repo/commit/88fc37a3c0a4dda553bdcfc80c178a58247f42fb" rel="nofollow"><code>88fc37a3c0</code></a>
|
<a href="http://localhost:3000/user/repo/commit/88fc37a3c0a4dda553bdcfc80c178a58247f42fb" rel="nofollow">user/repo@<code>88fc37a3c0</code></a>
|
||||||
com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb mit
|
com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb mit
|
||||||
<span class="emoji" data-alias="+1">👍</span>
|
<span class="emoji" data-alias="+1">👍</span>
|
||||||
<a href="mailto:mail@domain.com" rel="nofollow">mail@domain.com</a>
|
<a href="mailto:mail@domain.com" rel="nofollow">mail@domain.com</a>
|
||||||
|
|||||||
Reference in New Issue
Block a user