0
0
mirror of https://github.com/go-gitea/gitea.git synced 2026-04-30 07:44:29 +02:00

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

This commit is contained in:
silverwind 2026-02-01 09:11:20 +01:00 committed by GitHub
commit e316774a2c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
32 changed files with 645 additions and 769 deletions

2
.github/labeler.yml vendored
View File

@ -84,9 +84,9 @@ docs-update-needed:
topic/code-linting:
- changed-files:
- any-glob-to-any-file:
- ".eslintrc.cjs"
- ".golangci.yml"
- ".markdownlint.yaml"
- ".spectral.yaml"
- ".yamllint.yaml"
- "eslint*.config.*"
- "stylelint.config.*"

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

@ -314,16 +314,14 @@ lint-backend: lint-go lint-go-gitea-vet lint-editorconfig ## lint backend files
lint-backend-fix: lint-go-fix lint-go-gitea-vet lint-editorconfig ## lint backend files and fix issues
.PHONY: lint-js
lint-js: node_modules ## lint js files
lint-js: node_modules ## lint js and ts files
$(NODE_VARS) pnpm exec eslint --color --max-warnings=0 $(ESLINT_FILES)
$(NODE_VARS) pnpm exec vue-tsc
$(NODE_VARS) pnpm exec knip --no-progress --cache
.PHONY: lint-js-fix
lint-js-fix: node_modules ## lint js files and fix issues
lint-js-fix: node_modules ## lint js and ts files and fix issues
$(NODE_VARS) pnpm exec eslint --color --max-warnings=0 $(ESLINT_FILES) --fix
$(NODE_VARS) pnpm exec vue-tsc
$(NODE_VARS) pnpm exec knip --no-progress --cache --fix
.PHONY: lint-css
lint-css: node_modules ## lint css files

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

@ -1,18 +0,0 @@
import type {KnipConfig} from 'knip';
export default {
entry: [
'*.ts',
'tools/**/*.ts',
'tests/e2e/**/*.ts',
],
ignoreDependencies: [
// dependencies used in Makefile or tools
'@primer/octicons',
'markdownlint-cli',
'nolyfill',
'spectral-cli-bundle',
'vue-tsc',
'webpack-cli',
],
} satisfies KnipConfig;

View File

@ -4,6 +4,7 @@
package user
import (
"strconv"
"strings"
"code.gitea.io/gitea/modules/structs"
@ -23,10 +24,6 @@ func NewGhostUser() *User {
}
}
func IsGhostUserName(name string) bool {
return strings.EqualFold(name, GhostUserName)
}
// IsGhost check if user is fake user for a deleted account
func (u *User) IsGhost() bool {
if u == nil {
@ -41,10 +38,6 @@ const (
ActionsUserEmail = "teabot@gitea.io"
)
func IsGiteaActionsUserName(name string) bool {
return strings.EqualFold(name, ActionsUserName)
}
// NewActionsUser creates and returns a fake user for running the actions.
func NewActionsUser() *User {
return &User{
@ -61,15 +54,36 @@ func NewActionsUser() *User {
}
}
func NewActionsUserWithTaskID(id int64) *User {
u := NewActionsUser()
// LoginName is for only internal usage in this case, so it can be moved to other fields in the future
u.LoginSource = -1
u.LoginName = "@" + ActionsUserName + "/" + strconv.FormatInt(id, 10)
return u
}
func GetActionsUserTaskID(u *User) (int64, bool) {
if u == nil || u.ID != ActionsUserID {
return 0, false
}
prefix, payload, _ := strings.Cut(u.LoginName, "/")
if prefix != "@"+ActionsUserName {
return 0, false
} else if taskID, err := strconv.ParseInt(payload, 10, 64); err == nil {
return taskID, true
}
return 0, false
}
func (u *User) IsGiteaActions() bool {
return u != nil && u.ID == ActionsUserID
}
func GetSystemUserByName(name string) *User {
if IsGhostUserName(name) {
if strings.EqualFold(name, GhostUserName) {
return NewGhostUser()
}
if IsGiteaActionsUserName(name) {
if strings.EqualFold(name, ActionsUserName) {
return NewActionsUser()
}
return nil

View File

@ -16,14 +16,20 @@ func TestSystemUser(t *testing.T) {
assert.Equal(t, "Ghost", u.Name)
assert.Equal(t, "ghost", u.LowerName)
assert.True(t, u.IsGhost())
assert.True(t, IsGhostUserName("gHost"))
u = GetSystemUserByName("gHost")
require.NotNil(t, u)
assert.Equal(t, "Ghost", u.Name)
u, err = GetPossibleUserByID(t.Context(), -2)
require.NoError(t, err)
assert.Equal(t, "gitea-actions", u.Name)
assert.Equal(t, "gitea-actions", u.LowerName)
assert.True(t, u.IsGiteaActions())
assert.True(t, IsGiteaActionsUserName("Gitea-actionS"))
u = GetSystemUserByName("Gitea-actionS")
require.NotNil(t, u)
assert.Equal(t, "Gitea Actions", u.FullName)
_, err = GetPossibleUserByID(t.Context(), -3)
require.Error(t, err)

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

@ -271,7 +271,7 @@
"install.smtp_port": "SMTP 端口",
"install.smtp_from": "邮件发件人",
"install.smtp_from_invalid": "「邮件发件人」地址无效",
"install.smtp_from_helper": "请输入一个用于 Gitea 的邮箱地址,或者使用完整格式:「名称」<email@example.com>。",
"install.smtp_from_helper": "请输入一个用于 Gitea 的邮箱地址,或者使用完整格式:\"名称\" <email@example.com>。",
"install.mailer_user": "SMTP 用户名",
"install.mailer_password": "SMTP 密码",
"install.register_confirm": "需要邮件确认注册",
@ -1796,6 +1796,7 @@
"repo.pulls.remove_prefix": "删除 <strong>%s</strong> 前缀",
"repo.pulls.data_broken": "此合并请求因为派生仓库信息缺失而中断。",
"repo.pulls.files_conflicted": "此合并请求有变更与目标分支冲突。",
"repo.pulls.files_conflicted_no_listed_files": "(未列出冲突文件)",
"repo.pulls.is_checking": "正在进行合并冲突检查…",
"repo.pulls.is_ancestor": "此分支已经包含在目标分支中,没有什么可以合并。",
"repo.pulls.is_empty": "此分支上的更改已经在目标分支上。这将是一个空提交。",
@ -2932,7 +2933,7 @@
"admin.dashboard.delete_old_actions.started": "已开始从数据库中删除所有旧工作流记录。",
"admin.dashboard.update_checker": "更新检查器",
"admin.dashboard.delete_old_system_notices": "从数据库中删除所有旧系统通知",
"admin.dashboard.gc_lfs": "垃圾回收 LFS 元数据",
"admin.dashboard.gc_lfs": "对 LFS 元数据进行垃圾回收",
"admin.dashboard.stop_zombie_tasks": "停止僵尸工作流任务",
"admin.dashboard.stop_endless_tasks": "停止无限循环的工作流任务",
"admin.dashboard.cancel_abandoned_jobs": "取消已放弃的工作流任务",
@ -3324,7 +3325,7 @@
"admin.monitor.queue.numberinqueue": "队列中的数量",
"admin.monitor.queue.review_add": "查看 / 添加工作者",
"admin.monitor.queue.settings.title": "池设置",
"admin.monitor.queue.settings.desc": "因为工作者队列阻塞,池正在动态扩展。",
"admin.monitor.queue.settings.desc": "工作池会根据其工作队列阻塞情况动态增长。",
"admin.monitor.queue.settings.maxnumberworkers": "最大工作者数量",
"admin.monitor.queue.settings.maxnumberworkers.placeholder": "当前 %[1]d",
"admin.monitor.queue.settings.maxnumberworkers.error": "最大工作者数必须是数字",

View File

@ -1,6 +1,6 @@
{
"type": "module",
"packageManager": "pnpm@10.28.1",
"packageManager": "pnpm@10.28.2",
"engines": {
"node": ">= 22.6.0",
"pnpm": ">= 10.0.0"
@ -28,7 +28,7 @@
"clippie": "4.1.9",
"compare-versions": "6.1.1",
"cropperjs": "1.6.2",
"css-loader": "7.1.2",
"css-loader": "7.1.3",
"dayjs": "1.11.19",
"dropzone": "6.0.0-beta.2",
"easymde": "2.20.0",
@ -37,7 +37,7 @@
"idiomorph": "0.7.4",
"jquery": "4.0.0",
"js-yaml": "4.1.1",
"katex": "0.16.27",
"katex": "0.16.28",
"mermaid": "11.12.2",
"mini-css-extract-plugin": "2.10.0",
"monaco-editor": "0.55.1",
@ -68,7 +68,7 @@
"devDependencies": {
"@eslint-community/eslint-plugin-eslint-comments": "4.6.0",
"@eslint/json": "0.14.0",
"@playwright/test": "1.58.0",
"@playwright/test": "1.58.1",
"@stylistic/eslint-plugin": "5.7.1",
"@stylistic/stylelint-plugin": "5.0.1",
"@types/codemirror": "5.60.17",
@ -82,7 +82,7 @@
"@types/throttle-debounce": "5.0.2",
"@types/tinycolor2": "1.4.6",
"@types/toastify-js": "1.12.4",
"@typescript-eslint/parser": "8.53.1",
"@typescript-eslint/parser": "8.54.0",
"@vitejs/plugin-vue": "6.0.3",
"@vitest/eslint-plugin": "1.6.6",
"eslint": "9.39.2",
@ -90,34 +90,33 @@
"eslint-plugin-array-func": "5.1.0",
"eslint-plugin-github": "6.0.0",
"eslint-plugin-import-x": "4.16.1",
"eslint-plugin-playwright": "2.5.0",
"eslint-plugin-playwright": "2.5.1",
"eslint-plugin-regexp": "3.0.0",
"eslint-plugin-sonarjs": "3.0.5",
"eslint-plugin-sonarjs": "3.0.6",
"eslint-plugin-unicorn": "62.0.0",
"eslint-plugin-vue": "10.7.0",
"eslint-plugin-vue-scoped-css": "2.12.0",
"eslint-plugin-wc": "3.0.2",
"globals": "17.1.0",
"happy-dom": "20.3.7",
"globals": "17.2.0",
"happy-dom": "20.4.0",
"jiti": "2.6.1",
"knip": "5.82.1",
"markdownlint-cli": "0.47.0",
"material-icon-theme": "5.31.0",
"nolyfill": "1.0.44",
"postcss-html": "1.8.1",
"spectral-cli-bundle": "1.0.3",
"stylelint": "17.0.0",
"stylelint": "17.1.0",
"stylelint-config-recommended": "18.0.0",
"stylelint-declaration-block-no-ignored-properties": "2.8.0",
"stylelint-declaration-block-no-ignored-properties": "3.0.0",
"stylelint-declaration-strict-value": "1.10.11",
"stylelint-value-no-unknown-custom-properties": "6.1.1",
"svgo": "4.0.0",
"typescript": "5.9.3",
"typescript-eslint": "8.53.1",
"updates": "17.0.8",
"vite-string-plugin": "1.5.0",
"typescript-eslint": "8.54.0",
"updates": "17.0.9",
"vite-string-plugin": "2.0.0",
"vitest": "4.0.18",
"vue-tsc": "3.2.3"
"vue-tsc": "3.2.4"
},
"browserslist": [
"defaults"

970
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@ -188,8 +188,7 @@ func repoAssignment() func(ctx *context.APIContext) {
repo.Owner = owner
ctx.Repo.Repository = repo
if ctx.Doer != nil && ctx.Doer.ID == user_model.ActionsUserID {
taskID := ctx.Data["ActionsTaskID"].(int64)
if taskID, ok := user_model.GetActionsUserTaskID(ctx.Doer); ok {
ctx.Repo.Permission, err = access_model.GetActionsUserRepoPermission(ctx, repo, ctx.Doer, taskID)
if err != nil {
ctx.APIErrorInternal(err)
@ -349,11 +348,7 @@ func tokenRequiresScopes(requiredScopeCategories ...auth_model.AccessTokenScopeC
// Contexter middleware already checks token for user sign in process.
func reqToken() func(ctx *context.APIContext) {
return func(ctx *context.APIContext) {
// If actions token is present
if true == ctx.Data["IsActionsToken"] {
return
}
// if a real user is signed in, or the user is from a Actions task, we are good
if ctx.IsSigned {
return
}

View File

@ -22,6 +22,7 @@ import (
access_model "code.gitea.io/gitea/models/perm/access"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unit"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/git/gitcmd"
"code.gitea.io/gitea/modules/gitrepo"
@ -166,7 +167,7 @@ func httpBase(ctx *context.Context, optGitService ...string) *serviceHandler {
return nil
}
if ctx.IsBasicAuth && ctx.Data["IsApiToken"] != true && ctx.Data["IsActionsToken"] != true {
if ctx.IsBasicAuth && ctx.Data["IsApiToken"] != true && !ctx.Doer.IsGiteaActions() {
_, err = auth_model.GetTwoFactorByUID(ctx, ctx.Doer.ID)
if err == nil {
// TODO: This response should be changed to "invalid credentials" for security reasons once the expectation behind it (creating an app token to authenticate) is properly documented
@ -197,8 +198,7 @@ func httpBase(ctx *context.Context, optGitService ...string) *serviceHandler {
accessMode = perm.AccessModeRead
}
if ctx.Data["IsActionsToken"] == true {
taskID := ctx.Data["ActionsTaskID"].(int64)
if taskID, ok := user_model.GetActionsUserTaskID(ctx.Doer); ok {
p, err := access_model.GetActionsUserRepoPermission(ctx, repo, ctx.Doer, taskID)
if err != nil {
ctx.ServerError("GetActionsUserRepoPermission", err)

View File

@ -1530,7 +1530,7 @@ func registerWebRoutes(m *web.Router) {
m.Group("/{username}/{reponame}", func() {
m.Get("/{type:pulls}", repo.Issues)
m.Group("/{type:pulls}/{index}", func() {
m.Get("", repo.SetWhitespaceBehavior, repo.GetPullDiffStats, repo.ViewIssue)
m.Get("", repo.SetEditorconfigIfExists, repo.SetWhitespaceBehavior, repo.GetPullDiffStats, repo.ViewIssue)
m.Get(".diff", repo.DownloadPullDiff)
m.Get(".patch", repo.DownloadPullPatch)
m.Get("/merge_box", repo.ViewPullMergeBox)

View File

@ -117,12 +117,8 @@ func (b *Basic) Verify(req *http.Request, w http.ResponseWriter, store DataStore
task, err := actions_model.GetRunningTaskByToken(req.Context(), authToken)
if err == nil && task != nil {
log.Trace("Basic Authorization: Valid AccessToken for task[%d]", task.ID)
store.GetData()["LoginMethod"] = ActionTokenMethodName
store.GetData()["IsActionsToken"] = true
store.GetData()["ActionsTaskID"] = task.ID
return user_model.NewActionsUser(), nil
return user_model.NewActionsUserWithTaskID(task.ID), nil
}
if !setting.Service.EnableBasicAuth {

View File

@ -6,6 +6,7 @@ package auth
import (
"context"
"errors"
"net/http"
"strings"
"time"
@ -17,14 +18,12 @@ import (
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/timeutil"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/services/actions"
"code.gitea.io/gitea/services/oauth2_provider"
)
// Ensure the struct implements the interface.
var (
_ Method = &OAuth2{}
)
var _ Method = &OAuth2{}
// GetOAuthAccessTokenScopeAndUserID returns access token scope and user id
func GetOAuthAccessTokenScopeAndUserID(ctx context.Context, accessToken string) (auth_model.AccessTokenScope, int64) {
@ -106,18 +105,16 @@ func parseToken(req *http.Request) (string, bool) {
return "", false
}
// userIDFromToken returns the user id corresponding to the OAuth token.
// userFromToken returns the user corresponding to the OAuth token.
// It will set 'IsApiToken' to true if the token is an API token and
// set 'ApiTokenScope' to the scope of the access token
func (o *OAuth2) userIDFromToken(ctx context.Context, tokenSHA string, store DataStore) int64 {
// set 'ApiTokenScope' to the scope of the access token (TODO: this behavior should be fixed, don't set ctx.Data)
func (o *OAuth2) userFromToken(ctx context.Context, tokenSHA string, store DataStore) (*user_model.User, error) {
// Let's see if token is valid.
if strings.Contains(tokenSHA, ".") {
// First attempt to decode an actions JWT, returning the actions user
if taskID, err := actions.TokenToTaskID(tokenSHA); err == nil {
if CheckTaskIsRunning(ctx, taskID) {
store.GetData()["IsActionsToken"] = true
store.GetData()["ActionsTaskID"] = taskID
return user_model.ActionsUserID
return user_model.NewActionsUserWithTaskID(taskID), nil
}
}
@ -127,33 +124,27 @@ func (o *OAuth2) userIDFromToken(ctx context.Context, tokenSHA string, store Dat
store.GetData()["IsApiToken"] = true
store.GetData()["ApiTokenScope"] = accessTokenScope
}
return uid
return user_model.GetUserByID(ctx, uid)
}
t, err := auth_model.GetAccessTokenBySHA(ctx, tokenSHA)
if err != nil {
if auth_model.IsErrAccessTokenNotExist(err) {
// check task token
task, err := actions_model.GetRunningTaskByToken(ctx, tokenSHA)
if err == nil && task != nil {
if task, err := actions_model.GetRunningTaskByToken(ctx, tokenSHA); err == nil {
log.Trace("Basic Authorization: Valid AccessToken for task[%d]", task.ID)
store.GetData()["IsActionsToken"] = true
store.GetData()["ActionsTaskID"] = task.ID
return user_model.ActionsUserID
return user_model.NewActionsUserWithTaskID(task.ID), nil
}
} else if !auth_model.IsErrAccessTokenNotExist(err) && !auth_model.IsErrAccessTokenEmpty(err) {
log.Error("GetAccessTokenBySHA: %v", err)
}
return 0
return nil, err
}
t.UpdatedUnix = timeutil.TimeStampNow()
if err = auth_model.UpdateAccessToken(ctx, t); err != nil {
log.Error("UpdateAccessToken: %v", err)
}
store.GetData()["IsApiToken"] = true
store.GetData()["ApiTokenScope"] = t.Scope
return t.UID
return user_model.GetUserByID(ctx, t.UID)
}
// Verify extracts the user ID from the OAuth token in the query parameters
@ -173,21 +164,9 @@ func (o *OAuth2) Verify(req *http.Request, w http.ResponseWriter, store DataStor
return nil, nil
}
id := o.userIDFromToken(req.Context(), token, store)
if id <= 0 && id != -2 { // -2 means actions, so we need to allow it.
return nil, user_model.ErrUserNotExist{}
user, err := o.userFromToken(req.Context(), token, store)
if err != nil && !errors.Is(err, util.ErrNotExist) {
log.Error("userFromToken: %v", err) // the callers might ignore the error, so log it here
}
log.Trace("OAuth2 Authorization: Found token for user[%d]", id)
user, err := user_model.GetPossibleUserByID(req.Context(), id)
if err != nil {
if !user_model.IsErrUserNotExist(err) {
log.Error("GetUserByName: %v", err)
}
return nil, err
}
log.Trace("OAuth2 Authorization: Logged in user %-v", user)
return user, nil
return user, err
}

View File

@ -12,23 +12,26 @@ import (
"code.gitea.io/gitea/services/actions"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestUserIDFromToken(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
t.Run("Actions JWT", func(t *testing.T) {
const RunningTaskID = 47
const RunningTaskID int64 = 47
token, err := actions.CreateAuthorizationToken(RunningTaskID, 1, 2)
assert.NoError(t, err)
ds := make(reqctx.ContextData)
o := OAuth2{}
uid := o.userIDFromToken(t.Context(), token, ds)
assert.Equal(t, user_model.ActionsUserID, uid)
assert.Equal(t, true, ds["IsActionsToken"])
assert.Equal(t, ds["ActionsTaskID"], int64(RunningTaskID))
u, err := o.userFromToken(t.Context(), token, ds)
require.NoError(t, err)
assert.Equal(t, user_model.ActionsUserID, u.ID)
taskID, ok := user_model.GetActionsUserTaskID(u)
assert.True(t, ok)
assert.Equal(t, RunningTaskID, taskID)
})
}

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

@ -541,8 +541,7 @@ func authenticate(ctx *context.Context, repository *repo_model.Repository, autho
accessMode = perm_model.AccessModeWrite
}
if ctx.Data["IsActionsToken"] == true {
taskID := ctx.Data["ActionsTaskID"].(int64)
if taskID, ok := user_model.GetActionsUserTaskID(ctx.Doer); ok {
perm, err := access_model.GetActionsUserRepoPermission(ctx, repository, ctx.Doer, taskID)
if err != nil {
log.Error("Unable to GetActionsUserRepoPermission for task[%d] Error: %v", taskID, err)

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",

View File

@ -317,7 +317,7 @@ func getStatusPayloadInfo(p *api.CommitStatusPayload, linkFormatter linkFormatte
text = fmt.Sprintf("Commit Status changed: %s - %s", refLink, p.Description)
color = greenColor
if withSender {
if user_model.IsGiteaActionsUserName(p.Sender.UserName) {
if user_model.GetSystemUserByName(p.Sender.UserName) != nil {
text += " by " + p.Sender.FullName
} else {
text += " by " + linkFormatter(setting.AppURL+url.PathEscape(p.Sender.UserName), p.Sender.UserName)

View File

@ -1,3 +1,4 @@
// @ts-check
// TODO: Move to .ts after https://github.com/stylelint/stylelint/issues/8893 is fixed
import {fileURLToPath} from 'node:url';
@ -7,6 +8,7 @@ const cssVarFiles = [
fileURLToPath(new URL('web_src/css/themes/theme-gitea-dark.css', import.meta.url)),
];
/** @type {import('stylelint').Config} */
export default {
extends: 'stylelint-config-recommended',
reportUnscopedDisables: true,
@ -57,14 +59,14 @@ export default {
'@stylistic/block-opening-brace-space-before': 'always',
'@stylistic/color-hex-case': 'lower',
'@stylistic/declaration-bang-space-after': 'never',
'@stylistic/declaration-bang-space-before': null,
'@stylistic/declaration-bang-space-before': 'always',
'@stylistic/declaration-block-semicolon-newline-after': null,
'@stylistic/declaration-block-semicolon-newline-before': null,
'@stylistic/declaration-block-semicolon-space-after': null,
'@stylistic/declaration-block-semicolon-space-before': 'never',
'@stylistic/declaration-block-trailing-semicolon': null,
'@stylistic/declaration-colon-newline-after': null,
'@stylistic/declaration-colon-space-after': null,
'@stylistic/declaration-colon-space-after': 'always',
'@stylistic/declaration-colon-space-before': 'never',
'@stylistic/function-comma-newline-after': null,
'@stylistic/function-comma-newline-before': null,
@ -101,7 +103,7 @@ export default {
'@stylistic/selector-attribute-operator-space-before': null,
'@stylistic/selector-combinator-space-after': null,
'@stylistic/selector-combinator-space-before': null,
'@stylistic/selector-descendant-combinator-no-non-space': null,
'@stylistic/selector-descendant-combinator-no-non-space': true,
'@stylistic/selector-list-comma-newline-after': null,
'@stylistic/selector-list-comma-newline-before': null,
'@stylistic/selector-list-comma-space-after': 'always-single-line',

View File

@ -5,5 +5,6 @@ export default {
'@mcaptcha/vanilla-glue', // breaking changes in rc versions need to be handled
'cropperjs', // need to migrate to v2 but v2 is not compatible with v1
'tailwindcss', // need to migrate
'@eslint/json', // needs eslint 10
],
} satisfies Config;

View File

@ -46,7 +46,7 @@
}
.repository.wiki .wiki-content-toc ul ul {
border-left: 1px var(--color-secondary);
border-left: 1px var(--color-secondary);
border-left-style: dashed;
}

View File

@ -35,7 +35,7 @@ const baseOptions: MonacoOpts = {
renderLineHighlight: 'all',
renderLineHighlightOnlyWhenFocus: true,
rulers: [],
scrollbar: {horizontalScrollbarSize: 6, verticalScrollbarSize: 6},
scrollbar: {horizontalScrollbarSize: 6, verticalScrollbarSize: 6, alwaysConsumeMouseWheel: false},
scrollBeyondLastLine: false,
automaticLayout: true,
wrappingIndent: 'none',