0
0
mirror of https://github.com/go-gitea/gitea.git synced 2026-05-11 22:15:38 +02:00

Merge branch 'main' into aipolicy

This commit is contained in:
Giteabot 2026-02-21 03:43:48 +08:00 committed by GitHub
commit 415421ea71
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
83 changed files with 1842 additions and 1214 deletions

View File

@ -63,7 +63,6 @@ jobs:
RACE_ENABLED: true RACE_ENABLED: true
TEST_TAGS: gogit TEST_TAGS: gogit
TEST_LDAP: 1 TEST_LDAP: 1
USE_REPO_TEST_DIR: 1
test-sqlite: test-sqlite:
if: needs.files-changed.outputs.backend == 'true' || needs.files-changed.outputs.actions == 'true' if: needs.files-changed.outputs.backend == 'true' || needs.files-changed.outputs.actions == 'true'
@ -90,7 +89,6 @@ jobs:
TAGS: bindata gogit sqlite sqlite_unlock_notify TAGS: bindata gogit sqlite sqlite_unlock_notify
RACE_ENABLED: true RACE_ENABLED: true
TEST_TAGS: gogit sqlite sqlite_unlock_notify TEST_TAGS: gogit sqlite sqlite_unlock_notify
USE_REPO_TEST_DIR: 1
test-unit: test-unit:
if: needs.files-changed.outputs.backend == 'true' || needs.files-changed.outputs.actions == 'true' if: needs.files-changed.outputs.backend == 'true' || needs.files-changed.outputs.actions == 'true'
@ -206,7 +204,6 @@ jobs:
env: env:
TAGS: bindata TAGS: bindata
RACE_ENABLED: true RACE_ENABLED: true
USE_REPO_TEST_DIR: 1
TEST_INDEXER_CODE_ES_URL: "http://elastic:changeme@elasticsearch:9200" TEST_INDEXER_CODE_ES_URL: "http://elastic:changeme@elasticsearch:9200"
test-mssql: test-mssql:
@ -246,4 +243,3 @@ jobs:
timeout-minutes: 50 timeout-minutes: 50
env: env:
TAGS: bindata TAGS: bindata
USE_REPO_TEST_DIR: 1

1
.gitignore vendored
View File

@ -89,7 +89,6 @@ cpu.out
/vendor /vendor
/VERSION /VERSION
/.air /.air
/.go-licenses
# Files and folders that were previously generated # Files and folders that were previously generated
/public/assets/img/webpack /public/assets/img/webpack

View File

@ -67,35 +67,24 @@ linters:
revive: revive:
severity: error severity: error
rules: rules:
- name: atomic
- name: bare-return
- name: blank-imports - name: blank-imports
- name: constant-logical-expr - name: constant-logical-expr
- name: context-as-argument - name: context-as-argument
- name: context-keys-type - name: context-keys-type
- name: dot-imports - name: dot-imports
- name: duplicated-imports
- name: empty-lines - name: empty-lines
- name: error-naming
- name: error-return - name: error-return
- name: error-strings - name: error-strings
- name: errorf
- name: exported - name: exported
- name: identical-branches - name: identical-branches
- name: if-return - name: if-return
- name: increment-decrement - name: increment-decrement
- name: indent-error-flow
- name: modifies-value-receiver - name: modifies-value-receiver
- name: package-comments - name: package-comments
- name: range
- name: receiver-naming
- name: redefines-builtin-id - name: redefines-builtin-id
- name: string-of-int
- name: superfluous-else - name: superfluous-else
- name: time-naming - name: time-naming
- name: unconditional-recursion
- name: unexported-return - name: unexported-return
- name: unreachable-code
- name: var-declaration - name: var-declaration
- name: var-naming - name: var-naming
arguments: arguments:
@ -133,16 +122,12 @@ linters:
- linters: - linters:
- dupl - dupl
- errcheck - errcheck
- gocyclo
- gosec
- staticcheck - staticcheck
- unparam - unparam
path: _test\.go path: _test\.go
- linters: - linters:
- dupl - dupl
- errcheck - errcheck
- gocyclo
- gosec
path: models/migrations/v path: models/migrations/v
- linters: - linters:
- forbidigo - forbidigo
@ -154,7 +139,6 @@ linters:
- gocritic - gocritic
text: (?i)`ID' should not be capitalized text: (?i)`ID' should not be capitalized
- linters: - linters:
- deadcode
- unused - unused
text: (?i)swagger text: (?i)swagger
- linters: - linters:

View File

@ -197,7 +197,7 @@ Here's how to run the test suite:
## Translation ## Translation
All translation work happens on [Crowdin](https://translate.gitea.com). All translation work happens on [Crowdin](https://translate.gitea.com).
The only translation that is maintained in this repository is [the English translation](https://github.com/go-gitea/gitea/blob/main/options/locale/locale_en-US.ini). The only translation that is maintained in this repository is [the English translation](https://github.com/go-gitea/gitea/blob/main/options/locale/locale_en-US.json).
It is synced regularly with Crowdin. \ It is synced regularly with Crowdin. \
Other locales on main branch **should not** be updated manually as they will be overwritten with each sync. \ Other locales on main branch **should not** be updated manually as they will be overwritten with each sync. \
Once a language has reached a **satisfactory percentage** of translated keys (~25%), it will be synced back into this repo and included in the next released version. Once a language has reached a **satisfactory percentage** of translated keys (~25%), it will be synced back into this repo and included in the next released version.

View File

@ -1,5 +1,12 @@
# syntax=docker/dockerfile:1 # syntax=docker/dockerfile:1
# Build stage # Build frontend on the native platform to avoid QEMU-related issues with esbuild/webpack
FROM --platform=$BUILDPLATFORM docker.io/library/golang:1.26-alpine3.23 AS frontend-build
RUN apk --no-cache add build-base git nodejs pnpm
WORKDIR /src
COPY --exclude=.git/ . .
RUN --mount=type=cache,target=/root/.local/share/pnpm/store make frontend
# Build backend for each target platform
FROM docker.io/library/golang:1.26-alpine3.23 AS build-env FROM docker.io/library/golang:1.26-alpine3.23 AS build-env
ARG GOPROXY=direct ARG GOPROXY=direct
@ -12,22 +19,19 @@ ARG CGO_EXTRA_CFLAGS
# Build deps # Build deps
RUN apk --no-cache add \ RUN apk --no-cache add \
build-base \ build-base \
git \ git
nodejs \
pnpm
WORKDIR ${GOPATH}/src/code.gitea.io/gitea WORKDIR ${GOPATH}/src/code.gitea.io/gitea
# Use COPY but not "mount" because some directories like "node_modules" contain platform-depended contents and these directories need to be ignored. # Use COPY instead of bind mount as read-only one breaks makefile state tracking and read-write one needs binary to be moved as it's discarded.
# ".git" directory will be mounted later separately for getting version data. # ".git" directory is mounted separately later only for version data extraction.
# TODO: in the future, maybe we can pre-build the frontend assets on one platform and share them for different platforms, the benefit is that it won't be affected by webpack plugin compatibility problems, then the working directory can be fully mounted and the COPY is not needed.
COPY --exclude=.git/ . . COPY --exclude=.git/ . .
COPY --from=frontend-build /src/public/assets public/assets
# Build gitea, .git mount is required for version data # Build gitea, .git mount is required for version data
RUN --mount=type=cache,target=/go/pkg/mod \ RUN --mount=type=cache,target=/go/pkg/mod \
--mount=type=cache,target="/root/.cache/go-build" \ --mount=type=cache,target="/root/.cache/go-build" \
--mount=type=cache,target=/root/.local/share/pnpm/store \
--mount=type=bind,source=".git/",target=".git/" \ --mount=type=bind,source=".git/",target=".git/" \
make make backend
COPY docker/root /tmp/local COPY docker/root /tmp/local

View File

@ -1,5 +1,12 @@
# syntax=docker/dockerfile:1 # syntax=docker/dockerfile:1
# Build stage # Build frontend on the native platform to avoid QEMU-related issues with esbuild/webpack
FROM --platform=$BUILDPLATFORM docker.io/library/golang:1.26-alpine3.23 AS frontend-build
RUN apk --no-cache add build-base git nodejs pnpm
WORKDIR /src
COPY --exclude=.git/ . .
RUN --mount=type=cache,target=/root/.local/share/pnpm/store make frontend
# Build backend for each target platform
FROM docker.io/library/golang:1.26-alpine3.23 AS build-env FROM docker.io/library/golang:1.26-alpine3.23 AS build-env
ARG GOPROXY=direct ARG GOPROXY=direct
@ -12,20 +19,18 @@ ARG CGO_EXTRA_CFLAGS
# Build deps # Build deps
RUN apk --no-cache add \ RUN apk --no-cache add \
build-base \ build-base \
git \ git
nodejs \
pnpm
WORKDIR ${GOPATH}/src/code.gitea.io/gitea WORKDIR ${GOPATH}/src/code.gitea.io/gitea
# See the comments in Dockerfile # See the comments in Dockerfile
COPY --exclude=.git/ . . COPY --exclude=.git/ . .
COPY --from=frontend-build /src/public/assets public/assets
# Build gitea, .git mount is required for version data # Build gitea, .git mount is required for version data
RUN --mount=type=cache,target=/go/pkg/mod \ RUN --mount=type=cache,target=/go/pkg/mod \
--mount=type=cache,target="/root/.cache/go-build" \ --mount=type=cache,target="/root/.cache/go-build" \
--mount=type=cache,target=/root/.local/share/pnpm/store \
--mount=type=bind,source=".git/",target=".git/" \ --mount=type=bind,source=".git/",target=".git/" \
make make backend
COPY docker/rootless /tmp/local COPY docker/rootless /tmp/local

View File

@ -1,22 +1,5 @@
ifeq ($(USE_REPO_TEST_DIR),1)
# This rule replaces the whole Makefile when we're trying to use /tmp repository temporary files
location = $(CURDIR)/$(word $(words $(MAKEFILE_LIST)),$(MAKEFILE_LIST))
self := $(location)
%:
@tmpdir=`mktemp --tmpdir -d` ; \
echo Using temporary directory $$tmpdir for test repositories ; \
USE_REPO_TEST_DIR= $(MAKE) -f $(self) --no-print-directory REPO_TEST_DIR=$$tmpdir/ $@ ; \
STATUS=$$? ; rm -r "$$tmpdir" ; exit $$STATUS
else
# This is the "normal" part of the Makefile
DIST := dist DIST := dist
DIST_DIRS := $(DIST)/binaries $(DIST)/release DIST_DIRS := $(DIST)/binaries $(DIST)/release
IMPORT := code.gitea.io/gitea
# By default use go's 1.25 experimental json v2 library when building # By default use go's 1.25 experimental json v2 library when building
# TODO: remove when no longer experimental # TODO: remove when no longer experimental
@ -37,7 +20,6 @@ GXZ_PACKAGE ?= github.com/ulikunitz/xz/cmd/gxz@v0.5.15
MISSPELL_PACKAGE ?= github.com/golangci/misspell/cmd/misspell@v0.7.0 MISSPELL_PACKAGE ?= github.com/golangci/misspell/cmd/misspell@v0.7.0
SWAGGER_PACKAGE ?= github.com/go-swagger/go-swagger/cmd/swagger@v0.33.1 SWAGGER_PACKAGE ?= github.com/go-swagger/go-swagger/cmd/swagger@v0.33.1
XGO_PACKAGE ?= src.techknowlogick.com/xgo@latest XGO_PACKAGE ?= src.techknowlogick.com/xgo@latest
GO_LICENSES_PACKAGE ?= github.com/google/go-licenses@v1
GOVULNCHECK_PACKAGE ?= golang.org/x/vuln/cmd/govulncheck@v1 GOVULNCHECK_PACKAGE ?= golang.org/x/vuln/cmd/govulncheck@v1
ACTIONLINT_PACKAGE ?= github.com/rhysd/actionlint/cmd/actionlint@v1.7.10 ACTIONLINT_PACKAGE ?= github.com/rhysd/actionlint/cmd/actionlint@v1.7.10
@ -84,7 +66,6 @@ endif
EXTRA_GOFLAGS ?= EXTRA_GOFLAGS ?=
MAKE_VERSION := $(shell "$(MAKE)" -v | cat | head -n 1)
MAKE_EVIDENCE_DIR := .make_evidence MAKE_EVIDENCE_DIR := .make_evidence
GOTESTFLAGS ?= GOTESTFLAGS ?=
@ -130,7 +111,7 @@ ifeq ($(VERSION),main)
VERSION := main-nightly VERSION := main-nightly
endif endif
LDFLAGS := $(LDFLAGS) -X "main.MakeVersion=$(MAKE_VERSION)" -X "main.Version=$(GITEA_VERSION)" -X "main.Tags=$(TAGS)" LDFLAGS := $(LDFLAGS) -X "main.Version=$(GITEA_VERSION)" -X "main.Tags=$(TAGS)"
LINUX_ARCHS ?= linux/amd64,linux/386,linux/arm-5,linux/arm-6,linux/arm64,linux/riscv64 LINUX_ARCHS ?= linux/amd64,linux/386,linux/arm-5,linux/arm-6,linux/arm64,linux/riscv64
@ -150,7 +131,6 @@ SVG_DEST_DIR := public/assets/img/svg
AIR_TMP_DIR := .air AIR_TMP_DIR := .air
GO_LICENSE_TMP_DIR := .go-licenses
GO_LICENSE_FILE := assets/go-licenses.json GO_LICENSE_FILE := assets/go-licenses.json
TAGS ?= TAGS ?=
@ -159,7 +139,7 @@ TAGS_EVIDENCE := $(MAKE_EVIDENCE_DIR)/tags
TEST_TAGS ?= $(TAGS_SPLIT) sqlite sqlite_unlock_notify TEST_TAGS ?= $(TAGS_SPLIT) sqlite sqlite_unlock_notify
TAR_EXCLUDES := .git data indexers queues log node_modules $(EXECUTABLE) $(DIST) $(MAKE_EVIDENCE_DIR) $(AIR_TMP_DIR) $(GO_LICENSE_TMP_DIR) TAR_EXCLUDES := .git data indexers queues log node_modules $(EXECUTABLE) $(DIST) $(MAKE_EVIDENCE_DIR) $(AIR_TMP_DIR)
GO_DIRS := build cmd models modules routers services tests tools GO_DIRS := build cmd models modules routers services tests tools
WEB_DIRS := web_src/js web_src/css WEB_DIRS := web_src/js web_src/css
@ -229,7 +209,7 @@ clean: ## delete backend and integration files
e2e*.test \ e2e*.test \
tests/integration/gitea-integration-* \ tests/integration/gitea-integration-* \
tests/integration/indexers-* \ tests/integration/indexers-* \
tests/mysql.ini tests/pgsql.ini tests/mssql.ini man/ \ tests/sqlite.ini tests/mysql.ini tests/pgsql.ini tests/mssql.ini man/ \
tests/e2e/gitea-e2e-*/ \ tests/e2e/gitea-e2e-*/ \
tests/e2e/indexers-*/ \ tests/e2e/indexers-*/ \
tests/e2e/reports/ tests/e2e/test-artifacts/ tests/e2e/test-snapshots/ tests/e2e/reports/ tests/e2e/test-artifacts/ tests/e2e/test-snapshots/
@ -473,16 +453,11 @@ tidy-check: tidy
go-licenses: $(GO_LICENSE_FILE) ## regenerate go licenses go-licenses: $(GO_LICENSE_FILE) ## regenerate go licenses
$(GO_LICENSE_FILE): go.mod go.sum $(GO_LICENSE_FILE): go.mod go.sum
@rm -rf $(GO_LICENSE_FILE) GO=$(GO) $(GO) run build/generate-go-licenses.go $(GO_LICENSE_FILE)
$(GO) install $(GO_LICENSES_PACKAGE)
-GOOS=linux CGO_ENABLED=1 go-licenses save . --force --save_path=$(GO_LICENSE_TMP_DIR) 2>/dev/null
$(GO) run build/generate-go-licenses.go $(GO_LICENSE_TMP_DIR) $(GO_LICENSE_FILE)
@rm -rf $(GO_LICENSE_TMP_DIR)
generate-ini-sqlite: generate-ini-sqlite:
sed -e 's|{{REPO_TEST_DIR}}|${REPO_TEST_DIR}|g' \ sed -e 's|{{WORK_PATH}}|$(CURDIR)/tests/$(or $(TEST_TYPE),integration)/gitea-$(or $(TEST_TYPE),integration)-sqlite|g' \
-e 's|{{TEST_LOGGER}}|$(or $(TEST_LOGGER),test$(COMMA)file)|g' \ -e 's|{{TEST_LOGGER}}|$(or $(TEST_LOGGER),test$(COMMA)file)|g' \
-e 's|{{TEST_TYPE}}|$(or $(TEST_TYPE),integration)|g' \
tests/sqlite.ini.tmpl > tests/sqlite.ini tests/sqlite.ini.tmpl > tests/sqlite.ini
.PHONY: test-sqlite .PHONY: test-sqlite
@ -501,9 +476,8 @@ generate-ini-mysql:
-e 's|{{TEST_MYSQL_DBNAME}}|${TEST_MYSQL_DBNAME}|g' \ -e 's|{{TEST_MYSQL_DBNAME}}|${TEST_MYSQL_DBNAME}|g' \
-e 's|{{TEST_MYSQL_USERNAME}}|${TEST_MYSQL_USERNAME}|g' \ -e 's|{{TEST_MYSQL_USERNAME}}|${TEST_MYSQL_USERNAME}|g' \
-e 's|{{TEST_MYSQL_PASSWORD}}|${TEST_MYSQL_PASSWORD}|g' \ -e 's|{{TEST_MYSQL_PASSWORD}}|${TEST_MYSQL_PASSWORD}|g' \
-e 's|{{REPO_TEST_DIR}}|${REPO_TEST_DIR}|g' \ -e 's|{{WORK_PATH}}|$(CURDIR)/tests/$(or $(TEST_TYPE),integration)/gitea-$(or $(TEST_TYPE),integration)-mysql|g' \
-e 's|{{TEST_LOGGER}}|$(or $(TEST_LOGGER),test$(COMMA)file)|g' \ -e 's|{{TEST_LOGGER}}|$(or $(TEST_LOGGER),test$(COMMA)file)|g' \
-e 's|{{TEST_TYPE}}|$(or $(TEST_TYPE),integration)|g' \
tests/mysql.ini.tmpl > tests/mysql.ini tests/mysql.ini.tmpl > tests/mysql.ini
.PHONY: test-mysql .PHONY: test-mysql
@ -524,9 +498,8 @@ generate-ini-pgsql:
-e 's|{{TEST_PGSQL_PASSWORD}}|${TEST_PGSQL_PASSWORD}|g' \ -e 's|{{TEST_PGSQL_PASSWORD}}|${TEST_PGSQL_PASSWORD}|g' \
-e 's|{{TEST_PGSQL_SCHEMA}}|${TEST_PGSQL_SCHEMA}|g' \ -e 's|{{TEST_PGSQL_SCHEMA}}|${TEST_PGSQL_SCHEMA}|g' \
-e 's|{{TEST_MINIO_ENDPOINT}}|${TEST_MINIO_ENDPOINT}|g' \ -e 's|{{TEST_MINIO_ENDPOINT}}|${TEST_MINIO_ENDPOINT}|g' \
-e 's|{{REPO_TEST_DIR}}|${REPO_TEST_DIR}|g' \ -e 's|{{WORK_PATH}}|$(CURDIR)/tests/$(or $(TEST_TYPE),integration)/gitea-$(or $(TEST_TYPE),integration)-pgsql|g' \
-e 's|{{TEST_LOGGER}}|$(or $(TEST_LOGGER),test$(COMMA)file)|g' \ -e 's|{{TEST_LOGGER}}|$(or $(TEST_LOGGER),test$(COMMA)file)|g' \
-e 's|{{TEST_TYPE}}|$(or $(TEST_TYPE),integration)|g' \
tests/pgsql.ini.tmpl > tests/pgsql.ini tests/pgsql.ini.tmpl > tests/pgsql.ini
.PHONY: test-pgsql .PHONY: test-pgsql
@ -545,9 +518,8 @@ generate-ini-mssql:
-e 's|{{TEST_MSSQL_DBNAME}}|${TEST_MSSQL_DBNAME}|g' \ -e 's|{{TEST_MSSQL_DBNAME}}|${TEST_MSSQL_DBNAME}|g' \
-e 's|{{TEST_MSSQL_USERNAME}}|${TEST_MSSQL_USERNAME}|g' \ -e 's|{{TEST_MSSQL_USERNAME}}|${TEST_MSSQL_USERNAME}|g' \
-e 's|{{TEST_MSSQL_PASSWORD}}|${TEST_MSSQL_PASSWORD}|g' \ -e 's|{{TEST_MSSQL_PASSWORD}}|${TEST_MSSQL_PASSWORD}|g' \
-e 's|{{REPO_TEST_DIR}}|${REPO_TEST_DIR}|g' \ -e 's|{{WORK_PATH}}|$(CURDIR)/tests/$(or $(TEST_TYPE),integration)/gitea-$(or $(TEST_TYPE),integration)-mssql|g' \
-e 's|{{TEST_LOGGER}}|$(or $(TEST_LOGGER),test$(COMMA)file)|g' \ -e 's|{{TEST_LOGGER}}|$(or $(TEST_LOGGER),test$(COMMA)file)|g' \
-e 's|{{TEST_TYPE}}|$(or $(TEST_TYPE),integration)|g' \
tests/mssql.ini.tmpl > tests/mssql.ini tests/mssql.ini.tmpl > tests/mssql.ini
.PHONY: test-mssql .PHONY: test-mssql
@ -668,7 +640,7 @@ migrations.sqlite.test: $(GO_SOURCES) generate-ini-sqlite
GITEA_TEST_CONF=tests/sqlite.ini ./migrations.sqlite.test GITEA_TEST_CONF=tests/sqlite.ini ./migrations.sqlite.test
.PHONY: migrations.individual.mysql.test .PHONY: migrations.individual.mysql.test
migrations.individual.mysql.test: $(GO_SOURCES) migrations.individual.mysql.test: $(GO_SOURCES) generate-ini-mysql
GITEA_TEST_CONF=tests/mysql.ini $(GO) test $(GOTESTFLAGS) -tags='$(TEST_TAGS)' -p 1 $(MIGRATE_TEST_PACKAGES) GITEA_TEST_CONF=tests/mysql.ini $(GO) test $(GOTESTFLAGS) -tags='$(TEST_TAGS)' -p 1 $(MIGRATE_TEST_PACKAGES)
.PHONY: migrations.individual.sqlite.test\#% .PHONY: migrations.individual.sqlite.test\#%
@ -676,7 +648,7 @@ migrations.individual.sqlite.test\#%: $(GO_SOURCES) generate-ini-sqlite
GITEA_TEST_CONF=tests/sqlite.ini $(GO) test $(GOTESTFLAGS) -tags '$(TEST_TAGS)' code.gitea.io/gitea/models/migrations/$* GITEA_TEST_CONF=tests/sqlite.ini $(GO) test $(GOTESTFLAGS) -tags '$(TEST_TAGS)' code.gitea.io/gitea/models/migrations/$*
.PHONY: migrations.individual.pgsql.test .PHONY: migrations.individual.pgsql.test
migrations.individual.pgsql.test: $(GO_SOURCES) migrations.individual.pgsql.test: $(GO_SOURCES) generate-ini-pgsql
GITEA_TEST_CONF=tests/pgsql.ini $(GO) test $(GOTESTFLAGS) -tags='$(TEST_TAGS)' -p 1 $(MIGRATE_TEST_PACKAGES) GITEA_TEST_CONF=tests/pgsql.ini $(GO) test $(GOTESTFLAGS) -tags='$(TEST_TAGS)' -p 1 $(MIGRATE_TEST_PACKAGES)
.PHONY: migrations.individual.pgsql.test\#% .PHONY: migrations.individual.pgsql.test\#%
@ -741,7 +713,7 @@ generate-go: $(TAGS_PREREQ)
.PHONY: security-check .PHONY: security-check
security-check: security-check:
GOEXPERIMENT= go run $(GOVULNCHECK_PACKAGE) -show color ./... GOEXPERIMENT= go run $(GOVULNCHECK_PACKAGE) -show color ./... || true
$(EXECUTABLE): $(GO_SOURCES) $(TAGS_PREREQ) $(EXECUTABLE): $(GO_SOURCES) $(TAGS_PREREQ)
ifneq ($(and $(STATIC),$(findstring pam,$(TAGS))),) ifneq ($(and $(STATIC),$(findstring pam,$(TAGS))),)
@ -819,7 +791,6 @@ deps-tools: ## install tool dependencies
$(GO) install $(MISSPELL_PACKAGE) & \ $(GO) install $(MISSPELL_PACKAGE) & \
$(GO) install $(SWAGGER_PACKAGE) & \ $(GO) install $(SWAGGER_PACKAGE) & \
$(GO) install $(XGO_PACKAGE) & \ $(GO) install $(XGO_PACKAGE) & \
$(GO) install $(GO_LICENSES_PACKAGE) & \
$(GO) install $(GOVULNCHECK_PACKAGE) & \ $(GO) install $(GOVULNCHECK_PACKAGE) & \
$(GO) install $(ACTIONLINT_PACKAGE) & \ $(GO) install $(ACTIONLINT_PACKAGE) & \
wait wait
@ -908,9 +879,6 @@ docker:
docker build --disable-content-trust=false -t $(DOCKER_REF) . docker build --disable-content-trust=false -t $(DOCKER_REF) .
# support also build args docker build --build-arg GITEA_VERSION=v1.2.3 --build-arg TAGS="bindata sqlite sqlite_unlock_notify" . # support also build args docker build --build-arg GITEA_VERSION=v1.2.3 --build-arg TAGS="bindata sqlite sqlite_unlock_notify" .
# This endif closes the if at the top of the file
endif
# Disable parallel execution because it would break some targets that don't # Disable parallel execution because it would break some targets that don't
# specify exact dependencies like 'backend' which does currently not depend # specify exact dependencies like 'backend' which does currently not depend
# on 'frontend' to enable Node.js-less builds from source tarballs. # on 'frontend' to enable Node.js-less builds from source tarballs.

142
assets/go-licenses.json generated

File diff suppressed because one or more lines are too long

View File

@ -1,115 +0,0 @@
// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
//go:build ignore
package main
import (
"fmt"
"os"
"os/exec"
"path/filepath"
"strings"
"code.gitea.io/gitea/modules/container"
"code.gitea.io/gitea/modules/setting"
)
func main() {
if len(os.Args) != 2 {
println("usage: backport-locales <to-ref>")
println("eg: backport-locales release/v1.19")
os.Exit(1)
}
mustNoErr := func(err error) {
if err != nil {
panic(err)
}
}
collectInis := func(ref string) map[string]setting.ConfigProvider {
inis := map[string]setting.ConfigProvider{}
err := filepath.WalkDir("options/locale", func(path string, d os.DirEntry, err error) error {
if err != nil {
return err
}
if d.IsDir() || !strings.HasSuffix(d.Name(), ".ini") {
return nil
}
cfg, err := setting.NewConfigProviderForLocale(path)
mustNoErr(err)
inis[path] = cfg
fmt.Printf("collecting: %s @ %s\n", path, ref)
return nil
})
mustNoErr(err)
return inis
}
// collect new locales from current working directory
inisNew := collectInis("HEAD")
// switch to the target ref, and collect the old locales
cmd := exec.Command("git", "checkout", os.Args[1])
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
mustNoErr(cmd.Run())
inisOld := collectInis(os.Args[1])
// use old en-US as the base, and copy the new translations to the old locales
enUsOld := inisOld["options/locale/locale_en-US.ini"]
brokenWarned := make(container.Set[string])
for path, iniOld := range inisOld {
if iniOld == enUsOld {
continue
}
iniNew := inisNew[path]
if iniNew == nil {
continue
}
for _, secEnUS := range enUsOld.Sections() {
secOld := iniOld.Section(secEnUS.Name())
secNew := iniNew.Section(secEnUS.Name())
for _, keyEnUs := range secEnUS.Keys() {
if secNew.HasKey(keyEnUs.Name()) {
oldStr := secOld.Key(keyEnUs.Name()).String()
newStr := secNew.Key(keyEnUs.Name()).String()
broken := oldStr != "" && strings.Count(oldStr, "%") != strings.Count(newStr, "%")
broken = broken || strings.Contains(oldStr, "\n") || strings.Contains(oldStr, "\n")
if broken {
brokenWarned.Add(secOld.Name() + "." + keyEnUs.Name())
fmt.Println("----")
fmt.Printf("WARNING: skip broken locale: %s , [%s] %s\n", path, secEnUS.Name(), keyEnUs.Name())
fmt.Printf("\told: %s\n", strings.ReplaceAll(oldStr, "\n", "\\n"))
fmt.Printf("\tnew: %s\n", strings.ReplaceAll(newStr, "\n", "\\n"))
continue
}
secOld.Key(keyEnUs.Name()).SetValue(newStr)
}
}
}
mustNoErr(iniOld.SaveTo(path))
}
fmt.Println("========")
for path, iniNew := range inisNew {
for _, sec := range iniNew.Sections() {
for _, key := range sec.Keys() {
str := sec.Key(key.Name()).String()
broken := strings.Contains(str, "\n")
broken = broken || strings.HasPrefix(str, "`") != strings.HasSuffix(str, "`")
broken = broken || strings.HasPrefix(str, "\"`")
broken = broken || strings.HasPrefix(str, "`\"")
broken = broken || strings.Count(str, `"`)%2 == 1
broken = broken || strings.Count(str, "`")%2 == 1
if broken && !brokenWarned.Contains(sec.Name()+"."+key.Name()) {
fmt.Printf("WARNING: found broken locale: %s , [%s] %s\n", path, sec.Name(), key.Name())
fmt.Printf("\tstr: %s\n", strings.ReplaceAll(str, "\n", "\\n"))
fmt.Println("----")
}
}
}
}
}

View File

@ -8,99 +8,219 @@ package main
import ( import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"io/fs"
"os" "os"
"path" "os/exec"
"path/filepath" "path/filepath"
"regexp" "regexp"
"slices"
"sort" "sort"
"strings" "strings"
"code.gitea.io/gitea/modules/container"
) )
// regexp is based on go-license, excluding README and NOTICE // regexp is based on go-license, excluding README and NOTICE
// https://github.com/google/go-licenses/blob/master/licenses/find.go // https://github.com/google/go-licenses/blob/master/licenses/find.go
var licenseRe = regexp.MustCompile(`^(?i)((UN)?LICEN(S|C)E|COPYING).*$`) var licenseRe = regexp.MustCompile(`^(?i)((UN)?LICEN(S|C)E|COPYING).*$`)
// primaryLicenseRe matches exact primary license filenames without suffixes.
// When a directory has both primary and variant files (e.g. LICENSE and
// LICENSE.docs), only the primary files are kept.
var primaryLicenseRe = regexp.MustCompile(`^(?i)(LICEN[SC]E|COPYING)$`)
// ignoredNames are LicenseEntry.Name values to exclude from the output.
var ignoredNames = map[string]bool{
"code.gitea.io/gitea": true,
"code.gitea.io/gitea/options/license": true,
}
var excludedExt = map[string]bool{
".gitignore": true,
".go": true,
".mod": true,
".sum": true,
".toml": true,
".yaml": true,
".yml": true,
}
type ModuleInfo struct {
Path string
Dir string
PkgDirs []string // directories of packages imported from this module
}
type LicenseEntry struct { type LicenseEntry struct {
Name string `json:"name"` Name string `json:"name"`
Path string `json:"path"` Path string `json:"path"`
LicenseText string `json:"licenseText"` LicenseText string `json:"licenseText"`
} }
func main() { // getModules returns all dependency modules with their local directory paths
if len(os.Args) != 3 { // and the package directories used from each module.
fmt.Println("usage: go run generate-go-licenses.go <base-dir> <out-json-file>") func getModules(goCmd string) []ModuleInfo {
cmd := exec.Command(goCmd, "list", "-deps", "-f",
"{{if .Module}}{{.Module.Path}}\t{{.Module.Dir}}\t{{.Dir}}{{end}}", "./...")
cmd.Stderr = os.Stderr
// Use GOOS=linux with CGO to ensure we capture all platform-specific
// dependencies, matching the CI environment.
cmd.Env = append(os.Environ(), "GOOS=linux", "GOARCH=amd64", "CGO_ENABLED=1")
output, err := cmd.Output()
if err != nil {
fmt.Fprintf(os.Stderr, "failed to run 'go list -deps': %v\n", err)
os.Exit(1) os.Exit(1)
} }
base, out := os.Args[1], os.Args[2] var modules []ModuleInfo
seen := make(map[string]int) // module path -> index in modules
// Add ext for excluded files because license_test.go will be included for some reason. for _, line := range strings.Split(string(output), "\n") {
// And there are more files that should be excluded, check with: line = strings.TrimSpace(line)
// if line == "" {
// go run github.com/google/go-licenses@v1.6.0 save . --force --save_path=.go-licenses 2>/dev/null continue
// find .go-licenses -type f | while read FILE; do echo "${$(basename $FILE)##*.}"; done | sort -u
// AUTHORS
// COPYING
// LICENSE
// Makefile
// NOTICE
// gitignore
// go
// md
// mod
// sum
// toml
// txt
// yml
//
// It could be removed once we have a better regex.
excludedExt := container.SetOf(".gitignore", ".go", ".mod", ".sum", ".toml", ".yml")
var paths []string
err := filepath.WalkDir(base, func(path string, entry fs.DirEntry, err error) error {
if err != nil {
return err
} }
if entry.IsDir() || !licenseRe.MatchString(entry.Name()) || excludedExt.Contains(filepath.Ext(entry.Name())) { parts := strings.Split(line, "\t")
return nil if len(parts) != 3 {
continue
} }
paths = append(paths, path) modPath, modDir, pkgDir := parts[0], parts[1], parts[2]
return nil if idx, ok := seen[modPath]; ok {
}) modules[idx].PkgDirs = append(modules[idx].PkgDirs, pkgDir)
if err != nil { } else {
panic(err) seen[modPath] = len(modules)
modules = append(modules, ModuleInfo{
Path: modPath,
Dir: modDir,
PkgDirs: []string{pkgDir},
})
}
}
return modules
}
// findLicenseFiles scans a module's root directory and its used package
// directories for license files. It also walks up from each package directory
// to the module root, scanning intermediate directories. Subdirectory licenses
// are only included if their text differs from the root license(s).
func findLicenseFiles(mod ModuleInfo) []LicenseEntry {
var entries []LicenseEntry
seenTexts := make(map[string]bool)
// First, collect root-level license files.
entries = append(entries, scanDirForLicenses(mod.Dir, mod.Path, "")...)
for _, e := range entries {
seenTexts[e.LicenseText] = true
} }
sort.Strings(paths) // Then check each package directory and all intermediate parent directories
// up to the module root for license files with unique text.
seenDirs := map[string]bool{mod.Dir: true}
for _, pkgDir := range mod.PkgDirs {
for dir := pkgDir; dir != mod.Dir && strings.HasPrefix(dir, mod.Dir); dir = filepath.Dir(dir) {
if seenDirs[dir] {
continue
}
seenDirs[dir] = true
for _, e := range scanDirForLicenses(dir, mod.Path, mod.Dir) {
if !seenTexts[e.LicenseText] {
seenTexts[e.LicenseText] = true
entries = append(entries, e)
}
}
}
}
return entries
}
// scanDirForLicenses reads a single directory for license files and returns entries.
// If moduleRoot is non-empty, paths are made relative to it.
func scanDirForLicenses(dir, modulePath, moduleRoot string) []LicenseEntry {
dirEntries, err := os.ReadDir(dir)
if err != nil {
return nil
}
var entries []LicenseEntry var entries []LicenseEntry
for _, filePath := range paths { for _, entry := range dirEntries {
licenseText, err := os.ReadFile(filePath) if entry.IsDir() {
if err != nil { continue
panic(err)
} }
name := entry.Name()
pkgPath := filepath.ToSlash(filePath) if !licenseRe.MatchString(name) {
pkgPath = strings.TrimPrefix(pkgPath, base+"/") continue
pkgName := path.Dir(pkgPath) }
if excludedExt[strings.ToLower(filepath.Ext(name))] {
// There might be a bug somewhere in go-licenses that sometimes interprets the
// root package as "." and sometimes as "code.gitea.io/gitea". Workaround by
// removing both of them for the sake of stable output.
if pkgName == "." || pkgName == "code.gitea.io/gitea" {
continue continue
} }
content, err := os.ReadFile(filepath.Join(dir, name))
if err != nil {
continue
}
entryName := modulePath
entryPath := modulePath + "/" + name
if moduleRoot != "" {
rel, _ := filepath.Rel(moduleRoot, dir)
if rel != "." {
relSlash := filepath.ToSlash(rel)
entryName = modulePath + "/" + relSlash
entryPath = modulePath + "/" + relSlash + "/" + name
}
}
entries = append(entries, LicenseEntry{ entries = append(entries, LicenseEntry{
Name: pkgName, Name: entryName,
Path: pkgPath, Path: entryPath,
LicenseText: string(licenseText), LicenseText: string(content),
}) })
} }
// When multiple license files exist, prefer primary files (e.g. LICENSE)
// over variants with suffixes (e.g. LICENSE.docs, LICENSE-2.0.txt).
// If no primary file exists, keep only the first variant.
if len(entries) > 1 {
var primary []LicenseEntry
for _, e := range entries {
fileName := e.Path[strings.LastIndex(e.Path, "/")+1:]
if primaryLicenseRe.MatchString(fileName) {
primary = append(primary, e)
}
}
if len(primary) > 0 {
return primary
}
return entries[:1]
}
return entries
}
func main() {
if len(os.Args) != 2 {
fmt.Println("usage: go run generate-go-licenses.go <out-json-file>")
os.Exit(1)
}
out := os.Args[1]
goCmd := "go"
if env := os.Getenv("GO"); env != "" {
goCmd = env
}
modules := getModules(goCmd)
var entries []LicenseEntry
for _, mod := range modules {
entries = append(entries, findLicenseFiles(mod)...)
}
entries = slices.DeleteFunc(entries, func(e LicenseEntry) bool {
return ignoredNames[e.Name]
})
sort.Slice(entries, func(i, j int) bool {
return entries[i].Path < entries[j].Path
})
jsonBytes, err := json.MarshalIndent(entries, "", " ") jsonBytes, err := json.MarshalIndent(entries, "", " ")
if err != nil { if err != nil {
panic(err) panic(err)

View File

@ -2858,6 +2858,9 @@ LEVEL = Info
;ABANDONED_JOB_TIMEOUT = 24h ;ABANDONED_JOB_TIMEOUT = 24h
;; Strings committers can place inside a commit message or PR title to skip executing the corresponding actions workflow ;; Strings committers can place inside a commit message or PR title to skip executing the corresponding actions workflow
;SKIP_WORKFLOW_STRINGS = [skip ci],[ci skip],[no ci],[skip actions],[actions skip] ;SKIP_WORKFLOW_STRINGS = [skip ci],[ci skip],[no ci],[skip actions],[actions skip]
;; Comma-separated list of workflow directories, the first one to exist
;; in a repo is used to find Actions workflow files
;WORKFLOW_DIRS = .gitea/workflows,.github/workflows
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

View File

@ -211,7 +211,7 @@ export default defineConfig([
'@typescript-eslint/no-non-null-asserted-nullish-coalescing': [0], '@typescript-eslint/no-non-null-asserted-nullish-coalescing': [0],
'@typescript-eslint/no-non-null-asserted-optional-chain': [2], '@typescript-eslint/no-non-null-asserted-optional-chain': [2],
'@typescript-eslint/no-non-null-assertion': [0], '@typescript-eslint/no-non-null-assertion': [0],
'@typescript-eslint/no-redeclare': [0], '@typescript-eslint/no-redeclare': [2],
'@typescript-eslint/no-redundant-type-constituents': [2], '@typescript-eslint/no-redundant-type-constituents': [2],
'@typescript-eslint/no-require-imports': [2], '@typescript-eslint/no-require-imports': [2],
'@typescript-eslint/no-restricted-imports': [0], '@typescript-eslint/no-restricted-imports': [0],
@ -437,7 +437,7 @@ export default defineConfig([
'no-import-assign': [2], 'no-import-assign': [2],
'no-inline-comments': [0], 'no-inline-comments': [0],
'no-inner-declarations': [2], 'no-inner-declarations': [2],
'no-invalid-regexp': [2], 'no-invalid-regexp': [0], // handled by regexp/no-invalid-regexp
'no-invalid-this': [0], 'no-invalid-this': [0],
'no-irregular-whitespace': [2], 'no-irregular-whitespace': [2],
'no-iterator': [2], 'no-iterator': [2],
@ -551,7 +551,7 @@ export default defineConfig([
'no-new-func': [0], // handled by @typescript-eslint/no-implied-eval 'no-new-func': [0], // handled by @typescript-eslint/no-implied-eval
'no-new-native-nonconstructor': [2], 'no-new-native-nonconstructor': [2],
'no-new-object': [2], 'no-new-object': [2],
'no-new-symbol': [2], 'no-new-symbol': [0], // handled by no-new-native-nonconstructor
'no-new-wrappers': [2], 'no-new-wrappers': [2],
'no-new': [0], 'no-new': [0],
'no-nonoctal-decimal-escape': [2], 'no-nonoctal-decimal-escape': [2],
@ -582,7 +582,7 @@ export default defineConfig([
'no-template-curly-in-string': [2], 'no-template-curly-in-string': [2],
'no-ternary': [0], 'no-ternary': [0],
'no-this-before-super': [2], 'no-this-before-super': [2],
'no-throw-literal': [2], 'no-throw-literal': [0], // handled by @typescript-eslint/only-throw-error
'no-undef-init': [2], 'no-undef-init': [2],
'no-undef': [2], // it is still needed by eslint & IDE to prompt undefined names in real time 'no-undef': [2], // it is still needed by eslint & IDE to prompt undefined names in real time
'no-undefined': [0], 'no-undefined': [0],
@ -600,7 +600,7 @@ export default defineConfig([
'no-unused-vars': [0], // handled by @typescript-eslint/no-unused-vars 'no-unused-vars': [0], // handled by @typescript-eslint/no-unused-vars
'no-use-before-define': [0], // handled by @typescript-eslint/no-use-before-define 'no-use-before-define': [0], // handled by @typescript-eslint/no-use-before-define
'no-useless-assignment': [2], 'no-useless-assignment': [2],
'no-useless-backreference': [2], 'no-useless-backreference': [0], // handled by regexp/no-useless-backreference
'no-useless-call': [2], 'no-useless-call': [2],
'no-useless-catch': [2], 'no-useless-catch': [2],
'no-useless-computed-key': [2], 'no-useless-computed-key': [2],
@ -608,7 +608,7 @@ export default defineConfig([
'no-useless-constructor': [2], 'no-useless-constructor': [2],
'no-useless-escape': [2], 'no-useless-escape': [2],
'no-useless-rename': [2], 'no-useless-rename': [2],
'no-useless-return': [2], 'no-useless-return': [0], // handled by sonarjs/no-redundant-jump
'no-var': [2], 'no-var': [2],
'no-void': [2], 'no-void': [2],
'no-warning-comments': [0], 'no-warning-comments': [0],
@ -617,7 +617,7 @@ export default defineConfig([
'one-var-declaration-per-line': [0], 'one-var-declaration-per-line': [0],
'one-var': [0], 'one-var': [0],
'operator-assignment': [2, 'always'], 'operator-assignment': [2, 'always'],
'operator-linebreak': [2, 'after'], 'operator-linebreak': [0], // handled by @stylistic/operator-linebreak
'prefer-arrow-callback': [2, {allowNamedFunctions: true, allowUnboundThis: true}], 'prefer-arrow-callback': [2, {allowNamedFunctions: true, allowUnboundThis: true}],
'prefer-const': [2, {destructuring: 'all', ignoreReadBeforeAssign: true}], 'prefer-const': [2, {destructuring: 'all', ignoreReadBeforeAssign: true}],
'prefer-destructuring': [0], 'prefer-destructuring': [0],
@ -697,7 +697,7 @@ export default defineConfig([
'regexp/prefer-question-quantifier': [2], 'regexp/prefer-question-quantifier': [2],
'regexp/prefer-range': [2], 'regexp/prefer-range': [2],
'regexp/prefer-regexp-exec': [2], 'regexp/prefer-regexp-exec': [2],
'regexp/prefer-regexp-test': [2], 'regexp/prefer-regexp-test': [0], // handled by unicorn/prefer-regexp-test
'regexp/prefer-result-array-groups': [0], 'regexp/prefer-result-array-groups': [0],
'regexp/prefer-set-operation': [2], 'regexp/prefer-set-operation': [2],
'regexp/prefer-star-quantifier': [2], 'regexp/prefer-star-quantifier': [2],
@ -727,7 +727,7 @@ export default defineConfig([
'sonarjs/no-empty-collection': [2], 'sonarjs/no-empty-collection': [2],
'sonarjs/no-extra-arguments': [2], 'sonarjs/no-extra-arguments': [2],
'sonarjs/no-gratuitous-expressions': [2], 'sonarjs/no-gratuitous-expressions': [2],
'sonarjs/no-identical-conditions': [2], 'sonarjs/no-identical-conditions': [0], // handled by no-dupe-else-if
'sonarjs/no-identical-expressions': [2], 'sonarjs/no-identical-expressions': [2],
'sonarjs/no-identical-functions': [2, 5], 'sonarjs/no-identical-functions': [2, 5],
'sonarjs/no-ignored-return': [2], 'sonarjs/no-ignored-return': [2],
@ -740,7 +740,7 @@ export default defineConfig([
'sonarjs/no-small-switch': [0], 'sonarjs/no-small-switch': [0],
'sonarjs/no-unused-collection': [2], 'sonarjs/no-unused-collection': [2],
'sonarjs/no-use-of-empty-return-value': [2], 'sonarjs/no-use-of-empty-return-value': [2],
'sonarjs/no-useless-catch': [2], 'sonarjs/no-useless-catch': [0], // handled by no-useless-catch
'sonarjs/non-existent-operator': [2], 'sonarjs/non-existent-operator': [2],
'sonarjs/prefer-immediate-return': [0], 'sonarjs/prefer-immediate-return': [0],
'sonarjs/prefer-object-literal': [0], 'sonarjs/prefer-object-literal': [0],
@ -767,6 +767,7 @@ export default defineConfig([
'unicorn/filename-case': [0], 'unicorn/filename-case': [0],
'unicorn/import-index': [0], 'unicorn/import-index': [0],
'unicorn/import-style': [0], 'unicorn/import-style': [0],
'unicorn/isolated-functions': [2, {functions: []}],
'unicorn/new-for-builtins': [2], 'unicorn/new-for-builtins': [2],
'unicorn/no-abusive-eslint-disable': [0], 'unicorn/no-abusive-eslint-disable': [0],
'unicorn/no-anonymous-default-export': [0], 'unicorn/no-anonymous-default-export': [0],
@ -806,7 +807,7 @@ export default defineConfig([
'unicorn/no-unnecessary-await': [2], 'unicorn/no-unnecessary-await': [2],
'unicorn/no-unnecessary-polyfills': [2], 'unicorn/no-unnecessary-polyfills': [2],
'unicorn/no-unreadable-array-destructuring': [0], 'unicorn/no-unreadable-array-destructuring': [0],
'unicorn/no-unreadable-iife': [2], 'unicorn/no-unreadable-iife': [0],
'unicorn/no-unused-properties': [2], 'unicorn/no-unused-properties': [2],
'unicorn/no-useless-collection-argument': [2], 'unicorn/no-useless-collection-argument': [2],
'unicorn/no-useless-fallback-in-spread': [2], 'unicorn/no-useless-fallback-in-spread': [2],
@ -819,7 +820,7 @@ export default defineConfig([
'unicorn/number-literal-case': [0], 'unicorn/number-literal-case': [0],
'unicorn/numeric-separators-style': [0], 'unicorn/numeric-separators-style': [0],
'unicorn/prefer-add-event-listener': [2], 'unicorn/prefer-add-event-listener': [2],
'unicorn/prefer-array-find': [2], 'unicorn/prefer-array-find': [0], // handled by @typescript-eslint/prefer-find
'unicorn/prefer-array-flat': [2], 'unicorn/prefer-array-flat': [2],
'unicorn/prefer-array-flat-map': [2], 'unicorn/prefer-array-flat-map': [2],
'unicorn/prefer-array-index-of': [2], 'unicorn/prefer-array-index-of': [2],
@ -836,7 +837,7 @@ export default defineConfig([
'unicorn/prefer-event-target': [2], 'unicorn/prefer-event-target': [2],
'unicorn/prefer-export-from': [0], 'unicorn/prefer-export-from': [0],
'unicorn/prefer-global-this': [0], 'unicorn/prefer-global-this': [0],
'unicorn/prefer-includes': [2], 'unicorn/prefer-includes': [0], // handled by @typescript-eslint/prefer-includes
'unicorn/prefer-json-parse-buffer': [0], 'unicorn/prefer-json-parse-buffer': [0],
'unicorn/prefer-keyboard-event-key': [2], 'unicorn/prefer-keyboard-event-key': [2],
'unicorn/prefer-logical-operator-over-ternary': [2], 'unicorn/prefer-logical-operator-over-ternary': [2],
@ -863,7 +864,7 @@ export default defineConfig([
'unicorn/prefer-string-raw': [0], 'unicorn/prefer-string-raw': [0],
'unicorn/prefer-string-replace-all': [0], 'unicorn/prefer-string-replace-all': [0],
'unicorn/prefer-string-slice': [0], 'unicorn/prefer-string-slice': [0],
'unicorn/prefer-string-starts-ends-with': [2], 'unicorn/prefer-string-starts-ends-with': [0], // handled by @typescript-eslint/prefer-string-starts-ends-with
'unicorn/prefer-string-trim-start-end': [2], 'unicorn/prefer-string-trim-start-end': [2],
'unicorn/prefer-structured-clone': [2], 'unicorn/prefer-structured-clone': [2],
'unicorn/prefer-switch': [0], 'unicorn/prefer-switch': [0],

6
flake.lock generated
View File

@ -2,11 +2,11 @@
"nodes": { "nodes": {
"nixpkgs": { "nixpkgs": {
"locked": { "locked": {
"lastModified": 1760038930, "lastModified": 1771369470,
"narHash": "sha256-Oncbh0UmHjSlxO7ErQDM3KM0A5/Znfofj2BSzlHLeVw=", "narHash": "sha256-0NBlEBKkN3lufyvFegY4TYv5mCNHbi5OmBDrzihbBMQ=",
"owner": "nixos", "owner": "nixos",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "0b4defa2584313f3b781240b29d61f6f9f7e0df3", "rev": "0182a361324364ae3f436a63005877674cf45efb",
"type": "github" "type": "github"
}, },
"original": { "original": {

2
go.mod
View File

@ -53,7 +53,7 @@ require (
github.com/go-co-op/gocron v1.37.0 github.com/go-co-op/gocron v1.37.0
github.com/go-enry/go-enry/v2 v2.9.4 github.com/go-enry/go-enry/v2 v2.9.4
github.com/go-git/go-billy/v5 v5.7.0 github.com/go-git/go-billy/v5 v5.7.0
github.com/go-git/go-git/v5 v5.16.4 github.com/go-git/go-git/v5 v5.16.5
github.com/go-ldap/ldap/v3 v3.4.12 github.com/go-ldap/ldap/v3 v3.4.12
github.com/go-redsync/redsync/v4 v4.15.0 github.com/go-redsync/redsync/v4 v4.15.0
github.com/go-sql-driver/mysql v1.9.3 github.com/go-sql-driver/mysql v1.9.3

4
go.sum
View File

@ -332,8 +332,8 @@ github.com/go-git/go-billy/v5 v5.7.0 h1:83lBUJhGWhYp0ngzCMSgllhUSuoHP1iEWYjsPl9n
github.com/go-git/go-billy/v5 v5.7.0/go.mod h1:/1IUejTKH8xipsAcdfcSAlUlo2J7lkYV8GTKxAT/L3E= github.com/go-git/go-billy/v5 v5.7.0/go.mod h1:/1IUejTKH8xipsAcdfcSAlUlo2J7lkYV8GTKxAT/L3E=
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4= github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4=
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII= github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII=
github.com/go-git/go-git/v5 v5.16.4 h1:7ajIEZHZJULcyJebDLo99bGgS0jRrOxzZG4uCk2Yb2Y= github.com/go-git/go-git/v5 v5.16.5 h1:mdkuqblwr57kVfXri5TTH+nMFLNUxIj9Z7F5ykFbw5s=
github.com/go-git/go-git/v5 v5.16.4/go.mod h1:4Ge4alE/5gPs30F2H1esi2gPd69R0C39lolkucHBOp8= github.com/go-git/go-git/v5 v5.16.5/go.mod h1:QOMLpNf1qxuSY4StA/ArOdfFR2TrKEjJiye2kel2m+M=
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-ini/ini v1.67.0 h1:z6ZrTEZqSWOTyH2FlglNbNgARyHG8oLW9gMELqKr06A= github.com/go-ini/ini v1.67.0 h1:z6ZrTEZqSWOTyH2FlglNbNgARyHG8oLW9gMELqKr06A=

View File

@ -26,9 +26,8 @@ import (
// these flags will be set by the build flags // these flags will be set by the build flags
var ( var (
Version = "development" // program version for this build Version = "development" // program version for this build
Tags = "" // the Golang build tags Tags = "" // the Golang build tags
MakeVersion = "" // "make" program version if built with make
) )
func init() { func init() {
@ -50,9 +49,6 @@ func main() {
func formatBuiltWith() string { func formatBuiltWith() string {
version := runtime.Version() version := runtime.Version()
if len(MakeVersion) > 0 {
version = MakeVersion + ", " + runtime.Version()
}
if len(Tags) == 0 { if len(Tags) == 0 {
return " built with " + version return " built with " + version
} }

View File

@ -168,7 +168,7 @@ func (run *ActionRun) GetPushEventPayload() (*api.PushPayload, error) {
} }
func (run *ActionRun) GetPullRequestEventPayload() (*api.PullRequestPayload, error) { func (run *ActionRun) GetPullRequestEventPayload() (*api.PullRequestPayload, error) {
if run.Event.IsPullRequest() { if run.Event.IsPullRequest() || run.Event.IsPullRequestReview() {
var payload api.PullRequestPayload var payload api.PullRequestPayload
if err := json.Unmarshal([]byte(run.EventPayload), &payload); err != nil { if err := json.Unmarshal([]byte(run.EventPayload), &payload); err != nil {
return nil, err return nil, err

View File

@ -8,6 +8,7 @@ import (
"crypto/subtle" "crypto/subtle"
"errors" "errors"
"fmt" "fmt"
"strings"
"time" "time"
auth_model "code.gitea.io/gitea/models/auth" auth_model "code.gitea.io/gitea/models/auth"
@ -20,6 +21,7 @@ import (
runnerv1 "code.gitea.io/actions-proto-go/runner/v1" runnerv1 "code.gitea.io/actions-proto-go/runner/v1"
lru "github.com/hashicorp/golang-lru/v2" lru "github.com/hashicorp/golang-lru/v2"
"github.com/nektos/act/pkg/jobparser"
"google.golang.org/protobuf/types/known/timestamppb" "google.golang.org/protobuf/types/known/timestamppb"
"xorm.io/builder" "xorm.io/builder"
) )
@ -214,6 +216,20 @@ func GetRunningTaskByToken(ctx context.Context, token string) (*ActionTask, erro
return nil, errNotExist return nil, errNotExist
} }
func makeTaskStepDisplayName(step *jobparser.Step, limit int) (name string) {
if step.Name != "" {
name = step.Name // the step has an explicit name
} else {
// for unnamed step, its "String()" method tries to get a display name by its "name", "uses",
// "run" or "id" (last fallback), we add the "Run " prefix for unnamed steps for better display
// for multi-line "run" scripts, only use the first line to match GitHub's behavior
// https://github.com/actions/runner/blob/66800900843747f37591b077091dd2c8cf2c1796/src/Runner.Worker/Handlers/ScriptHandler.cs#L45-L58
runStr, _, _ := strings.Cut(strings.TrimSpace(step.Run), "\n")
name = "Run " + util.IfZero(strings.TrimSpace(runStr), step.String())
}
return util.EllipsisDisplayString(name, limit) // database column has a length limit
}
func CreateTaskForRunner(ctx context.Context, runner *ActionRunner) (*ActionTask, bool, error) { func CreateTaskForRunner(ctx context.Context, runner *ActionRunner) (*ActionTask, bool, error) {
ctx, committer, err := db.TxContext(ctx) ctx, committer, err := db.TxContext(ctx)
if err != nil { if err != nil {
@ -293,9 +309,8 @@ func CreateTaskForRunner(ctx context.Context, runner *ActionRunner) (*ActionTask
if len(workflowJob.Steps) > 0 { if len(workflowJob.Steps) > 0 {
steps := make([]*ActionTaskStep, len(workflowJob.Steps)) steps := make([]*ActionTaskStep, len(workflowJob.Steps))
for i, v := range workflowJob.Steps { for i, v := range workflowJob.Steps {
name := util.EllipsisDisplayString(v.String(), 255)
steps[i] = &ActionTaskStep{ steps[i] = &ActionTaskStep{
Name: name, Name: makeTaskStepDisplayName(v, 255),
TaskID: task.ID, TaskID: task.ID,
Index: int64(i), Index: int64(i),
RepoID: task.RepoID, RepoID: task.RepoID,

View File

@ -14,7 +14,7 @@ import (
// ActionTaskStep represents a step of ActionTask // ActionTaskStep represents a step of ActionTask
type ActionTaskStep struct { type ActionTaskStep struct {
ID int64 ID int64
Name string `xorm:"VARCHAR(255)"` Name string `xorm:"VARCHAR(255)"` // the step name, for display purpose only, it will be truncated if it is too long
TaskID int64 `xorm:"index unique(task_index)"` TaskID int64 `xorm:"index unique(task_index)"`
Index int64 `xorm:"index unique(task_index)"` Index int64 `xorm:"index unique(task_index)"`
RepoID int64 `xorm:"index"` RepoID int64 `xorm:"index"`

View File

@ -0,0 +1,76 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package actions
import (
"strings"
"testing"
"github.com/nektos/act/pkg/jobparser"
"github.com/stretchr/testify/assert"
)
func TestMakeTaskStepDisplayName(t *testing.T) {
tests := []struct {
name string
jobStep *jobparser.Step
expected string
}{
{
name: "explicit name",
jobStep: &jobparser.Step{
Name: "Test Step",
},
expected: "Test Step",
},
{
name: "uses step",
jobStep: &jobparser.Step{
Uses: "actions/checkout@v4",
},
expected: "Run actions/checkout@v4",
},
{
name: "single-line run",
jobStep: &jobparser.Step{
Run: "echo hello",
},
expected: "Run echo hello",
},
{
name: "multi-line run block scalar",
jobStep: &jobparser.Step{
Run: "\n echo hello \r\n echo world \n ",
},
expected: "Run echo hello",
},
{
name: "fallback to id",
jobStep: &jobparser.Step{
ID: "step-id",
},
expected: "Run step-id",
},
{
name: "very long name truncated",
jobStep: &jobparser.Step{
Name: strings.Repeat("a", 300),
},
expected: strings.Repeat("a", 252) + "…",
},
{
name: "very long run truncated",
jobStep: &jobparser.Step{
Run: strings.Repeat("a", 300),
},
expected: "Run " + strings.Repeat("a", 248) + "…",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := makeTaskStepDisplayName(tt.jobStep, 255)
assert.Equal(t, tt.expected, result)
})
}
}

View File

@ -19,14 +19,14 @@ type UserHeatmapData struct {
Contributions int64 `json:"contributions"` Contributions int64 `json:"contributions"`
} }
// GetUserHeatmapDataByUser returns an array of UserHeatmapData // GetUserHeatmapDataByUser returns an array of UserHeatmapData, it checks whether doer can access user's activity
func GetUserHeatmapDataByUser(ctx context.Context, user, doer *user_model.User) ([]*UserHeatmapData, error) { func GetUserHeatmapDataByUser(ctx context.Context, user, doer *user_model.User) ([]*UserHeatmapData, error) {
return getUserHeatmapData(ctx, user, nil, doer) return getUserHeatmapData(ctx, user, nil, doer)
} }
// GetUserHeatmapDataByUserTeam returns an array of UserHeatmapData // GetUserHeatmapDataByOrgTeam returns an array of UserHeatmapData, it checks whether doer can access org's activity
func GetUserHeatmapDataByUserTeam(ctx context.Context, user *user_model.User, team *organization.Team, doer *user_model.User) ([]*UserHeatmapData, error) { func GetUserHeatmapDataByOrgTeam(ctx context.Context, org *organization.Organization, team *organization.Team, doer *user_model.User) ([]*UserHeatmapData, error) {
return getUserHeatmapData(ctx, user, team, doer) return getUserHeatmapData(ctx, org.AsUser(), team, doer)
} }
func getUserHeatmapData(ctx context.Context, user *user_model.User, team *organization.Team, doer *user_model.User) ([]*UserHeatmapData, error) { func getUserHeatmapData(ctx context.Context, user *user_model.User, team *organization.Team, doer *user_model.User) ([]*UserHeatmapData, error) {
@ -71,12 +71,3 @@ func getUserHeatmapData(ctx context.Context, user *user_model.User, team *organi
OrderBy("timestamp"). OrderBy("timestamp").
Find(&hdata) Find(&hdata)
} }
// GetTotalContributionsInHeatmap returns the total number of contributions in a heatmap
func GetTotalContributionsInHeatmap(hdata []*UserHeatmapData) int64 {
var total int64
for _, v := range hdata {
total += v.Contributions
}
return total
}

View File

@ -139,26 +139,7 @@
updated: 1683636626 updated: 1683636626
need_approval: 0 need_approval: 0
approved_by: 0 approved_by: 0
-
id: 804
title: "use a private action"
repo_id: 60
owner_id: 40
workflow_id: "run.yaml"
index: 189
trigger_user_id: 40
ref: "refs/heads/master"
commit_sha: "6e64b26de7ba966d01d90ecfaf5c7f14ef203e86"
event: "push"
trigger_event: "push"
is_fork_pull_request: 0
status: 1
started: 1683636528
stopped: 1683636626
created: 1683636108
updated: 1683636626
need_approval: 0
approved_by: 0
- -
id: 805 id: 805
title: "update actions" title: "update actions"

View File

@ -129,20 +129,7 @@
status: 5 status: 5
started: 1683636528 started: 1683636528
stopped: 1683636626 stopped: 1683636626
-
id: 205
run_id: 804
repo_id: 6
owner_id: 10
commit_sha: 6e64b26de7ba966d01d90ecfaf5c7f14ef203e86
is_fork_pull_request: 0
name: job_2
attempt: 1
job_id: job_2
task_id: 48
status: 1
started: 1683636528
stopped: 1683636626
- -
id: 206 id: 206
run_id: 805 run_id: 805

View File

@ -177,26 +177,7 @@
log_length: 0 log_length: 0
log_size: 0 log_size: 0
log_expired: 0 log_expired: 0
-
id: 55
job_id: 205
attempt: 1
runner_id: 1
status: 6 # 6 is the status code for "running"
started: 1683636528
stopped: 1683636626
repo_id: 6
owner_id: 10
commit_sha: 6e64b26de7ba966d01d90ecfaf5c7f14ef203e86
is_fork_pull_request: 0
token_hash: b8d3962425466b6709b9ac51446f93260c54afe8e7b6d3686e34f991fb8a8953822b0deed86fe41a103f34bc48dbc478422b
token_salt: ERxJGHvg3I
token_last_eight: 182199eb
log_filename: collaborative-owner-test/1a/49.log
log_in_storage: 1
log_length: 707
log_size: 90179
log_expired: 0
- -
id: 56 id: 56
attempt: 1 attempt: 1

View File

@ -397,10 +397,16 @@ func RenameBranch(ctx context.Context, repo *repo_model.Repository, from, to str
if protectedBranch != nil { if protectedBranch != nil {
// there is a protect rule for this branch // there is a protect rule for this branch
protectedBranch.RuleName = to existingRule, err := GetProtectedBranchRuleByName(ctx, repo.ID, to)
if _, err = sess.ID(protectedBranch.ID).Cols("branch_name").Update(protectedBranch); err != nil { if err != nil {
return err return err
} }
if existingRule == nil || existingRule.ID == protectedBranch.ID {
protectedBranch.RuleName = to
if _, err = sess.ID(protectedBranch.ID).Cols("branch_name").Update(protectedBranch); err != nil {
return err
}
}
} else { } else {
// some glob protect rules may match this branch // some glob protect rules may match this branch
protected, err := IsBranchProtected(ctx, repo.ID, from) protected, err := IsBranchProtected(ctx, repo.ID, from)

View File

@ -159,6 +159,53 @@ func TestRenameBranch(t *testing.T) {
}) })
} }
func TestRenameBranchProtectedRuleConflict(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
master := unittest.AssertExistsAndLoadBean(t, &git_model.Branch{RepoID: repo1.ID, Name: "master"})
devBranch := &git_model.Branch{
RepoID: repo1.ID,
Name: "dev",
CommitID: master.CommitID,
CommitMessage: master.CommitMessage,
CommitTime: master.CommitTime,
PusherID: master.PusherID,
}
assert.NoError(t, db.Insert(t.Context(), devBranch))
pbDev := git_model.ProtectedBranch{
RepoID: repo1.ID,
RuleName: "dev",
CanPush: true,
}
assert.NoError(t, git_model.UpdateProtectBranch(t.Context(), repo1, &pbDev, git_model.WhitelistOptions{}))
pbMain := git_model.ProtectedBranch{
RepoID: repo1.ID,
RuleName: "main",
CanPush: true,
}
assert.NoError(t, git_model.UpdateProtectBranch(t.Context(), repo1, &pbMain, git_model.WhitelistOptions{}))
assert.NoError(t, git_model.RenameBranch(t.Context(), repo1, "dev", "main", func(ctx context.Context, isDefault bool) error {
return nil
}))
unittest.AssertNotExistsBean(t, &git_model.Branch{RepoID: repo1.ID, Name: "dev"})
unittest.AssertExistsAndLoadBean(t, &git_model.Branch{RepoID: repo1.ID, Name: "main"})
protectedDev, err := git_model.GetProtectedBranchRuleByName(t.Context(), repo1.ID, "dev")
assert.NoError(t, err)
assert.NotNil(t, protectedDev)
assert.Equal(t, "dev", protectedDev.RuleName)
protectedMainByID, err := git_model.GetProtectedBranchRuleByID(t.Context(), repo1.ID, pbMain.ID)
assert.NoError(t, err)
assert.NotNil(t, protectedMainByID)
assert.Equal(t, "main", protectedMainByID.RuleName)
}
func TestOnlyGetDeletedBranchOnCorrectRepo(t *testing.T) { func TestOnlyGetDeletedBranchOnCorrectRepo(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase()) assert.NoError(t, unittest.PrepareTestDatabase())

View File

@ -10,7 +10,6 @@ import (
"path" "path"
"path/filepath" "path/filepath"
"testing" "testing"
"time"
"code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/models/unittest" "code.gitea.io/gitea/models/unittest"
@ -27,18 +26,6 @@ import (
// FIXME: this file shouldn't be in a normal package, it should only be compiled for tests // FIXME: this file shouldn't be in a normal package, it should only be compiled for tests
func removeAllWithRetry(dir string) error {
var err error
for range 20 {
err = os.RemoveAll(dir)
if err == nil {
break
}
time.Sleep(100 * time.Millisecond)
}
return err
}
func newXORMEngine(t *testing.T) (*xorm.Engine, error) { func newXORMEngine(t *testing.T) (*xorm.Engine, error) {
if err := db.InitEngine(t.Context()); err != nil { if err := db.InitEngine(t.Context()); err != nil {
return nil, err return nil, err
@ -213,13 +200,12 @@ func LoadTableSchemasMap(t *testing.T, x *xorm.Engine) map[string]*schemas.Table
return tableMap return tableMap
} }
func MainTest(m *testing.M) { func mainTest(m *testing.M) int {
testlogger.Init() testlogger.Init()
setting.SetupGiteaTestEnv()
tmpDataPath, cleanup, err := tempdir.OsTempDir("gitea-test").MkdirTempRandom("data") tmpDataPath, cleanup, err := tempdir.OsTempDir("gitea-test").MkdirTempRandom("data")
if err != nil { if err != nil {
testlogger.Fatalf("Unable to create temporary data path %v\n", err) testlogger.Panicf("Unable to create temporary data path %v\n", err)
} }
defer cleanup() defer cleanup()
@ -227,15 +213,13 @@ func MainTest(m *testing.M) {
unittest.InitSettingsForTesting() unittest.InitSettingsForTesting()
if err = git.InitFull(); err != nil { if err = git.InitFull(); err != nil {
testlogger.Fatalf("Unable to InitFull: %v\n", err) testlogger.Panicf("Unable to InitFull: %v\n", err)
} }
setting.LoadDBSetting() setting.LoadDBSetting()
setting.InitLoggersForTest() setting.InitLoggersForTest()
return m.Run()
exitStatus := m.Run() }
if err := removeAllWithRetry(setting.RepoRootPath); err != nil { func MainTest(m *testing.M) {
_, _ = fmt.Fprintf(os.Stderr, "os.RemoveAll: %v\n", err) os.Exit(mainTest(m))
}
os.Exit(exitStatus)
} }

View File

@ -4,6 +4,7 @@
package unittest_test package unittest_test
import ( import (
"os"
"path/filepath" "path/filepath"
"testing" "testing"
@ -58,9 +59,14 @@ func NewFixturesLoaderVendorGoTestfixtures(e *xorm.Engine, opts unittest.Fixture
} }
*/ */
func TestMain(m *testing.M) {
setting.SetupGiteaTestEnv()
os.Exit(m.Run())
}
func prepareTestFixturesLoaders(t testing.TB) unittest.FixturesOptions { func prepareTestFixturesLoaders(t testing.TB) unittest.FixturesOptions {
_ = user_model.User{} _ = user_model.User{}
giteaRoot := setting.SetupGiteaTestEnv() giteaRoot := setting.GetGiteaTestSourceRoot()
opts := unittest.FixturesOptions{Dir: filepath.Join(giteaRoot, "models", "fixtures"), Files: []string{ opts := unittest.FixturesOptions{Dir: filepath.Join(giteaRoot, "models", "fixtures"), Files: []string{
"user.yml", "user.yml",
}} }}

View File

@ -4,10 +4,12 @@
package unittest package unittest
import ( import (
"errors"
"os" "os"
"path/filepath" "path/filepath"
"strings" "strings"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/util"
) )
@ -39,7 +41,20 @@ func SyncFile(srcPath, destPath string) error {
// SyncDirs synchronizes files recursively from source to target directory. // SyncDirs synchronizes files recursively from source to target directory.
// It returns error when error occurs in underlying functions. // It returns error when error occurs in underlying functions.
func SyncDirs(srcPath, destPath string) error { func SyncDirs(srcPath, destPath string) error {
err := os.MkdirAll(destPath, os.ModePerm) destPath = filepath.Clean(destPath)
destPathAbs, err := filepath.Abs(destPath)
if err != nil {
return err
}
devDataPathAbs, err := filepath.Abs(filepath.Join(setting.GetGiteaTestSourceRoot(), "data"))
if err != nil {
return err
}
if strings.HasPrefix(destPathAbs+string(filepath.Separator), devDataPathAbs+string(filepath.Separator)) {
return errors.New("destination path should not be inside Gitea data directory, otherwise your data for dev mode will be removed")
}
err = os.MkdirAll(destPath, os.ModePerm)
if err != nil { if err != nil {
return err return err
} }

View File

@ -21,6 +21,7 @@ import (
"code.gitea.io/gitea/modules/setting/config" "code.gitea.io/gitea/modules/setting/config"
"code.gitea.io/gitea/modules/storage" "code.gitea.io/gitea/modules/storage"
"code.gitea.io/gitea/modules/tempdir" "code.gitea.io/gitea/modules/tempdir"
"code.gitea.io/gitea/modules/testlogger"
"code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/util"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
@ -28,16 +29,10 @@ import (
"xorm.io/xorm/names" "xorm.io/xorm/names"
) )
var giteaRoot string
func fatalTestError(fmtStr string, args ...any) {
_, _ = fmt.Fprintf(os.Stderr, fmtStr, args...)
os.Exit(1)
}
// InitSettingsForTesting initializes config provider and load common settings for tests // InitSettingsForTesting initializes config provider and load common settings for tests
func InitSettingsForTesting() { func InitSettingsForTesting() {
setting.IsInTesting = true setting.SetupGiteaTestEnv()
log.OsExiter = func(code int) { log.OsExiter = func(code int) {
if code != 0 { if code != 0 {
// non-zero exit code (log.Fatal) shouldn't occur during testing, if it happens, show a full stacktrace for more details // non-zero exit code (log.Fatal) shouldn't occur during testing, if it happens, show a full stacktrace for more details
@ -49,8 +44,12 @@ func InitSettingsForTesting() {
setting.CustomConf = filepath.Join(setting.CustomPath, "conf/app-unittest-tmp.ini") setting.CustomConf = filepath.Join(setting.CustomPath, "conf/app-unittest-tmp.ini")
_ = os.Remove(setting.CustomConf) _ = os.Remove(setting.CustomConf)
} }
setting.InitCfgProvider(setting.CustomConf)
setting.LoadCommonSettings() // init paths and config system for testing
getTestEnv := func(key string) string {
return ""
}
setting.InitWorkPathAndCommonConfig(getTestEnv, setting.ArgWorkPathAndCustomConf{CustomConf: setting.CustomConf})
if err := setting.PrepareAppDataPath(); err != nil { if err := setting.PrepareAppDataPath(); err != nil {
log.Fatal("Can not prepare APP_DATA_PATH: %v", err) log.Fatal("Can not prepare APP_DATA_PATH: %v", err)
@ -71,16 +70,18 @@ type TestOptions struct {
// MainTest a reusable TestMain(..) function for unit tests that need to use a // MainTest a reusable TestMain(..) function for unit tests that need to use a
// test database. Creates the test database, and sets necessary settings. // test database. Creates the test database, and sets necessary settings.
func MainTest(m *testing.M, testOptsArg ...*TestOptions) { func MainTest(m *testing.M, testOptsArg ...*TestOptions) {
testOpts := util.OptionalArg(testOptsArg, &TestOptions{}) os.Exit(mainTest(m, testOptsArg...))
giteaRoot = setting.SetupGiteaTestEnv() }
InitSettingsForTesting()
func mainTest(m *testing.M, testOptsArg ...*TestOptions) int {
testOpts := util.OptionalArg(testOptsArg, &TestOptions{})
InitSettingsForTesting()
giteaRoot := setting.GetGiteaTestSourceRoot()
fixturesOpts := FixturesOptions{Dir: filepath.Join(giteaRoot, "models", "fixtures"), Files: testOpts.FixtureFiles} fixturesOpts := FixturesOptions{Dir: filepath.Join(giteaRoot, "models", "fixtures"), Files: testOpts.FixtureFiles}
if err := CreateTestEngine(fixturesOpts); err != nil { if err := CreateTestEngine(fixturesOpts); err != nil {
fatalTestError("Error creating test engine: %v\n", err) testlogger.Panicf("Error creating test engine: %v\n", err)
} }
setting.IsInTesting = true
setting.AppURL = "https://try.gitea.io/" setting.AppURL = "https://try.gitea.io/"
setting.Domain = "try.gitea.io" setting.Domain = "try.gitea.io"
setting.RunUser = "runuser" setting.RunUser = "runuser"
@ -92,20 +93,18 @@ func MainTest(m *testing.M, testOptsArg ...*TestOptions) {
setting.Repository.DefaultBranch = "master" // many test code still assume that default branch is called "master" setting.Repository.DefaultBranch = "master" // many test code still assume that default branch is called "master"
repoRootPath, cleanup1, err := tempdir.OsTempDir("gitea-test").MkdirTempRandom("repos") repoRootPath, cleanup1, err := tempdir.OsTempDir("gitea-test").MkdirTempRandom("repos")
if err != nil { if err != nil {
fatalTestError("TempDir: %v\n", err) testlogger.Panicf("TempDir: %v\n", err)
} }
defer cleanup1() defer cleanup1()
setting.RepoRootPath = repoRootPath setting.RepoRootPath = repoRootPath
appDataPath, cleanup2, err := tempdir.OsTempDir("gitea-test").MkdirTempRandom("appdata") appDataPath, cleanup2, err := tempdir.OsTempDir("gitea-test").MkdirTempRandom("appdata")
if err != nil { if err != nil {
fatalTestError("TempDir: %v\n", err) testlogger.Panicf("TempDir: %v\n", err)
} }
defer cleanup2() defer cleanup2()
setting.AppDataPath = appDataPath setting.AppDataPath = appDataPath
setting.AppWorkPath = giteaRoot
setting.StaticRootPath = giteaRoot
setting.GravatarSource = "https://secure.gravatar.com/avatar/" setting.GravatarSource = "https://secure.gravatar.com/avatar/"
setting.Attachment.Storage.Path = filepath.Join(setting.AppDataPath, "attachments") setting.Attachment.Storage.Path = filepath.Join(setting.AppDataPath, "attachments")
@ -129,22 +128,22 @@ func MainTest(m *testing.M, testOptsArg ...*TestOptions) {
config.SetDynGetter(system.NewDatabaseDynKeyGetter()) config.SetDynGetter(system.NewDatabaseDynKeyGetter())
if err = cache.Init(); err != nil { if err = cache.Init(); err != nil {
fatalTestError("cache.Init: %v\n", err) testlogger.Panicf("cache.Init: %v\n", err)
} }
if err = storage.Init(); err != nil { if err = storage.Init(); err != nil {
fatalTestError("storage.Init: %v\n", err) testlogger.Panicf("storage.Init: %v\n", err)
} }
if err = SyncDirs(filepath.Join(giteaRoot, "tests", "gitea-repositories-meta"), setting.RepoRootPath); err != nil { if err = SyncDirs(filepath.Join(giteaRoot, "tests", "gitea-repositories-meta"), setting.RepoRootPath); err != nil {
fatalTestError("util.SyncDirs: %v\n", err) testlogger.Panicf("util.SyncDirs: %v\n", err)
} }
if err = git.InitFull(); err != nil { if err = git.InitFull(); err != nil {
fatalTestError("git.Init: %v\n", err) testlogger.Panicf("git.Init: %v\n", err)
} }
if testOpts.SetUp != nil { if testOpts.SetUp != nil {
if err := testOpts.SetUp(); err != nil { if err := testOpts.SetUp(); err != nil {
fatalTestError("set up failed: %v\n", err) testlogger.Panicf("set up failed: %v\n", err)
} }
} }
@ -152,10 +151,10 @@ func MainTest(m *testing.M, testOptsArg ...*TestOptions) {
if testOpts.TearDown != nil { if testOpts.TearDown != nil {
if err := testOpts.TearDown(); err != nil { if err := testOpts.TearDown(); err != nil {
fatalTestError("tear down failed: %v\n", err) testlogger.Panicf("tear down failed: %v\n", err)
} }
} }
os.Exit(exitStatus) return exitStatus
} }
// FixturesOptions fixtures needs to be loaded options // FixturesOptions fixtures needs to be loaded options
@ -196,7 +195,6 @@ func PrepareTestDatabase() error {
// by tests that use the above MainTest(..) function. // by tests that use the above MainTest(..) function.
func PrepareTestEnv(t testing.TB) { func PrepareTestEnv(t testing.TB) {
assert.NoError(t, PrepareTestDatabase()) assert.NoError(t, PrepareTestDatabase())
metaPath := filepath.Join(giteaRoot, "tests", "gitea-repositories-meta") metaPath := filepath.Join(setting.GetGiteaTestSourceRoot(), "tests", "gitea-repositories-meta")
assert.NoError(t, SyncDirs(metaPath, setting.RepoRootPath)) assert.NoError(t, SyncDirs(metaPath, setting.RepoRootPath))
setting.SetupGiteaTestEnv()
} }

View File

@ -11,6 +11,7 @@ import (
"code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/glob" "code.gitea.io/gitea/modules/glob"
"code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
api "code.gitea.io/gitea/modules/structs" api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/util"
webhook_module "code.gitea.io/gitea/modules/webhook" webhook_module "code.gitea.io/gitea/modules/webhook"
@ -41,22 +42,30 @@ func IsWorkflow(path string) bool {
return false return false
} }
return strings.HasPrefix(path, ".gitea/workflows") || strings.HasPrefix(path, ".github/workflows") for _, workflowDir := range setting.Actions.WorkflowDirs {
if strings.HasPrefix(path, workflowDir+"/") {
return true
}
}
return false
} }
func ListWorkflows(commit *git.Commit) (string, git.Entries, error) { func ListWorkflows(commit *git.Commit) (string, git.Entries, error) {
rpath := ".gitea/workflows" var tree *git.Tree
tree, err := commit.SubTree(rpath) var err error
if _, ok := err.(git.ErrNotExist); ok { var workflowDir string
rpath = ".github/workflows" for _, workflowDir = range setting.Actions.WorkflowDirs {
tree, err = commit.SubTree(rpath) tree, err = commit.SubTree(workflowDir)
if err == nil {
break
}
if !git.IsErrNotExist(err) {
return "", nil, err
}
} }
if _, ok := err.(git.ErrNotExist); ok { if tree == nil {
return "", nil, nil return "", nil, nil
} }
if err != nil {
return "", nil, err
}
entries, err := tree.ListEntriesRecursiveFast() entries, err := tree.ListEntriesRecursiveFast()
if err != nil { if err != nil {
@ -69,7 +78,7 @@ func ListWorkflows(commit *git.Commit) (string, git.Entries, error) {
ret = append(ret, entry) ret = append(ret, entry)
} }
} }
return rpath, ret, nil return workflowDir, ret, nil
} }
func GetContentFromEntry(entry *git.TreeEntry) ([]byte, error) { func GetContentFromEntry(entry *git.TreeEntry) ([]byte, error) {

View File

@ -7,12 +7,83 @@ import (
"testing" "testing"
"code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/setting"
api "code.gitea.io/gitea/modules/structs" api "code.gitea.io/gitea/modules/structs"
webhook_module "code.gitea.io/gitea/modules/webhook" webhook_module "code.gitea.io/gitea/modules/webhook"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
func TestIsWorkflow(t *testing.T) {
oldDirs := setting.Actions.WorkflowDirs
defer func() {
setting.Actions.WorkflowDirs = oldDirs
}()
tests := []struct {
name string
dirs []string
path string
expected bool
}{
{
name: "default with yml extension",
dirs: []string{".gitea/workflows", ".github/workflows"},
path: ".gitea/workflows/test.yml",
expected: true,
},
{
name: "default with yaml extension",
dirs: []string{".gitea/workflows", ".github/workflows"},
path: ".github/workflows/test.yaml",
expected: true,
},
{
name: "only gitea configured, github path rejected",
dirs: []string{".gitea/workflows"},
path: ".github/workflows/test.yml",
expected: false,
},
{
name: "only github configured, gitea path rejected",
dirs: []string{".github/workflows"},
path: ".gitea/workflows/test.yml",
expected: false,
},
{
name: "custom workflow dir",
dirs: []string{".custom/workflows"},
path: ".custom/workflows/deploy.yml",
expected: true,
},
{
name: "non-workflow file",
dirs: []string{".gitea/workflows", ".github/workflows"},
path: ".gitea/workflows/readme.md",
expected: false,
},
{
name: "directory boundary",
dirs: []string{".gitea/workflows"},
path: ".gitea/workflows2/test.yml",
expected: false,
},
{
name: "unrelated path",
dirs: []string{".gitea/workflows", ".github/workflows"},
path: "src/main.go",
expected: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
setting.Actions.WorkflowDirs = tt.dirs
assert.Equal(t, tt.expected, IsWorkflow(tt.path))
})
}
}
func TestDetectMatched(t *testing.T) { func TestDetectMatched(t *testing.T) {
testCases := []struct { testCases := []struct {
desc string desc string

View File

@ -37,6 +37,10 @@ type CommitSignature struct {
// Message returns the commit message. Same as retrieving CommitMessage directly. // Message returns the commit message. Same as retrieving CommitMessage directly.
func (c *Commit) Message() string { func (c *Commit) Message() string {
// FIXME: GIT-COMMIT-MESSAGE-ENCODING: this logic is not right
// * When need to use commit message in templates/database, it should be valid UTF-8
// * When need to get the original commit message, it should just use "c.CommitMessage"
// It's not easy to refactor at the moment, many templates need to be updated and tested
return c.CommitMessage return c.CommitMessage
} }

View File

@ -17,6 +17,7 @@ import (
"code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/tempdir" "code.gitea.io/gitea/modules/tempdir"
"code.gitea.io/gitea/modules/testlogger"
"github.com/hashicorp/go-version" "github.com/hashicorp/go-version"
) )
@ -185,21 +186,19 @@ func InitFull() (err error) {
// RunGitTests helps to init the git module and run tests. // RunGitTests helps to init the git module and run tests.
// FIXME: GIT-PACKAGE-DEPENDENCY: the dependency is not right, setting.Git.HomePath is initialized in this package but used in gitcmd package // FIXME: GIT-PACKAGE-DEPENDENCY: the dependency is not right, setting.Git.HomePath is initialized in this package but used in gitcmd package
func RunGitTests(m interface{ Run() int }) { func RunGitTests(m interface{ Run() int }) {
fatalf := func(exitCode int, format string, args ...any) { os.Exit(runGitTests(m))
_, _ = fmt.Fprintf(os.Stderr, format, args...) }
os.Exit(exitCode)
} func runGitTests(m interface{ Run() int }) int {
gitHomePath, cleanup, err := tempdir.OsTempDir("gitea-test").MkdirTempRandom("git-home") gitHomePath, cleanup, err := tempdir.OsTempDir("gitea-test").MkdirTempRandom("git-home")
if err != nil { if err != nil {
fatalf(1, "unable to create temp dir: %s", err.Error()) testlogger.Panicf("unable to create temp dir: %s", err.Error())
} }
defer cleanup() defer cleanup()
setting.Git.HomePath = gitHomePath setting.Git.HomePath = gitHomePath
if err = InitFull(); err != nil { if err = InitFull(); err != nil {
fatalf(1, "failed to call Init: %s", err.Error()) testlogger.Panicf("failed to call Init: %s", err.Error())
}
if exitCode := m.Run(); exitCode != 0 {
fatalf(exitCode, "run test failed, ExitCode=%d", exitCode)
} }
return m.Run()
} }

View File

@ -12,23 +12,27 @@ import (
"code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/tempdir" "code.gitea.io/gitea/modules/tempdir"
"code.gitea.io/gitea/modules/testlogger"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
func TestMain(m *testing.M) { func testMain(m *testing.M) int {
// FIXME: GIT-PACKAGE-DEPENDENCY: the dependency is not right. // FIXME: GIT-PACKAGE-DEPENDENCY: the dependency is not right.
// "setting.Git.HomePath" is initialized in "git" package but really used in "gitcmd" package // "setting.Git.HomePath" is initialized in "git" package but really used in "gitcmd" package
gitHomePath, cleanup, err := tempdir.OsTempDir("gitea-test").MkdirTempRandom("git-home") gitHomePath, cleanup, err := tempdir.OsTempDir("gitea-test").MkdirTempRandom("git-home")
if err != nil { if err != nil {
_, _ = fmt.Fprintf(os.Stderr, "unable to create temp dir: %v", err) testlogger.Panicf("failed to create temp dir: %v", err)
os.Exit(1)
} }
defer cleanup() defer cleanup()
setting.Git.HomePath = gitHomePath setting.Git.HomePath = gitHomePath
os.Exit(m.Run()) return m.Run()
}
func TestMain(m *testing.M) {
os.Exit(testMain(m))
} }
func TestRunWithContextStd(t *testing.T) { func TestRunWithContextStd(t *testing.T) {

View File

@ -13,12 +13,13 @@ import (
func TestMain(m *testing.M) { func TestMain(m *testing.M) {
// resolve repository path relative to the test directory // resolve repository path relative to the test directory
testRootDir := setting.SetupGiteaTestEnv() setting.SetupGiteaTestEnv()
giteaRoot := setting.GetGiteaTestSourceRoot()
repoPath = func(repo Repository) string { repoPath = func(repo Repository) string {
if filepath.IsAbs(repo.RelativePath()) { if filepath.IsAbs(repo.RelativePath()) {
return repo.RelativePath() // for testing purpose only return repo.RelativePath() // for testing purpose only
} }
return filepath.Join(testRootDir, "modules/git/tests/repos", repo.RelativePath()) return filepath.Join(giteaRoot, "modules/git/tests/repos", repo.RelativePath())
} }
git.RunGitTests(m) git.RunGitTests(m)
} }

View File

@ -8,7 +8,6 @@ import (
"io" "io"
"net/http" "net/http"
"net/url" "net/url"
"os"
"strings" "strings"
"testing" "testing"
"time" "time"
@ -20,10 +19,6 @@ const (
dummyToken = "10000000-aaaa-bbbb-cccc-000000000001" dummyToken = "10000000-aaaa-bbbb-cccc-000000000001"
) )
func TestMain(m *testing.M) {
os.Exit(m.Run())
}
type mockTransport struct{} type mockTransport struct{}
func (mockTransport) RoundTrip(req *http.Request) (*http.Response, error) { func (mockTransport) RoundTrip(req *http.Request) (*http.Response, error) {

View File

@ -65,7 +65,6 @@ func (o *VirtualSessionProvider) Read(sid string) (session.RawStore, error) {
return nil, fmt.Errorf("check if '%s' exist failed: %w", sid, err) return nil, fmt.Errorf("check if '%s' exist failed: %w", sid, err)
} }
kv := make(map[any]any) kv := make(map[any]any)
kv["_old_uid"] = "0"
return NewVirtualStore(o, sid, kv), nil return NewVirtualStore(o, sid, kv), nil
} }
@ -160,7 +159,7 @@ func (s *VirtualStore) Release() error {
// Now need to lock the provider // Now need to lock the provider
s.p.lock.Lock() s.p.lock.Lock()
defer s.p.lock.Unlock() defer s.p.lock.Unlock()
if oldUID, ok := s.data["_old_uid"]; (ok && (oldUID != "0" || len(s.data) > 1)) || (!ok && len(s.data) > 0) { if len(s.data) > 0 {
// Now ensure that we don't exist! // Now ensure that we don't exist!
realProvider := s.p.provider realProvider := s.p.provider

View File

@ -4,6 +4,7 @@
package setting package setting
import ( import (
"errors"
"fmt" "fmt"
"strings" "strings"
"time" "time"
@ -25,10 +26,12 @@ var (
EndlessTaskTimeout time.Duration `ini:"ENDLESS_TASK_TIMEOUT"` EndlessTaskTimeout time.Duration `ini:"ENDLESS_TASK_TIMEOUT"`
AbandonedJobTimeout time.Duration `ini:"ABANDONED_JOB_TIMEOUT"` AbandonedJobTimeout time.Duration `ini:"ABANDONED_JOB_TIMEOUT"`
SkipWorkflowStrings []string `ini:"SKIP_WORKFLOW_STRINGS"` SkipWorkflowStrings []string `ini:"SKIP_WORKFLOW_STRINGS"`
WorkflowDirs []string `ini:"WORKFLOW_DIRS"`
}{ }{
Enabled: true, Enabled: true,
DefaultActionsURL: defaultActionsURLGitHub, DefaultActionsURL: defaultActionsURLGitHub,
SkipWorkflowStrings: []string{"[skip ci]", "[ci skip]", "[no ci]", "[skip actions]", "[actions skip]"}, SkipWorkflowStrings: []string{"[skip ci]", "[ci skip]", "[no ci]", "[skip actions]", "[actions skip]"},
WorkflowDirs: []string{".gitea/workflows", ".github/workflows"},
} }
) )
@ -119,5 +122,20 @@ func loadActionsFrom(rootCfg ConfigProvider) error {
return fmt.Errorf("invalid [actions] LOG_COMPRESSION: %q", Actions.LogCompression) return fmt.Errorf("invalid [actions] LOG_COMPRESSION: %q", Actions.LogCompression)
} }
workflowDirs := make([]string, 0, len(Actions.WorkflowDirs))
for _, dir := range Actions.WorkflowDirs {
dir = strings.TrimSpace(dir)
if dir == "" {
continue
}
dir = strings.ReplaceAll(dir, `\`, `/`)
dir = strings.TrimRight(dir, "/")
workflowDirs = append(workflowDirs, dir)
}
if len(workflowDirs) == 0 {
return errors.New("[actions] WORKFLOW_DIRS must contain at least one entry")
}
Actions.WorkflowDirs = workflowDirs
return nil return nil
} }

View File

@ -97,6 +97,65 @@ STORAGE_TYPE = minio
assert.Equal(t, "actions_artifacts", filepath.Base(Actions.ArtifactStorage.Path)) assert.Equal(t, "actions_artifacts", filepath.Base(Actions.ArtifactStorage.Path))
} }
func Test_WorkflowDirs(t *testing.T) {
oldActions := Actions
defer func() {
Actions = oldActions
}()
tests := []struct {
name string
iniStr string
wantDirs []string
wantErr bool
}{
{
name: "default",
iniStr: `[actions]`,
wantDirs: []string{".gitea/workflows", ".github/workflows"},
},
{
name: "single dir",
iniStr: "[actions]\nWORKFLOW_DIRS = .github/workflows",
wantDirs: []string{".github/workflows"},
},
{
name: "custom order",
iniStr: "[actions]\nWORKFLOW_DIRS = .github/workflows,.gitea/workflows",
wantDirs: []string{".github/workflows", ".gitea/workflows"},
},
{
name: "whitespace trimming",
iniStr: "[actions]\nWORKFLOW_DIRS = .gitea/workflows , .github/workflows ",
wantDirs: []string{".gitea/workflows", ".github/workflows"},
},
{
name: "trailing slash normalization",
iniStr: "[actions]\nWORKFLOW_DIRS = .gitea/workflows/,.github/workflows/",
wantDirs: []string{".gitea/workflows", ".github/workflows"},
},
{
name: "only commas and whitespace",
iniStr: "[actions]\nWORKFLOW_DIRS = , , ,",
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
cfg, err := NewConfigProviderFromData(tt.iniStr)
require.NoError(t, err)
err = loadActionsFrom(cfg)
if tt.wantErr {
require.Error(t, err)
return
}
require.NoError(t, err)
assert.Equal(t, tt.wantDirs, Actions.WorkflowDirs)
})
}
}
func Test_getDefaultActionsURLForActions(t *testing.T) { func Test_getDefaultActionsURLForActions(t *testing.T) {
oldActions := Actions oldActions := Actions
oldAppURL := AppURL oldAppURL := AppURL

View File

@ -13,7 +13,18 @@ import (
"code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/util"
) )
func SetupGiteaTestEnv() string { var giteaTestSourceRoot *string
func GetGiteaTestSourceRoot() string {
return *giteaTestSourceRoot
}
func SetupGiteaTestEnv() {
if giteaTestSourceRoot != nil {
return // already initialized
}
IsInTesting = true
giteaRoot := os.Getenv("GITEA_TEST_ROOT") giteaRoot := os.Getenv("GITEA_TEST_ROOT")
if giteaRoot == "" { if giteaRoot == "" {
_, filename, _, _ := runtime.Caller(0) _, filename, _, _ := runtime.Caller(0)
@ -27,6 +38,7 @@ func SetupGiteaTestEnv() string {
appWorkPathBuiltin = giteaRoot appWorkPathBuiltin = giteaRoot
AppWorkPath = giteaRoot AppWorkPath = giteaRoot
AppPath = filepath.Join(giteaRoot, "gitea") + util.Iif(IsWindows, ".exe", "") AppPath = filepath.Join(giteaRoot, "gitea") + util.Iif(IsWindows, ".exe", "")
StaticRootPath = giteaRoot // need to load assets (options, public) from the source code directory for testing
// giteaConf (GITEA_CONF) must be relative because it is used in the git hooks as "$GITEA_ROOT/$GITEA_CONF" // giteaConf (GITEA_CONF) must be relative because it is used in the git hooks as "$GITEA_ROOT/$GITEA_CONF"
giteaConf := os.Getenv("GITEA_TEST_CONF") giteaConf := os.Getenv("GITEA_TEST_CONF")
@ -56,6 +68,5 @@ func SetupGiteaTestEnv() string {
// TODO: some git repo hooks (test fixtures) still use these env variables, need to be refactored in the future // TODO: some git repo hooks (test fixtures) still use these env variables, need to be refactored in the future
_ = os.Setenv("GITEA_ROOT", giteaRoot) _ = os.Setenv("GITEA_ROOT", giteaRoot)
_ = os.Setenv("GITEA_CONF", giteaConf) // test fixture git hooks use "$GITEA_ROOT/$GITEA_CONF" in their scripts _ = os.Setenv("GITEA_CONF", giteaConf) // test fixture git hooks use "$GITEA_ROOT/$GITEA_CONF" in their scripts
giteaTestSourceRoot = &giteaRoot
return giteaRoot
} }

View File

@ -173,7 +173,7 @@ func Init() {
log.RegisterEventWriter("test", newTestLoggerWriter) log.RegisterEventWriter("test", newTestLoggerWriter)
} }
func Fatalf(format string, args ...any) { func Panicf(format string, args ...any) {
Printf(format+"\n", args...) // don't call os.Exit, otherwise the "defer" functions won't be executed
os.Exit(1) panic(fmt.Sprintf(format, args...))
} }

View File

@ -32,11 +32,7 @@ func TestMain(m *testing.M) {
// setup // setup
translation.InitLocales(context.Background()) translation.InitLocales(context.Background())
BaseDate = time.Date(2000, time.January, 1, 0, 0, 0, 0, time.UTC) BaseDate = time.Date(2000, time.January, 1, 0, 0, 0, 0, time.UTC)
os.Exit(m.Run())
// run the tests
retVal := m.Run()
os.Exit(retVal)
} }
func TestTimeSincePro(t *testing.T) { func TestTimeSincePro(t *testing.T) {

View File

@ -98,6 +98,20 @@ func (h HookEventType) IsPullRequest() bool {
return h.Event() == "pull_request" return h.Event() == "pull_request"
} }
// IsPullRequestReview returns true for pull request review events
// (approved, rejected, comment). These events use the same PullRequestPayload
// as regular pull_request events.
func (h HookEventType) IsPullRequestReview() bool {
switch h {
case HookEventPullRequestReviewApproved,
HookEventPullRequestReviewRejected,
HookEventPullRequestReviewComment:
return true
default:
return false
}
}
// HookType is the type of a webhook // HookType is the type of a webhook
type HookType = string type HookType = string

View File

@ -1,6 +1,6 @@
{ {
"type": "module", "type": "module",
"packageManager": "pnpm@10.29.2", "packageManager": "pnpm@10.30.0",
"engines": { "engines": {
"node": ">= 22.6.0", "node": ">= 22.6.0",
"pnpm": ">= 10.0.0" "pnpm": ">= 10.0.0"
@ -16,20 +16,20 @@
"@github/text-expander-element": "2.9.4", "@github/text-expander-element": "2.9.4",
"@mcaptcha/vanilla-glue": "0.1.0-alpha-3", "@mcaptcha/vanilla-glue": "0.1.0-alpha-3",
"@mermaid-js/layout-elk": "0.2.0", "@mermaid-js/layout-elk": "0.2.0",
"@primer/octicons": "19.21.2", "@primer/octicons": "19.22.0",
"@resvg/resvg-wasm": "2.6.2", "@resvg/resvg-wasm": "2.6.2",
"@silverwind/vue3-calendar-heatmap": "2.1.1", "@silverwind/vue3-calendar-heatmap": "2.1.1",
"@techknowlogick/license-checker-webpack-plugin": "0.3.0", "@techknowlogick/license-checker-webpack-plugin": "0.3.0",
"add-asset-webpack-plugin": "3.1.1", "add-asset-webpack-plugin": "3.1.1",
"ansi_up": "6.0.6", "ansi_up": "6.0.6",
"asciinema-player": "3.14.0", "asciinema-player": "3.14.15",
"chart.js": "4.5.1", "chart.js": "4.5.1",
"chartjs-adapter-dayjs-4": "1.0.4", "chartjs-adapter-dayjs-4": "1.0.4",
"chartjs-plugin-zoom": "2.2.0", "chartjs-plugin-zoom": "2.2.0",
"clippie": "4.1.10", "clippie": "4.1.10",
"compare-versions": "6.1.1", "compare-versions": "6.1.1",
"cropperjs": "1.6.2", "cropperjs": "1.6.2",
"css-loader": "7.1.3", "css-loader": "7.1.4",
"dayjs": "1.11.19", "dayjs": "1.11.19",
"dropzone": "6.0.0-beta.2", "dropzone": "6.0.0-beta.2",
"easymde": "2.20.0", "easymde": "2.20.0",
@ -39,7 +39,7 @@
"jquery": "4.0.0", "jquery": "4.0.0",
"js-yaml": "4.1.1", "js-yaml": "4.1.1",
"katex": "0.16.28", "katex": "0.16.28",
"mermaid": "11.12.2", "mermaid": "11.12.3",
"mini-css-extract-plugin": "2.10.0", "mini-css-extract-plugin": "2.10.0",
"monaco-editor": "0.55.1", "monaco-editor": "0.55.1",
"monaco-editor-webpack-plugin": "7.1.1", "monaco-editor-webpack-plugin": "7.1.1",
@ -47,12 +47,12 @@
"pdfobject": "2.3.1", "pdfobject": "2.3.1",
"perfect-debounce": "2.1.0", "perfect-debounce": "2.1.0",
"postcss": "8.5.6", "postcss": "8.5.6",
"postcss-loader": "8.2.0", "postcss-loader": "8.2.1",
"sortablejs": "1.15.6", "sortablejs": "1.15.7",
"swagger-ui-dist": "5.31.0", "swagger-ui-dist": "5.31.1",
"tailwindcss": "3.4.17", "tailwindcss": "3.4.17",
"throttle-debounce": "5.0.2", "throttle-debounce": "5.0.2",
"tinycolor2": "1.6.0", "colord": "2.9.3",
"tippy.js": "6.3.7", "tippy.js": "6.3.7",
"toastify-js": "1.12.0", "toastify-js": "1.12.0",
"tributejs": "5.1.3", "tributejs": "5.1.3",
@ -62,7 +62,7 @@
"vue-bar-graph": "2.2.0", "vue-bar-graph": "2.2.0",
"vue-chartjs": "5.3.3", "vue-chartjs": "5.3.3",
"vue-loader": "17.4.2", "vue-loader": "17.4.2",
"webpack": "5.105.0", "webpack": "5.105.2",
"webpack-cli": "6.0.1", "webpack-cli": "6.0.1",
"wrap-ansi": "9.0.2" "wrap-ansi": "9.0.2"
}, },
@ -77,15 +77,15 @@
"@types/jquery": "3.5.33", "@types/jquery": "3.5.33",
"@types/js-yaml": "4.0.9", "@types/js-yaml": "4.0.9",
"@types/katex": "0.16.8", "@types/katex": "0.16.8",
"@types/node": "25.2.3",
"@types/pdfobject": "2.2.5", "@types/pdfobject": "2.2.5",
"@types/sortablejs": "1.15.9", "@types/sortablejs": "1.15.9",
"@types/swagger-ui-dist": "3.30.6", "@types/swagger-ui-dist": "3.30.6",
"@types/throttle-debounce": "5.0.2", "@types/throttle-debounce": "5.0.2",
"@types/tinycolor2": "1.4.6",
"@types/toastify-js": "1.12.4", "@types/toastify-js": "1.12.4",
"@typescript-eslint/parser": "8.55.0", "@typescript-eslint/parser": "8.56.0",
"@vitejs/plugin-vue": "6.0.4", "@vitejs/plugin-vue": "6.0.4",
"@vitest/eslint-plugin": "1.6.7", "@vitest/eslint-plugin": "1.6.9",
"eslint": "9.39.2", "eslint": "9.39.2",
"eslint-import-resolver-typescript": "4.4.4", "eslint-import-resolver-typescript": "4.4.4",
"eslint-plugin-array-func": "5.1.0", "eslint-plugin-array-func": "5.1.0",
@ -93,35 +93,32 @@
"eslint-plugin-import-x": "4.16.1", "eslint-plugin-import-x": "4.16.1",
"eslint-plugin-playwright": "2.5.1", "eslint-plugin-playwright": "2.5.1",
"eslint-plugin-regexp": "3.0.0", "eslint-plugin-regexp": "3.0.0",
"eslint-plugin-sonarjs": "3.0.6", "eslint-plugin-sonarjs": "3.0.7",
"eslint-plugin-unicorn": "62.0.0", "eslint-plugin-unicorn": "63.0.0",
"eslint-plugin-vue": "10.7.0", "eslint-plugin-vue": "10.8.0",
"eslint-plugin-vue-scoped-css": "2.12.0", "eslint-plugin-vue-scoped-css": "2.12.0",
"eslint-plugin-wc": "3.1.0", "eslint-plugin-wc": "3.1.0",
"globals": "17.3.0", "globals": "17.3.0",
"happy-dom": "20.6.0", "happy-dom": "20.6.1",
"jiti": "2.6.1", "jiti": "2.6.1",
"markdownlint-cli": "0.47.0", "markdownlint-cli": "0.47.0",
"material-icon-theme": "5.31.0", "material-icon-theme": "5.31.0",
"nolyfill": "1.0.44", "nolyfill": "1.0.44",
"postcss-html": "1.8.1", "postcss-html": "1.8.1",
"spectral-cli-bundle": "1.0.4", "spectral-cli-bundle": "1.0.7",
"stylelint": "17.1.1", "stylelint": "17.3.0",
"stylelint-config-recommended": "18.0.0", "stylelint-config-recommended": "18.0.0",
"stylelint-declaration-block-no-ignored-properties": "3.0.0", "stylelint-declaration-block-no-ignored-properties": "3.0.0",
"stylelint-declaration-strict-value": "1.10.11", "stylelint-declaration-strict-value": "1.10.11",
"stylelint-value-no-unknown-custom-properties": "6.1.1", "stylelint-value-no-unknown-custom-properties": "6.1.1",
"svgo": "4.0.0", "svgo": "4.0.0",
"typescript": "5.9.3", "typescript": "5.9.3",
"typescript-eslint": "8.55.0", "typescript-eslint": "8.56.0",
"updates": "17.4.0", "updates": "17.5.7",
"vite-string-plugin": "2.0.1", "vite-string-plugin": "2.0.1",
"vitest": "4.0.18", "vitest": "4.0.18",
"vue-tsc": "3.2.4" "vue-tsc": "3.2.4"
}, },
"browserslist": [
"defaults"
],
"pnpm": { "pnpm": {
"overrides": { "overrides": {
"array-includes": "npm:@nolyfill/array-includes@^1", "array-includes": "npm:@nolyfill/array-includes@^1",

826
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" class="svg octicon-book-locked" width="16" height="16" aria-hidden="true"><path d="M12 6a3 3 0 0 1 3 3v1.168c.591.281 1 .884 1 1.582v2.5A1.75 1.75 0 0 1 14.25 16h-4.5A1.75 1.75 0 0 1 8 14.25v-2.5c0-.698.409-1.301 1-1.582V9a3 3 0 0 1 3-3m0 1.5A1.5 1.5 0 0 0 10.5 9v1h3V9A1.5 1.5 0 0 0 12 7.5"/><path d="M5.003 1c1.227 0 2.317.59 3.001 1.501A3.75 3.75 0 0 1 11.005 1h4.245a.75.75 0 0 1 .75.75V5.5a.75.75 0 0 1-1.5 0v-3h-3.495c-1.21 0-2.204.956-2.255 2.153V6.5a.75.75 0 0 1-1.5 0V4.69A2.25 2.25 0 0 0 5.003 2.5H1.5v9h3.758c.612 0 1.208.15 1.74.429l.005.001a.75.75 0 0 1-.705 1.324l-.001-.001v.002A2.25 2.25 0 0 0 5.258 13H.75a.75.75 0 0 1-.75-.75V1.75A.75.75 0 0 1 .75 1z"/></svg>

After

Width:  |  Height:  |  Size: 737 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" class="svg octicon-comment-locked" width="16" height="16" aria-hidden="true"><path d="M12 6a3 3 0 0 1 3 3v1.168c.591.281 1 .884 1 1.582v2.5A1.75 1.75 0 0 1 14.25 16h-4.5A1.75 1.75 0 0 1 8 14.25v-2.5c0-.698.409-1.301 1-1.582V9a3 3 0 0 1 3-3m0 1.5A1.5 1.5 0 0 0 10.5 9v1h3V9A1.5 1.5 0 0 0 12 7.5"/><path d="M10.25 1A1.75 1.75 0 0 1 12 2.75v1.5a.75.75 0 0 1-1.5 0v-1.5a.25.25 0 0 0-.25-.25h-8.5a.25.25 0 0 0-.25.25v5.5a.25.25 0 0 0 .25.25h1c.199 0 .39.079.53.22.141.14.22.331.22.53v2.19l2.72-2.72a.75.75 0 0 1 .53-.22h.35a.75.75 0 0 1 0 1.5h-.039l-2.574 2.573A1.457 1.457 0 0 1 2 11.543V10h-.25A1.75 1.75 0 0 1 0 8.25v-5.5A1.75 1.75 0 0 1 1.75 1zm4 2c.966 0 1.75.784 1.75 1.75v.75q0-.091-.006-.164A.75.75 0 0 1 14.5 5.25v-.5a.25.25 0 0 0-.25-.25h-.5a.75.75 0 0 1-.53-.22.747.747 0 0 1 0-1.06.75.75 0 0 1 .53-.22z"/></svg>

After

Width:  |  Height:  |  Size: 878 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" class="svg octicon-git-pull-request-locked" width="16" height="16" aria-hidden="true"><path d="M12 6a3 3 0 0 1 3 3v1.169c.591.281 1 .883 1 1.581v2.5A1.75 1.75 0 0 1 14.25 16h-4.5A1.75 1.75 0 0 1 8 14.25v-2.5c0-.698.409-1.3 1-1.581V9a3 3 0 0 1 3-3m0 1.5A1.5 1.5 0 0 0 10.5 9v1h3V9A1.5 1.5 0 0 0 12 7.5M3.25 1A2.25 2.25 0 0 1 4 5.372v5.257a2.25 2.25 0 1 1-1.5 0V5.372A2.252 2.252 0 0 1 3.25 1m0 1.5a.75.75 0 1 0 0 1.5.75.75 0 0 0 0-1.5m0 9.5a.75.75 0 1 0 0 1.5.75.75 0 0 0 0-1.5M10 .854a.25.25 0 0 0-.427-.177L7.177 3.073a.25.25 0 0 0 0 .355l2.396 2.395A.25.25 0 0 0 10 5.646z"/><path d="M11.997 2.708A2.5 2.5 0 0 0 11 2.5h-1V4h1c.5 0 .891 0 .956.597a.735.735 0 0 0 .746.674.75.75 0 0 0 .746-.674c0-.097 0-.147-.066-.356 0 0-.041-.122-.073-.198a2.2 2.2 0 0 0-.209-.393 2 2 0 0 0-.327-.412l-.039-.036a3 3 0 0 0-.127-.114q-.014-.014-.03-.026a2 2 0 0 0-.172-.129l-.035-.023a3 3 0 0 0-.156-.095l-.047-.025a3 3 0 0 0-.17-.082"/></svg>

After

Width:  |  Height:  |  Size: 987 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" class="svg octicon-issue-locked" width="16" height="16" aria-hidden="true"><path d="M12.001 6a3 3 0 0 1 3 3v1.168c.591.281 1 .884 1 1.582v2.5a1.75 1.75 0 0 1-1.75 1.75h-4.5a1.75 1.75 0 0 1-1.75-1.75v-2.5c0-.698.409-1.301 1-1.582V9a3 3 0 0 1 3-3m0 1.5a1.5 1.5 0 0 0-1.5 1.5v1h3V9a1.5 1.5 0 0 0-1.5-1.5"/><path d="M5.095.546a8 8 0 0 1 3.847-.49l.259.035a8 8 0 0 1 3.58 1.494l.207.16a8 8 0 0 1 2.148 2.639c.187.369-.005.807-.391.959s-.817-.04-1.013-.406a6.5 6.5 0 0 0-1.242-1.635l-.11-.105-.052-.046a6 6 0 0 0-.226-.193l-.049-.04-.042-.031a6 6 0 0 0-.249-.187l-.082-.057a6 6 0 0 0-.683-.411l-.028-.014a6.5 6.5 0 0 0-1.146-.458l-.039-.011a7 7 0 0 0-.376-.095l-.018-.005a6 6 0 0 0-.409-.075l-.003-.001h-.003l-.015-.002a8 8 0 0 0-.479-.051 6 6 0 0 0-.26-.015l-.155-.004L8 1.5q-.084.001-.168.004-.081 0-.162.004a6 6 0 0 0-.37.029l-.069.009q-.165.02-.325.047l-.079.014a7 7 0 0 0-.383.082q-.405.1-.788.249l-.016.005a6.6 6.6 0 0 0-1.096.553l-.083.053a7 7 0 0 0-.288.197l-.022.017a6 6 0 0 0-.609.509l-.064.061q-.123.119-.238.243l-.038.039a7 7 0 0 0-.254.296l-.015.019q-.017.021-.033.044a6 6 0 0 0-.188.249q-.028.037-.054.076a6.5 6.5 0 0 0-.89 1.854l-.014.048a7 7 0 0 0-.084.327l-.02.089a6.4 6.4 0 0 0-.145 1.159l-.003.129L1.5 8q.001.099.005.196l.003.102a7 7 0 0 0 .034.434q.022.184.052.366l.007.034c.148.84.456 1.625.893 2.321l.034.052q.087.135.18.266l.054.076q.115.157.239.306l.024.029q.113.133.232.259l.073.077q.095.098.195.193.043.043.088.084a7 7 0 0 0 .299.259q.093.074.187.145l.072.052.146.104a6.5 6.5 0 0 0 1.929.904c.399.112.68.492.615.901s-.45.691-.851.588a8 8 0 0 1-3.041-1.528l-.202-.169A8.01 8.01 0 0 1 .059 7.03l.036-.259a8 8 0 0 1 1.507-3.574l.161-.207A8 8 0 0 1 5.095.546"/><path d="M8.001 6.5c.259 0 .511.068.733.192A4 4 0 0 0 8.001 9v.5a1.503 1.503 0 0 1-1.5-1.5 1.503 1.503 0 0 1 1.5-1.5"/></svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@ -27,6 +27,7 @@ import (
"code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/storage" "code.gitea.io/gitea/modules/storage"
"code.gitea.io/gitea/modules/templates" "code.gitea.io/gitea/modules/templates"
"code.gitea.io/gitea/modules/translation"
"code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/web" "code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/routers/common" "code.gitea.io/gitea/routers/common"
@ -302,7 +303,7 @@ func ViewPost(ctx *context_module.Context) {
resp.State.CurrentJob.Steps = make([]*ViewJobStep, 0) // marshal to '[]' instead fo 'null' in json resp.State.CurrentJob.Steps = make([]*ViewJobStep, 0) // marshal to '[]' instead fo 'null' in json
resp.Logs.StepsLog = make([]*ViewStepLog, 0) // marshal to '[]' instead fo 'null' in json resp.Logs.StepsLog = make([]*ViewStepLog, 0) // marshal to '[]' instead fo 'null' in json
if task != nil { if task != nil {
steps, logs, err := convertToViewModel(ctx, req.LogCursors, task) steps, logs, err := convertToViewModel(ctx, ctx.Locale, req.LogCursors, task)
if err != nil { if err != nil {
ctx.ServerError("convertToViewModel", err) ctx.ServerError("convertToViewModel", err)
return return
@ -314,7 +315,7 @@ func ViewPost(ctx *context_module.Context) {
ctx.JSON(http.StatusOK, resp) ctx.JSON(http.StatusOK, resp)
} }
func convertToViewModel(ctx *context_module.Context, cursors []LogCursor, task *actions_model.ActionTask) ([]*ViewJobStep, []*ViewStepLog, error) { func convertToViewModel(ctx context.Context, locale translation.Locale, cursors []LogCursor, task *actions_model.ActionTask) ([]*ViewJobStep, []*ViewStepLog, error) {
var viewJobs []*ViewJobStep var viewJobs []*ViewJobStep
var logs []*ViewStepLog var logs []*ViewStepLog
@ -344,7 +345,7 @@ func convertToViewModel(ctx *context_module.Context, cursors []LogCursor, task *
Lines: []*ViewStepLogLine{ Lines: []*ViewStepLogLine{
{ {
Index: 1, Index: 1,
Message: ctx.Locale.TrString("actions.runs.expire_log_message"), Message: locale.TrString("actions.runs.expire_log_message"),
// Timestamp doesn't mean anything when the log is expired. // Timestamp doesn't mean anything when the log is expired.
// Set it to the task's updated time since it's probably the time when the log has expired. // Set it to the task's updated time since it's probably the time when the log has expired.
Timestamp: float64(task.Updated.AsTime().UnixNano()) / float64(time.Second), Timestamp: float64(task.Updated.AsTime().UnixNano()) / float64(time.Second),

View File

@ -0,0 +1,47 @@
// Copyright 2025 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package actions
import (
"testing"
actions_model "code.gitea.io/gitea/models/actions"
"code.gitea.io/gitea/modules/timeutil"
"code.gitea.io/gitea/modules/translation"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestConvertToViewModel(t *testing.T) {
task := &actions_model.ActionTask{
Status: actions_model.StatusSuccess,
Steps: []*actions_model.ActionTaskStep{
{Name: "Run step-name", Index: 0, Status: actions_model.StatusSuccess, LogLength: 1, Started: timeutil.TimeStamp(1), Stopped: timeutil.TimeStamp(5)},
},
Stopped: timeutil.TimeStamp(20),
}
viewJobSteps, _, err := convertToViewModel(t.Context(), translation.MockLocale{}, nil, task)
require.NoError(t, err)
expectedViewJobs := []*ViewJobStep{
{
Summary: "Set up job",
Duration: "0s",
Status: "success",
},
{
Summary: "Run step-name",
Duration: "4s",
Status: "success",
},
{
Summary: "Complete job",
Duration: "15s",
Status: "success",
},
}
assert.Equal(t, expectedViewJobs, viewJobSteps)
}

View File

@ -7,7 +7,6 @@ import (
gocontext "context" gocontext "context"
"encoding/csv" "encoding/csv"
"errors" "errors"
"fmt"
"io" "io"
"net/http" "net/http"
"net/url" "net/url"
@ -426,6 +425,36 @@ func ParseCompareInfo(ctx *context.Context) *git_service.CompareInfo {
return compareInfo return compareInfo
} }
func prepareNewPullRequestTitleContent(ci *git_service.CompareInfo, commits []*git_model.SignCommitWithStatuses) (title, content string) {
title = ci.HeadRef.ShortName()
if len(commits) > 0 {
// the "commits" are from "ShowPrettyFormatLogToList", which is ordered from newest to oldest, here take the oldest one
c := commits[len(commits)-1]
title = strings.TrimSpace(c.UserCommit.Summary())
}
if len(commits) == 1 {
// FIXME: GIT-COMMIT-MESSAGE-ENCODING: try to convert the encoding for commit message explicitly, ideally it should be done by a git commit struct method
c := commits[0]
_, content, _ = strings.Cut(strings.TrimSpace(c.UserCommit.CommitMessage), "\n")
content = strings.TrimSpace(content)
content = string(charset.ToUTF8([]byte(content), charset.ConvertOpts{}))
}
var titleTrailer string
// TODO: 255 doesn't seem to be a good limit for title, just keep the old behavior
title, titleTrailer = util.EllipsisDisplayStringX(title, 255)
if titleTrailer != "" {
if content != "" {
content = titleTrailer + "\n\n" + content
} else {
content = titleTrailer + "\n"
}
}
return title, content
}
// PrepareCompareDiff renders compare diff page // PrepareCompareDiff renders compare diff page
func PrepareCompareDiff( func PrepareCompareDiff(
ctx *context.Context, ctx *context.Context,
@ -539,30 +568,7 @@ func PrepareCompareDiff(
ctx.Data["Commits"] = commits ctx.Data["Commits"] = commits
ctx.Data["CommitCount"] = len(commits) ctx.Data["CommitCount"] = len(commits)
title := ci.HeadRef.ShortName() ctx.Data["title"], ctx.Data["content"] = prepareNewPullRequestTitleContent(ci, commits)
if len(commits) == 1 {
c := commits[0]
title = strings.TrimSpace(c.UserCommit.Summary())
body := strings.Split(strings.TrimSpace(c.UserCommit.Message()), "\n")
if len(body) > 1 {
ctx.Data["content"] = strings.Join(body[1:], "\n")
}
}
if len(title) > 255 {
var trailer string
title, trailer = util.EllipsisDisplayStringX(title, 255)
if len(trailer) > 0 {
if ctx.Data["content"] != nil {
ctx.Data["content"] = fmt.Sprintf("%s\n\n%s", trailer, ctx.Data["content"])
} else {
ctx.Data["content"] = trailer + "\n"
}
}
}
ctx.Data["title"] = title
ctx.Data["Username"] = ci.HeadRepo.OwnerName ctx.Data["Username"] = ci.HeadRepo.OwnerName
ctx.Data["Reponame"] = ci.HeadRepo.Name ctx.Data["Reponame"] = ci.HeadRepo.Name

View File

@ -4,9 +4,16 @@
package repo package repo
import ( import (
"strings"
"testing" "testing"
"unicode/utf8"
asymkey_model "code.gitea.io/gitea/models/asymkey"
git_model "code.gitea.io/gitea/models/git"
issues_model "code.gitea.io/gitea/models/issues" issues_model "code.gitea.io/gitea/models/issues"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/git"
git_service "code.gitea.io/gitea/services/git"
"code.gitea.io/gitea/services/gitdiff" "code.gitea.io/gitea/services/gitdiff"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
@ -38,3 +45,47 @@ func TestAttachCommentsToLines(t *testing.T) {
assert.Equal(t, int64(300), section.Lines[1].Comments[0].ID) assert.Equal(t, int64(300), section.Lines[1].Comments[0].ID)
assert.Equal(t, int64(301), section.Lines[1].Comments[1].ID) assert.Equal(t, int64(301), section.Lines[1].Comments[1].ID)
} }
func TestNewPullRequestTitleContent(t *testing.T) {
ci := &git_service.CompareInfo{HeadRef: "refs/heads/head-branch"}
mockCommit := func(msg string) *git_model.SignCommitWithStatuses {
return &git_model.SignCommitWithStatuses{
SignCommit: &asymkey_model.SignCommit{
UserCommit: &user_model.UserCommit{
Commit: &git.Commit{
CommitMessage: msg,
},
},
},
}
}
title, content := prepareNewPullRequestTitleContent(ci, nil)
assert.Equal(t, "head-branch", title)
assert.Empty(t, content)
title, content = prepareNewPullRequestTitleContent(ci, []*git_model.SignCommitWithStatuses{mockCommit("title-only")})
assert.Equal(t, "title-only", title)
assert.Empty(t, content)
title, content = prepareNewPullRequestTitleContent(ci, []*git_model.SignCommitWithStatuses{mockCommit("title-" + strings.Repeat("a", 255))})
assert.Equal(t, "title-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa…", title)
assert.Equal(t, "…aaaaaaaaa\n", content)
title, content = prepareNewPullRequestTitleContent(ci, []*git_model.SignCommitWithStatuses{mockCommit("title\nbody")})
assert.Equal(t, "title", title)
assert.Equal(t, "body", content)
title, content = prepareNewPullRequestTitleContent(ci, []*git_model.SignCommitWithStatuses{mockCommit("a\xf0\xf0\xf0\nb\xf0\xf0\xf0")})
assert.Equal(t, "a?", title) // FIXME: GIT-COMMIT-MESSAGE-ENCODING: "title" doesn't use the same charset converting logic as "content"
assert.Equal(t, "b"+string(utf8.RuneError)+string(utf8.RuneError), content)
title, content = prepareNewPullRequestTitleContent(ci, []*git_model.SignCommitWithStatuses{
// ordered from newest to oldest
mockCommit("title2\nbody2"),
mockCommit("title1\nbody1"),
})
assert.Equal(t, "title1", title)
assert.Empty(t, content)
}

View File

@ -0,0 +1,66 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package user
import (
"net/http"
"net/url"
activities_model "code.gitea.io/gitea/models/activities"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/services/context"
)
func prepareHeatmapURL(ctx *context.Context) {
ctx.Data["EnableHeatmap"] = setting.Service.EnableUserHeatmap
if !setting.Service.EnableUserHeatmap {
return
}
if ctx.Org.Organization == nil {
// for individual user
ctx.Data["HeatmapURL"] = ctx.Doer.HomeLink() + "/-/heatmap"
return
}
// for org or team
heatmapURL := ctx.Org.Organization.OrganisationLink() + "/dashboard/-/heatmap"
if ctx.Org.Team != nil {
heatmapURL += "/" + url.PathEscape(ctx.Org.Team.LowerName)
}
ctx.Data["HeatmapURL"] = heatmapURL
}
func writeHeatmapJSON(ctx *context.Context, hdata []*activities_model.UserHeatmapData) {
data := make([][2]int64, len(hdata))
var total int64
for i, v := range hdata {
data[i] = [2]int64{int64(v.Timestamp), v.Contributions}
total += v.Contributions
}
ctx.JSON(http.StatusOK, map[string]any{
"heatmapData": data,
"totalContributions": total,
})
}
// DashboardHeatmap returns heatmap data as JSON, for the individual user, organization or team dashboard.
func DashboardHeatmap(ctx *context.Context) {
if !setting.Service.EnableUserHeatmap {
ctx.NotFound(nil)
return
}
var data []*activities_model.UserHeatmapData
var err error
if ctx.Org.Organization == nil {
data, err = activities_model.GetUserHeatmapDataByUser(ctx, ctx.ContextUser, ctx.Doer)
} else {
data, err = activities_model.GetUserHeatmapDataByOrgTeam(ctx, ctx.Org.Organization, ctx.Org.Team, ctx.Doer)
}
if err != nil {
ctx.ServerError("GetUserHeatmapData", err)
return
}
writeHeatmapJSON(ctx, data)
}

View File

@ -54,8 +54,8 @@ const (
tplProfile templates.TplName = "user/profile" tplProfile templates.TplName = "user/profile"
) )
// getDashboardContextUser finds out which context user dashboard is being viewed as . // prepareDashboardContextUserOrgTeams finds out which context user dashboard is being viewed as .
func getDashboardContextUser(ctx *context.Context) *user_model.User { func prepareDashboardContextUserOrgTeams(ctx *context.Context) *user_model.User {
ctxUser := ctx.Doer ctxUser := ctx.Doer
orgName := ctx.PathParam("org") orgName := ctx.PathParam("org")
if len(orgName) > 0 { if len(orgName) > 0 {
@ -76,7 +76,7 @@ func getDashboardContextUser(ctx *context.Context) *user_model.User {
// Dashboard render the dashboard page // Dashboard render the dashboard page
func Dashboard(ctx *context.Context) { func Dashboard(ctx *context.Context) {
ctxUser := getDashboardContextUser(ctx) ctxUser := prepareDashboardContextUserOrgTeams(ctx)
if ctx.Written() { if ctx.Written() {
return return
} }
@ -109,15 +109,7 @@ func Dashboard(ctx *context.Context) {
"uid": uid, "uid": uid,
} }
if setting.Service.EnableUserHeatmap { prepareHeatmapURL(ctx)
data, err := activities_model.GetUserHeatmapDataByUserTeam(ctx, ctxUser, ctx.Org.Team, ctx.Doer)
if err != nil {
ctx.ServerError("GetUserHeatmapDataByUserTeam", err)
return
}
ctx.Data["HeatmapData"] = data
ctx.Data["HeatmapTotalContributions"] = activities_model.GetTotalContributionsInHeatmap(data)
}
feeds, count, err := feed_service.GetFeedsForDashboard(ctx, activities_model.GetFeedsOptions{ feeds, count, err := feed_service.GetFeedsForDashboard(ctx, activities_model.GetFeedsOptions{
RequestedUser: ctxUser, RequestedUser: ctxUser,
@ -156,7 +148,7 @@ func Milestones(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("milestones") ctx.Data["Title"] = ctx.Tr("milestones")
ctx.Data["PageIsMilestonesDashboard"] = true ctx.Data["PageIsMilestonesDashboard"] = true
ctxUser := getDashboardContextUser(ctx) ctxUser := prepareDashboardContextUserOrgTeams(ctx)
if ctx.Written() { if ctx.Written() {
return return
} }
@ -371,7 +363,7 @@ func buildIssueOverview(ctx *context.Context, unitType unit.Type) {
// Return with NotFound or ServerError if unsuccessful. // Return with NotFound or ServerError if unsuccessful.
// ---------------------------------------------------- // ----------------------------------------------------
ctxUser := getDashboardContextUser(ctx) ctxUser := prepareDashboardContextUserOrgTeams(ctx)
if ctx.Written() { if ctx.Written() {
return return
} }

View File

@ -103,6 +103,7 @@ func prepareUserProfileTabData(ctx *context.Context, profileDbRepo *repo_model.R
repos []*repo_model.Repository repos []*repo_model.Repository
count int64 count int64
total int total int
curRows int
orderBy db.SearchOrderBy orderBy db.SearchOrderBy
) )
@ -161,21 +162,15 @@ func prepareUserProfileTabData(ctx *context.Context, profileDbRepo *repo_model.R
ctx.Data["Cards"] = following ctx.Data["Cards"] = following
total = int(numFollowing) total = int(numFollowing)
case "activity": case "activity":
// prepare heatmap data if setting.Service.EnableUserHeatmap && activities_model.ActivityReadable(ctx.ContextUser, ctx.Doer) {
if setting.Service.EnableUserHeatmap { ctx.Data["EnableHeatmap"] = true
data, err := activities_model.GetUserHeatmapDataByUser(ctx, ctx.ContextUser, ctx.Doer) ctx.Data["HeatmapURL"] = ctx.ContextUser.HomeLink() + "/-/heatmap"
if err != nil {
ctx.ServerError("GetUserHeatmapDataByUser", err)
return
}
ctx.Data["HeatmapData"] = data
ctx.Data["HeatmapTotalContributions"] = activities_model.GetTotalContributionsInHeatmap(data)
} }
date := ctx.FormString("date") date := ctx.FormString("date")
pagingNum = setting.UI.FeedPagingNum pagingNum = setting.UI.FeedPagingNum
showPrivate := ctx.IsSigned && (ctx.Doer.IsAdmin || ctx.Doer.ID == ctx.ContextUser.ID) showPrivate := ctx.IsSigned && (ctx.Doer.IsAdmin || ctx.Doer.ID == ctx.ContextUser.ID)
items, count, err := feed_service.GetFeeds(ctx, activities_model.GetFeedsOptions{ items, feedCount, err := feed_service.GetFeedsForDashboard(ctx, activities_model.GetFeedsOptions{
RequestedUser: ctx.ContextUser, RequestedUser: ctx.ContextUser,
Actor: ctx.Doer, Actor: ctx.Doer,
IncludePrivate: showPrivate, IncludePrivate: showPrivate,
@ -193,8 +188,8 @@ func prepareUserProfileTabData(ctx *context.Context, profileDbRepo *repo_model.R
} }
ctx.Data["Feeds"] = items ctx.Data["Feeds"] = items
ctx.Data["Date"] = date ctx.Data["Date"] = date
curRows = len(items)
total = int(count) total = feedCount
case "stars": case "stars":
ctx.Data["PageIsProfileStarList"] = true ctx.Data["PageIsProfileStarList"] = true
ctx.Data["ShowRepoOwnerOnList"] = true ctx.Data["ShowRepoOwnerOnList"] = true
@ -316,6 +311,9 @@ func prepareUserProfileTabData(ctx *context.Context, profileDbRepo *repo_model.R
} }
pager := context.NewPagination(total, pagingNum, page, 5) pager := context.NewPagination(total, pagingNum, page, 5)
if tab == "activity" {
pager.WithCurRows(curRows)
}
pager.AddParamFromRequest(ctx.Req) pager.AddParamFromRequest(ctx.Req)
ctx.Data["Page"] = pager ctx.Data["Page"] = pager
} }

View File

@ -888,6 +888,8 @@ func registerWebRoutes(m *web.Router) {
m.Group("/{org}", func() { m.Group("/{org}", func() {
m.Get("/dashboard", user.Dashboard) m.Get("/dashboard", user.Dashboard)
m.Get("/dashboard/{team}", user.Dashboard) m.Get("/dashboard/{team}", user.Dashboard)
m.Get("/dashboard/-/heatmap", user.DashboardHeatmap)
m.Get("/dashboard/-/heatmap/{team}", user.DashboardHeatmap)
m.Get("/issues", user.Issues) m.Get("/issues", user.Issues)
m.Get("/issues/{team}", user.Issues) m.Get("/issues/{team}", user.Issues)
m.Get("/pulls", user.Pulls) m.Get("/pulls", user.Pulls)
@ -1024,6 +1026,7 @@ func registerWebRoutes(m *web.Router) {
} }
m.Get("/repositories", org.Repositories) m.Get("/repositories", org.Repositories)
m.Get("/heatmap", user.DashboardHeatmap)
m.Group("/projects", func() { m.Group("/projects", func() {
m.Group("", func() { m.Group("", func() {

View File

@ -115,6 +115,21 @@ func getCommitStatusEventNameAndCommitID(run *actions_model.ActionRun) (event, c
return "", "", errors.New("head of pull request is missing in event payload") return "", "", errors.New("head of pull request is missing in event payload")
} }
commitID = payload.PullRequest.Head.Sha commitID = payload.PullRequest.Head.Sha
case // pull_request_review events share the same PullRequestPayload as pull_request
webhook_module.HookEventPullRequestReviewApproved,
webhook_module.HookEventPullRequestReviewRejected,
webhook_module.HookEventPullRequestReviewComment:
event = run.TriggerEvent
payload, err := run.GetPullRequestEventPayload()
if err != nil {
return "", "", fmt.Errorf("GetPullRequestEventPayload: %w", err)
}
if payload.PullRequest == nil {
return "", "", errors.New("pull request is missing in event payload")
} else if payload.PullRequest.Head == nil {
return "", "", errors.New("head of pull request is missing in event payload")
}
commitID = payload.PullRequest.Head.Sha
case webhook_module.HookEventRelease: case webhook_module.HookEventRelease:
event = string(run.Event) event = string(run.Event)
commitID = run.CommitSHA commitID = run.CommitSHA

View File

@ -18,7 +18,6 @@ import (
func TestMain(m *testing.M) { func TestMain(m *testing.M) {
unittest.MainTest(m) unittest.MainTest(m)
os.Exit(m.Run())
} }
func TestInitToken(t *testing.T) { func TestInitToken(t *testing.T) {

View File

@ -16,10 +16,14 @@ import (
"code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/util"
) )
type themeCollection struct {
themeList []*ThemeMetaInfo
themeMap map[string]*ThemeMetaInfo
}
var ( var (
availableThemes []*ThemeMetaInfo themeMu sync.RWMutex
availableThemeMap map[string]*ThemeMetaInfo availableThemes *themeCollection
themeOnce sync.Once
) )
const ( const (
@ -129,23 +133,13 @@ func parseThemeMetaInfo(fileName, cssContent string) *ThemeMetaInfo {
return themeInfo return themeInfo
} }
func initThemes() { func loadThemesFromAssets() (themeList []*ThemeMetaInfo, themeMap map[string]*ThemeMetaInfo) {
availableThemes = nil
defer func() {
availableThemeMap = map[string]*ThemeMetaInfo{}
for _, theme := range availableThemes {
availableThemeMap[theme.InternalName] = theme
}
if availableThemeMap[setting.UI.DefaultTheme] == nil {
setting.LogStartupProblem(1, log.ERROR, "Default theme %q is not available, please correct the '[ui].DEFAULT_THEME' setting in the config file", setting.UI.DefaultTheme)
}
}()
cssFiles, err := public.AssetFS().ListFiles("assets/css") cssFiles, err := public.AssetFS().ListFiles("assets/css")
if err != nil { if err != nil {
log.Error("Failed to list themes: %v", err) log.Error("Failed to list themes: %v", err)
availableThemes = []*ThemeMetaInfo{defaultThemeMetaInfoByInternalName(setting.UI.DefaultTheme)} return nil, nil
return
} }
var foundThemes []*ThemeMetaInfo var foundThemes []*ThemeMetaInfo
for _, fileName := range cssFiles { for _, fileName := range cssFiles {
if strings.HasPrefix(fileName, fileNamePrefix) && strings.HasSuffix(fileName, fileNameSuffix) { if strings.HasPrefix(fileName, fileNamePrefix) && strings.HasSuffix(fileName, fileNameSuffix) {
@ -157,39 +151,84 @@ func initThemes() {
foundThemes = append(foundThemes, parseThemeMetaInfo(fileName, util.UnsafeBytesToString(content))) foundThemes = append(foundThemes, parseThemeMetaInfo(fileName, util.UnsafeBytesToString(content)))
} }
} }
themeList = foundThemes
if len(setting.UI.Themes) > 0 { if len(setting.UI.Themes) > 0 {
themeList = nil // only allow the themes specified in the setting
allowedThemes := container.SetOf(setting.UI.Themes...) allowedThemes := container.SetOf(setting.UI.Themes...)
for _, theme := range foundThemes { for _, theme := range foundThemes {
if allowedThemes.Contains(theme.InternalName) { if allowedThemes.Contains(theme.InternalName) {
availableThemes = append(availableThemes, theme) themeList = append(themeList, theme)
} }
} }
} else {
availableThemes = foundThemes
} }
sort.Slice(availableThemes, func(i, j int) bool {
if availableThemes[i].InternalName == setting.UI.DefaultTheme { sort.Slice(themeList, func(i, j int) bool {
if themeList[i].InternalName == setting.UI.DefaultTheme {
return true return true
} }
if availableThemes[i].ColorblindType != availableThemes[j].ColorblindType { if themeList[i].ColorblindType != themeList[j].ColorblindType {
return availableThemes[i].ColorblindType < availableThemes[j].ColorblindType return themeList[i].ColorblindType < themeList[j].ColorblindType
} }
return availableThemes[i].DisplayName < availableThemes[j].DisplayName return themeList[i].DisplayName < themeList[j].DisplayName
}) })
if len(availableThemes) == 0 {
setting.LogStartupProblem(1, log.ERROR, "No theme candidate in asset files, but Gitea requires there should be at least one usable theme") themeMap = map[string]*ThemeMetaInfo{}
availableThemes = []*ThemeMetaInfo{defaultThemeMetaInfoByInternalName(setting.UI.DefaultTheme)} for _, theme := range themeList {
themeMap[theme.InternalName] = theme
} }
return themeList, themeMap
}
func getAvailableThemes() (themeList []*ThemeMetaInfo, themeMap map[string]*ThemeMetaInfo) {
themeMu.RLock()
if availableThemes != nil {
themeList, themeMap = availableThemes.themeList, availableThemes.themeMap
}
themeMu.RUnlock()
if len(themeList) != 0 {
return themeList, themeMap
}
themeMu.Lock()
defer themeMu.Unlock()
// no need to double-check "availableThemes.themeList" since the loading isn't really slow, to keep code simple
themeList, themeMap = loadThemesFromAssets()
hasAvailableThemes := len(themeList) > 0
if !hasAvailableThemes {
defaultTheme := defaultThemeMetaInfoByInternalName(setting.UI.DefaultTheme)
themeList = []*ThemeMetaInfo{defaultTheme}
themeMap = map[string]*ThemeMetaInfo{setting.UI.DefaultTheme: defaultTheme}
}
if setting.IsProd {
if !hasAvailableThemes {
setting.LogStartupProblem(1, log.ERROR, "No theme candidate in asset files, but Gitea requires there should be at least one usable theme")
}
if themeMap[setting.UI.DefaultTheme] == nil {
setting.LogStartupProblem(1, log.ERROR, "Default theme %q is not available, please correct the '[ui].DEFAULT_THEME' setting in the config file", setting.UI.DefaultTheme)
}
availableThemes = &themeCollection{themeList, themeMap}
return themeList, themeMap
}
// In dev mode, only store the loaded themes if the list is not empty, in case the frontend is still being built.
// TBH, there still could be a data-race that the themes are only partially built then the list is incomplete for first time loading.
// Such edge case can be handled by checking whether the loaded themes are the same in a period or there is a flag file, but it is an over-kill, so, no.
if hasAvailableThemes {
availableThemes = &themeCollection{themeList, themeMap}
}
return themeList, themeMap
} }
func GetAvailableThemes() []*ThemeMetaInfo { func GetAvailableThemes() []*ThemeMetaInfo {
themeOnce.Do(initThemes) themes, _ := getAvailableThemes()
return availableThemes return themes
} }
func GetThemeMetaInfo(internalName string) *ThemeMetaInfo { func GetThemeMetaInfo(internalName string) *ThemeMetaInfo {
themeOnce.Do(initThemes) _, themeMap := getAvailableThemes()
return availableThemeMap[internalName] return themeMap[internalName]
} }
// GuaranteeGetThemeMetaInfo guarantees to return a non-nil ThemeMetaInfo, // GuaranteeGetThemeMetaInfo guarantees to return a non-nil ThemeMetaInfo,

View File

@ -6,7 +6,7 @@
<div class=" tw-mr-4 not-mobile">{{ctx.AvatarUtils.Avatar .SignedUser 40}}</div> <div class=" tw-mr-4 not-mobile">{{ctx.AvatarUtils.Avatar .SignedUser 40}}</div>
<div class="ui segment content tw-my-0 avatar-content-left-arrow"> <div class="ui segment content tw-my-0 avatar-content-left-arrow">
<div class="field"> <div class="field">
<input name="title" data-global-init="initInputAutoFocusEnd" id="issue_title" required maxlength="255" autocomplete="off" <input name="title" data-global-init="autoFocusEnd" id="issue_title" required maxlength="255" autocomplete="off"
placeholder="{{ctx.Locale.Tr "repo.milestones.title"}}" placeholder="{{ctx.Locale.Tr "repo.milestones.title"}}"
value="{{if .TitleQuery}}{{.TitleQuery}}{{else if .IssueTemplateTitle}}{{.IssueTemplateTitle}}{{else}}{{.title}}{{end}}" value="{{if .TitleQuery}}{{.TitleQuery}}{{else if .IssueTemplateTitle}}{{.IssueTemplateTitle}}{{else}}{{.title}}{{end}}"
> >

View File

@ -1,8 +1,8 @@
{{if .HeatmapData}} {{if .EnableHeatmap}}
<div class="activity-heatmap-container"> <div class="activity-heatmap-container">
<div id="user-heatmap" class="is-loading" <div id="user-heatmap" class="is-loading"
data-heatmap-data="{{JsonUtils.EncodeToString .HeatmapData}}" data-heatmap-url="{{.HeatmapURL}}"
data-locale-total-contributions="{{ctx.Locale.Tr "heatmap.number_of_contributions_in_the_last_12_months" (ctx.Locale.PrettyNumber .HeatmapTotalContributions)}}" data-locale-total-contributions="{{ctx.Locale.Tr "heatmap.number_of_contributions_in_the_last_12_months" "%s"}}"
data-locale-no-contributions="{{ctx.Locale.Tr "heatmap.no_contributions"}}" data-locale-no-contributions="{{ctx.Locale.Tr "heatmap.no_contributions"}}"
data-locale-more="{{ctx.Locale.Tr "heatmap.more"}}" data-locale-more="{{ctx.Locale.Tr "heatmap.more"}}"
data-locale-less="{{ctx.Locale.Tr "heatmap.less"}}" data-locale-less="{{ctx.Locale.Tr "heatmap.less"}}"

View File

@ -37,7 +37,7 @@ func TestMain(m *testing.M) {
graceful.InitManager(managerCtx) graceful.InitManager(managerCtx)
defer cancel() defer cancel()
tests.InitTest(false) tests.InitTest()
testE2eWebRoutes = routers.NormalRoutes() testE2eWebRoutes = routers.NormalRoutes()
err := unittest.InitFixtures( err := unittest.InitFixtures(

View File

@ -9,8 +9,8 @@ import (
"net/url" "net/url"
"testing" "testing"
actions_model "code.gitea.io/gitea/models/actions"
auth_model "code.gitea.io/gitea/models/auth" auth_model "code.gitea.io/gitea/models/auth"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unittest" "code.gitea.io/gitea/models/unittest"
user_model "code.gitea.io/gitea/models/user" user_model "code.gitea.io/gitea/models/user"
@ -19,30 +19,52 @@ import (
func TestActionsCollaborativeOwner(t *testing.T) { func TestActionsCollaborativeOwner(t *testing.T) {
onGiteaRun(t, func(t *testing.T, u *url.URL) { onGiteaRun(t, func(t *testing.T, u *url.URL) {
// user2 is the owner of "reusable_workflow" repo // user2 is the owner of the private "reusable_workflow" repo
user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) user2Session := loginUser(t, "user2")
user2Session := loginUser(t, user2.Name)
user2Token := getTokenForLoggedInUser(t, user2Session, auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser) user2Token := getTokenForLoggedInUser(t, user2Session, auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser)
repo := createActionsTestRepo(t, user2Token, "reusable_workflow", true) apiReusableWorkflowRepo := createActionsTestRepo(t, user2Token, "reusable_workflow", true)
reusableWorkflowRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: apiReusableWorkflowRepo.ID})
// a private repo(id=6) of user10 will try to clone "reusable_workflow" repo // user4 is the owner of the private caller repo
user10 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 10}) user4 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4})
// task id is 55 and its repo_id=6 user4Session := loginUser(t, user4.Name)
task := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionTask{ID: 55, RepoID: 6}) user4Token := getTokenForLoggedInUser(t, user4Session, auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser)
taskToken := "674f727a81ed2f195bccab036cccf86a182199eb" apiCallerRepo := createActionsTestRepo(t, user4Token, "caller_workflow", true)
tokenHash := auth_model.HashToken(taskToken, task.TokenSalt) callerRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: apiCallerRepo.ID})
assert.Equal(t, task.TokenHash, tokenHash)
// create a mock runner for caller
runner := newMockRunner()
runner.registerAsRepoRunner(t, callerRepo.OwnerName, callerRepo.Name, "mock-runner", []string{"ubuntu-latest"}, false)
// init the workflow for caller
wfTreePath := ".gitea/workflows/test_collaborative_owner.yml"
wfFileContent := `name: Test Collaborative Owner
on: push
jobs:
job1:
runs-on: ubuntu-latest
steps:
- run: echo 'test collaborative owner'
`
opts := getWorkflowCreateFileOptions(user4, callerRepo.DefaultBranch, "create "+wfTreePath, wfFileContent)
createWorkflowFile(t, user4Token, callerRepo.OwnerName, callerRepo.Name, wfTreePath, opts)
// fetch the task and get its token
task := runner.fetchTask(t)
taskToken := task.Secrets["GITEA_TOKEN"]
assert.NotEmpty(t, taskToken)
// prepare for clone
dstPath := t.TempDir() dstPath := t.TempDir()
u.Path = fmt.Sprintf("%s/%s.git", repo.Owner.UserName, repo.Name) u.Path = fmt.Sprintf("%s/%s.git", "user2", "reusable_workflow")
u.User = url.UserPassword("gitea-actions", taskToken) u.User = url.UserPassword("gitea-actions", taskToken)
// the git clone will fail // the git clone will fail
doGitCloneFail(u)(t) doGitCloneFail(u)(t)
// add user10 to the list of collaborative owners // add user10 to the list of collaborative owners
req := NewRequestWithValues(t, "POST", fmt.Sprintf("/%s/%s/settings/actions/general/collaborative_owner/add", repo.Owner.UserName, repo.Name), map[string]string{ req := NewRequestWithValues(t, "POST", fmt.Sprintf("/%s/%s/settings/actions/general/collaborative_owner/add", reusableWorkflowRepo.OwnerName, reusableWorkflowRepo.Name), map[string]string{
"collaborative_owner": user10.Name, "collaborative_owner": user4.Name,
}) })
user2Session.MakeRequest(t, req, http.StatusOK) user2Session.MakeRequest(t, req, http.StatusOK)
@ -50,7 +72,7 @@ func TestActionsCollaborativeOwner(t *testing.T) {
doGitClone(dstPath, u)(t) doGitClone(dstPath, u)(t)
// remove user10 from the list of collaborative owners // remove user10 from the list of collaborative owners
req = NewRequest(t, "POST", fmt.Sprintf("/%s/%s/settings/actions/general/collaborative_owner/delete?id=%d", repo.Owner.UserName, repo.Name, user10.ID)) req = NewRequest(t, "POST", fmt.Sprintf("/%s/%s/settings/actions/general/collaborative_owner/delete?id=%d", reusableWorkflowRepo.OwnerName, reusableWorkflowRepo.Name, user4.ID))
user2Session.MakeRequest(t, req, http.StatusOK) user2Session.MakeRequest(t, req, http.StatusOK)
// the git clone will fail // the git clone will fail

View File

@ -691,6 +691,144 @@ func insertFakeStatus(t *testing.T, repo *repo_model.Repository, sha, targetURL,
assert.NoError(t, err) assert.NoError(t, err)
} }
func TestPullRequestReviewCommitStatusEvent(t *testing.T) {
onGiteaRun(t, func(t *testing.T, u *url.URL) {
user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) // owner of the repo
user4 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4}) // reviewer
// create a repo
repo, err := repo_service.CreateRepository(t.Context(), user2, user2, repo_service.CreateRepoOptions{
Name: "repo-pull-request-review",
Description: "test pull-request-review commit status",
AutoInit: true,
Gitignores: "Go",
License: "MIT",
Readme: "Default",
DefaultBranch: "main",
IsPrivate: false,
})
assert.NoError(t, err)
assert.NotEmpty(t, repo)
// add user4 as collaborator so they can review
ctx := NewAPITestContext(t, repo.OwnerName, repo.Name, auth_model.AccessTokenScopeWriteRepository)
t.Run("AddUser4AsCollaboratorWithWriteAccess", doAPIAddCollaborator(ctx, "user4", perm.AccessModeWrite))
// add workflow file that triggers on pull_request_review
addWorkflow, err := files_service.ChangeRepoFiles(t.Context(), repo, user2, &files_service.ChangeRepoFilesOptions{
Files: []*files_service.ChangeRepoFile{
{
Operation: "create",
TreePath: ".gitea/workflows/pr-review.yml",
ContentReader: strings.NewReader(`name: test
on:
pull_request_review:
types: [submitted]
jobs:
test:
runs-on: ubuntu-latest
steps:
- run: echo helloworld
`),
},
},
Message: "add workflow",
OldBranch: "main",
NewBranch: "main",
Author: &files_service.IdentityOptions{
GitUserName: user2.Name,
GitUserEmail: user2.Email,
},
Committer: &files_service.IdentityOptions{
GitUserName: user2.Name,
GitUserEmail: user2.Email,
},
Dates: &files_service.CommitDateOptions{
Author: time.Now(),
Committer: time.Now(),
},
})
assert.NoError(t, err)
assert.NotEmpty(t, addWorkflow)
// create a branch and a PR
testBranch := "test-review-branch"
err = repo_service.CreateNewBranch(t.Context(), user2, repo, "main", testBranch)
assert.NoError(t, err)
// add a file on the test branch so the PR has changes
addFileResp, err := files_service.ChangeRepoFiles(t.Context(), repo, user2, &files_service.ChangeRepoFilesOptions{
Files: []*files_service.ChangeRepoFile{
{
Operation: "create",
TreePath: "test.txt",
ContentReader: strings.NewReader("test content"),
},
},
Message: "add test file",
OldBranch: testBranch,
NewBranch: testBranch,
Author: &files_service.IdentityOptions{
GitUserName: user2.Name,
GitUserEmail: user2.Email,
},
Committer: &files_service.IdentityOptions{
GitUserName: user2.Name,
GitUserEmail: user2.Email,
},
Dates: &files_service.CommitDateOptions{
Author: time.Now(),
Committer: time.Now(),
},
})
assert.NoError(t, err)
assert.NotEmpty(t, addFileResp)
sha := addFileResp.Commit.SHA
pullIssue := &issues_model.Issue{
RepoID: repo.ID,
Title: "A test PR for review",
PosterID: user2.ID,
Poster: user2,
IsPull: true,
}
pullRequest := &issues_model.PullRequest{
HeadRepoID: repo.ID,
BaseRepoID: repo.ID,
HeadBranch: testBranch,
BaseBranch: "main",
HeadRepo: repo,
BaseRepo: repo,
Type: issues_model.PullRequestGitea,
}
prOpts := &pull_service.NewPullRequestOptions{Repo: repo, Issue: pullIssue, PullRequest: pullRequest}
err = pull_service.NewPullRequest(t.Context(), prOpts)
assert.NoError(t, err)
// submit an approval review as user4
gitRepo, err := gitrepo.OpenRepository(t.Context(), repo)
assert.NoError(t, err)
defer gitRepo.Close()
_, _, err = pull_service.SubmitReview(t.Context(), user4, gitRepo, pullIssue, issues_model.ReviewTypeApprove, "lgtm", sha, nil)
assert.NoError(t, err)
// verify that a commit status was created for the review event
assert.Eventually(t, func() bool {
latestCommitStatuses, err := git_model.GetLatestCommitStatus(t.Context(), repo.ID, sha, db.ListOptionsAll)
assert.NoError(t, err)
if len(latestCommitStatuses) == 0 {
return false
}
if latestCommitStatuses[0].State == commitstatus.CommitStatusPending {
insertFakeStatus(t, repo, sha, latestCommitStatuses[0].TargetURL, latestCommitStatuses[0].Context)
return true
}
return false
}, 1*time.Second, 100*time.Millisecond)
})
}
func TestWorkflowDispatchPublicApi(t *testing.T) { func TestWorkflowDispatchPublicApi(t *testing.T) {
onGiteaRun(t, func(t *testing.T, u *url.URL) { onGiteaRun(t, func(t *testing.T, u *url.URL) {
user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})

View File

@ -0,0 +1,59 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package integration
import (
"net/http"
"testing"
"time"
"code.gitea.io/gitea/modules/timeutil"
"code.gitea.io/gitea/tests"
"github.com/stretchr/testify/assert"
)
func TestHeatmapEndpoints(t *testing.T) {
defer tests.PrepareTestEnv(t)()
// Mock time so fixture actions fall within the heatmap's time window
timeutil.MockSet(time.Date(2021, 1, 1, 0, 0, 0, 0, time.UTC))
defer timeutil.MockUnset()
session := loginUser(t, "user2")
t.Run("UserProfile", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
req := NewRequest(t, "GET", "/user2/-/heatmap")
resp := session.MakeRequest(t, req, http.StatusOK)
var result map[string]any
DecodeJSON(t, resp, &result)
assert.Contains(t, result, "heatmapData")
assert.Contains(t, result, "totalContributions")
assert.Positive(t, result["totalContributions"])
})
t.Run("OrgDashboard", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
req := NewRequest(t, "GET", "/org/org3/dashboard/-/heatmap")
resp := session.MakeRequest(t, req, http.StatusOK)
var result map[string]any
DecodeJSON(t, resp, &result)
assert.Contains(t, result, "heatmapData")
assert.Contains(t, result, "totalContributions")
})
t.Run("OrgTeamDashboard", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
req := NewRequest(t, "GET", "/org/org3/dashboard/-/heatmap/team1")
resp := session.MakeRequest(t, req, http.StatusOK)
var result map[string]any
DecodeJSON(t, resp, &result)
assert.Contains(t, result, "heatmapData")
assert.Contains(t, result, "totalContributions")
})
}

View File

@ -1,7 +1,6 @@
// Copyright 2017 The Gitea Authors. All rights reserved. // Copyright 2017 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
//nolint:forbidigo // use of print functions is allowed in tests
package integration package integration
import ( import (
@ -27,6 +26,7 @@ import (
"code.gitea.io/gitea/modules/json" "code.gitea.io/gitea/modules/json"
"code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/testlogger"
"code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/web" "code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/modules/web/middleware" "code.gitea.io/gitea/modules/web/middleware"
@ -79,14 +79,14 @@ func NewNilResponseHashSumRecorder() *NilResponseHashSumRecorder {
} }
} }
func TestMain(m *testing.M) { func testMain(m *testing.M) int {
defer log.GetManager().Close() defer log.GetManager().Close()
managerCtx, cancel := context.WithCancel(context.Background()) managerCtx, cancel := context.WithCancel(context.Background())
graceful.InitManager(managerCtx) graceful.InitManager(managerCtx)
defer cancel() defer cancel()
tests.InitTest(true) tests.InitTest()
testWebRoutes = routers.NormalRoutes() testWebRoutes = routers.NormalRoutes()
err := unittest.InitFixtures( err := unittest.InitFixtures(
@ -95,8 +95,7 @@ func TestMain(m *testing.M) {
}, },
) )
if err != nil { if err != nil {
fmt.Printf("Error initializing test database: %v\n", err) testlogger.Panicf("InitFixtures: %v", err)
os.Exit(1)
} }
// FIXME: the console logger is deleted by mistake, so if there is any `log.Fatal`, developers won't see any error message. // FIXME: the console logger is deleted by mistake, so if there is any `log.Fatal`, developers won't see any error message.
@ -104,15 +103,16 @@ func TestMain(m *testing.M) {
exitCode := m.Run() exitCode := m.Run()
if err = util.RemoveAll(setting.Indexer.IssuePath); err != nil { if err = util.RemoveAll(setting.Indexer.IssuePath); err != nil {
fmt.Printf("util.RemoveAll: %v\n", err) log.Error("Failed to remove indexer path: %v", err)
os.Exit(1)
} }
if err = util.RemoveAll(setting.Indexer.RepoPath); err != nil { if err = util.RemoveAll(setting.Indexer.RepoPath); err != nil {
fmt.Printf("Unable to remove repo indexer: %v\n", err) log.Error("Failed to remove indexer path: %v", err)
os.Exit(1)
} }
return exitCode
}
os.Exit(exitCode) func TestMain(m *testing.M) {
os.Exit(testMain(m))
} }
type TestSession struct { type TestSession struct {

View File

@ -36,8 +36,6 @@ var currentEngine *xorm.Engine
func initMigrationTest(t *testing.T) func() { func initMigrationTest(t *testing.T) func() {
testlogger.Init() testlogger.Init()
setting.SetupGiteaTestEnv()
unittest.InitSettingsForTesting() unittest.InitSettingsForTesting()
assert.NotEmpty(t, setting.RepoRootPath) assert.NotEmpty(t, setting.RepoRootPath)

View File

@ -176,41 +176,6 @@ func TestPullCreate(t *testing.T) {
}) })
} }
func TestPullCreate_TitleEscape(t *testing.T) {
onGiteaRun(t, func(t *testing.T, u *url.URL) {
session := loginUser(t, "user1")
testRepoFork(t, session, "user2", "repo1", "user1", "repo1", "")
testEditFile(t, session, "user1", "repo1", "master", "README.md", "Hello, World (Edited)\n")
resp := testPullCreate(t, session, "user1", "repo1", false, "master", "master", "<i>XSS PR</i>")
// check the redirected URL
url := test.RedirectURL(resp)
assert.Regexp(t, "^/user2/repo1/pulls/[0-9]*$", url)
// Edit title
req := NewRequest(t, "GET", url)
resp = session.MakeRequest(t, req, http.StatusOK)
htmlDoc := NewHTMLParser(t, resp.Body)
editTestTitleURL, exists := htmlDoc.doc.Find(".issue-title-buttons button[data-update-url]").First().Attr("data-update-url")
assert.True(t, exists, "The template has changed")
req = NewRequestWithValues(t, "POST", editTestTitleURL, map[string]string{
"title": "<u>XSS PR</u>",
})
session.MakeRequest(t, req, http.StatusOK)
req = NewRequest(t, "GET", url)
resp = session.MakeRequest(t, req, http.StatusOK)
htmlDoc = NewHTMLParser(t, resp.Body)
titleHTML, err := htmlDoc.doc.Find(".comment-list .timeline-item.event .comment-text-line b").First().Html()
assert.NoError(t, err)
assert.Equal(t, "<strike>&lt;i&gt;XSS PR&lt;/i&gt;</strike>", titleHTML)
titleHTML, err = htmlDoc.doc.Find(".comment-list .timeline-item.event .comment-text-line b").Next().Html()
assert.NoError(t, err)
assert.Equal(t, "&lt;u&gt;XSS PR&lt;/u&gt;", titleHTML)
})
}
func testUIDeleteBranch(t *testing.T, session *TestSession, ownerName, repoName, branchName string) { func testUIDeleteBranch(t *testing.T, session *TestSession, ownerName, repoName, branchName string) {
relURL := "/" + path.Join(ownerName, repoName, "branches") relURL := "/" + path.Join(ownerName, repoName, "branches")
req := NewRequestWithValues(t, "POST", relURL+"/delete", map[string]string{ req := NewRequestWithValues(t, "POST", relURL+"/delete", map[string]string{

View File

@ -1,38 +0,0 @@
// Copyright 2017 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package integration
import (
"net/http"
"testing"
"code.gitea.io/gitea/models/unittest"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/tests"
"github.com/stretchr/testify/assert"
)
func TestXSSUserFullName(t *testing.T) {
defer tests.PrepareTestEnv(t)()
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
const fullName = `name & <script class="evil">alert('Oh no!');</script>`
session := loginUser(t, user.Name)
req := NewRequestWithValues(t, "POST", "/user/settings", map[string]string{
"name": user.Name,
"full_name": fullName,
"email": user.Email,
"language": "en-US",
})
session.MakeRequest(t, req, http.StatusSeeOther)
req = NewRequestf(t, "GET", "/%s", user.Name)
resp := session.MakeRequest(t, req, http.StatusOK)
htmlDoc := NewHTMLParser(t, resp.Body)
assert.Equal(t, 0, htmlDoc.doc.Find("script.evil").Length())
assert.Equal(t, fullName,
htmlDoc.doc.Find("div.content").Find(".header.text.center").Text(),
)
}

View File

@ -1,3 +1,4 @@
WORK_PATH = {{WORK_PATH}}
APP_NAME = Gitea: Git with a cup of tea APP_NAME = Gitea: Git with a cup of tea
RUN_MODE = prod RUN_MODE = prod
@ -11,11 +12,9 @@ SSL_MODE = disable
[indexer] [indexer]
REPO_INDEXER_ENABLED = true REPO_INDEXER_ENABLED = true
REPO_INDEXER_PATH = tests/{{TEST_TYPE}}/gitea-{{TEST_TYPE}}-mssql/indexers/repos.bleve
[queue.issue_indexer] [queue.issue_indexer]
TYPE = level TYPE = level
DATADIR = tests/{{TEST_TYPE}}/gitea-{{TEST_TYPE}}-mssql/indexers/issues.queue
[queue] [queue]
TYPE = immediate TYPE = immediate
@ -29,15 +28,6 @@ TYPE = immediate
[queue.webhook_sender] [queue.webhook_sender]
TYPE = immediate TYPE = immediate
[repository]
ROOT = {{REPO_TEST_DIR}}tests/{{TEST_TYPE}}/gitea-{{TEST_TYPE}}-mssql/gitea-repositories
[repository.local]
LOCAL_COPY_PATH = tests/{{TEST_TYPE}}/gitea-{{TEST_TYPE}}-mssql/tmp/local-repo
[repository.upload]
TEMP_PATH = tests/{{TEST_TYPE}}/gitea-{{TEST_TYPE}}-mssql/tmp/uploads
[repository.signing] [repository.signing]
SIGNING_KEY = none SIGNING_KEY = none
@ -53,14 +43,13 @@ START_SSH_SERVER = true
LFS_START_SERVER = true LFS_START_SERVER = true
OFFLINE_MODE = false OFFLINE_MODE = false
LFS_JWT_SECRET = Tv_MjmZuHqpIY6GFl12ebgkRAMt4RlWt0v4EHKSXO0w LFS_JWT_SECRET = Tv_MjmZuHqpIY6GFl12ebgkRAMt4RlWt0v4EHKSXO0w
APP_DATA_PATH = tests/{{TEST_TYPE}}/gitea-{{TEST_TYPE}}-mssql/data
BUILTIN_SSH_SERVER_USER = git BUILTIN_SSH_SERVER_USER = git
SSH_TRUSTED_USER_CA_KEYS = ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQCb4DC1dMFnJ6pXWo7GMxTchtzmJHYzfN6sZ9FAPFR4ijMLfGki+olvOMO5Fql1/yGnGfbELQa1S6y4shSvj/5K+zUFScmEXYf3Gcr87RqilLkyk16RS+cHNB1u87xTHbETaa3nyCJeGQRpd4IQ4NKob745mwDZ7jQBH8AZEng50Oh8y8fi8skBBBzaYp1ilgvzG740L7uex6fHV62myq0SXeCa+oJUjq326FU8y+Vsa32H8A3e7tOgXZPdt2TVNltx2S9H2WO8RMi7LfaSwARNfy1zu+bfR50r6ef8Yx5YKCMz4wWb1SHU1GS800mjOjlInLQORYRNMlSwR1+vLlVDciOqFapDSbj+YOVOawR0R1aqlSKpZkt33DuOBPx9qe6CVnIi7Z+Px/KqM+OLCzlLY/RS+LbxQpDWcfTVRiP+S5qRTcE3M3UioN/e0BE/1+MpX90IGpvVkA63ILYbKEa4bM3ASL7ChTCr6xN5XT+GpVJveFKK1cfNx9ExHI4rzYE= SSH_TRUSTED_USER_CA_KEYS = ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQCb4DC1dMFnJ6pXWo7GMxTchtzmJHYzfN6sZ9FAPFR4ijMLfGki+olvOMO5Fql1/yGnGfbELQa1S6y4shSvj/5K+zUFScmEXYf3Gcr87RqilLkyk16RS+cHNB1u87xTHbETaa3nyCJeGQRpd4IQ4NKob745mwDZ7jQBH8AZEng50Oh8y8fi8skBBBzaYp1ilgvzG740L7uex6fHV62myq0SXeCa+oJUjq326FU8y+Vsa32H8A3e7tOgXZPdt2TVNltx2S9H2WO8RMi7LfaSwARNfy1zu+bfR50r6ef8Yx5YKCMz4wWb1SHU1GS800mjOjlInLQORYRNMlSwR1+vLlVDciOqFapDSbj+YOVOawR0R1aqlSKpZkt33DuOBPx9qe6CVnIi7Z+Px/KqM+OLCzlLY/RS+LbxQpDWcfTVRiP+S5qRTcE3M3UioN/e0BE/1+MpX90IGpvVkA63ILYbKEa4bM3ASL7ChTCr6xN5XT+GpVJveFKK1cfNx9ExHI4rzYE=
[mailer] [mailer]
ENABLED = true ENABLED = true
PROTOCOL = dummy PROTOCOL = dummy
FROM = mssql-{{TEST_TYPE}}-test@gitea.io FROM = mssql-integration-test@gitea.io
[service] [service]
REGISTER_EMAIL_CONFIRM = false REGISTER_EMAIL_CONFIRM = false
@ -76,16 +65,12 @@ ENABLE_NOTIFY_MAIL = true
[picture] [picture]
DISABLE_GRAVATAR = false DISABLE_GRAVATAR = false
ENABLE_FEDERATED_AVATAR = false ENABLE_FEDERATED_AVATAR = false
AVATAR_UPLOAD_PATH = tests/{{TEST_TYPE}}/gitea-{{TEST_TYPE}}-mssql/data/avatars
REPOSITORY_AVATAR_UPLOAD_PATH = tests/{{TEST_TYPE}}/gitea-{{TEST_TYPE}}-mssql/data/repo-avatars
[session] [session]
PROVIDER = file PROVIDER = file
PROVIDER_CONFIG = tests/{{TEST_TYPE}}/gitea-{{TEST_TYPE}}-mssql/data/sessions
[log] [log]
MODE = {{TEST_LOGGER}} MODE = {{TEST_LOGGER}}
ROOT_PATH = {{REPO_TEST_DIR}}mssql-log
ENABLE_SSH_LOG = true ENABLE_SSH_LOG = true
logger.xorm.MODE = file logger.xorm.MODE = file

View File

@ -1,3 +1,4 @@
WORK_PATH = {{WORK_PATH}}
APP_NAME = Gitea: Git with a cup of tea APP_NAME = Gitea: Git with a cup of tea
RUN_MODE = prod RUN_MODE = prod
@ -11,13 +12,11 @@ SSL_MODE = disable
[indexer] [indexer]
REPO_INDEXER_ENABLED = true REPO_INDEXER_ENABLED = true
REPO_INDEXER_PATH = tests/{{TEST_TYPE}}/gitea-{{TEST_TYPE}}-mysql/indexers/repos.bleve
ISSUE_INDEXER_TYPE = elasticsearch ISSUE_INDEXER_TYPE = elasticsearch
ISSUE_INDEXER_CONN_STR = http://elastic:changeme@elasticsearch:9200 ISSUE_INDEXER_CONN_STR = http://elastic:changeme@elasticsearch:9200
[queue.issue_indexer] [queue.issue_indexer]
TYPE = level TYPE = level
DATADIR = tests/{{TEST_TYPE}}/gitea-{{TEST_TYPE}}-mysql/indexers/issues.queue
[queue] [queue]
TYPE = immediate TYPE = immediate
@ -31,15 +30,6 @@ TYPE = immediate
[queue.webhook_sender] [queue.webhook_sender]
TYPE = immediate TYPE = immediate
[repository]
ROOT = {{REPO_TEST_DIR}}tests/{{TEST_TYPE}}/gitea-{{TEST_TYPE}}-mysql/gitea-repositories
[repository.local]
LOCAL_COPY_PATH = tests/{{TEST_TYPE}}/gitea-{{TEST_TYPE}}-mysql/tmp/local-repo
[repository.upload]
TEMP_PATH = tests/{{TEST_TYPE}}/gitea-{{TEST_TYPE}}-mysql/tmp/uploads
[repository.signing] [repository.signing]
SIGNING_KEY = none SIGNING_KEY = none
@ -51,7 +41,6 @@ LOCAL_ROOT_URL = http://127.0.0.1:3001/
DISABLE_SSH = false DISABLE_SSH = false
SSH_LISTEN_HOST = localhost SSH_LISTEN_HOST = localhost
SSH_PORT = 2201 SSH_PORT = 2201
APP_DATA_PATH = tests/{{TEST_TYPE}}/gitea-{{TEST_TYPE}}-mysql/data
BUILTIN_SSH_SERVER_USER = git BUILTIN_SSH_SERVER_USER = git
START_SSH_SERVER = true START_SSH_SERVER = true
OFFLINE_MODE = false OFFLINE_MODE = false
@ -63,7 +52,7 @@ SSH_TRUSTED_USER_CA_KEYS = ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQCb4DC1dMFnJ6pXW
[mailer] [mailer]
ENABLED = true ENABLED = true
PROTOCOL = dummy PROTOCOL = dummy
FROM = mysql-{{TEST_TYPE}}-test@gitea.io FROM = mysql-integration-test@gitea.io
[service] [service]
REGISTER_EMAIL_CONFIRM = false REGISTER_EMAIL_CONFIRM = false
@ -82,11 +71,9 @@ ENABLE_FEDERATED_AVATAR = false
[session] [session]
PROVIDER = file PROVIDER = file
PROVIDER_CONFIG = tests/{{TEST_TYPE}}/gitea-{{TEST_TYPE}}-mysql/data/sessions
[log] [log]
MODE = {{TEST_LOGGER}} MODE = {{TEST_LOGGER}}
ROOT_PATH = {{REPO_TEST_DIR}}mysql-log
ENABLE_SSH_LOG = true ENABLE_SSH_LOG = true
logger.xorm.MODE = file logger.xorm.MODE = file
@ -103,9 +90,6 @@ SECRET_KEY = 9pCviYTWSb
INTERNAL_TOKEN = eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYmYiOjE0OTU1NTE2MTh9.hhSVGOANkaKk3vfCd2jDOIww4pUk0xtg9JRde5UogyQ INTERNAL_TOKEN = eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYmYiOjE0OTU1NTE2MTh9.hhSVGOANkaKk3vfCd2jDOIww4pUk0xtg9JRde5UogyQ
DISABLE_QUERY_AUTH_TOKEN = true DISABLE_QUERY_AUTH_TOKEN = true
[lfs]
PATH = tests/{{TEST_TYPE}}/gitea-{{TEST_TYPE}}-mysql/data/lfs
[packages] [packages]
ENABLED = true ENABLED = true

View File

@ -1,3 +1,4 @@
WORK_PATH = {{WORK_PATH}}
APP_NAME = Gitea: Git with a cup of tea APP_NAME = Gitea: Git with a cup of tea
RUN_MODE = prod RUN_MODE = prod
@ -12,11 +13,9 @@ SSL_MODE = disable
[indexer] [indexer]
REPO_INDEXER_ENABLED = true REPO_INDEXER_ENABLED = true
REPO_INDEXER_PATH = tests/{{TEST_TYPE}}/gitea-{{TEST_TYPE}}-pgsql/indexers/repos.bleve
[queue.issue_indexer] [queue.issue_indexer]
TYPE = level TYPE = level
DATADIR = tests/{{TEST_TYPE}}/gitea-{{TEST_TYPE}}-pgsql/indexers/issues.queue
[queue] [queue]
TYPE = immediate TYPE = immediate
@ -30,15 +29,6 @@ TYPE = immediate
[queue.webhook_sender] [queue.webhook_sender]
TYPE = immediate TYPE = immediate
[repository]
ROOT = {{REPO_TEST_DIR}}tests/{{TEST_TYPE}}/gitea-{{TEST_TYPE}}-pgsql/gitea-repositories
[repository.local]
LOCAL_COPY_PATH = tests/{{TEST_TYPE}}/gitea-{{TEST_TYPE}}-pgsql/tmp/local-repo
[repository.upload]
TEMP_PATH = tests/{{TEST_TYPE}}/gitea-{{TEST_TYPE}}-pgsql/tmp/uploads
[repository.signing] [repository.signing]
SIGNING_KEY = none SIGNING_KEY = none
@ -54,14 +44,13 @@ START_SSH_SERVER = true
LFS_START_SERVER = true LFS_START_SERVER = true
OFFLINE_MODE = false OFFLINE_MODE = false
LFS_JWT_SECRET = Tv_MjmZuHqpIY6GFl12ebgkRAMt4RlWt0v4EHKSXO0w LFS_JWT_SECRET = Tv_MjmZuHqpIY6GFl12ebgkRAMt4RlWt0v4EHKSXO0w
APP_DATA_PATH = tests/{{TEST_TYPE}}/gitea-{{TEST_TYPE}}-pgsql/data
BUILTIN_SSH_SERVER_USER = git BUILTIN_SSH_SERVER_USER = git
SSH_TRUSTED_USER_CA_KEYS = ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQCb4DC1dMFnJ6pXWo7GMxTchtzmJHYzfN6sZ9FAPFR4ijMLfGki+olvOMO5Fql1/yGnGfbELQa1S6y4shSvj/5K+zUFScmEXYf3Gcr87RqilLkyk16RS+cHNB1u87xTHbETaa3nyCJeGQRpd4IQ4NKob745mwDZ7jQBH8AZEng50Oh8y8fi8skBBBzaYp1ilgvzG740L7uex6fHV62myq0SXeCa+oJUjq326FU8y+Vsa32H8A3e7tOgXZPdt2TVNltx2S9H2WO8RMi7LfaSwARNfy1zu+bfR50r6ef8Yx5YKCMz4wWb1SHU1GS800mjOjlInLQORYRNMlSwR1+vLlVDciOqFapDSbj+YOVOawR0R1aqlSKpZkt33DuOBPx9qe6CVnIi7Z+Px/KqM+OLCzlLY/RS+LbxQpDWcfTVRiP+S5qRTcE3M3UioN/e0BE/1+MpX90IGpvVkA63ILYbKEa4bM3ASL7ChTCr6xN5XT+GpVJveFKK1cfNx9ExHI4rzYE= SSH_TRUSTED_USER_CA_KEYS = ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQCb4DC1dMFnJ6pXWo7GMxTchtzmJHYzfN6sZ9FAPFR4ijMLfGki+olvOMO5Fql1/yGnGfbELQa1S6y4shSvj/5K+zUFScmEXYf3Gcr87RqilLkyk16RS+cHNB1u87xTHbETaa3nyCJeGQRpd4IQ4NKob745mwDZ7jQBH8AZEng50Oh8y8fi8skBBBzaYp1ilgvzG740L7uex6fHV62myq0SXeCa+oJUjq326FU8y+Vsa32H8A3e7tOgXZPdt2TVNltx2S9H2WO8RMi7LfaSwARNfy1zu+bfR50r6ef8Yx5YKCMz4wWb1SHU1GS800mjOjlInLQORYRNMlSwR1+vLlVDciOqFapDSbj+YOVOawR0R1aqlSKpZkt33DuOBPx9qe6CVnIi7Z+Px/KqM+OLCzlLY/RS+LbxQpDWcfTVRiP+S5qRTcE3M3UioN/e0BE/1+MpX90IGpvVkA63ILYbKEa4bM3ASL7ChTCr6xN5XT+GpVJveFKK1cfNx9ExHI4rzYE=
[mailer] [mailer]
ENABLED = true ENABLED = true
PROTOCOL = dummy PROTOCOL = dummy
FROM = pgsql-{{TEST_TYPE}}-test@gitea.io FROM = pgsql-integration-test@gitea.io
[service] [service]
REGISTER_EMAIL_CONFIRM = false REGISTER_EMAIL_CONFIRM = false
@ -77,16 +66,12 @@ ENABLE_NOTIFY_MAIL = true
[picture] [picture]
DISABLE_GRAVATAR = false DISABLE_GRAVATAR = false
ENABLE_FEDERATED_AVATAR = false ENABLE_FEDERATED_AVATAR = false
AVATAR_UPLOAD_PATH = tests/{{TEST_TYPE}}/gitea-{{TEST_TYPE}}-pgsql/data/avatars
REPOSITORY_AVATAR_UPLOAD_PATH = tests/{{TEST_TYPE}}/gitea-{{TEST_TYPE}}-pgsql/data/repo-avatars
[session] [session]
PROVIDER = file PROVIDER = file
PROVIDER_CONFIG = tests/{{TEST_TYPE}}/gitea-{{TEST_TYPE}}-pgsql/data/sessions
[log] [log]
MODE = {{TEST_LOGGER}} MODE = {{TEST_LOGGER}}
ROOT_PATH = {{REPO_TEST_DIR}}pgsql-log
ENABLE_SSH_LOG = true ENABLE_SSH_LOG = true
logger.xorm.MODE = file logger.xorm.MODE = file

View File

@ -1,17 +1,16 @@
WORK_PATH = {{WORK_PATH}}
APP_NAME = Gitea: Git with a cup of tea APP_NAME = Gitea: Git with a cup of tea
RUN_MODE = prod RUN_MODE = prod
[database] [database]
DB_TYPE = sqlite3 DB_TYPE = sqlite3
PATH = tests/{{TEST_TYPE}}/gitea-{{TEST_TYPE}}-sqlite/gitea.db PATH = gitea.db
[indexer] [indexer]
REPO_INDEXER_ENABLED = true REPO_INDEXER_ENABLED = true
REPO_INDEXER_PATH = tests/{{TEST_TYPE}}/gitea-{{TEST_TYPE}}-sqlite/indexers/repos.bleve
[queue.issue_indexer] [queue.issue_indexer]
TYPE = level TYPE = level
DATADIR = tests/{{TEST_TYPE}}/gitea-{{TEST_TYPE}}-sqlite/indexers/issues.queue
[queue] [queue]
TYPE = immediate TYPE = immediate
@ -25,15 +24,6 @@ TYPE = immediate
[queue.webhook_sender] [queue.webhook_sender]
TYPE = immediate TYPE = immediate
[repository]
ROOT = {{REPO_TEST_DIR}}tests/{{TEST_TYPE}}/gitea-{{TEST_TYPE}}-sqlite/gitea-repositories
[repository.local]
LOCAL_COPY_PATH = tests/{{TEST_TYPE}}/gitea-{{TEST_TYPE}}-sqlite/tmp/local-repo
[repository.upload]
TEMP_PATH = tests/{{TEST_TYPE}}/gitea-{{TEST_TYPE}}-sqlite/tmp/uploads
[repository.signing] [repository.signing]
SIGNING_KEY = none SIGNING_KEY = none
@ -49,18 +39,14 @@ START_SSH_SERVER = true
LFS_START_SERVER = true LFS_START_SERVER = true
OFFLINE_MODE = false OFFLINE_MODE = false
LFS_JWT_SECRET = Tv_MjmZuHqpIY6GFl12ebgkRAMt4RlWt0v4EHKSXO0w LFS_JWT_SECRET = Tv_MjmZuHqpIY6GFl12ebgkRAMt4RlWt0v4EHKSXO0w
APP_DATA_PATH = tests/{{TEST_TYPE}}/gitea-{{TEST_TYPE}}-sqlite/data
ENABLE_GZIP = true ENABLE_GZIP = true
BUILTIN_SSH_SERVER_USER = git BUILTIN_SSH_SERVER_USER = git
SSH_TRUSTED_USER_CA_KEYS = ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQCb4DC1dMFnJ6pXWo7GMxTchtzmJHYzfN6sZ9FAPFR4ijMLfGki+olvOMO5Fql1/yGnGfbELQa1S6y4shSvj/5K+zUFScmEXYf3Gcr87RqilLkyk16RS+cHNB1u87xTHbETaa3nyCJeGQRpd4IQ4NKob745mwDZ7jQBH8AZEng50Oh8y8fi8skBBBzaYp1ilgvzG740L7uex6fHV62myq0SXeCa+oJUjq326FU8y+Vsa32H8A3e7tOgXZPdt2TVNltx2S9H2WO8RMi7LfaSwARNfy1zu+bfR50r6ef8Yx5YKCMz4wWb1SHU1GS800mjOjlInLQORYRNMlSwR1+vLlVDciOqFapDSbj+YOVOawR0R1aqlSKpZkt33DuOBPx9qe6CVnIi7Z+Px/KqM+OLCzlLY/RS+LbxQpDWcfTVRiP+S5qRTcE3M3UioN/e0BE/1+MpX90IGpvVkA63ILYbKEa4bM3ASL7ChTCr6xN5XT+GpVJveFKK1cfNx9ExHI4rzYE= SSH_TRUSTED_USER_CA_KEYS = ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQCb4DC1dMFnJ6pXWo7GMxTchtzmJHYzfN6sZ9FAPFR4ijMLfGki+olvOMO5Fql1/yGnGfbELQa1S6y4shSvj/5K+zUFScmEXYf3Gcr87RqilLkyk16RS+cHNB1u87xTHbETaa3nyCJeGQRpd4IQ4NKob745mwDZ7jQBH8AZEng50Oh8y8fi8skBBBzaYp1ilgvzG740L7uex6fHV62myq0SXeCa+oJUjq326FU8y+Vsa32H8A3e7tOgXZPdt2TVNltx2S9H2WO8RMi7LfaSwARNfy1zu+bfR50r6ef8Yx5YKCMz4wWb1SHU1GS800mjOjlInLQORYRNMlSwR1+vLlVDciOqFapDSbj+YOVOawR0R1aqlSKpZkt33DuOBPx9qe6CVnIi7Z+Px/KqM+OLCzlLY/RS+LbxQpDWcfTVRiP+S5qRTcE3M3UioN/e0BE/1+MpX90IGpvVkA63ILYbKEa4bM3ASL7ChTCr6xN5XT+GpVJveFKK1cfNx9ExHI4rzYE=
[attachment]
PATH = tests/{{TEST_TYPE}}/gitea-{{TEST_TYPE}}-sqlite/data/attachments
[mailer] [mailer]
ENABLED = true ENABLED = true
PROTOCOL = dummy PROTOCOL = dummy
FROM = sqlite-{{TEST_TYPE}}-test@gitea.io FROM = sqlite-integration-test@gitea.io
[service] [service]
REGISTER_EMAIL_CONFIRM = false REGISTER_EMAIL_CONFIRM = false
@ -76,16 +62,12 @@ NO_REPLY_ADDRESS = noreply.example.org
[picture] [picture]
DISABLE_GRAVATAR = false DISABLE_GRAVATAR = false
ENABLE_FEDERATED_AVATAR = false ENABLE_FEDERATED_AVATAR = false
AVATAR_UPLOAD_PATH = tests/{{TEST_TYPE}}/gitea-{{TEST_TYPE}}-sqlite/data/avatars
REPOSITORY_AVATAR_UPLOAD_PATH = tests/{{TEST_TYPE}}/gitea-{{TEST_TYPE}}-sqlite/data/repo-avatars
[session] [session]
PROVIDER = file PROVIDER = file
PROVIDER_CONFIG = tests/{{TEST_TYPE}}/gitea-{{TEST_TYPE}}-sqlite/data/sessions
[log] [log]
MODE = {{TEST_LOGGER}} MODE = {{TEST_LOGGER}}
ROOT_PATH = {{REPO_TEST_DIR}}sqlite-log
ENABLE_SSH_LOG = true ENABLE_SSH_LOG = true
logger.xorm.MODE = file logger.xorm.MODE = file
@ -105,9 +87,6 @@ DISABLE_QUERY_AUTH_TOKEN = true
[oauth2] [oauth2]
JWT_SECRET = KZb_QLUd4fYVyxetjxC4eZkrBgWM2SndOOWDNtgUUko JWT_SECRET = KZb_QLUd4fYVyxetjxC4eZkrBgWM2SndOOWDNtgUUko
[lfs]
PATH = tests/{{TEST_TYPE}}/gitea-{{TEST_TYPE}}-sqlite/data/lfs
[packages] [packages]
ENABLED = true ENABLED = true

View File

@ -24,11 +24,8 @@ import (
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
func InitTest(requireGitea bool) { func InitTest() {
testlogger.Init() testlogger.Init()
setting.SetupGiteaTestEnv()
unittest.InitSettingsForTesting() unittest.InitSettingsForTesting()
setting.Repository.DefaultBranch = "master" // many test code still assume that default branch is called "master" setting.Repository.DefaultBranch = "master" // many test code still assume that default branch is called "master"
@ -38,7 +35,7 @@ func InitTest(requireGitea bool) {
setting.LoadDBSetting() setting.LoadDBSetting()
if err := storage.Init(); err != nil { if err := storage.Init(); err != nil {
testlogger.Fatalf("Init storage failed: %v\n", err) testlogger.Panicf("Init storage failed: %v\n", err)
} }
switch { switch {

View File

@ -44,6 +44,7 @@
"stripInternal": true, "stripInternal": true,
"verbatimModuleSyntax": true, "verbatimModuleSyntax": true,
"types": [ "types": [
"node",
"webpack/module", "webpack/module",
"vitest/globals", "vitest/globals",
"./web_src/js/globals.d.ts", "./web_src/js/globals.d.ts",

View File

@ -1,4 +1,4 @@
import tinycolor from 'tinycolor2'; import {colord} from 'colord';
import {basename, extname, isObject, isDarkTheme} from '../utils.ts'; import {basename, extname, isObject, isDarkTheme} from '../utils.ts';
import {onInputDebounce} from '../utils/dom.ts'; import {onInputDebounce} from '../utils/dom.ts';
import type MonacoNamespace from 'monaco-editor'; import type MonacoNamespace from 'monaco-editor';
@ -94,7 +94,7 @@ function updateTheme(monaco: Monaco): void {
// https://github.com/microsoft/monaco-editor/issues/2427 // https://github.com/microsoft/monaco-editor/issues/2427
// also, monaco can only parse 6-digit hex colors, so we convert the colors to that format // also, monaco can only parse 6-digit hex colors, so we convert the colors to that format
const styles = window.getComputedStyle(document.documentElement); const styles = window.getComputedStyle(document.documentElement);
const getColor = (name: string) => tinycolor(styles.getPropertyValue(name).trim()).toString('hex6'); const getColor = (name: string) => colord(styles.getPropertyValue(name).trim()).alpha(1).toHex();
monaco.editor.defineTheme('gitea', { monaco.editor.defineTheme('gitea', {
base: isDarkTheme() ? 'vs-dark' : 'vs', base: isDarkTheme() ? 'vs-dark' : 'vs',

View File

@ -2,6 +2,7 @@ import {POST} from '../modules/fetch.ts';
import {addDelegatedEventListener, hideElem, isElemVisible, showElem, toggleElem} from '../utils/dom.ts'; import {addDelegatedEventListener, hideElem, isElemVisible, showElem, toggleElem} from '../utils/dom.ts';
import {fomanticQuery} from '../modules/fomantic/base.ts'; import {fomanticQuery} from '../modules/fomantic/base.ts';
import {camelize} from 'vue'; import {camelize} from 'vue';
import {applyAutoFocus} from './common-page.ts';
export function initGlobalButtonClickOnEnter(): void { export function initGlobalButtonClickOnEnter(): void {
addDelegatedEventListener(document, 'keypress', 'div.ui.button, span.ui.button', (el, e: KeyboardEvent) => { addDelegatedEventListener(document, 'keypress', 'div.ui.button, span.ui.button', (el, e: KeyboardEvent) => {
@ -88,7 +89,7 @@ function onShowPanelClick(el: HTMLElement, e: MouseEvent) {
const elems = el.classList.contains('toggle') ? toggleElem(sel) : showElem(sel); const elems = el.classList.contains('toggle') ? toggleElem(sel) : showElem(sel);
for (const elem of elems) { for (const elem of elems) {
if (isElemVisible(elem as HTMLElement)) { if (isElemVisible(elem as HTMLElement)) {
elem.querySelector<HTMLElement>('[autofocus]')?.focus(); applyAutoFocus(elem);
} }
} }
} }

View File

@ -116,12 +116,30 @@ function attachInputDirAuto(el: Partial<HTMLInputElement | HTMLTextAreaElement>)
} }
} }
function autoFocusEnd(el: HTMLInputElement | HTMLTextAreaElement) {
el.focus();
el.setSelectionRange(el.value.length, el.value.length);
}
export function applyAutoFocus(container: Element) {
// https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/autofocus
// "autofocus" behavior is defined by the standard: when a container (e.g.: dialog) becomes visible, focus the element with "autofocus" attribute
// Fomantic UI already supports it for its modal dialog, we need to cover more cases (e.g.: ".show-panel" button)
// Here is just a simple support, we don't expect more than one element that need "autofocus" appearing in the same container
container.querySelector<HTMLElement>('[autofocus]')?.focus();
// Also, apply our autoFocusEnd behavior
// TODO: GLOBAL-INIT-MULTIPLE-FUNCTIONS: use "~=" operator in case we would extend the "data-global-init" to support more functions in the future.
const el = container.querySelector<HTMLInputElement>('[data-global-init~="autoFocusEnd"]');
if (el) autoFocusEnd(el);
}
export function initGlobalInput() { export function initGlobalInput() {
registerGlobalSelectorFunc('input, textarea', attachInputDirAuto); registerGlobalSelectorFunc('input, textarea', attachInputDirAuto);
registerGlobalInitFunc('initInputAutoFocusEnd', (el: HTMLInputElement) => {
el.focus(); // expects only one such element on one page. If there are many, then the last one gets the focus. // autoFocusEnd is used for autofocus an input/textarea and move the cursor to the end of the text.
el.setSelectionRange(el.value.length, el.value.length); // It is useful for "New Issue"/"New PR" pages when the title is pre-filled with prefix text (e.g.: from template or commit message)
}); // The native "autofocus" isn't used because there is a delay between "focused (DOM rendering)" and "move cursor to end (our JS)", it causes flickers.
registerGlobalInitFunc('autoFocusEnd', autoFocusEnd);
} }
/** /**

View File

@ -1,14 +1,25 @@
import {createApp} from 'vue'; import {createApp} from 'vue';
import ActivityHeatmap from '../components/ActivityHeatmap.vue'; import ActivityHeatmap from '../components/ActivityHeatmap.vue';
import {translateMonth, translateDay} from '../utils.ts'; import {translateMonth, translateDay} from '../utils.ts';
import {GET} from '../modules/fetch.ts';
export function initHeatmap() { type HeatmapResponse = {
const el = document.querySelector('#user-heatmap'); heatmapData: Array<[number, number]>; // [[1617235200, 2]] = [unix timestamp, count]
totalContributions: number;
};
export async function initHeatmap() {
const el = document.querySelector<HTMLElement>('#user-heatmap');
if (!el) return; if (!el) return;
try { try {
const url = el.getAttribute('data-heatmap-url')!;
const resp = await GET(url);
if (!resp.ok) throw new Error(`Failed to load heatmap data: ${resp.status} ${resp.statusText}`);
const {heatmapData, totalContributions} = await resp.json() as HeatmapResponse;
const heatmap: Record<string, number> = {}; const heatmap: Record<string, number> = {};
for (const {contributions, timestamp} of JSON.parse(el.getAttribute('data-heatmap-data')!)) { for (const [timestamp, contributions] of heatmapData) {
// Convert to user timezone and sum contributions by date // Convert to user timezone and sum contributions by date
const dateStr = new Date(timestamp * 1000).toDateString(); const dateStr = new Date(timestamp * 1000).toDateString();
heatmap[dateStr] = (heatmap[dateStr] || 0) + contributions; heatmap[dateStr] = (heatmap[dateStr] || 0) + contributions;
@ -18,6 +29,9 @@ export function initHeatmap() {
return {date: new Date(v), count: heatmap[v]}; return {date: new Date(v), count: heatmap[v]};
}); });
const totalFormatted = totalContributions.toLocaleString();
const textTotalContributions = el.getAttribute('data-locale-total-contributions')!.replace('%s', totalFormatted);
// last heatmap tooltip localization attempt https://github.com/go-gitea/gitea/pull/24131/commits/a83761cbbae3c2e3b4bced71e680f44432073ac8 // last heatmap tooltip localization attempt https://github.com/go-gitea/gitea/pull/24131/commits/a83761cbbae3c2e3b4bced71e680f44432073ac8
const locale = { const locale = {
heatMapLocale: { heatMapLocale: {
@ -28,7 +42,7 @@ export function initHeatmap() {
less: el.getAttribute('data-locale-less'), less: el.getAttribute('data-locale-less'),
}, },
tooltipUnit: 'contributions', tooltipUnit: 'contributions',
textTotalContributions: el.getAttribute('data-locale-total-contributions'), textTotalContributions,
noDataText: el.getAttribute('data-locale-no-contributions'), noDataText: el.getAttribute('data-locale-no-contributions'),
}; };

View File

@ -42,6 +42,7 @@ export function registerGlobalInitFunc<T extends HTMLElement>(name: string, hand
} }
function callGlobalInitFunc(el: HTMLElement) { function callGlobalInitFunc(el: HTMLElement) {
// TODO: GLOBAL-INIT-MULTIPLE-FUNCTIONS: maybe in the future we need to extend it to support multiple functions, for example: `data-global-init="func1 func2 func3"`
const initFunc = el.getAttribute('data-global-init')!; const initFunc = el.getAttribute('data-global-init')!;
const func = globalInitFuncs[initFunc]; const func = globalInitFuncs[initFunc];
if (!func) throw new Error(`Global init function "${initFunc}" not found`); if (!func) throw new Error(`Global init function "${initFunc}" not found`);

View File

@ -1,22 +1,21 @@
import tinycolor from 'tinycolor2'; import {colord} from 'colord';
import type {ColorInput} from 'tinycolor2'; import type {AnyColor} from 'colord';
/** Returns relative luminance for a SRGB color - https://en.wikipedia.org/wiki/Relative_luminance */ /** Returns relative luminance for a SRGB color - https://en.wikipedia.org/wiki/Relative_luminance */
// Keep this in sync with modules/util/color.go // Keep this in sync with modules/util/color.go
function getRelativeLuminance(color: ColorInput): number { function getRelativeLuminance(color: AnyColor): number {
const {r, g, b} = tinycolor(color).toRgb(); const {r, g, b} = colord(color).toRgb();
return (0.2126729 * r + 0.7151522 * g + 0.072175 * b) / 255; return (0.2126729 * r + 0.7151522 * g + 0.072175 * b) / 255;
} }
function useLightText(backgroundColor: ColorInput): boolean { function useLightText(backgroundColor: AnyColor): boolean {
return getRelativeLuminance(backgroundColor) < 0.453; return getRelativeLuminance(backgroundColor) < 0.453;
} }
/** Given a background color, returns a black or white foreground color that the highest /** Given a background color, returns a black or white foreground color with the highest contrast ratio. */
* contrast ratio. */
// In the future, the APCA contrast function, or CSS `contrast-color` will be better. // In the future, the APCA contrast function, or CSS `contrast-color` will be better.
// https://github.com/color-js/color.js/blob/eb7b53f7a13bb716ec8b28c7a56f052cd599acd9/src/contrast/APCA.js#L42 // https://github.com/color-js/color.js/blob/eb7b53f7a13bb716ec8b28c7a56f052cd599acd9/src/contrast/APCA.js#L42
export function contrastColor(backgroundColor: ColorInput): string { export function contrastColor(backgroundColor: AnyColor): string {
return useLightText(backgroundColor) ? '#fff' : '#000'; return useLightText(backgroundColor) ? '#fff' : '#000';
} }