mirror of
				https://github.com/go-gitea/gitea.git
				synced 2025-10-31 16:01:32 +01: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…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user