0
0
mirror of https://github.com/go-gitea/gitea.git synced 2026-05-07 06:03:48 +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
TEST_TAGS: gogit
TEST_LDAP: 1
USE_REPO_TEST_DIR: 1
test-sqlite:
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
RACE_ENABLED: true
TEST_TAGS: gogit sqlite sqlite_unlock_notify
USE_REPO_TEST_DIR: 1
test-unit:
if: needs.files-changed.outputs.backend == 'true' || needs.files-changed.outputs.actions == 'true'
@ -206,7 +204,6 @@ jobs:
env:
TAGS: bindata
RACE_ENABLED: true
USE_REPO_TEST_DIR: 1
TEST_INDEXER_CODE_ES_URL: "http://elastic:changeme@elasticsearch:9200"
test-mssql:
@ -246,4 +243,3 @@ jobs:
timeout-minutes: 50
env:
TAGS: bindata
USE_REPO_TEST_DIR: 1

1
.gitignore vendored
View File

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

View File

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

View File

@ -197,7 +197,7 @@ Here's how to run the test suite:
## Translation
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. \
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.

View File

@ -1,5 +1,12 @@
# 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
ARG GOPROXY=direct
@ -12,22 +19,19 @@ ARG CGO_EXTRA_CFLAGS
# Build deps
RUN apk --no-cache add \
build-base \
git \
nodejs \
pnpm
git
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.
# ".git" directory will be mounted later separately for getting version data.
# 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.
# 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 is mounted separately later only for version data extraction.
COPY --exclude=.git/ . .
COPY --from=frontend-build /src/public/assets public/assets
# Build gitea, .git mount is required for version data
RUN --mount=type=cache,target=/go/pkg/mod \
--mount=type=cache,target="/root/.cache/go-build" \
--mount=type=cache,target=/root/.local/share/pnpm/store \
--mount=type=bind,source=".git/",target=".git/" \
make
make backend
COPY docker/root /tmp/local

View File

@ -1,5 +1,12 @@
# 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
ARG GOPROXY=direct
@ -12,20 +19,18 @@ ARG CGO_EXTRA_CFLAGS
# Build deps
RUN apk --no-cache add \
build-base \
git \
nodejs \
pnpm
git
WORKDIR ${GOPATH}/src/code.gitea.io/gitea
# See the comments in Dockerfile
COPY --exclude=.git/ . .
COPY --from=frontend-build /src/public/assets public/assets
# Build gitea, .git mount is required for version data
RUN --mount=type=cache,target=/go/pkg/mod \
--mount=type=cache,target="/root/.cache/go-build" \
--mount=type=cache,target=/root/.local/share/pnpm/store \
--mount=type=bind,source=".git/",target=".git/" \
make
make backend
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_DIRS := $(DIST)/binaries $(DIST)/release
IMPORT := code.gitea.io/gitea
# By default use go's 1.25 experimental json v2 library when building
# 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
SWAGGER_PACKAGE ?= github.com/go-swagger/go-swagger/cmd/swagger@v0.33.1
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
ACTIONLINT_PACKAGE ?= github.com/rhysd/actionlint/cmd/actionlint@v1.7.10
@ -84,7 +66,6 @@ endif
EXTRA_GOFLAGS ?=
MAKE_VERSION := $(shell "$(MAKE)" -v | cat | head -n 1)
MAKE_EVIDENCE_DIR := .make_evidence
GOTESTFLAGS ?=
@ -130,7 +111,7 @@ ifeq ($(VERSION),main)
VERSION := main-nightly
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
@ -150,7 +131,6 @@ SVG_DEST_DIR := public/assets/img/svg
AIR_TMP_DIR := .air
GO_LICENSE_TMP_DIR := .go-licenses
GO_LICENSE_FILE := assets/go-licenses.json
TAGS ?=
@ -159,7 +139,7 @@ TAGS_EVIDENCE := $(MAKE_EVIDENCE_DIR)/tags
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
WEB_DIRS := web_src/js web_src/css
@ -229,7 +209,7 @@ clean: ## delete backend and integration files
e2e*.test \
tests/integration/gitea-integration-* \
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/indexers-*/ \
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_LICENSE_FILE): go.mod go.sum
@rm -rf $(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)
GO=$(GO) $(GO) run build/generate-go-licenses.go $(GO_LICENSE_FILE)
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_TYPE}}|$(or $(TEST_TYPE),integration)|g' \
tests/sqlite.ini.tmpl > tests/sqlite.ini
.PHONY: test-sqlite
@ -501,9 +476,8 @@ generate-ini-mysql:
-e 's|{{TEST_MYSQL_DBNAME}}|${TEST_MYSQL_DBNAME}|g' \
-e 's|{{TEST_MYSQL_USERNAME}}|${TEST_MYSQL_USERNAME}|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_TYPE}}|$(or $(TEST_TYPE),integration)|g' \
tests/mysql.ini.tmpl > tests/mysql.ini
.PHONY: test-mysql
@ -524,9 +498,8 @@ generate-ini-pgsql:
-e 's|{{TEST_PGSQL_PASSWORD}}|${TEST_PGSQL_PASSWORD}|g' \
-e 's|{{TEST_PGSQL_SCHEMA}}|${TEST_PGSQL_SCHEMA}|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_TYPE}}|$(or $(TEST_TYPE),integration)|g' \
tests/pgsql.ini.tmpl > tests/pgsql.ini
.PHONY: test-pgsql
@ -545,9 +518,8 @@ generate-ini-mssql:
-e 's|{{TEST_MSSQL_DBNAME}}|${TEST_MSSQL_DBNAME}|g' \
-e 's|{{TEST_MSSQL_USERNAME}}|${TEST_MSSQL_USERNAME}|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_TYPE}}|$(or $(TEST_TYPE),integration)|g' \
tests/mssql.ini.tmpl > tests/mssql.ini
.PHONY: test-mssql
@ -668,7 +640,7 @@ migrations.sqlite.test: $(GO_SOURCES) generate-ini-sqlite
GITEA_TEST_CONF=tests/sqlite.ini ./migrations.sqlite.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)
.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/$*
.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)
.PHONY: migrations.individual.pgsql.test\#%
@ -741,7 +713,7 @@ generate-go: $(TAGS_PREREQ)
.PHONY: security-check
security-check:
GOEXPERIMENT= go run $(GOVULNCHECK_PACKAGE) -show color ./...
GOEXPERIMENT= go run $(GOVULNCHECK_PACKAGE) -show color ./... || true
$(EXECUTABLE): $(GO_SOURCES) $(TAGS_PREREQ)
ifneq ($(and $(STATIC),$(findstring pam,$(TAGS))),)
@ -819,7 +791,6 @@ deps-tools: ## install tool dependencies
$(GO) install $(MISSPELL_PACKAGE) & \
$(GO) install $(SWAGGER_PACKAGE) & \
$(GO) install $(XGO_PACKAGE) & \
$(GO) install $(GO_LICENSES_PACKAGE) & \
$(GO) install $(GOVULNCHECK_PACKAGE) & \
$(GO) install $(ACTIONLINT_PACKAGE) & \
wait
@ -908,9 +879,6 @@ docker:
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" .
# This endif closes the if at the top of the file
endif
# Disable parallel execution because it would break some targets that don't
# specify exact dependencies like 'backend' which does currently not depend
# 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 (
"encoding/json"
"fmt"
"io/fs"
"os"
"path"
"os/exec"
"path/filepath"
"regexp"
"slices"
"sort"
"strings"
"code.gitea.io/gitea/modules/container"
)
// regexp is based on go-license, excluding README and NOTICE
// https://github.com/google/go-licenses/blob/master/licenses/find.go
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 {
Name string `json:"name"`
Path string `json:"path"`
LicenseText string `json:"licenseText"`
}
func main() {
if len(os.Args) != 3 {
fmt.Println("usage: go run generate-go-licenses.go <base-dir> <out-json-file>")
// getModules returns all dependency modules with their local directory paths
// and the package directories used from each module.
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)
}
base, out := os.Args[1], os.Args[2]
// Add ext for excluded files because license_test.go will be included for some reason.
// And there are more files that should be excluded, check with:
//
// go run github.com/google/go-licenses@v1.6.0 save . --force --save_path=.go-licenses 2>/dev/null
// 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
var modules []ModuleInfo
seen := make(map[string]int) // module path -> index in modules
for _, line := range strings.Split(string(output), "\n") {
line = strings.TrimSpace(line)
if line == "" {
continue
}
if entry.IsDir() || !licenseRe.MatchString(entry.Name()) || excludedExt.Contains(filepath.Ext(entry.Name())) {
return nil
parts := strings.Split(line, "\t")
if len(parts) != 3 {
continue
}
paths = append(paths, path)
return nil
})
if err != nil {
panic(err)
modPath, modDir, pkgDir := parts[0], parts[1], parts[2]
if idx, ok := seen[modPath]; ok {
modules[idx].PkgDirs = append(modules[idx].PkgDirs, pkgDir)
} else {
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
for _, filePath := range paths {
licenseText, err := os.ReadFile(filePath)
if err != nil {
panic(err)
for _, entry := range dirEntries {
if entry.IsDir() {
continue
}
pkgPath := filepath.ToSlash(filePath)
pkgPath = strings.TrimPrefix(pkgPath, base+"/")
pkgName := path.Dir(pkgPath)
// 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" {
name := entry.Name()
if !licenseRe.MatchString(name) {
continue
}
if excludedExt[strings.ToLower(filepath.Ext(name))] {
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{
Name: pkgName,
Path: pkgPath,
LicenseText: string(licenseText),
Name: entryName,
Path: entryPath,
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, "", " ")
if err != nil {
panic(err)

View File

@ -2858,6 +2858,9 @@ LEVEL = Info
;ABANDONED_JOB_TIMEOUT = 24h
;; 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]
;; 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-optional-chain': [2],
'@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-require-imports': [2],
'@typescript-eslint/no-restricted-imports': [0],
@ -437,7 +437,7 @@ export default defineConfig([
'no-import-assign': [2],
'no-inline-comments': [0],
'no-inner-declarations': [2],
'no-invalid-regexp': [2],
'no-invalid-regexp': [0], // handled by regexp/no-invalid-regexp
'no-invalid-this': [0],
'no-irregular-whitespace': [2],
'no-iterator': [2],
@ -551,7 +551,7 @@ export default defineConfig([
'no-new-func': [0], // handled by @typescript-eslint/no-implied-eval
'no-new-native-nonconstructor': [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': [0],
'no-nonoctal-decimal-escape': [2],
@ -582,7 +582,7 @@ export default defineConfig([
'no-template-curly-in-string': [2],
'no-ternary': [0],
'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': [2], // it is still needed by eslint & IDE to prompt undefined names in real time
'no-undefined': [0],
@ -600,7 +600,7 @@ export default defineConfig([
'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-useless-assignment': [2],
'no-useless-backreference': [2],
'no-useless-backreference': [0], // handled by regexp/no-useless-backreference
'no-useless-call': [2],
'no-useless-catch': [2],
'no-useless-computed-key': [2],
@ -608,7 +608,7 @@ export default defineConfig([
'no-useless-constructor': [2],
'no-useless-escape': [2],
'no-useless-rename': [2],
'no-useless-return': [2],
'no-useless-return': [0], // handled by sonarjs/no-redundant-jump
'no-var': [2],
'no-void': [2],
'no-warning-comments': [0],
@ -617,7 +617,7 @@ export default defineConfig([
'one-var-declaration-per-line': [0],
'one-var': [0],
'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-const': [2, {destructuring: 'all', ignoreReadBeforeAssign: true}],
'prefer-destructuring': [0],
@ -697,7 +697,7 @@ export default defineConfig([
'regexp/prefer-question-quantifier': [2],
'regexp/prefer-range': [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-set-operation': [2],
'regexp/prefer-star-quantifier': [2],
@ -727,7 +727,7 @@ export default defineConfig([
'sonarjs/no-empty-collection': [2],
'sonarjs/no-extra-arguments': [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-functions': [2, 5],
'sonarjs/no-ignored-return': [2],
@ -740,7 +740,7 @@ export default defineConfig([
'sonarjs/no-small-switch': [0],
'sonarjs/no-unused-collection': [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/prefer-immediate-return': [0],
'sonarjs/prefer-object-literal': [0],
@ -767,6 +767,7 @@ export default defineConfig([
'unicorn/filename-case': [0],
'unicorn/import-index': [0],
'unicorn/import-style': [0],
'unicorn/isolated-functions': [2, {functions: []}],
'unicorn/new-for-builtins': [2],
'unicorn/no-abusive-eslint-disable': [0],
'unicorn/no-anonymous-default-export': [0],
@ -806,7 +807,7 @@ export default defineConfig([
'unicorn/no-unnecessary-await': [2],
'unicorn/no-unnecessary-polyfills': [2],
'unicorn/no-unreadable-array-destructuring': [0],
'unicorn/no-unreadable-iife': [2],
'unicorn/no-unreadable-iife': [0],
'unicorn/no-unused-properties': [2],
'unicorn/no-useless-collection-argument': [2],
'unicorn/no-useless-fallback-in-spread': [2],
@ -819,7 +820,7 @@ export default defineConfig([
'unicorn/number-literal-case': [0],
'unicorn/numeric-separators-style': [0],
'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-map': [2],
'unicorn/prefer-array-index-of': [2],
@ -836,7 +837,7 @@ export default defineConfig([
'unicorn/prefer-event-target': [2],
'unicorn/prefer-export-from': [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-keyboard-event-key': [2],
'unicorn/prefer-logical-operator-over-ternary': [2],
@ -863,7 +864,7 @@ export default defineConfig([
'unicorn/prefer-string-raw': [0],
'unicorn/prefer-string-replace-all': [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-structured-clone': [2],
'unicorn/prefer-switch': [0],

6
flake.lock generated
View File

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

2
go.mod
View File

@ -53,7 +53,7 @@ require (
github.com/go-co-op/gocron v1.37.0
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-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-redsync/redsync/v4 v4.15.0
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-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/v5 v5.16.4 h1:7ajIEZHZJULcyJebDLo99bGgS0jRrOxzZG4uCk2Yb2Y=
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 h1:mdkuqblwr57kVfXri5TTH+nMFLNUxIj9Z7F5ykFbw5s=
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/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
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
var (
Version = "development" // program version for this build
Tags = "" // the Golang build tags
MakeVersion = "" // "make" program version if built with make
Version = "development" // program version for this build
Tags = "" // the Golang build tags
)
func init() {
@ -50,9 +49,6 @@ func main() {
func formatBuiltWith() string {
version := runtime.Version()
if len(MakeVersion) > 0 {
version = MakeVersion + ", " + runtime.Version()
}
if len(Tags) == 0 {
return " built with " + version
}

View File

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

View File

@ -8,6 +8,7 @@ import (
"crypto/subtle"
"errors"
"fmt"
"strings"
"time"
auth_model "code.gitea.io/gitea/models/auth"
@ -20,6 +21,7 @@ import (
runnerv1 "code.gitea.io/actions-proto-go/runner/v1"
lru "github.com/hashicorp/golang-lru/v2"
"github.com/nektos/act/pkg/jobparser"
"google.golang.org/protobuf/types/known/timestamppb"
"xorm.io/builder"
)
@ -214,6 +216,20 @@ func GetRunningTaskByToken(ctx context.Context, token string) (*ActionTask, erro
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) {
ctx, committer, err := db.TxContext(ctx)
if err != nil {
@ -293,9 +309,8 @@ func CreateTaskForRunner(ctx context.Context, runner *ActionRunner) (*ActionTask
if len(workflowJob.Steps) > 0 {
steps := make([]*ActionTaskStep, len(workflowJob.Steps))
for i, v := range workflowJob.Steps {
name := util.EllipsisDisplayString(v.String(), 255)
steps[i] = &ActionTaskStep{
Name: name,
Name: makeTaskStepDisplayName(v, 255),
TaskID: task.ID,
Index: int64(i),
RepoID: task.RepoID,

View File

@ -14,7 +14,7 @@ import (
// ActionTaskStep represents a step of ActionTask
type ActionTaskStep struct {
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)"`
Index int64 `xorm:"index unique(task_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"`
}
// 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) {
return getUserHeatmapData(ctx, user, nil, doer)
}
// GetUserHeatmapDataByUserTeam returns an array of UserHeatmapData
func GetUserHeatmapDataByUserTeam(ctx context.Context, user *user_model.User, team *organization.Team, doer *user_model.User) ([]*UserHeatmapData, error) {
return getUserHeatmapData(ctx, user, team, doer)
// GetUserHeatmapDataByOrgTeam returns an array of UserHeatmapData, it checks whether doer can access org's activity
func GetUserHeatmapDataByOrgTeam(ctx context.Context, org *organization.Organization, team *organization.Team, doer *user_model.User) ([]*UserHeatmapData, error) {
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) {
@ -71,12 +71,3 @@ func getUserHeatmapData(ctx context.Context, user *user_model.User, team *organi
OrderBy("timestamp").
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
need_approval: 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
title: "update actions"

View File

@ -129,20 +129,7 @@
status: 5
started: 1683636528
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
run_id: 805

View File

@ -177,26 +177,7 @@
log_length: 0
log_size: 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
attempt: 1

View File

@ -397,10 +397,16 @@ func RenameBranch(ctx context.Context, repo *repo_model.Repository, from, to str
if protectedBranch != nil {
// there is a protect rule for this branch
protectedBranch.RuleName = to
if _, err = sess.ID(protectedBranch.ID).Cols("branch_name").Update(protectedBranch); err != nil {
existingRule, err := GetProtectedBranchRuleByName(ctx, repo.ID, to)
if err != nil {
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 {
// some glob protect rules may match this branch
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) {
assert.NoError(t, unittest.PrepareTestDatabase())

View File

@ -10,7 +10,6 @@ import (
"path"
"path/filepath"
"testing"
"time"
"code.gitea.io/gitea/models/db"
"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
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) {
if err := db.InitEngine(t.Context()); err != nil {
return nil, err
@ -213,13 +200,12 @@ func LoadTableSchemasMap(t *testing.T, x *xorm.Engine) map[string]*schemas.Table
return tableMap
}
func MainTest(m *testing.M) {
func mainTest(m *testing.M) int {
testlogger.Init()
setting.SetupGiteaTestEnv()
tmpDataPath, cleanup, err := tempdir.OsTempDir("gitea-test").MkdirTempRandom("data")
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()
@ -227,15 +213,13 @@ func MainTest(m *testing.M) {
unittest.InitSettingsForTesting()
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.InitLoggersForTest()
exitStatus := m.Run()
if err := removeAllWithRetry(setting.RepoRootPath); err != nil {
_, _ = fmt.Fprintf(os.Stderr, "os.RemoveAll: %v\n", err)
}
os.Exit(exitStatus)
return m.Run()
}
func MainTest(m *testing.M) {
os.Exit(mainTest(m))
}

View File

@ -4,6 +4,7 @@
package unittest_test
import (
"os"
"path/filepath"
"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 {
_ = user_model.User{}
giteaRoot := setting.SetupGiteaTestEnv()
giteaRoot := setting.GetGiteaTestSourceRoot()
opts := unittest.FixturesOptions{Dir: filepath.Join(giteaRoot, "models", "fixtures"), Files: []string{
"user.yml",
}}

View File

@ -4,10 +4,12 @@
package unittest
import (
"errors"
"os"
"path/filepath"
"strings"
"code.gitea.io/gitea/modules/setting"
"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.
// It returns error when error occurs in underlying functions.
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 {
return err
}

View File

@ -21,6 +21,7 @@ import (
"code.gitea.io/gitea/modules/setting/config"
"code.gitea.io/gitea/modules/storage"
"code.gitea.io/gitea/modules/tempdir"
"code.gitea.io/gitea/modules/testlogger"
"code.gitea.io/gitea/modules/util"
"github.com/stretchr/testify/assert"
@ -28,16 +29,10 @@ import (
"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
func InitSettingsForTesting() {
setting.IsInTesting = true
setting.SetupGiteaTestEnv()
log.OsExiter = func(code int) {
if code != 0 {
// 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")
_ = 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 {
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
// test database. Creates the test database, and sets necessary settings.
func MainTest(m *testing.M, testOptsArg ...*TestOptions) {
testOpts := util.OptionalArg(testOptsArg, &TestOptions{})
giteaRoot = setting.SetupGiteaTestEnv()
InitSettingsForTesting()
os.Exit(mainTest(m, testOptsArg...))
}
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}
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.Domain = "try.gitea.io"
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"
repoRootPath, cleanup1, err := tempdir.OsTempDir("gitea-test").MkdirTempRandom("repos")
if err != nil {
fatalTestError("TempDir: %v\n", err)
testlogger.Panicf("TempDir: %v\n", err)
}
defer cleanup1()
setting.RepoRootPath = repoRootPath
appDataPath, cleanup2, err := tempdir.OsTempDir("gitea-test").MkdirTempRandom("appdata")
if err != nil {
fatalTestError("TempDir: %v\n", err)
testlogger.Panicf("TempDir: %v\n", err)
}
defer cleanup2()
setting.AppDataPath = appDataPath
setting.AppWorkPath = giteaRoot
setting.StaticRootPath = giteaRoot
setting.GravatarSource = "https://secure.gravatar.com/avatar/"
setting.Attachment.Storage.Path = filepath.Join(setting.AppDataPath, "attachments")
@ -129,22 +128,22 @@ func MainTest(m *testing.M, testOptsArg ...*TestOptions) {
config.SetDynGetter(system.NewDatabaseDynKeyGetter())
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 {
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 {
fatalTestError("util.SyncDirs: %v\n", err)
testlogger.Panicf("util.SyncDirs: %v\n", err)
}
if err = git.InitFull(); err != nil {
fatalTestError("git.Init: %v\n", err)
testlogger.Panicf("git.Init: %v\n", err)
}
if testOpts.SetUp != 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 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
@ -196,7 +195,6 @@ func PrepareTestDatabase() error {
// by tests that use the above MainTest(..) function.
func PrepareTestEnv(t testing.TB) {
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))
setting.SetupGiteaTestEnv()
}

View File

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

View File

@ -7,12 +7,83 @@ import (
"testing"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/setting"
api "code.gitea.io/gitea/modules/structs"
webhook_module "code.gitea.io/gitea/modules/webhook"
"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) {
testCases := []struct {
desc string

View File

@ -37,6 +37,10 @@ type CommitSignature struct {
// Message returns the commit message. Same as retrieving CommitMessage directly.
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
}

View File

@ -17,6 +17,7 @@ import (
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/tempdir"
"code.gitea.io/gitea/modules/testlogger"
"github.com/hashicorp/go-version"
)
@ -185,21 +186,19 @@ func InitFull() (err error) {
// 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
func RunGitTests(m interface{ Run() int }) {
fatalf := func(exitCode int, format string, args ...any) {
_, _ = fmt.Fprintf(os.Stderr, format, args...)
os.Exit(exitCode)
}
os.Exit(runGitTests(m))
}
func runGitTests(m interface{ Run() int }) int {
gitHomePath, cleanup, err := tempdir.OsTempDir("gitea-test").MkdirTempRandom("git-home")
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()
setting.Git.HomePath = gitHomePath
if err = InitFull(); err != nil {
fatalf(1, "failed to call Init: %s", err.Error())
}
if exitCode := m.Run(); exitCode != 0 {
fatalf(exitCode, "run test failed, ExitCode=%d", exitCode)
testlogger.Panicf("failed to call Init: %s", err.Error())
}
return m.Run()
}

View File

@ -12,23 +12,27 @@ import (
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/tempdir"
"code.gitea.io/gitea/modules/testlogger"
"github.com/stretchr/testify/assert"
"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.
// "setting.Git.HomePath" is initialized in "git" package but really used in "gitcmd" package
gitHomePath, cleanup, err := tempdir.OsTempDir("gitea-test").MkdirTempRandom("git-home")
if err != nil {
_, _ = fmt.Fprintf(os.Stderr, "unable to create temp dir: %v", err)
os.Exit(1)
testlogger.Panicf("failed to create temp dir: %v", err)
}
defer cleanup()
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) {

View File

@ -13,12 +13,13 @@ import (
func TestMain(m *testing.M) {
// resolve repository path relative to the test directory
testRootDir := setting.SetupGiteaTestEnv()
setting.SetupGiteaTestEnv()
giteaRoot := setting.GetGiteaTestSourceRoot()
repoPath = func(repo Repository) string {
if filepath.IsAbs(repo.RelativePath()) {
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)
}

View File

@ -8,7 +8,6 @@ import (
"io"
"net/http"
"net/url"
"os"
"strings"
"testing"
"time"
@ -20,10 +19,6 @@ const (
dummyToken = "10000000-aaaa-bbbb-cccc-000000000001"
)
func TestMain(m *testing.M) {
os.Exit(m.Run())
}
type mockTransport struct{}
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)
}
kv := make(map[any]any)
kv["_old_uid"] = "0"
return NewVirtualStore(o, sid, kv), nil
}
@ -160,7 +159,7 @@ func (s *VirtualStore) Release() error {
// Now need to lock the provider
s.p.lock.Lock()
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!
realProvider := s.p.provider

View File

@ -4,6 +4,7 @@
package setting
import (
"errors"
"fmt"
"strings"
"time"
@ -25,10 +26,12 @@ var (
EndlessTaskTimeout time.Duration `ini:"ENDLESS_TASK_TIMEOUT"`
AbandonedJobTimeout time.Duration `ini:"ABANDONED_JOB_TIMEOUT"`
SkipWorkflowStrings []string `ini:"SKIP_WORKFLOW_STRINGS"`
WorkflowDirs []string `ini:"WORKFLOW_DIRS"`
}{
Enabled: true,
DefaultActionsURL: defaultActionsURLGitHub,
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)
}
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
}

View File

@ -97,6 +97,65 @@ STORAGE_TYPE = minio
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) {
oldActions := Actions
oldAppURL := AppURL

View File

@ -13,7 +13,18 @@ import (
"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")
if giteaRoot == "" {
_, filename, _, _ := runtime.Caller(0)
@ -27,6 +38,7 @@ func SetupGiteaTestEnv() string {
appWorkPathBuiltin = giteaRoot
AppWorkPath = giteaRoot
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 := 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
_ = os.Setenv("GITEA_ROOT", giteaRoot)
_ = os.Setenv("GITEA_CONF", giteaConf) // test fixture git hooks use "$GITEA_ROOT/$GITEA_CONF" in their scripts
return giteaRoot
giteaTestSourceRoot = &giteaRoot
}

View File

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

View File

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

View File

@ -98,6 +98,20 @@ func (h HookEventType) IsPullRequest() bool {
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
type HookType = string

View File

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

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"
"encoding/csv"
"errors"
"fmt"
"io"
"net/http"
"net/url"
@ -426,6 +425,36 @@ func ParseCompareInfo(ctx *context.Context) *git_service.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
func PrepareCompareDiff(
ctx *context.Context,
@ -539,30 +568,7 @@ func PrepareCompareDiff(
ctx.Data["Commits"] = commits
ctx.Data["CommitCount"] = len(commits)
title := ci.HeadRef.ShortName()
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["title"], ctx.Data["content"] = prepareNewPullRequestTitleContent(ci, commits)
ctx.Data["Username"] = ci.HeadRepo.OwnerName
ctx.Data["Reponame"] = ci.HeadRepo.Name

View File

@ -4,9 +4,16 @@
package repo
import (
"strings"
"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"
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"
"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(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"
)
// getDashboardContextUser finds out which context user dashboard is being viewed as .
func getDashboardContextUser(ctx *context.Context) *user_model.User {
// prepareDashboardContextUserOrgTeams finds out which context user dashboard is being viewed as .
func prepareDashboardContextUserOrgTeams(ctx *context.Context) *user_model.User {
ctxUser := ctx.Doer
orgName := ctx.PathParam("org")
if len(orgName) > 0 {
@ -76,7 +76,7 @@ func getDashboardContextUser(ctx *context.Context) *user_model.User {
// Dashboard render the dashboard page
func Dashboard(ctx *context.Context) {
ctxUser := getDashboardContextUser(ctx)
ctxUser := prepareDashboardContextUserOrgTeams(ctx)
if ctx.Written() {
return
}
@ -109,15 +109,7 @@ func Dashboard(ctx *context.Context) {
"uid": uid,
}
if setting.Service.EnableUserHeatmap {
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)
}
prepareHeatmapURL(ctx)
feeds, count, err := feed_service.GetFeedsForDashboard(ctx, activities_model.GetFeedsOptions{
RequestedUser: ctxUser,
@ -156,7 +148,7 @@ func Milestones(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("milestones")
ctx.Data["PageIsMilestonesDashboard"] = true
ctxUser := getDashboardContextUser(ctx)
ctxUser := prepareDashboardContextUserOrgTeams(ctx)
if ctx.Written() {
return
}
@ -371,7 +363,7 @@ func buildIssueOverview(ctx *context.Context, unitType unit.Type) {
// Return with NotFound or ServerError if unsuccessful.
// ----------------------------------------------------
ctxUser := getDashboardContextUser(ctx)
ctxUser := prepareDashboardContextUserOrgTeams(ctx)
if ctx.Written() {
return
}

View File

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

View File

@ -888,6 +888,8 @@ func registerWebRoutes(m *web.Router) {
m.Group("/{org}", func() {
m.Get("/dashboard", 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/{team}", user.Issues)
m.Get("/pulls", user.Pulls)
@ -1024,6 +1026,7 @@ func registerWebRoutes(m *web.Router) {
}
m.Get("/repositories", org.Repositories)
m.Get("/heatmap", user.DashboardHeatmap)
m.Group("/projects", 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")
}
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:
event = string(run.Event)
commitID = run.CommitSHA

View File

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

View File

@ -16,10 +16,14 @@ import (
"code.gitea.io/gitea/modules/util"
)
type themeCollection struct {
themeList []*ThemeMetaInfo
themeMap map[string]*ThemeMetaInfo
}
var (
availableThemes []*ThemeMetaInfo
availableThemeMap map[string]*ThemeMetaInfo
themeOnce sync.Once
themeMu sync.RWMutex
availableThemes *themeCollection
)
const (
@ -129,23 +133,13 @@ func parseThemeMetaInfo(fileName, cssContent string) *ThemeMetaInfo {
return themeInfo
}
func initThemes() {
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)
}
}()
func loadThemesFromAssets() (themeList []*ThemeMetaInfo, themeMap map[string]*ThemeMetaInfo) {
cssFiles, err := public.AssetFS().ListFiles("assets/css")
if err != nil {
log.Error("Failed to list themes: %v", err)
availableThemes = []*ThemeMetaInfo{defaultThemeMetaInfoByInternalName(setting.UI.DefaultTheme)}
return
return nil, nil
}
var foundThemes []*ThemeMetaInfo
for _, fileName := range cssFiles {
if strings.HasPrefix(fileName, fileNamePrefix) && strings.HasSuffix(fileName, fileNameSuffix) {
@ -157,39 +151,84 @@ func initThemes() {
foundThemes = append(foundThemes, parseThemeMetaInfo(fileName, util.UnsafeBytesToString(content)))
}
}
themeList = foundThemes
if len(setting.UI.Themes) > 0 {
themeList = nil // only allow the themes specified in the setting
allowedThemes := container.SetOf(setting.UI.Themes...)
for _, theme := range foundThemes {
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
}
if availableThemes[i].ColorblindType != availableThemes[j].ColorblindType {
return availableThemes[i].ColorblindType < availableThemes[j].ColorblindType
if themeList[i].ColorblindType != themeList[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")
availableThemes = []*ThemeMetaInfo{defaultThemeMetaInfoByInternalName(setting.UI.DefaultTheme)}
themeMap = map[string]*ThemeMetaInfo{}
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 {
themeOnce.Do(initThemes)
return availableThemes
themes, _ := getAvailableThemes()
return themes
}
func GetThemeMetaInfo(internalName string) *ThemeMetaInfo {
themeOnce.Do(initThemes)
return availableThemeMap[internalName]
_, themeMap := getAvailableThemes()
return themeMap[internalName]
}
// 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="ui segment content tw-my-0 avatar-content-left-arrow">
<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"}}"
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 id="user-heatmap" class="is-loading"
data-heatmap-data="{{JsonUtils.EncodeToString .HeatmapData}}"
data-locale-total-contributions="{{ctx.Locale.Tr "heatmap.number_of_contributions_in_the_last_12_months" (ctx.Locale.PrettyNumber .HeatmapTotalContributions)}}"
data-heatmap-url="{{.HeatmapURL}}"
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-more="{{ctx.Locale.Tr "heatmap.more"}}"
data-locale-less="{{ctx.Locale.Tr "heatmap.less"}}"

View File

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

View File

@ -9,8 +9,8 @@ import (
"net/url"
"testing"
actions_model "code.gitea.io/gitea/models/actions"
auth_model "code.gitea.io/gitea/models/auth"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unittest"
user_model "code.gitea.io/gitea/models/user"
@ -19,30 +19,52 @@ import (
func TestActionsCollaborativeOwner(t *testing.T) {
onGiteaRun(t, func(t *testing.T, u *url.URL) {
// user2 is the owner of "reusable_workflow" repo
user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
user2Session := loginUser(t, user2.Name)
// user2 is the owner of the private "reusable_workflow" repo
user2Session := loginUser(t, "user2")
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
user10 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 10})
// task id is 55 and its repo_id=6
task := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionTask{ID: 55, RepoID: 6})
taskToken := "674f727a81ed2f195bccab036cccf86a182199eb"
tokenHash := auth_model.HashToken(taskToken, task.TokenSalt)
assert.Equal(t, task.TokenHash, tokenHash)
// user4 is the owner of the private caller repo
user4 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4})
user4Session := loginUser(t, user4.Name)
user4Token := getTokenForLoggedInUser(t, user4Session, auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser)
apiCallerRepo := createActionsTestRepo(t, user4Token, "caller_workflow", true)
callerRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: apiCallerRepo.ID})
// 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()
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)
// the git clone will fail
doGitCloneFail(u)(t)
// 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{
"collaborative_owner": user10.Name,
req := NewRequestWithValues(t, "POST", fmt.Sprintf("/%s/%s/settings/actions/general/collaborative_owner/add", reusableWorkflowRepo.OwnerName, reusableWorkflowRepo.Name), map[string]string{
"collaborative_owner": user4.Name,
})
user2Session.MakeRequest(t, req, http.StatusOK)
@ -50,7 +72,7 @@ func TestActionsCollaborativeOwner(t *testing.T) {
doGitClone(dstPath, u)(t)
// 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)
// 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)
}
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) {
onGiteaRun(t, func(t *testing.T, u *url.URL) {
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.
// SPDX-License-Identifier: MIT
//nolint:forbidigo // use of print functions is allowed in tests
package integration
import (
@ -27,6 +26,7 @@ import (
"code.gitea.io/gitea/modules/json"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/testlogger"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/web"
"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()
managerCtx, cancel := context.WithCancel(context.Background())
graceful.InitManager(managerCtx)
defer cancel()
tests.InitTest(true)
tests.InitTest()
testWebRoutes = routers.NormalRoutes()
err := unittest.InitFixtures(
@ -95,8 +95,7 @@ func TestMain(m *testing.M) {
},
)
if err != nil {
fmt.Printf("Error initializing test database: %v\n", err)
os.Exit(1)
testlogger.Panicf("InitFixtures: %v", err)
}
// 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()
if err = util.RemoveAll(setting.Indexer.IssuePath); err != nil {
fmt.Printf("util.RemoveAll: %v\n", err)
os.Exit(1)
log.Error("Failed to remove indexer path: %v", err)
}
if err = util.RemoveAll(setting.Indexer.RepoPath); err != nil {
fmt.Printf("Unable to remove repo indexer: %v\n", err)
os.Exit(1)
log.Error("Failed to remove indexer path: %v", err)
}
return exitCode
}
os.Exit(exitCode)
func TestMain(m *testing.M) {
os.Exit(testMain(m))
}
type TestSession struct {

View File

@ -36,8 +36,6 @@ var currentEngine *xorm.Engine
func initMigrationTest(t *testing.T) func() {
testlogger.Init()
setting.SetupGiteaTestEnv()
unittest.InitSettingsForTesting()
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) {
relURL := "/" + path.Join(ownerName, repoName, "branches")
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
RUN_MODE = prod
@ -11,11 +12,9 @@ SSL_MODE = disable
[indexer]
REPO_INDEXER_ENABLED = true
REPO_INDEXER_PATH = tests/{{TEST_TYPE}}/gitea-{{TEST_TYPE}}-mssql/indexers/repos.bleve
[queue.issue_indexer]
TYPE = level
DATADIR = tests/{{TEST_TYPE}}/gitea-{{TEST_TYPE}}-mssql/indexers/issues.queue
[queue]
TYPE = immediate
@ -29,15 +28,6 @@ TYPE = immediate
[queue.webhook_sender]
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]
SIGNING_KEY = none
@ -53,14 +43,13 @@ START_SSH_SERVER = true
LFS_START_SERVER = true
OFFLINE_MODE = false
LFS_JWT_SECRET = Tv_MjmZuHqpIY6GFl12ebgkRAMt4RlWt0v4EHKSXO0w
APP_DATA_PATH = tests/{{TEST_TYPE}}/gitea-{{TEST_TYPE}}-mssql/data
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=
[mailer]
ENABLED = true
PROTOCOL = dummy
FROM = mssql-{{TEST_TYPE}}-test@gitea.io
FROM = mssql-integration-test@gitea.io
[service]
REGISTER_EMAIL_CONFIRM = false
@ -76,16 +65,12 @@ ENABLE_NOTIFY_MAIL = true
[picture]
DISABLE_GRAVATAR = 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]
PROVIDER = file
PROVIDER_CONFIG = tests/{{TEST_TYPE}}/gitea-{{TEST_TYPE}}-mssql/data/sessions
[log]
MODE = {{TEST_LOGGER}}
ROOT_PATH = {{REPO_TEST_DIR}}mssql-log
ENABLE_SSH_LOG = true
logger.xorm.MODE = file

View File

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

View File

@ -1,3 +1,4 @@
WORK_PATH = {{WORK_PATH}}
APP_NAME = Gitea: Git with a cup of tea
RUN_MODE = prod
@ -12,11 +13,9 @@ SSL_MODE = disable
[indexer]
REPO_INDEXER_ENABLED = true
REPO_INDEXER_PATH = tests/{{TEST_TYPE}}/gitea-{{TEST_TYPE}}-pgsql/indexers/repos.bleve
[queue.issue_indexer]
TYPE = level
DATADIR = tests/{{TEST_TYPE}}/gitea-{{TEST_TYPE}}-pgsql/indexers/issues.queue
[queue]
TYPE = immediate
@ -30,15 +29,6 @@ TYPE = immediate
[queue.webhook_sender]
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]
SIGNING_KEY = none
@ -54,14 +44,13 @@ START_SSH_SERVER = true
LFS_START_SERVER = true
OFFLINE_MODE = false
LFS_JWT_SECRET = Tv_MjmZuHqpIY6GFl12ebgkRAMt4RlWt0v4EHKSXO0w
APP_DATA_PATH = tests/{{TEST_TYPE}}/gitea-{{TEST_TYPE}}-pgsql/data
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=
[mailer]
ENABLED = true
PROTOCOL = dummy
FROM = pgsql-{{TEST_TYPE}}-test@gitea.io
FROM = pgsql-integration-test@gitea.io
[service]
REGISTER_EMAIL_CONFIRM = false
@ -77,16 +66,12 @@ ENABLE_NOTIFY_MAIL = true
[picture]
DISABLE_GRAVATAR = 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]
PROVIDER = file
PROVIDER_CONFIG = tests/{{TEST_TYPE}}/gitea-{{TEST_TYPE}}-pgsql/data/sessions
[log]
MODE = {{TEST_LOGGER}}
ROOT_PATH = {{REPO_TEST_DIR}}pgsql-log
ENABLE_SSH_LOG = true
logger.xorm.MODE = file

View File

@ -1,17 +1,16 @@
WORK_PATH = {{WORK_PATH}}
APP_NAME = Gitea: Git with a cup of tea
RUN_MODE = prod
[database]
DB_TYPE = sqlite3
PATH = tests/{{TEST_TYPE}}/gitea-{{TEST_TYPE}}-sqlite/gitea.db
PATH = gitea.db
[indexer]
REPO_INDEXER_ENABLED = true
REPO_INDEXER_PATH = tests/{{TEST_TYPE}}/gitea-{{TEST_TYPE}}-sqlite/indexers/repos.bleve
[queue.issue_indexer]
TYPE = level
DATADIR = tests/{{TEST_TYPE}}/gitea-{{TEST_TYPE}}-sqlite/indexers/issues.queue
[queue]
TYPE = immediate
@ -25,15 +24,6 @@ TYPE = immediate
[queue.webhook_sender]
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]
SIGNING_KEY = none
@ -49,18 +39,14 @@ START_SSH_SERVER = true
LFS_START_SERVER = true
OFFLINE_MODE = false
LFS_JWT_SECRET = Tv_MjmZuHqpIY6GFl12ebgkRAMt4RlWt0v4EHKSXO0w
APP_DATA_PATH = tests/{{TEST_TYPE}}/gitea-{{TEST_TYPE}}-sqlite/data
ENABLE_GZIP = true
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=
[attachment]
PATH = tests/{{TEST_TYPE}}/gitea-{{TEST_TYPE}}-sqlite/data/attachments
[mailer]
ENABLED = true
PROTOCOL = dummy
FROM = sqlite-{{TEST_TYPE}}-test@gitea.io
FROM = sqlite-integration-test@gitea.io
[service]
REGISTER_EMAIL_CONFIRM = false
@ -76,16 +62,12 @@ NO_REPLY_ADDRESS = noreply.example.org
[picture]
DISABLE_GRAVATAR = 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]
PROVIDER = file
PROVIDER_CONFIG = tests/{{TEST_TYPE}}/gitea-{{TEST_TYPE}}-sqlite/data/sessions
[log]
MODE = {{TEST_LOGGER}}
ROOT_PATH = {{REPO_TEST_DIR}}sqlite-log
ENABLE_SSH_LOG = true
logger.xorm.MODE = file
@ -105,9 +87,6 @@ DISABLE_QUERY_AUTH_TOKEN = true
[oauth2]
JWT_SECRET = KZb_QLUd4fYVyxetjxC4eZkrBgWM2SndOOWDNtgUUko
[lfs]
PATH = tests/{{TEST_TYPE}}/gitea-{{TEST_TYPE}}-sqlite/data/lfs
[packages]
ENABLED = true

View File

@ -24,11 +24,8 @@ import (
"github.com/stretchr/testify/assert"
)
func InitTest(requireGitea bool) {
func InitTest() {
testlogger.Init()
setting.SetupGiteaTestEnv()
unittest.InitSettingsForTesting()
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()
if err := storage.Init(); err != nil {
testlogger.Fatalf("Init storage failed: %v\n", err)
testlogger.Panicf("Init storage failed: %v\n", err)
}
switch {

View File

@ -44,6 +44,7 @@
"stripInternal": true,
"verbatimModuleSyntax": true,
"types": [
"node",
"webpack/module",
"vitest/globals",
"./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 {onInputDebounce} from '../utils/dom.ts';
import type MonacoNamespace from 'monaco-editor';
@ -94,7 +94,7 @@ function updateTheme(monaco: Monaco): void {
// 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
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', {
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 {fomanticQuery} from '../modules/fomantic/base.ts';
import {camelize} from 'vue';
import {applyAutoFocus} from './common-page.ts';
export function initGlobalButtonClickOnEnter(): void {
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);
for (const elem of elems) {
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() {
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.
el.setSelectionRange(el.value.length, el.value.length);
});
// autoFocusEnd is used for autofocus an input/textarea and move the cursor to the end of the text.
// 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 ActivityHeatmap from '../components/ActivityHeatmap.vue';
import {translateMonth, translateDay} from '../utils.ts';
import {GET} from '../modules/fetch.ts';
export function initHeatmap() {
const el = document.querySelector('#user-heatmap');
type HeatmapResponse = {
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;
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> = {};
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
const dateStr = new Date(timestamp * 1000).toDateString();
heatmap[dateStr] = (heatmap[dateStr] || 0) + contributions;
@ -18,6 +29,9 @@ export function initHeatmap() {
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
const locale = {
heatMapLocale: {
@ -28,7 +42,7 @@ export function initHeatmap() {
less: el.getAttribute('data-locale-less'),
},
tooltipUnit: 'contributions',
textTotalContributions: el.getAttribute('data-locale-total-contributions'),
textTotalContributions,
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) {
// 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 func = globalInitFuncs[initFunc];
if (!func) throw new Error(`Global init function "${initFunc}" not found`);

View File

@ -1,22 +1,21 @@
import tinycolor from 'tinycolor2';
import type {ColorInput} from 'tinycolor2';
import {colord} from 'colord';
import type {AnyColor} from 'colord';
/** Returns relative luminance for a SRGB color - https://en.wikipedia.org/wiki/Relative_luminance */
// Keep this in sync with modules/util/color.go
function getRelativeLuminance(color: ColorInput): number {
const {r, g, b} = tinycolor(color).toRgb();
function getRelativeLuminance(color: AnyColor): number {
const {r, g, b} = colord(color).toRgb();
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;
}
/** Given a background color, returns a black or white foreground color that the highest
* contrast ratio. */
/** Given a background color, returns a black or white foreground color with the highest contrast ratio. */
// 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
export function contrastColor(backgroundColor: ColorInput): string {
export function contrastColor(backgroundColor: AnyColor): string {
return useLightText(backgroundColor) ? '#fff' : '#000';
}