From efee6d7ce4076bb3bbb6ac320c3bf65be92a73b8 Mon Sep 17 00:00:00 2001 From: Giteabot Date: Sun, 4 Oct 2026 06:10:58 -0700 Subject: [PATCH] fix(markup): link team mentions that use a different org name case (#39436) (#39588) Backport #39436 by @breken-ai The `org` render meta is lower-cased (`Repository.composeCommonMetas` sets `metas["org"] = strings.ToLower(repo.OwnerName)`), but `mentionProcessor` compares it with the org part of the mention exactly as typed, while the team part is lower-cased before its lookup. So in a repository of the org `MyOrg`, `@MyOrg/developers` stays plain text, although `ResolveIssueMentionsByVisibility` lower-cases the name and still notifies that team. This compares the org part case-insensitively and adds a render test for team mentions. The code is the same on `release/v1.27`, so it could be backported. ### Verification - `go test ./modules/markup/ -run TestRender_TeamMention -count=1`: the `@Org1/Developers` case renders as plain text on current main and as the team link with this change; `go test ./modules/markup/...` passes. - `golangci-lint run ./modules/markup/` (v2.13.2): 0 issues. ### AI assistance This contribution was prepared with AI assistance. The behavior, the metas and mention-resolution code paths, the patch and the test were reviewed manually. > Built by breken, your AI support engineer - breken.ai - this one's on us. Co-authored-by: breken Co-authored-by: wxiaoguang --- modules/markup/html_mention.go | 14 ++++++++------ modules/markup/html_test.go | 15 +++++++++++++++ modules/references/references.go | 2 +- 3 files changed, 24 insertions(+), 7 deletions(-) diff --git a/modules/markup/html_mention.go b/modules/markup/html_mention.go index a8f8d18c451..3da1a55c9fb 100644 --- a/modules/markup/html_mention.go +++ b/modules/markup/html_mention.go @@ -5,6 +5,7 @@ package markup import ( "fmt" + "net/url" "strings" "gitea.dev/modules/references" @@ -26,12 +27,13 @@ func mentionProcessor(ctx *RenderContext, node *html.Node) { loc.Start += start loc.End += start mention := node.Data[loc.Start:loc.End] - teams, ok := ctx.RenderOptions.Metas["teams"] + orgLowerTeams, checkOrgTeams := ctx.RenderOptions.Metas["teams"] // in format ",team1,team2,...,team-n,", always lowercase - if ok && strings.Contains(mention, "/") { - mentionOrgAndTeam := strings.Split(mention, "/") - if mentionOrgAndTeam[0][1:] == ctx.RenderOptions.Metas["org"] && strings.Contains(teams, ","+strings.ToLower(mentionOrgAndTeam[1])+",") { - link := fmt.Sprintf("/:root/org/%s/teams/%s", ctx.RenderOptions.Metas["org"], mentionOrgAndTeam[1]) + if checkOrgTeams && strings.Contains(mention, "/") { + mentionOrg, teamName, _ := strings.Cut(mention, "/") + orgName := mentionOrg[1:] // remove the '@' prefix + if strings.EqualFold(orgName, ctx.RenderOptions.Metas["org"]) && strings.Contains(orgLowerTeams, ","+strings.ToLower(teamName)+",") { + link := fmt.Sprintf("/:root/org/%s/teams/%s", url.PathEscape(orgName), url.PathEscape(teamName)) replaceContent(node, loc.Start, loc.End, createLink(ctx, link, mention, "" /*mention*/)) node = node.NextSibling.NextSibling start = 0 @@ -43,7 +45,7 @@ func mentionProcessor(ctx *RenderContext, node *html.Node) { mentionedUsername := mention[1:] if DefaultRenderHelperFuncs != nil && DefaultRenderHelperFuncs.IsUsernameMentionable(ctx, mentionedUsername) { - link := "/:root/" + mentionedUsername + link := "/:root/" + url.PathEscape(mentionedUsername) replaceContent(node, loc.Start, loc.End, createLink(ctx, link, mention, "" /*mention*/)) node = node.NextSibling.NextSibling start = 0 diff --git a/modules/markup/html_test.go b/modules/markup/html_test.go index f4b44a5cbe1..bdf0ad3e579 100644 --- a/modules/markup/html_test.go +++ b/modules/markup/html_test.go @@ -605,3 +605,18 @@ func TestIssue18471(t *testing.T) { assert.NoError(t, err) assert.Equal(t, `783b039...da951ce`, res.String()) } + +func TestRender_TeamMention(t *testing.T) { + // the "org" meta is lower-cased, see Repository.composeCommonMetas + metas := map[string]string{"user": "Org1", "repo": "repo1", "org": "org1", "teams": ",developers,"} + test := func(input, expected string) { + rctx := markup.NewTestRenderContext(markup.TestAppURL, metas).WithRelativePath("a.md") + buffer, err := testRenderString(rctx, input) + assert.NoError(t, err) + assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer)) + } + test("@org1/developers", `

@org1/developers

`) + test("@Org1/Developers", `

@Org1/Developers

`) + test("@org2/developers", `

@org2/developers

`) + test("@org1/testers", `

@org1/testers

`) +} diff --git a/modules/references/references.go b/modules/references/references.go index db8f9d606dd..88530f509ef 100644 --- a/modules/references/references.go +++ b/modules/references/references.go @@ -240,7 +240,7 @@ func FindAllMentionsBytes(content []byte) []RefSpan { } // FindFirstMentionBytes matches the first mention in then given content -// and returns the location of the unvalidated user name, including the @ prefix. +// and returns the location of the unvalidated username, including the @ prefix. func FindFirstMentionBytes(content []byte) (bool, RefSpan) { mention := mentionPattern.FindSubmatchIndex(content) if mention == nil {