diff --git a/models/unittest/testdb.go b/models/unittest/testdb.go
index 5a1c27dbeae88..a59f1464e3e15 100644
--- a/models/unittest/testdb.go
+++ b/models/unittest/testdb.go
@@ -206,7 +206,7 @@ func CreateTestEngine(opts FixturesOptions) error {
x, err := xorm.NewEngine("sqlite3", "file::memory:?cache=shared&_txlock=immediate")
if err != nil {
if strings.Contains(err.Error(), "unknown driver") {
- return fmt.Errorf(`sqlite3 requires: import _ "github.com/mattn/go-sqlite3" or -tags sqlite,sqlite_unlock_notify%s%w`, "\n", err)
+ return fmt.Errorf(`sqlite3 requires: -tags sqlite,sqlite_unlock_notify%s%w`, "\n", err)
}
return err
}
diff --git a/modules/markup/html_commit.go b/modules/markup/html_commit.go
index 358e7b06ba538..aa1b7d034a594 100644
--- a/modules/markup/html_commit.go
+++ b/modules/markup/html_commit.go
@@ -8,6 +8,7 @@ import (
"strings"
"code.gitea.io/gitea/modules/base"
+ "code.gitea.io/gitea/modules/references"
"code.gitea.io/gitea/modules/util"
"golang.org/x/net/html"
@@ -194,3 +195,21 @@ func hashCurrentPatternProcessor(ctx *RenderContext, node *html.Node) {
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
+ }
+
+ reftext := ref.Owner + "/" + ref.Name + "@" + base.ShortSha(ref.CommitSha)
+ linkHref := ctx.RenderHelper.ResolveLink(util.URLJoin(ref.Owner, ref.Name, "commit", ref.CommitSha), LinkTypeApp)
+ link := createLink(ctx, linkHref, reftext, "commit")
+
+ replaceContent(node, ref.RefLocation.Start, ref.RefLocation.End, link)
+ node = node.NextSibling.NextSibling
+ }
+}
diff --git a/modules/markup/html_issue.go b/modules/markup/html_issue.go
index e64ec76c3d2fc..7a6f33011a7c0 100644
--- a/modules/markup/html_issue.go
+++ b/modules/markup/html_issue.go
@@ -4,9 +4,9 @@
package markup
import (
+ "strconv"
"strings"
- "code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/httplib"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/references"
@@ -16,8 +16,16 @@ import (
"code.gitea.io/gitea/modules/util"
"golang.org/x/net/html"
+ "golang.org/x/net/html/atom"
)
+type RenderIssueIconTitleOptions struct {
+ OwnerName string
+ RepoName string
+ LinkHref string
+ IssueIndex int64
+}
+
func fullIssuePatternProcessor(ctx *RenderContext, node *html.Node) {
if ctx.RenderOptions.Metas == nil {
return
@@ -66,6 +74,27 @@ func fullIssuePatternProcessor(ctx *RenderContext, node *html.Node) {
}
}
+func createIssueLinkContentWithSummary(ctx *RenderContext, linkHref string, ref *references.RenderizableReference) *html.Node {
+ if DefaultRenderHelperFuncs.RenderRepoIssueIconTitle == nil {
+ return nil
+ }
+ issueIndex, _ := strconv.ParseInt(ref.Issue, 10, 64)
+ h, err := DefaultRenderHelperFuncs.RenderRepoIssueIconTitle(ctx, RenderIssueIconTitleOptions{
+ OwnerName: ref.Owner,
+ RepoName: ref.Name,
+ LinkHref: linkHref,
+ IssueIndex: issueIndex,
+ })
+ if err != nil {
+ log.Error("RenderRepoIssueIconTitle failed: %v", err)
+ return nil
+ }
+ if h == "" {
+ return nil
+ }
+ return &html.Node{Type: html.RawNode, Data: string(ctx.RenderInternal.ProtectSafeAttrs(h))}
+}
+
func issueIndexPatternProcessor(ctx *RenderContext, node *html.Node) {
if ctx.RenderOptions.Metas == nil {
return
@@ -76,32 +105,28 @@ func issueIndexPatternProcessor(ctx *RenderContext, node *html.Node) {
// old logic: crossLinkOnly := ctx.RenderOptions.Metas["mode"] == "document" && !ctx.IsWiki
crossLinkOnly := ctx.RenderOptions.Metas["markupAllowShortIssuePattern"] != "true"
- var (
- found bool
- ref *references.RenderizableReference
- )
+ var ref *references.RenderizableReference
next := node.NextSibling
-
for node != nil && node != next {
_, hasExtTrackFormat := ctx.RenderOptions.Metas["format"]
// Repos with external issue trackers might still need to reference local PRs
// We need to concern with the first one that shows up in the text, whichever it is
isNumericStyle := ctx.RenderOptions.Metas["style"] == "" || ctx.RenderOptions.Metas["style"] == IssueNameStyleNumeric
- foundNumeric, refNumeric := references.FindRenderizableReferenceNumeric(node.Data, hasExtTrackFormat && !isNumericStyle, crossLinkOnly)
+ refNumeric := references.FindRenderizableReferenceNumeric(node.Data, hasExtTrackFormat && !isNumericStyle, crossLinkOnly)
switch ctx.RenderOptions.Metas["style"] {
case "", IssueNameStyleNumeric:
- found, ref = foundNumeric, refNumeric
+ ref = refNumeric
case IssueNameStyleAlphanumeric:
- found, ref = references.FindRenderizableReferenceAlphanumeric(node.Data)
+ ref = references.FindRenderizableReferenceAlphanumeric(node.Data)
case IssueNameStyleRegexp:
pattern, err := regexplru.GetCompiled(ctx.RenderOptions.Metas["regexp"])
if err != nil {
return
}
- found, ref = references.FindRenderizableReferenceRegexp(node.Data, pattern)
+ ref = references.FindRenderizableReferenceRegexp(node.Data, pattern)
}
// Repos with external issue trackers might still need to reference local PRs
@@ -109,17 +134,17 @@ func issueIndexPatternProcessor(ctx *RenderContext, node *html.Node) {
if hasExtTrackFormat && !isNumericStyle && refNumeric != nil {
// If numeric (PR) was found, and it was BEFORE the non-numeric pattern, use that
// Allow a free-pass when non-numeric pattern wasn't found.
- if found && (ref == nil || refNumeric.RefLocation.Start < ref.RefLocation.Start) {
- found = foundNumeric
+ if ref == nil || refNumeric.RefLocation.Start < ref.RefLocation.Start {
ref = refNumeric
}
}
- if !found {
+
+ if ref == nil {
return
}
var link *html.Node
- reftext := node.Data[ref.RefLocation.Start:ref.RefLocation.End]
+ refText := node.Data[ref.RefLocation.Start:ref.RefLocation.End]
if hasExtTrackFormat && !ref.IsPull {
ctx.RenderOptions.Metas["index"] = ref.Issue
@@ -129,18 +154,23 @@ func issueIndexPatternProcessor(ctx *RenderContext, node *html.Node) {
log.Error("unable to expand template vars for ref %s, err: %v", ref.Issue, err)
}
- link = createLink(ctx, res, reftext, "ref-issue ref-external-issue")
+ link = createLink(ctx, res, refText, "ref-issue ref-external-issue")
} else {
// Path determines the type of link that will be rendered. It's unknown at this point whether
// the linked item is actually a PR or an issue. Luckily it's of no real consequence because
// Gitea will redirect on click as appropriate.
+ issueOwner := util.Iif(ref.Owner == "", ctx.RenderOptions.Metas["user"], ref.Owner)
+ issueRepo := util.Iif(ref.Owner == "", ctx.RenderOptions.Metas["repo"], ref.Name)
issuePath := util.Iif(ref.IsPull, "pulls", "issues")
- if ref.Owner == "" {
- linkHref := ctx.RenderHelper.ResolveLink(util.URLJoin(ctx.RenderOptions.Metas["user"], ctx.RenderOptions.Metas["repo"], issuePath, ref.Issue), LinkTypeApp)
- link = createLink(ctx, linkHref, reftext, "ref-issue")
- } else {
- linkHref := ctx.RenderHelper.ResolveLink(util.URLJoin(ref.Owner, ref.Name, issuePath, ref.Issue), LinkTypeApp)
- link = createLink(ctx, linkHref, reftext, "ref-issue")
+ linkHref := ctx.RenderHelper.ResolveLink(util.URLJoin(issueOwner, issueRepo, issuePath, ref.Issue), LinkTypeApp)
+
+ // at the moment, only render the issue index in a full line (or simple line) as icon+title
+ // otherwise it would be too noisy for "take #1 as an example" in a sentence
+ if node.Parent.DataAtom == atom.Li && ref.RefLocation.Start < 20 && ref.RefLocation.End == len(node.Data) {
+ link = createIssueLinkContentWithSummary(ctx, linkHref, ref)
+ }
+ if link == nil {
+ link = createLink(ctx, linkHref, refText, "ref-issue")
}
}
@@ -168,21 +198,3 @@ func issueIndexPatternProcessor(ctx *RenderContext, node *html.Node) {
node = node.NextSibling.NextSibling.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
- }
-
- reftext := ref.Owner + "/" + ref.Name + "@" + base.ShortSha(ref.CommitSha)
- linkHref := ctx.RenderHelper.ResolveLink(util.URLJoin(ref.Owner, ref.Name, "commit", ref.CommitSha), LinkTypeApp)
- link := createLink(ctx, linkHref, reftext, "commit")
-
- replaceContent(node, ref.RefLocation.Start, ref.RefLocation.End, link)
- node = node.NextSibling.NextSibling
- }
-}
diff --git a/modules/markup/html_issue_test.go b/modules/markup/html_issue_test.go
new file mode 100644
index 0000000000000..8d189fbdf62e0
--- /dev/null
+++ b/modules/markup/html_issue_test.go
@@ -0,0 +1,72 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package markup_test
+
+import (
+ "context"
+ "html/template"
+ "strings"
+ "testing"
+
+ "code.gitea.io/gitea/modules/htmlutil"
+ "code.gitea.io/gitea/modules/markup"
+ "code.gitea.io/gitea/modules/markup/markdown"
+ testModule "code.gitea.io/gitea/modules/test"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+func TestRender_IssueList(t *testing.T) {
+ defer testModule.MockVariableValue(&markup.RenderBehaviorForTesting.DisableAdditionalAttributes, true)()
+ markup.Init(&markup.RenderHelperFuncs{
+ RenderRepoIssueIconTitle: func(ctx context.Context, opts markup.RenderIssueIconTitleOptions) (template.HTML, error) {
+ return htmlutil.HTMLFormat("
issue #%d
", opts.IssueIndex), nil
+ },
+ })
+
+ test := func(input, expected string) {
+ rctx := markup.NewTestRenderContext(markup.TestAppURL, map[string]string{
+ "user": "test-user", "repo": "test-repo",
+ "markupAllowShortIssuePattern": "true",
+ })
+ out, err := markdown.RenderString(rctx, input)
+ require.NoError(t, err)
+ assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(string(out)))
+ }
+
+ t.Run("NormalIssueRef", func(t *testing.T) {
+ test(
+ "#12345",
+ `#12345
`,
+ )
+ })
+
+ t.Run("ListIssueRef", func(t *testing.T) {
+ test(
+ "* #12345",
+ ``,
+ )
+ })
+
+ t.Run("ListIssueRefNormal", func(t *testing.T) {
+ test(
+ "* foo #12345 bar",
+ ``,
+ )
+ })
+
+ t.Run("ListTodoIssueRef", func(t *testing.T) {
+ test(
+ "* [ ] #12345",
+ ``,
+ )
+ })
+}
diff --git a/modules/markup/render_helper.go b/modules/markup/render_helper.go
index 82796ef274558..8ff0e7d6fb41c 100644
--- a/modules/markup/render_helper.go
+++ b/modules/markup/render_helper.go
@@ -38,6 +38,7 @@ type RenderHelper interface {
type RenderHelperFuncs struct {
IsUsernameMentionable func(ctx context.Context, username string) bool
RenderRepoFileCodePreview func(ctx context.Context, options RenderCodePreviewOptions) (template.HTML, error)
+ RenderRepoIssueIconTitle func(ctx context.Context, options RenderIssueIconTitleOptions) (template.HTML, error)
}
var DefaultRenderHelperFuncs *RenderHelperFuncs
diff --git a/modules/references/references.go b/modules/references/references.go
index 6e549cb875851..460bf3236b4ff 100644
--- a/modules/references/references.go
+++ b/modules/references/references.go
@@ -330,22 +330,22 @@ func FindAllIssueReferences(content string) []IssueReference {
}
// FindRenderizableReferenceNumeric returns the first unvalidated reference found in a string.
-func FindRenderizableReferenceNumeric(content string, prOnly, crossLinkOnly bool) (bool, *RenderizableReference) {
+func FindRenderizableReferenceNumeric(content string, prOnly, crossLinkOnly bool) *RenderizableReference {
var match []int
if !crossLinkOnly {
match = issueNumericPattern.FindStringSubmatchIndex(content)
}
if match == nil {
if match = crossReferenceIssueNumericPattern.FindStringSubmatchIndex(content); match == nil {
- return false, nil
+ return nil
}
}
r := getCrossReference(util.UnsafeStringToBytes(content), match[2], match[3], false, prOnly)
if r == nil {
- return false, nil
+ return nil
}
- return true, &RenderizableReference{
+ return &RenderizableReference{
Issue: r.issue,
Owner: r.owner,
Name: r.name,
@@ -372,15 +372,14 @@ func FindRenderizableCommitCrossReference(content string) (bool, *RenderizableRe
}
// FindRenderizableReferenceRegexp returns the first regexp unvalidated references found in a string.
-func FindRenderizableReferenceRegexp(content string, pattern *regexp.Regexp) (bool, *RenderizableReference) {
+func FindRenderizableReferenceRegexp(content string, pattern *regexp.Regexp) *RenderizableReference {
match := pattern.FindStringSubmatchIndex(content)
if len(match) < 4 {
- return false, nil
+ return nil
}
action, location := findActionKeywords([]byte(content), match[2])
-
- return true, &RenderizableReference{
+ return &RenderizableReference{
Issue: content[match[2]:match[3]],
RefLocation: &RefSpan{Start: match[0], End: match[1]},
Action: action,
@@ -390,15 +389,14 @@ func FindRenderizableReferenceRegexp(content string, pattern *regexp.Regexp) (bo
}
// FindRenderizableReferenceAlphanumeric returns the first alphanumeric unvalidated references found in a string.
-func FindRenderizableReferenceAlphanumeric(content string) (bool, *RenderizableReference) {
+func FindRenderizableReferenceAlphanumeric(content string) *RenderizableReference {
match := issueAlphanumericPattern.FindStringSubmatchIndex(content)
if match == nil {
- return false, nil
+ return nil
}
action, location := findActionKeywords([]byte(content), match[2])
-
- return true, &RenderizableReference{
+ return &RenderizableReference{
Issue: content[match[2]:match[3]],
RefLocation: &RefSpan{Start: match[2], End: match[3]},
Action: action,
diff --git a/modules/references/references_test.go b/modules/references/references_test.go
index e224c919e9216..5bb4de3717768 100644
--- a/modules/references/references_test.go
+++ b/modules/references/references_test.go
@@ -249,11 +249,10 @@ func TestFindAllIssueReferences(t *testing.T) {
}
for _, fixture := range alnumFixtures {
- found, ref := FindRenderizableReferenceAlphanumeric(fixture.input)
+ ref := FindRenderizableReferenceAlphanumeric(fixture.input)
if fixture.issue == "" {
- assert.False(t, found, "Failed to parse: {%s}", fixture.input)
+ assert.Nil(t, ref, "Failed to parse: {%s}", fixture.input)
} else {
- assert.True(t, found, "Failed to parse: {%s}", fixture.input)
assert.Equal(t, fixture.issue, ref.Issue, "Failed to parse: {%s}", fixture.input)
assert.Equal(t, fixture.refLocation, ref.RefLocation, "Failed to parse: {%s}", fixture.input)
assert.Equal(t, fixture.action, ref.Action, "Failed to parse: {%s}", fixture.input)
diff --git a/modules/svg/processor.go b/modules/svg/processor.go
index 82248fb0c1216..4fcb11a57d0c2 100644
--- a/modules/svg/processor.go
+++ b/modules/svg/processor.go
@@ -10,7 +10,7 @@ import (
"sync"
)
-type normalizeVarsStruct struct {
+type globalVarsStruct struct {
reXMLDoc,
reComment,
reAttrXMLNs,
@@ -18,26 +18,23 @@ type normalizeVarsStruct struct {
reAttrClassPrefix *regexp.Regexp
}
-var (
- normalizeVars *normalizeVarsStruct
- normalizeVarsOnce sync.Once
-)
+var globalVars = sync.OnceValue(func() *globalVarsStruct {
+ return &globalVarsStruct{
+ reXMLDoc: regexp.MustCompile(`(?s)<\?xml.*?>`),
+ reComment: regexp.MustCompile(`(?s)`),
+
+ reAttrXMLNs: regexp.MustCompile(`(?s)\s+xmlns\s*=\s*"[^"]*"`),
+ reAttrSize: regexp.MustCompile(`(?s)\s+(width|height)\s*=\s*"[^"]+"`),
+ reAttrClassPrefix: regexp.MustCompile(`(?s)\s+class\s*=\s*"`),
+ }
+})
// Normalize normalizes the SVG content: set default width/height, remove unnecessary tags/attributes
// It's designed to work with valid SVG content. For invalid SVG content, the returned content is not guaranteed.
func Normalize(data []byte, size int) []byte {
- normalizeVarsOnce.Do(func() {
- normalizeVars = &normalizeVarsStruct{
- reXMLDoc: regexp.MustCompile(`(?s)<\?xml.*?>`),
- reComment: regexp.MustCompile(`(?s)`),
-
- reAttrXMLNs: regexp.MustCompile(`(?s)\s+xmlns\s*=\s*"[^"]*"`),
- reAttrSize: regexp.MustCompile(`(?s)\s+(width|height)\s*=\s*"[^"]+"`),
- reAttrClassPrefix: regexp.MustCompile(`(?s)\s+class\s*=\s*"`),
- }
- })
- data = normalizeVars.reXMLDoc.ReplaceAll(data, nil)
- data = normalizeVars.reComment.ReplaceAll(data, nil)
+ vars := globalVars()
+ data = vars.reXMLDoc.ReplaceAll(data, nil)
+ data = vars.reComment.ReplaceAll(data, nil)
data = bytes.TrimSpace(data)
svgTag, svgRemaining, ok := bytes.Cut(data, []byte(">"))
@@ -45,9 +42,9 @@ func Normalize(data []byte, size int) []byte {
return data
}
normalized := bytes.Clone(svgTag)
- normalized = normalizeVars.reAttrXMLNs.ReplaceAll(normalized, nil)
- normalized = normalizeVars.reAttrSize.ReplaceAll(normalized, nil)
- normalized = normalizeVars.reAttrClassPrefix.ReplaceAll(normalized, []byte(` class="`))
+ normalized = vars.reAttrXMLNs.ReplaceAll(normalized, nil)
+ normalized = vars.reAttrSize.ReplaceAll(normalized, nil)
+ normalized = vars.reAttrClassPrefix.ReplaceAll(normalized, []byte(` class="`))
normalized = bytes.TrimSpace(normalized)
normalized = fmt.Appendf(normalized, ` width="%d" height="%d"`, size, size)
if !bytes.Contains(normalized, []byte(` class="`)) {
diff --git a/routers/init.go b/routers/init.go
index 2091f5967acac..72953e9aa0d31 100644
--- a/routers/init.go
+++ b/routers/init.go
@@ -133,7 +133,7 @@ func InitWebInstalled(ctx context.Context) {
highlight.NewContext()
external.RegisterRenderers()
- markup.Init(markup_service.ProcessorHelper())
+ markup.Init(markup_service.FormalRenderHelperFuncs())
if setting.EnableSQLite3 {
log.Info("SQLite3 support is enabled")
diff --git a/services/context/context_response.go b/services/context/context_response.go
index c43a649b49e12..14ab527a7479b 100644
--- a/services/context/context_response.go
+++ b/services/context/context_response.go
@@ -107,7 +107,7 @@ func (ctx *Context) JSONTemplate(tmpl base.TplName) {
}
// RenderToHTML renders the template content to a HTML string
-func (ctx *Context) RenderToHTML(name base.TplName, data map[string]any) (template.HTML, error) {
+func (ctx *Context) RenderToHTML(name base.TplName, data any) (template.HTML, error) {
var buf strings.Builder
err := ctx.Render.HTML(&buf, 0, string(name), data, ctx.TemplateContext)
return template.HTML(buf.String()), err
diff --git a/services/markup/main_test.go b/services/markup/main_test.go
index 5553ebc058948..d04a18bfa17e9 100644
--- a/services/markup/main_test.go
+++ b/services/markup/main_test.go
@@ -11,6 +11,6 @@ import (
func TestMain(m *testing.M) {
unittest.MainTest(m, &unittest.TestOptions{
- FixtureFiles: []string{"user.yml", "repository.yml", "access.yml", "repo_unit.yml"},
+ FixtureFiles: []string{"user.yml", "repository.yml", "access.yml", "repo_unit.yml", "issue.yml"},
})
}
diff --git a/services/markup/processorhelper.go b/services/markup/renderhelper.go
similarity index 88%
rename from services/markup/processorhelper.go
rename to services/markup/renderhelper.go
index 1f1abf496a3e0..4b9852b48bf2f 100644
--- a/services/markup/processorhelper.go
+++ b/services/markup/renderhelper.go
@@ -11,9 +11,10 @@ import (
gitea_context "code.gitea.io/gitea/services/context"
)
-func ProcessorHelper() *markup.RenderHelperFuncs {
+func FormalRenderHelperFuncs() *markup.RenderHelperFuncs {
return &markup.RenderHelperFuncs{
RenderRepoFileCodePreview: renderRepoFileCodePreview,
+ RenderRepoIssueIconTitle: renderRepoIssueIconTitle,
IsUsernameMentionable: func(ctx context.Context, username string) bool {
mentionedUser, err := user.GetUserByName(ctx, username)
if err != nil {
diff --git a/services/markup/processorhelper_codepreview.go b/services/markup/renderhelper_codepreview.go
similarity index 97%
rename from services/markup/processorhelper_codepreview.go
rename to services/markup/renderhelper_codepreview.go
index 0500e57e4610e..170c70c4098e3 100644
--- a/services/markup/processorhelper_codepreview.go
+++ b/services/markup/renderhelper_codepreview.go
@@ -18,6 +18,7 @@ import (
"code.gitea.io/gitea/modules/indexer/code"
"code.gitea.io/gitea/modules/markup"
"code.gitea.io/gitea/modules/setting"
+ "code.gitea.io/gitea/modules/util"
gitea_context "code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/repository/files"
)
@@ -46,7 +47,7 @@ func renderRepoFileCodePreview(ctx context.Context, opts markup.RenderCodePrevie
return "", err
}
if !perms.CanRead(unit.TypeCode) {
- return "", fmt.Errorf("no permission")
+ return "", util.ErrPermissionDenied
}
gitRepo, err := gitrepo.OpenRepository(ctx, dbRepo)
diff --git a/services/markup/processorhelper_codepreview_test.go b/services/markup/renderhelper_codepreview_test.go
similarity index 95%
rename from services/markup/processorhelper_codepreview_test.go
rename to services/markup/renderhelper_codepreview_test.go
index 154e4e8e44118..ea945584b427e 100644
--- a/services/markup/processorhelper_codepreview_test.go
+++ b/services/markup/renderhelper_codepreview_test.go
@@ -9,12 +9,13 @@ import (
"code.gitea.io/gitea/models/unittest"
"code.gitea.io/gitea/modules/markup"
"code.gitea.io/gitea/modules/templates"
+ "code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/services/contexttest"
"github.com/stretchr/testify/assert"
)
-func TestProcessorHelperCodePreview(t *testing.T) {
+func TestRenderHelperCodePreview(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
ctx, _ := contexttest.MockContext(t, "/", contexttest.MockContextOption{Render: templates.HTMLRenderer()})
@@ -79,5 +80,5 @@ func TestProcessorHelperCodePreview(t *testing.T) {
LineStart: 1,
LineStop: 10,
})
- assert.ErrorContains(t, err, "no permission")
+ assert.ErrorIs(t, err, util.ErrPermissionDenied)
}
diff --git a/services/markup/renderhelper_issueicontitle.go b/services/markup/renderhelper_issueicontitle.go
new file mode 100644
index 0000000000000..53a508e908145
--- /dev/null
+++ b/services/markup/renderhelper_issueicontitle.go
@@ -0,0 +1,66 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package markup
+
+import (
+ "context"
+ "fmt"
+ "html/template"
+
+ "code.gitea.io/gitea/models/issues"
+ "code.gitea.io/gitea/models/perm/access"
+ "code.gitea.io/gitea/models/repo"
+ "code.gitea.io/gitea/modules/htmlutil"
+ "code.gitea.io/gitea/modules/markup"
+ "code.gitea.io/gitea/modules/util"
+ gitea_context "code.gitea.io/gitea/services/context"
+)
+
+func renderRepoIssueIconTitle(ctx context.Context, opts markup.RenderIssueIconTitleOptions) (_ template.HTML, err error) {
+ webCtx, ok := ctx.Value(gitea_context.WebContextKey).(*gitea_context.Context)
+ if !ok {
+ return "", fmt.Errorf("context is not a web context")
+ }
+
+ textIssueIndex := fmt.Sprintf("(#%d)", opts.IssueIndex)
+ dbRepo := webCtx.Repo.Repository
+ if opts.OwnerName != "" {
+ dbRepo, err = repo.GetRepositoryByOwnerAndName(ctx, opts.OwnerName, opts.RepoName)
+ if err != nil {
+ return "", err
+ }
+ textIssueIndex = fmt.Sprintf("(%s/%s#%d)", dbRepo.OwnerName, dbRepo.Name, opts.IssueIndex)
+ }
+ if dbRepo == nil {
+ return "", nil
+ }
+
+ issue, err := issues.GetIssueByIndex(ctx, dbRepo.ID, opts.IssueIndex)
+ if err != nil {
+ return "", err
+ }
+
+ if webCtx.Repo.Repository == nil || dbRepo.ID != webCtx.Repo.Repository.ID {
+ perms, err := access.GetUserRepoPermission(ctx, dbRepo, webCtx.Doer)
+ if err != nil {
+ return "", err
+ }
+ if !perms.CanReadIssuesOrPulls(issue.IsPull) {
+ return "", util.ErrPermissionDenied
+ }
+ }
+
+ if issue.IsPull {
+ if err = issue.LoadPullRequest(ctx); err != nil {
+ return "", err
+ }
+ }
+
+ htmlIcon, err := webCtx.RenderToHTML("shared/issueicon", issue)
+ if err != nil {
+ return "", err
+ }
+
+ return htmlutil.HTMLFormat(`%s %s %s`, opts.LinkHref, htmlIcon, issue.Title, textIssueIndex), nil
+}
diff --git a/services/markup/renderhelper_issueicontitle_test.go b/services/markup/renderhelper_issueicontitle_test.go
new file mode 100644
index 0000000000000..adce8401e076b
--- /dev/null
+++ b/services/markup/renderhelper_issueicontitle_test.go
@@ -0,0 +1,49 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package markup
+
+import (
+ "testing"
+
+ "code.gitea.io/gitea/models/repo"
+ "code.gitea.io/gitea/models/unittest"
+ "code.gitea.io/gitea/modules/markup"
+ "code.gitea.io/gitea/modules/templates"
+ "code.gitea.io/gitea/modules/util"
+ "code.gitea.io/gitea/services/contexttest"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestRenderHelperIssueIconTitle(t *testing.T) {
+ assert.NoError(t, unittest.PrepareTestDatabase())
+
+ ctx, _ := contexttest.MockContext(t, "/", contexttest.MockContextOption{Render: templates.HTMLRenderer()})
+ ctx.Repo.Repository = unittest.AssertExistsAndLoadBean(t, &repo.Repository{ID: 1})
+ htm, err := renderRepoIssueIconTitle(ctx, markup.RenderIssueIconTitleOptions{
+ LinkHref: "/link",
+ IssueIndex: 1,
+ })
+ assert.NoError(t, err)
+ assert.Equal(t, `octicon-issue-opened(16/text green) issue1 (#1)`, string(htm))
+
+ ctx, _ = contexttest.MockContext(t, "/", contexttest.MockContextOption{Render: templates.HTMLRenderer()})
+ htm, err = renderRepoIssueIconTitle(ctx, markup.RenderIssueIconTitleOptions{
+ OwnerName: "user2",
+ RepoName: "repo1",
+ LinkHref: "/link",
+ IssueIndex: 1,
+ })
+ assert.NoError(t, err)
+ assert.Equal(t, `octicon-issue-opened(16/text green) issue1 (user2/repo1#1)`, string(htm))
+
+ ctx, _ = contexttest.MockContext(t, "/", contexttest.MockContextOption{Render: templates.HTMLRenderer()})
+ _, err = renderRepoIssueIconTitle(ctx, markup.RenderIssueIconTitleOptions{
+ OwnerName: "user2",
+ RepoName: "repo2",
+ LinkHref: "/link",
+ IssueIndex: 2,
+ })
+ assert.ErrorIs(t, err, util.ErrPermissionDenied)
+}
diff --git a/services/markup/processorhelper_test.go b/services/markup/renderhelper_mention_test.go
similarity index 61%
rename from services/markup/processorhelper_test.go
rename to services/markup/renderhelper_mention_test.go
index 170edae0e0beb..f0c0eb9926ad5 100644
--- a/services/markup/processorhelper_test.go
+++ b/services/markup/renderhelper_mention_test.go
@@ -18,7 +18,7 @@ import (
"github.com/stretchr/testify/assert"
)
-func TestProcessorHelper(t *testing.T) {
+func TestRenderHelperMention(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
userPublic := "user1"
@@ -32,10 +32,10 @@ func TestProcessorHelper(t *testing.T) {
unittest.AssertCount(t, &user.User{Name: userNoSuch}, 0)
// when using general context, use user's visibility to check
- assert.True(t, ProcessorHelper().IsUsernameMentionable(context.Background(), userPublic))
- assert.False(t, ProcessorHelper().IsUsernameMentionable(context.Background(), userLimited))
- assert.False(t, ProcessorHelper().IsUsernameMentionable(context.Background(), userPrivate))
- assert.False(t, ProcessorHelper().IsUsernameMentionable(context.Background(), userNoSuch))
+ assert.True(t, FormalRenderHelperFuncs().IsUsernameMentionable(context.Background(), userPublic))
+ assert.False(t, FormalRenderHelperFuncs().IsUsernameMentionable(context.Background(), userLimited))
+ assert.False(t, FormalRenderHelperFuncs().IsUsernameMentionable(context.Background(), userPrivate))
+ assert.False(t, FormalRenderHelperFuncs().IsUsernameMentionable(context.Background(), userNoSuch))
// when using web context, use user.IsUserVisibleToViewer to check
req, err := http.NewRequest("GET", "/", nil)
@@ -44,11 +44,11 @@ func TestProcessorHelper(t *testing.T) {
defer baseCleanUp()
giteaCtx := gitea_context.NewWebContext(base, &contexttest.MockRender{}, nil)
- assert.True(t, ProcessorHelper().IsUsernameMentionable(giteaCtx, userPublic))
- assert.False(t, ProcessorHelper().IsUsernameMentionable(giteaCtx, userPrivate))
+ assert.True(t, FormalRenderHelperFuncs().IsUsernameMentionable(giteaCtx, userPublic))
+ assert.False(t, FormalRenderHelperFuncs().IsUsernameMentionable(giteaCtx, userPrivate))
giteaCtx.Doer, err = user.GetUserByName(db.DefaultContext, userPrivate)
assert.NoError(t, err)
- assert.True(t, ProcessorHelper().IsUsernameMentionable(giteaCtx, userPublic))
- assert.True(t, ProcessorHelper().IsUsernameMentionable(giteaCtx, userPrivate))
+ assert.True(t, FormalRenderHelperFuncs().IsUsernameMentionable(giteaCtx, userPublic))
+ assert.True(t, FormalRenderHelperFuncs().IsUsernameMentionable(giteaCtx, userPrivate))
}
diff --git a/templates/shared/issueicon.tmpl b/templates/shared/issueicon.tmpl
index a62714e988ca3..f828de5c6694e 100644
--- a/templates/shared/issueicon.tmpl
+++ b/templates/shared/issueicon.tmpl
@@ -1,25 +1,25 @@
-{{if .IsPull}}
- {{if not .PullRequest}}
+{{- if .IsPull -}}
+ {{- if not .PullRequest -}}
No PullRequest
- {{else}}
- {{if .IsClosed}}
- {{if .PullRequest.HasMerged}}
- {{svg "octicon-git-merge" 16 "text purple"}}
- {{else}}
- {{svg "octicon-git-pull-request" 16 "text red"}}
- {{end}}
- {{else}}
- {{if .PullRequest.IsWorkInProgress ctx}}
- {{svg "octicon-git-pull-request-draft" 16 "text grey"}}
- {{else}}
- {{svg "octicon-git-pull-request" 16 "text green"}}
- {{end}}
- {{end}}
- {{end}}
-{{else}}
- {{if .IsClosed}}
- {{svg "octicon-issue-closed" 16 "text red"}}
- {{else}}
- {{svg "octicon-issue-opened" 16 "text green"}}
- {{end}}
-{{end}}
+ {{- else -}}
+ {{- if .IsClosed -}}
+ {{- if .PullRequest.HasMerged -}}
+ {{- svg "octicon-git-merge" 16 "text purple" -}}
+ {{- else -}}
+ {{- svg "octicon-git-pull-request" 16 "text red" -}}
+ {{- end -}}
+ {{- else -}}
+ {{- if .PullRequest.IsWorkInProgress ctx -}}
+ {{- svg "octicon-git-pull-request-draft" 16 "text grey" -}}
+ {{- else -}}
+ {{- svg "octicon-git-pull-request" 16 "text green" -}}
+ {{- end -}}
+ {{- end -}}
+ {{- end -}}
+{{- else -}}
+ {{- if .IsClosed -}}
+ {{- svg "octicon-issue-closed" 16 "text red" -}}
+ {{- else -}}
+ {{- svg "octicon-issue-opened" 16 "text green" -}}
+ {{- end -}}
+{{- end -}}
diff --git a/tests/test_utils.go b/tests/test_utils.go
index deefdd43c5ff4..15637292d4620 100644
--- a/tests/test_utils.go
+++ b/tests/test_utils.go
@@ -61,7 +61,7 @@ func InitTest(requireGitea bool) {
_ = os.Setenv("GITEA_CONF", giteaConf)
fmt.Printf("Environment variable $GITEA_CONF not set, use default: %s\n", giteaConf)
if !setting.EnableSQLite3 {
- testlogger.Fatalf(`sqlite3 requires: import _ "github.com/mattn/go-sqlite3" or -tags sqlite,sqlite_unlock_notify` + "\n")
+ testlogger.Fatalf(`sqlite3 requires: -tags sqlite,sqlite_unlock_notify` + "\n")
}
}
if !filepath.IsAbs(giteaConf) {