Merge branch 'main' into copilot/fix-issue-24991

This commit is contained in:
silverwind
2026-01-30 22:11:16 +01:00
committed by GitHub
12 changed files with 203 additions and 24 deletions

7
AGENTS.md Normal file
View File

@@ -0,0 +1,7 @@
# Instructions for agents
- Use `make help` to find available development targets
- Before committing go code changes, run `make fmt`
- Before committing `go.mod` changes, run `make tidy`
- Before committing new `.go` files, add the current year into the copyright header
- Before committing files, removed any trailing whitespace

View File

@@ -1329,9 +1329,12 @@ LEVEL = Info
;; Leave it empty to allow users to select any theme from "{CustomPath}/public/assets/css/theme-*.css"
;THEMES =
;;
;; The icons for file list (basic/material), this is a temporary option which will be replaced by a user setting in the future.
;; The icon theme for files (basic/material)
;FILE_ICON_THEME = material
;;
;; The icon theme for folders (basic/material)
;FOLDER_ICON_THEME = basic
;;
;; All available reactions users can choose on issues/prs and comments.
;; Values can be emoji alias (:smile:) or a unicode emoji.
;; For custom reactions, add a tightly cropped square image to public/assets/img/emoji/reaction_name.png

6
go.mod
View File

@@ -28,7 +28,7 @@ require (
github.com/ProtonMail/go-crypto v1.3.0
github.com/PuerkitoBio/goquery v1.10.3
github.com/SaveTheRbtz/zstd-seekable-format-go/pkg v0.8.0
github.com/alecthomas/chroma/v2 v2.23.0
github.com/alecthomas/chroma/v2 v2.23.1
github.com/aws/aws-sdk-go-v2/credentials v1.18.10
github.com/aws/aws-sdk-go-v2/service/codecommit v1.32.2
github.com/blakesmith/ar v0.0.0-20190502131153-809d4375e1fb
@@ -50,10 +50,10 @@ require (
github.com/gliderlabs/ssh v0.3.8
github.com/go-ap/activitypub v0.0.0-20250810115208-cb73b20a1742
github.com/go-ap/jsonld v0.0.0-20221030091449-f2a191312c73
github.com/go-chi/chi/v5 v5.2.3
github.com/go-chi/chi/v5 v5.2.4
github.com/go-chi/cors v1.2.2
github.com/go-co-op/gocron v1.37.0
github.com/go-enry/go-enry/v2 v2.9.2
github.com/go-enry/go-enry/v2 v2.9.4
github.com/go-git/go-billy/v5 v5.6.2
github.com/go-git/go-git/v5 v5.16.3
github.com/go-ldap/ldap/v3 v3.4.11

12
go.sum
View File

@@ -98,8 +98,8 @@ github.com/SaveTheRbtz/zstd-seekable-format-go/pkg v0.8.0/go.mod h1:1HmmMEVsr+0R
github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0=
github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k=
github.com/alecthomas/chroma/v2 v2.2.0/go.mod h1:vf4zrexSH54oEjJ7EdB65tGNHmH3pGZmVkgTP5RHvAs=
github.com/alecthomas/chroma/v2 v2.23.0 h1:u/Orux1J0eLuZDeQ44froV8smumheieI0EofhbyKhhk=
github.com/alecthomas/chroma/v2 v2.23.0/go.mod h1:NqVhfBR0lte5Ouh3DcthuUCTUpDC9cxBOfyMbMQPs3o=
github.com/alecthomas/chroma/v2 v2.23.1 h1:nv2AVZdTyClGbVQkIzlDm/rnhk1E9bU9nXwmZ/Vk/iY=
github.com/alecthomas/chroma/v2 v2.23.1/go.mod h1:NqVhfBR0lte5Ouh3DcthuUCTUpDC9cxBOfyMbMQPs3o=
github.com/alecthomas/repr v0.0.0-20220113201626-b1b626ac65ae/go.mod h1:2kn6fqh/zIyPLmm3ugklbEi5hg5wS435eygvNfaDQL8=
github.com/alecthomas/repr v0.5.2 h1:SU73FTI9D1P5UNtvseffFSGmdNci/O6RsqzeXJtP0Qs=
github.com/alecthomas/repr v0.5.2/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
@@ -316,14 +316,14 @@ github.com/go-ap/jsonld v0.0.0-20221030091449-f2a191312c73/go.mod h1:jyveZeGw5La
github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 h1:BP4M0CvQ4S3TGls2FvczZtj5Re/2ZzkV9VwqPHH/3Bo=
github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0=
github.com/go-chi/chi/v5 v5.0.1/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
github.com/go-chi/chi/v5 v5.2.3 h1:WQIt9uxdsAbgIYgid+BpYc+liqQZGMHRaUwp0JUcvdE=
github.com/go-chi/chi/v5 v5.2.3/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops=
github.com/go-chi/chi/v5 v5.2.4 h1:WtFKPHwlywe8Srng8j2BhOD9312j9cGUxG1SP4V2cR4=
github.com/go-chi/chi/v5 v5.2.4/go.mod h1:X7Gx4mteadT3eDOMTsXzmI4/rwUpOwBHLpAfupzFJP0=
github.com/go-chi/cors v1.2.2 h1:Jmey33TE+b+rB7fT8MUy1u0I4L+NARQlK6LhzKPSyQE=
github.com/go-chi/cors v1.2.2/go.mod h1:sSbTewc+6wYHBBCW7ytsFSn836hqM7JxpglAy2Vzc58=
github.com/go-co-op/gocron v1.37.0 h1:ZYDJGtQ4OMhTLKOKMIch+/CY70Brbb1dGdooLEhh7b0=
github.com/go-co-op/gocron v1.37.0/go.mod h1:3L/n6BkO7ABj+TrfSVXLRzsP26zmikL4ISkLQ0O8iNY=
github.com/go-enry/go-enry/v2 v2.9.2 h1:giOQAtCgBX08kosrX818DCQJTCNtKwoPBGu0qb6nKTY=
github.com/go-enry/go-enry/v2 v2.9.2/go.mod h1:9yrj4ES1YrbNb1Wb7/PWYr2bpaCXUGRt0uafN0ISyG8=
github.com/go-enry/go-enry/v2 v2.9.4 h1:DS4l06/NgMzYjsJ2J52wORo6UsfFDjDCwfAn7w3gG44=
github.com/go-enry/go-enry/v2 v2.9.4/go.mod h1:9yrj4ES1YrbNb1Wb7/PWYr2bpaCXUGRt0uafN0ISyG8=
github.com/go-enry/go-oniguruma v1.2.1 h1:k8aAMuJfMrqm/56SG2lV9Cfti6tC4x8673aHCcBk+eo=
github.com/go-enry/go-oniguruma v1.2.1/go.mod h1:bWDhYP+S6xZQgiRL7wlTScFYBe023B6ilRZbCAD5Hf4=
github.com/go-fed/httpsig v1.1.1-0.20201223112313-55836744818e h1:oRq/fiirun5HqlEWMLIcDmLpIELlG4iGbd0s8iqgPi8=

View File

@@ -34,7 +34,13 @@ func (p *RenderedIconPool) RenderToHTML() template.HTML {
}
func RenderEntryIconHTML(renderedIconPool *RenderedIconPool, entry *EntryInfo) template.HTML {
if setting.UI.FileIconTheme == "material" {
// Use folder theme for directories and symlinks to directories
theme := setting.UI.FileIconTheme
if entry.EntryMode.IsDir() || (entry.EntryMode.IsLink() && entry.SymlinkToMode.IsDir()) {
theme = setting.UI.FolderIconTheme
}
if theme == "material" {
return DefaultMaterialIconProvider().EntryIconHTML(renderedIconPool, entry)
}
return BasicEntryIconHTML(entry)

View File

@@ -0,0 +1,75 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package fileicon_test
import (
"testing"
"code.gitea.io/gitea/modules/fileicon"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/test"
"github.com/stretchr/testify/assert"
)
func TestRenderEntryIconHTML_WithDifferentThemes(t *testing.T) {
// Test that folder icons use the folder theme
t.Run("FolderUsesBasicTheme", func(t *testing.T) {
defer test.MockVariableValue(&setting.UI.FileIconTheme, "material")()
defer test.MockVariableValue(&setting.UI.FolderIconTheme, "basic")()
folderEntry := &fileicon.EntryInfo{
BaseName: "testfolder",
EntryMode: git.EntryModeTree,
}
html := fileicon.RenderEntryIconHTML(nil, folderEntry)
// Basic theme renders octicon classes
assert.Contains(t, string(html), "octicon-file-directory-fill")
})
t.Run("FileUsesMaterialTheme", func(t *testing.T) {
defer test.MockVariableValue(&setting.UI.FileIconTheme, "material")()
defer test.MockVariableValue(&setting.UI.FolderIconTheme, "basic")()
fileEntry := &fileicon.EntryInfo{
BaseName: "test.js",
EntryMode: git.EntryModeBlob,
}
html := fileicon.RenderEntryIconHTML(nil, fileEntry)
// Material theme for files renders material icons
assert.Contains(t, string(html), "svg-mfi-")
})
t.Run("SymlinkToFolderUsesBasicTheme", func(t *testing.T) {
defer test.MockVariableValue(&setting.UI.FileIconTheme, "material")()
defer test.MockVariableValue(&setting.UI.FolderIconTheme, "basic")()
symlinkEntry := &fileicon.EntryInfo{
BaseName: "link",
EntryMode: git.EntryModeSymlink,
SymlinkToMode: git.EntryModeTree,
}
html := fileicon.RenderEntryIconHTML(nil, symlinkEntry)
// Symlinks to folders should use folder theme
assert.Contains(t, string(html), "octicon-file-directory-symlink")
})
t.Run("BothMaterialTheme", func(t *testing.T) {
defer test.MockVariableValue(&setting.UI.FileIconTheme, "material")()
defer test.MockVariableValue(&setting.UI.FolderIconTheme, "material")()
folderEntry := &fileicon.EntryInfo{
BaseName: "testfolder",
EntryMode: git.EntryModeTree,
}
html := fileicon.RenderEntryIconHTML(nil, folderEntry)
// Material theme for folders renders material folder icons
assert.Contains(t, string(html), "svg-mfi-")
})
}

View File

@@ -11,12 +11,12 @@ import (
func Test_loadMailerFrom(t *testing.T) {
kases := map[string]*Mailer{
"smtp.mydomain.com": {
SMTPAddr: "smtp.mydomain.com",
"smtp.mydomain.test": {
SMTPAddr: "smtp.mydomain.test",
SMTPPort: "465",
},
"smtp.mydomain.com:123": {
SMTPAddr: "smtp.mydomain.com",
"smtp.mydomain.test:123": {
SMTPAddr: "smtp.mydomain.test",
SMTPPort: "123",
},
":123": {

View File

@@ -29,6 +29,7 @@ var UI = struct {
DefaultTheme string
Themes []string
FileIconTheme string
FolderIconTheme string
Reactions []string
ReactionsLookup container.Set[string] `ini:"-"`
CustomEmojis []string
@@ -88,6 +89,7 @@ var UI = struct {
MaxDisplayFileSize: 8388608,
DefaultTheme: `gitea-auto`,
FileIconTheme: `material`,
FolderIconTheme: `basic`,
Reactions: []string{`+1`, `-1`, `laugh`, `hooray`, `confused`, `heart`, `rocket`, `eyes`},
CustomEmojis: []string{`git`, `gitea`, `codeberg`, `gitlab`, `github`, `gogs`},
CustomEmojisMap: map[string]string{"git": ":git:", "gitea": ":gitea:", "codeberg": ":codeberg:", "gitlab": ":gitlab:", "github": ":github:", "gogs": ":gogs:"},

View File

@@ -84,7 +84,11 @@ func GetCompareInfo(ctx context.Context, baseRepo, headRepo *repo_model.Reposito
// We have a common base - therefore we know that ... should work
if !fileOnly {
compareInfo.Commits, err = headGitRepo.ShowPrettyFormatLogToList(ctx, compareInfo.BaseCommitID+compareInfo.CompareSeparator+compareInfo.HeadCommitID)
// In git log/rev-list, the "..." syntax represents the symmetric difference between two references,
// which is different from the meaning of "..." in git diff (where it implies diffing from the merge base).
// For listing PR commits, we must use merge-base..head to include only the commits introduced by the head branch.
// Otherwise, commits newly pushed to the base branch would also be included, which is incorrect.
compareInfo.Commits, err = headGitRepo.ShowPrettyFormatLogToList(ctx, compareInfo.MergeBase+".."+compareInfo.HeadCommitID)
if err != nil {
return nil, fmt.Errorf("ShowPrettyFormatLogToList: %w", err)
}

View File

@@ -89,6 +89,24 @@ func issueAddTime(ctx context.Context, issue *issues_model.Issue, doer *user_mod
return err
}
// isSelfReference checks if a commit is the merge commit of the PR it references.
// This prevents creating self-referencing timeline entries when a PR merge commit
// contains a reference to its own PR number in the commit message.
func isSelfReference(ctx context.Context, issue *issues_model.Issue, commitSHA string) bool {
if !issue.IsPull {
return false
}
if err := issue.LoadPullRequest(ctx); err != nil {
if !issues_model.IsErrPullRequestNotExist(err) {
log.Error("LoadPullRequest: %v", err)
}
return false
}
return issue.PullRequest.MergedCommitID == commitSHA
}
// getIssueFromRef returns the issue referenced by a ref. Returns a nil *Issue
// if the provided ref references a non-existent issue.
func getIssueFromRef(ctx context.Context, repo *repo_model.Repository, index int64) (*issues_model.Issue, error) {
@@ -158,6 +176,11 @@ func UpdateIssuesCommit(ctx context.Context, doer *user_model.User, repo *repo_m
continue
}
// Skip self-references: if this commit is the merge commit of the PR it references
if isSelfReference(ctx, refIssue, c.Sha1) {
continue
}
message := fmt.Sprintf(`<a href="%s/commit/%s">%s</a>`, html.EscapeString(repo.Link()), html.EscapeString(url.PathEscape(c.Sha1)), html.EscapeString(strings.SplitN(c.Message, "\n", 2)[0]))
if err = CreateRefComment(ctx, doer, refRepo, refIssue, message, c.Sha1); err != nil {
if errors.Is(err, user_model.ErrBlockedUser) {

View File

@@ -298,3 +298,59 @@ func TestUpdateIssuesCommit_AnotherRepoNoPermission(t *testing.T) {
unittest.AssertNotExistsBean(t, issueBean, "is_closed=1")
unittest.CheckConsistencyFor(t, &activities_model.Action{})
}
func TestUpdateIssuesCommit_SelfReference(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
// Test that a PR merge commit that references its own PR does not create a self-reference comment
// PR #2 (issue_id=2) has merged_commit_id: 1a8823cd1a9549fde083f992f6b9b87a7ab74fb3
pushCommits := []*repository.PushCommit{
{
Sha1: "1a8823cd1a9549fde083f992f6b9b87a7ab74fb3",
CommitterEmail: "user2@example.com",
CommitterName: "User Two",
AuthorEmail: "user2@example.com",
AuthorName: "User Two",
Message: "Merge pull request 'issue2' (#2) from branch1 into master",
},
}
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
selfRefCommentBean := &issues_model.Comment{
Type: issues_model.CommentTypeCommitRef,
CommitSHA: "1a8823cd1a9549fde083f992f6b9b87a7ab74fb3",
PosterID: user.ID,
IssueID: 2,
}
unittest.AssertNotExistsBean(t, selfRefCommentBean)
assert.NoError(t, UpdateIssuesCommit(t.Context(), user, repo, pushCommits, repo.DefaultBranch))
unittest.AssertNotExistsBean(t, selfRefCommentBean)
unittest.CheckConsistencyFor(t, &activities_model.Action{})
// Test that normal commit references are still created
pushCommits2 := []*repository.PushCommit{
{
Sha1: "abcdef9876543210",
CommitterEmail: "user2@example.com",
CommitterName: "User Two",
AuthorEmail: "user2@example.com",
AuthorName: "User Two",
Message: "Fix bug, refs #1",
},
}
otherRefCommentBean := &issues_model.Comment{
Type: issues_model.CommentTypeCommitRef,
CommitSHA: "abcdef9876543210",
PosterID: user.ID,
IssueID: 1,
}
unittest.AssertNotExistsBean(t, otherRefCommentBean)
assert.NoError(t, UpdateIssuesCommit(t.Context(), user, repo, pushCommits2, repo.DefaultBranch))
unittest.AssertExistsAndLoadBean(t, otherRefCommentBean)
unittest.CheckConsistencyFor(t, &activities_model.Action{})
}

View File

@@ -56,6 +56,7 @@ func TestGetTreeBySHA(t *testing.T) {
func TestGetTreeViewNodes(t *testing.T) {
unittest.PrepareTestEnv(t)
ctx, _ := contexttest.MockContext(t, "user2/repo1")
ctx.Repo.RefFullName = git.RefNameFromBranch("sub-home-md-img-check")
contexttest.LoadRepo(t, ctx, 1)
@@ -69,11 +70,13 @@ func TestGetTreeViewNodes(t *testing.T) {
mockIconForFile := func(id string) template.HTML {
return template.HTML(`<svg class="svg git-entry-icon octicon-file" width="16" height="16" aria-hidden="true"><use href="#` + id + `"></use></svg>`)
}
mockIconForFolder := func(id string) template.HTML {
return template.HTML(`<svg class="svg git-entry-icon octicon-file-directory-fill" width="16" height="16" aria-hidden="true"><use href="#` + id + `"></use></svg>`)
mockIconForFolder := func() template.HTML {
// With basic theme (default for folders), we get octicon icons without IDs
return template.HTML(`<span>octicon-file-directory-fill(16/)</span>`)
}
mockOpenIconForFolder := func(id string) template.HTML {
return template.HTML(`<svg class="svg git-entry-icon octicon-file-directory-open-fill" width="16" height="16" aria-hidden="true"><use href="#` + id + `"></use></svg>`)
mockOpenIconForFolder := func() template.HTML {
// With basic theme (default for folders), we get octicon icons without IDs
return template.HTML(`<span>octicon-file-directory-open-fill(16/)</span>`)
}
treeNodes, err := GetTreeViewNodes(ctx, curRepoLink, renderedIconPool, ctx.Repo.Commit, "", "")
assert.NoError(t, err)
@@ -82,8 +85,8 @@ func TestGetTreeViewNodes(t *testing.T) {
EntryName: "docs",
EntryMode: "tree",
FullPath: "docs",
EntryIcon: mockIconForFolder(`svg-mfi-folder-docs`),
EntryIconOpen: mockOpenIconForFolder(`svg-mfi-folder-docs`),
EntryIcon: mockIconForFolder(),
EntryIconOpen: mockOpenIconForFolder(),
},
}, treeNodes)
@@ -94,8 +97,8 @@ func TestGetTreeViewNodes(t *testing.T) {
EntryName: "docs",
EntryMode: "tree",
FullPath: "docs",
EntryIcon: mockIconForFolder(`svg-mfi-folder-docs`),
EntryIconOpen: mockOpenIconForFolder(`svg-mfi-folder-docs`),
EntryIcon: mockIconForFolder(),
EntryIconOpen: mockOpenIconForFolder(),
Children: []*TreeViewNode{
{
EntryName: "README.md",