mirror of
https://github.com/go-gitea/gitea
synced 2025-01-04 23:14:31 +00:00
Add sub issue list support (#32940)
Just like GitHub, show issue icon/title when the issue number is in a list
This commit is contained in:
parent
02c64e48b7
commit
781c6df40f
@ -206,7 +206,7 @@ func CreateTestEngine(opts FixturesOptions) error {
|
|||||||
x, err := xorm.NewEngine("sqlite3", "file::memory:?cache=shared&_txlock=immediate")
|
x, err := xorm.NewEngine("sqlite3", "file::memory:?cache=shared&_txlock=immediate")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if strings.Contains(err.Error(), "unknown driver") {
|
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
|
return err
|
||||||
}
|
}
|
||||||
|
@ -8,6 +8,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"code.gitea.io/gitea/modules/base"
|
"code.gitea.io/gitea/modules/base"
|
||||||
|
"code.gitea.io/gitea/modules/references"
|
||||||
"code.gitea.io/gitea/modules/util"
|
"code.gitea.io/gitea/modules/util"
|
||||||
|
|
||||||
"golang.org/x/net/html"
|
"golang.org/x/net/html"
|
||||||
@ -194,3 +195,21 @@ func hashCurrentPatternProcessor(ctx *RenderContext, node *html.Node) {
|
|||||||
node = node.NextSibling.NextSibling
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -4,9 +4,9 @@
|
|||||||
package markup
|
package markup
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"code.gitea.io/gitea/modules/base"
|
|
||||||
"code.gitea.io/gitea/modules/httplib"
|
"code.gitea.io/gitea/modules/httplib"
|
||||||
"code.gitea.io/gitea/modules/log"
|
"code.gitea.io/gitea/modules/log"
|
||||||
"code.gitea.io/gitea/modules/references"
|
"code.gitea.io/gitea/modules/references"
|
||||||
@ -16,8 +16,16 @@ import (
|
|||||||
"code.gitea.io/gitea/modules/util"
|
"code.gitea.io/gitea/modules/util"
|
||||||
|
|
||||||
"golang.org/x/net/html"
|
"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) {
|
func fullIssuePatternProcessor(ctx *RenderContext, node *html.Node) {
|
||||||
if ctx.RenderOptions.Metas == nil {
|
if ctx.RenderOptions.Metas == nil {
|
||||||
return
|
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) {
|
func issueIndexPatternProcessor(ctx *RenderContext, node *html.Node) {
|
||||||
if ctx.RenderOptions.Metas == nil {
|
if ctx.RenderOptions.Metas == nil {
|
||||||
return
|
return
|
||||||
@ -76,32 +105,28 @@ func issueIndexPatternProcessor(ctx *RenderContext, node *html.Node) {
|
|||||||
// old logic: crossLinkOnly := ctx.RenderOptions.Metas["mode"] == "document" && !ctx.IsWiki
|
// old logic: crossLinkOnly := ctx.RenderOptions.Metas["mode"] == "document" && !ctx.IsWiki
|
||||||
crossLinkOnly := ctx.RenderOptions.Metas["markupAllowShortIssuePattern"] != "true"
|
crossLinkOnly := ctx.RenderOptions.Metas["markupAllowShortIssuePattern"] != "true"
|
||||||
|
|
||||||
var (
|
var ref *references.RenderizableReference
|
||||||
found bool
|
|
||||||
ref *references.RenderizableReference
|
|
||||||
)
|
|
||||||
|
|
||||||
next := node.NextSibling
|
next := node.NextSibling
|
||||||
|
|
||||||
for node != nil && node != next {
|
for node != nil && node != next {
|
||||||
_, hasExtTrackFormat := ctx.RenderOptions.Metas["format"]
|
_, hasExtTrackFormat := ctx.RenderOptions.Metas["format"]
|
||||||
|
|
||||||
// Repos with external issue trackers might still need to reference local PRs
|
// 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
|
// 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
|
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"] {
|
switch ctx.RenderOptions.Metas["style"] {
|
||||||
case "", IssueNameStyleNumeric:
|
case "", IssueNameStyleNumeric:
|
||||||
found, ref = foundNumeric, refNumeric
|
ref = refNumeric
|
||||||
case IssueNameStyleAlphanumeric:
|
case IssueNameStyleAlphanumeric:
|
||||||
found, ref = references.FindRenderizableReferenceAlphanumeric(node.Data)
|
ref = references.FindRenderizableReferenceAlphanumeric(node.Data)
|
||||||
case IssueNameStyleRegexp:
|
case IssueNameStyleRegexp:
|
||||||
pattern, err := regexplru.GetCompiled(ctx.RenderOptions.Metas["regexp"])
|
pattern, err := regexplru.GetCompiled(ctx.RenderOptions.Metas["regexp"])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
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
|
// 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 hasExtTrackFormat && !isNumericStyle && refNumeric != nil {
|
||||||
// If numeric (PR) was found, and it was BEFORE the non-numeric pattern, use that
|
// 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.
|
// Allow a free-pass when non-numeric pattern wasn't found.
|
||||||
if found && (ref == nil || refNumeric.RefLocation.Start < ref.RefLocation.Start) {
|
if ref == nil || refNumeric.RefLocation.Start < ref.RefLocation.Start {
|
||||||
found = foundNumeric
|
|
||||||
ref = refNumeric
|
ref = refNumeric
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if !found {
|
|
||||||
|
if ref == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var link *html.Node
|
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 {
|
if hasExtTrackFormat && !ref.IsPull {
|
||||||
ctx.RenderOptions.Metas["index"] = ref.Issue
|
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)
|
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 {
|
} else {
|
||||||
// Path determines the type of link that will be rendered. It's unknown at this point whether
|
// 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
|
// 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.
|
// 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")
|
issuePath := util.Iif(ref.IsPull, "pulls", "issues")
|
||||||
if ref.Owner == "" {
|
linkHref := ctx.RenderHelper.ResolveLink(util.URLJoin(issueOwner, issueRepo, issuePath, ref.Issue), LinkTypeApp)
|
||||||
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")
|
// at the moment, only render the issue index in a full line (or simple line) as icon+title
|
||||||
} else {
|
// otherwise it would be too noisy for "take #1 as an example" in a sentence
|
||||||
linkHref := ctx.RenderHelper.ResolveLink(util.URLJoin(ref.Owner, ref.Name, issuePath, ref.Issue), LinkTypeApp)
|
if node.Parent.DataAtom == atom.Li && ref.RefLocation.Start < 20 && ref.RefLocation.End == len(node.Data) {
|
||||||
link = createLink(ctx, linkHref, reftext, "ref-issue")
|
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
|
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
72
modules/markup/html_issue_test.go
Normal file
72
modules/markup/html_issue_test.go
Normal file
@ -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("<div>issue #%d</div>", 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",
|
||||||
|
`<p><a href="http://localhost:3000/test-user/test-repo/issues/12345" class="ref-issue" rel="nofollow">#12345</a></p>`,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("ListIssueRef", func(t *testing.T) {
|
||||||
|
test(
|
||||||
|
"* #12345",
|
||||||
|
`<ul>
|
||||||
|
<li><div>issue #12345</div></li>
|
||||||
|
</ul>`,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("ListIssueRefNormal", func(t *testing.T) {
|
||||||
|
test(
|
||||||
|
"* foo #12345 bar",
|
||||||
|
`<ul>
|
||||||
|
<li>foo <a href="http://localhost:3000/test-user/test-repo/issues/12345" class="ref-issue" rel="nofollow">#12345</a> bar</li>
|
||||||
|
</ul>`,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("ListTodoIssueRef", func(t *testing.T) {
|
||||||
|
test(
|
||||||
|
"* [ ] #12345",
|
||||||
|
`<ul>
|
||||||
|
<li class="task-list-item"><input type="checkbox" disabled="" data-source-position="2"/><div>issue #12345</div></li>
|
||||||
|
</ul>`,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
@ -38,6 +38,7 @@ type RenderHelper interface {
|
|||||||
type RenderHelperFuncs struct {
|
type RenderHelperFuncs struct {
|
||||||
IsUsernameMentionable func(ctx context.Context, username string) bool
|
IsUsernameMentionable func(ctx context.Context, username string) bool
|
||||||
RenderRepoFileCodePreview func(ctx context.Context, options RenderCodePreviewOptions) (template.HTML, error)
|
RenderRepoFileCodePreview func(ctx context.Context, options RenderCodePreviewOptions) (template.HTML, error)
|
||||||
|
RenderRepoIssueIconTitle func(ctx context.Context, options RenderIssueIconTitleOptions) (template.HTML, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
var DefaultRenderHelperFuncs *RenderHelperFuncs
|
var DefaultRenderHelperFuncs *RenderHelperFuncs
|
||||||
|
@ -330,22 +330,22 @@ func FindAllIssueReferences(content string) []IssueReference {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// FindRenderizableReferenceNumeric returns the first unvalidated reference found in a string.
|
// 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
|
var match []int
|
||||||
if !crossLinkOnly {
|
if !crossLinkOnly {
|
||||||
match = issueNumericPattern.FindStringSubmatchIndex(content)
|
match = issueNumericPattern.FindStringSubmatchIndex(content)
|
||||||
}
|
}
|
||||||
if match == nil {
|
if match == nil {
|
||||||
if match = crossReferenceIssueNumericPattern.FindStringSubmatchIndex(content); 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)
|
r := getCrossReference(util.UnsafeStringToBytes(content), match[2], match[3], false, prOnly)
|
||||||
if r == nil {
|
if r == nil {
|
||||||
return false, nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return true, &RenderizableReference{
|
return &RenderizableReference{
|
||||||
Issue: r.issue,
|
Issue: r.issue,
|
||||||
Owner: r.owner,
|
Owner: r.owner,
|
||||||
Name: r.name,
|
Name: r.name,
|
||||||
@ -372,15 +372,14 @@ func FindRenderizableCommitCrossReference(content string) (bool, *RenderizableRe
|
|||||||
}
|
}
|
||||||
|
|
||||||
// FindRenderizableReferenceRegexp returns the first regexp unvalidated references found in a string.
|
// 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)
|
match := pattern.FindStringSubmatchIndex(content)
|
||||||
if len(match) < 4 {
|
if len(match) < 4 {
|
||||||
return false, nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
action, location := findActionKeywords([]byte(content), match[2])
|
action, location := findActionKeywords([]byte(content), match[2])
|
||||||
|
return &RenderizableReference{
|
||||||
return true, &RenderizableReference{
|
|
||||||
Issue: content[match[2]:match[3]],
|
Issue: content[match[2]:match[3]],
|
||||||
RefLocation: &RefSpan{Start: match[0], End: match[1]},
|
RefLocation: &RefSpan{Start: match[0], End: match[1]},
|
||||||
Action: action,
|
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.
|
// 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)
|
match := issueAlphanumericPattern.FindStringSubmatchIndex(content)
|
||||||
if match == nil {
|
if match == nil {
|
||||||
return false, nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
action, location := findActionKeywords([]byte(content), match[2])
|
action, location := findActionKeywords([]byte(content), match[2])
|
||||||
|
return &RenderizableReference{
|
||||||
return true, &RenderizableReference{
|
|
||||||
Issue: content[match[2]:match[3]],
|
Issue: content[match[2]:match[3]],
|
||||||
RefLocation: &RefSpan{Start: match[2], End: match[3]},
|
RefLocation: &RefSpan{Start: match[2], End: match[3]},
|
||||||
Action: action,
|
Action: action,
|
||||||
|
@ -249,11 +249,10 @@ func TestFindAllIssueReferences(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for _, fixture := range alnumFixtures {
|
for _, fixture := range alnumFixtures {
|
||||||
found, ref := FindRenderizableReferenceAlphanumeric(fixture.input)
|
ref := FindRenderizableReferenceAlphanumeric(fixture.input)
|
||||||
if fixture.issue == "" {
|
if fixture.issue == "" {
|
||||||
assert.False(t, found, "Failed to parse: {%s}", fixture.input)
|
assert.Nil(t, ref, "Failed to parse: {%s}", fixture.input)
|
||||||
} else {
|
} 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.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.refLocation, ref.RefLocation, "Failed to parse: {%s}", fixture.input)
|
||||||
assert.Equal(t, fixture.action, ref.Action, "Failed to parse: {%s}", fixture.input)
|
assert.Equal(t, fixture.action, ref.Action, "Failed to parse: {%s}", fixture.input)
|
||||||
|
@ -10,7 +10,7 @@ import (
|
|||||||
"sync"
|
"sync"
|
||||||
)
|
)
|
||||||
|
|
||||||
type normalizeVarsStruct struct {
|
type globalVarsStruct struct {
|
||||||
reXMLDoc,
|
reXMLDoc,
|
||||||
reComment,
|
reComment,
|
||||||
reAttrXMLNs,
|
reAttrXMLNs,
|
||||||
@ -18,26 +18,23 @@ type normalizeVarsStruct struct {
|
|||||||
reAttrClassPrefix *regexp.Regexp
|
reAttrClassPrefix *regexp.Regexp
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var globalVars = sync.OnceValue(func() *globalVarsStruct {
|
||||||
normalizeVars *normalizeVarsStruct
|
return &globalVarsStruct{
|
||||||
normalizeVarsOnce sync.Once
|
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
|
// 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.
|
// 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 {
|
func Normalize(data []byte, size int) []byte {
|
||||||
normalizeVarsOnce.Do(func() {
|
vars := globalVars()
|
||||||
normalizeVars = &normalizeVarsStruct{
|
data = vars.reXMLDoc.ReplaceAll(data, nil)
|
||||||
reXMLDoc: regexp.MustCompile(`(?s)<\?xml.*?>`),
|
data = vars.reComment.ReplaceAll(data, nil)
|
||||||
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)
|
|
||||||
|
|
||||||
data = bytes.TrimSpace(data)
|
data = bytes.TrimSpace(data)
|
||||||
svgTag, svgRemaining, ok := bytes.Cut(data, []byte(">"))
|
svgTag, svgRemaining, ok := bytes.Cut(data, []byte(">"))
|
||||||
@ -45,9 +42,9 @@ func Normalize(data []byte, size int) []byte {
|
|||||||
return data
|
return data
|
||||||
}
|
}
|
||||||
normalized := bytes.Clone(svgTag)
|
normalized := bytes.Clone(svgTag)
|
||||||
normalized = normalizeVars.reAttrXMLNs.ReplaceAll(normalized, nil)
|
normalized = vars.reAttrXMLNs.ReplaceAll(normalized, nil)
|
||||||
normalized = normalizeVars.reAttrSize.ReplaceAll(normalized, nil)
|
normalized = vars.reAttrSize.ReplaceAll(normalized, nil)
|
||||||
normalized = normalizeVars.reAttrClassPrefix.ReplaceAll(normalized, []byte(` class="`))
|
normalized = vars.reAttrClassPrefix.ReplaceAll(normalized, []byte(` class="`))
|
||||||
normalized = bytes.TrimSpace(normalized)
|
normalized = bytes.TrimSpace(normalized)
|
||||||
normalized = fmt.Appendf(normalized, ` width="%d" height="%d"`, size, size)
|
normalized = fmt.Appendf(normalized, ` width="%d" height="%d"`, size, size)
|
||||||
if !bytes.Contains(normalized, []byte(` class="`)) {
|
if !bytes.Contains(normalized, []byte(` class="`)) {
|
||||||
|
@ -133,7 +133,7 @@ func InitWebInstalled(ctx context.Context) {
|
|||||||
|
|
||||||
highlight.NewContext()
|
highlight.NewContext()
|
||||||
external.RegisterRenderers()
|
external.RegisterRenderers()
|
||||||
markup.Init(markup_service.ProcessorHelper())
|
markup.Init(markup_service.FormalRenderHelperFuncs())
|
||||||
|
|
||||||
if setting.EnableSQLite3 {
|
if setting.EnableSQLite3 {
|
||||||
log.Info("SQLite3 support is enabled")
|
log.Info("SQLite3 support is enabled")
|
||||||
|
@ -106,7 +106,7 @@ func (ctx *Context) JSONTemplate(tmpl templates.TplName) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// RenderToHTML renders the template content to a HTML string
|
// RenderToHTML renders the template content to a HTML string
|
||||||
func (ctx *Context) RenderToHTML(name templates.TplName, data map[string]any) (template.HTML, error) {
|
func (ctx *Context) RenderToHTML(name templates.TplName, data any) (template.HTML, error) {
|
||||||
var buf strings.Builder
|
var buf strings.Builder
|
||||||
err := ctx.Render.HTML(&buf, 0, name, data, ctx.TemplateContext)
|
err := ctx.Render.HTML(&buf, 0, name, data, ctx.TemplateContext)
|
||||||
return template.HTML(buf.String()), err
|
return template.HTML(buf.String()), err
|
||||||
|
@ -11,6 +11,6 @@ import (
|
|||||||
|
|
||||||
func TestMain(m *testing.M) {
|
func TestMain(m *testing.M) {
|
||||||
unittest.MainTest(m, &unittest.TestOptions{
|
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"},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -11,9 +11,10 @@ import (
|
|||||||
gitea_context "code.gitea.io/gitea/services/context"
|
gitea_context "code.gitea.io/gitea/services/context"
|
||||||
)
|
)
|
||||||
|
|
||||||
func ProcessorHelper() *markup.RenderHelperFuncs {
|
func FormalRenderHelperFuncs() *markup.RenderHelperFuncs {
|
||||||
return &markup.RenderHelperFuncs{
|
return &markup.RenderHelperFuncs{
|
||||||
RenderRepoFileCodePreview: renderRepoFileCodePreview,
|
RenderRepoFileCodePreview: renderRepoFileCodePreview,
|
||||||
|
RenderRepoIssueIconTitle: renderRepoIssueIconTitle,
|
||||||
IsUsernameMentionable: func(ctx context.Context, username string) bool {
|
IsUsernameMentionable: func(ctx context.Context, username string) bool {
|
||||||
mentionedUser, err := user.GetUserByName(ctx, username)
|
mentionedUser, err := user.GetUserByName(ctx, username)
|
||||||
if err != nil {
|
if err != nil {
|
@ -18,6 +18,7 @@ import (
|
|||||||
"code.gitea.io/gitea/modules/indexer/code"
|
"code.gitea.io/gitea/modules/indexer/code"
|
||||||
"code.gitea.io/gitea/modules/markup"
|
"code.gitea.io/gitea/modules/markup"
|
||||||
"code.gitea.io/gitea/modules/setting"
|
"code.gitea.io/gitea/modules/setting"
|
||||||
|
"code.gitea.io/gitea/modules/util"
|
||||||
gitea_context "code.gitea.io/gitea/services/context"
|
gitea_context "code.gitea.io/gitea/services/context"
|
||||||
"code.gitea.io/gitea/services/repository/files"
|
"code.gitea.io/gitea/services/repository/files"
|
||||||
)
|
)
|
||||||
@ -46,7 +47,7 @@ func renderRepoFileCodePreview(ctx context.Context, opts markup.RenderCodePrevie
|
|||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
if !perms.CanRead(unit.TypeCode) {
|
if !perms.CanRead(unit.TypeCode) {
|
||||||
return "", fmt.Errorf("no permission")
|
return "", util.ErrPermissionDenied
|
||||||
}
|
}
|
||||||
|
|
||||||
gitRepo, err := gitrepo.OpenRepository(ctx, dbRepo)
|
gitRepo, err := gitrepo.OpenRepository(ctx, dbRepo)
|
@ -9,12 +9,13 @@ import (
|
|||||||
"code.gitea.io/gitea/models/unittest"
|
"code.gitea.io/gitea/models/unittest"
|
||||||
"code.gitea.io/gitea/modules/markup"
|
"code.gitea.io/gitea/modules/markup"
|
||||||
"code.gitea.io/gitea/modules/templates"
|
"code.gitea.io/gitea/modules/templates"
|
||||||
|
"code.gitea.io/gitea/modules/util"
|
||||||
"code.gitea.io/gitea/services/contexttest"
|
"code.gitea.io/gitea/services/contexttest"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestProcessorHelperCodePreview(t *testing.T) {
|
func TestRenderHelperCodePreview(t *testing.T) {
|
||||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||||
|
|
||||||
ctx, _ := contexttest.MockContext(t, "/", contexttest.MockContextOption{Render: templates.HTMLRenderer()})
|
ctx, _ := contexttest.MockContext(t, "/", contexttest.MockContextOption{Render: templates.HTMLRenderer()})
|
||||||
@ -79,5 +80,5 @@ func TestProcessorHelperCodePreview(t *testing.T) {
|
|||||||
LineStart: 1,
|
LineStart: 1,
|
||||||
LineStop: 10,
|
LineStop: 10,
|
||||||
})
|
})
|
||||||
assert.ErrorContains(t, err, "no permission")
|
assert.ErrorIs(t, err, util.ErrPermissionDenied)
|
||||||
}
|
}
|
66
services/markup/renderhelper_issueicontitle.go
Normal file
66
services/markup/renderhelper_issueicontitle.go
Normal file
@ -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(`<a href="%s">%s %s %s</a>`, opts.LinkHref, htmlIcon, issue.Title, textIssueIndex), nil
|
||||||
|
}
|
49
services/markup/renderhelper_issueicontitle_test.go
Normal file
49
services/markup/renderhelper_issueicontitle_test.go
Normal file
@ -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, `<a href="/link"><span>octicon-issue-opened(16/text green)</span> issue1 (#1)</a>`, 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, `<a href="/link"><span>octicon-issue-opened(16/text green)</span> issue1 (user2/repo1#1)</a>`, 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)
|
||||||
|
}
|
@ -18,7 +18,7 @@ import (
|
|||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestProcessorHelper(t *testing.T) {
|
func TestRenderHelperMention(t *testing.T) {
|
||||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||||
|
|
||||||
userPublic := "user1"
|
userPublic := "user1"
|
||||||
@ -32,10 +32,10 @@ func TestProcessorHelper(t *testing.T) {
|
|||||||
unittest.AssertCount(t, &user.User{Name: userNoSuch}, 0)
|
unittest.AssertCount(t, &user.User{Name: userNoSuch}, 0)
|
||||||
|
|
||||||
// when using general context, use user's visibility to check
|
// when using general context, use user's visibility to check
|
||||||
assert.True(t, ProcessorHelper().IsUsernameMentionable(context.Background(), userPublic))
|
assert.True(t, FormalRenderHelperFuncs().IsUsernameMentionable(context.Background(), userPublic))
|
||||||
assert.False(t, ProcessorHelper().IsUsernameMentionable(context.Background(), userLimited))
|
assert.False(t, FormalRenderHelperFuncs().IsUsernameMentionable(context.Background(), userLimited))
|
||||||
assert.False(t, ProcessorHelper().IsUsernameMentionable(context.Background(), userPrivate))
|
assert.False(t, FormalRenderHelperFuncs().IsUsernameMentionable(context.Background(), userPrivate))
|
||||||
assert.False(t, ProcessorHelper().IsUsernameMentionable(context.Background(), userNoSuch))
|
assert.False(t, FormalRenderHelperFuncs().IsUsernameMentionable(context.Background(), userNoSuch))
|
||||||
|
|
||||||
// when using web context, use user.IsUserVisibleToViewer to check
|
// when using web context, use user.IsUserVisibleToViewer to check
|
||||||
req, err := http.NewRequest("GET", "/", nil)
|
req, err := http.NewRequest("GET", "/", nil)
|
||||||
@ -44,11 +44,11 @@ func TestProcessorHelper(t *testing.T) {
|
|||||||
defer baseCleanUp()
|
defer baseCleanUp()
|
||||||
giteaCtx := gitea_context.NewWebContext(base, &contexttest.MockRender{}, nil)
|
giteaCtx := gitea_context.NewWebContext(base, &contexttest.MockRender{}, nil)
|
||||||
|
|
||||||
assert.True(t, ProcessorHelper().IsUsernameMentionable(giteaCtx, userPublic))
|
assert.True(t, FormalRenderHelperFuncs().IsUsernameMentionable(giteaCtx, userPublic))
|
||||||
assert.False(t, ProcessorHelper().IsUsernameMentionable(giteaCtx, userPrivate))
|
assert.False(t, FormalRenderHelperFuncs().IsUsernameMentionable(giteaCtx, userPrivate))
|
||||||
|
|
||||||
giteaCtx.Doer, err = user.GetUserByName(db.DefaultContext, userPrivate)
|
giteaCtx.Doer, err = user.GetUserByName(db.DefaultContext, userPrivate)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.True(t, ProcessorHelper().IsUsernameMentionable(giteaCtx, userPublic))
|
assert.True(t, FormalRenderHelperFuncs().IsUsernameMentionable(giteaCtx, userPublic))
|
||||||
assert.True(t, ProcessorHelper().IsUsernameMentionable(giteaCtx, userPrivate))
|
assert.True(t, FormalRenderHelperFuncs().IsUsernameMentionable(giteaCtx, userPrivate))
|
||||||
}
|
}
|
@ -1,25 +1,25 @@
|
|||||||
{{if .IsPull}}
|
{{- if .IsPull -}}
|
||||||
{{if not .PullRequest}}
|
{{- if not .PullRequest -}}
|
||||||
No PullRequest
|
No PullRequest
|
||||||
{{else}}
|
{{- else -}}
|
||||||
{{if .IsClosed}}
|
{{- if .IsClosed -}}
|
||||||
{{if .PullRequest.HasMerged}}
|
{{- if .PullRequest.HasMerged -}}
|
||||||
{{svg "octicon-git-merge" 16 "text purple"}}
|
{{- svg "octicon-git-merge" 16 "text purple" -}}
|
||||||
{{else}}
|
{{- else -}}
|
||||||
{{svg "octicon-git-pull-request" 16 "text red"}}
|
{{- svg "octicon-git-pull-request" 16 "text red" -}}
|
||||||
{{end}}
|
{{- end -}}
|
||||||
{{else}}
|
{{- else -}}
|
||||||
{{if .PullRequest.IsWorkInProgress ctx}}
|
{{- if .PullRequest.IsWorkInProgress ctx -}}
|
||||||
{{svg "octicon-git-pull-request-draft" 16 "text grey"}}
|
{{- svg "octicon-git-pull-request-draft" 16 "text grey" -}}
|
||||||
{{else}}
|
{{- else -}}
|
||||||
{{svg "octicon-git-pull-request" 16 "text green"}}
|
{{- svg "octicon-git-pull-request" 16 "text green" -}}
|
||||||
{{end}}
|
{{- end -}}
|
||||||
{{end}}
|
{{- end -}}
|
||||||
{{end}}
|
{{- end -}}
|
||||||
{{else}}
|
{{- else -}}
|
||||||
{{if .IsClosed}}
|
{{- if .IsClosed -}}
|
||||||
{{svg "octicon-issue-closed" 16 "text red"}}
|
{{- svg "octicon-issue-closed" 16 "text red" -}}
|
||||||
{{else}}
|
{{- else -}}
|
||||||
{{svg "octicon-issue-opened" 16 "text green"}}
|
{{- svg "octicon-issue-opened" 16 "text green" -}}
|
||||||
{{end}}
|
{{- end -}}
|
||||||
{{end}}
|
{{- end -}}
|
||||||
|
@ -58,7 +58,7 @@ func InitTest(requireGitea bool) {
|
|||||||
_ = os.Setenv("GITEA_CONF", giteaConf)
|
_ = os.Setenv("GITEA_CONF", giteaConf)
|
||||||
fmt.Printf("Environment variable $GITEA_CONF not set, use default: %s\n", giteaConf)
|
fmt.Printf("Environment variable $GITEA_CONF not set, use default: %s\n", giteaConf)
|
||||||
if !setting.EnableSQLite3 {
|
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) {
|
if !filepath.IsAbs(giteaConf) {
|
||||||
|
Loading…
Reference in New Issue
Block a user