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:
commit
415421ea71
4
.github/workflows/pull-db-tests.yml
vendored
4
.github/workflows/pull-db-tests.yml
vendored
@ -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
1
.gitignore
vendored
@ -89,7 +89,6 @@ cpu.out
|
||||
/vendor
|
||||
/VERSION
|
||||
/.air
|
||||
/.go-licenses
|
||||
|
||||
# Files and folders that were previously generated
|
||||
/public/assets/img/webpack
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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.
|
||||
|
||||
22
Dockerfile
22
Dockerfile
@ -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
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
54
Makefile
54
Makefile
@ -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
142
assets/go-licenses.json
generated
File diff suppressed because one or more lines are too long
@ -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("----")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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)
|
||||
|
||||
@ -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
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
@ -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
6
flake.lock
generated
@ -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
2
go.mod
@ -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
4
go.sum
@ -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=
|
||||
|
||||
8
main.go
8
main.go
@ -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
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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"`
|
||||
|
||||
76
models/actions/task_test.go
Normal file
76
models/actions/task_test.go
Normal 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)
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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())
|
||||
|
||||
|
||||
@ -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))
|
||||
}
|
||||
|
||||
@ -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",
|
||||
}}
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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()
|
||||
}
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
|
||||
@ -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()
|
||||
}
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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...))
|
||||
}
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
45
package.json
45
package.json
@ -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
826
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
1
public/assets/img/svg/octicon-book-locked.svg
generated
Normal file
1
public/assets/img/svg/octicon-book-locked.svg
generated
Normal 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 |
1
public/assets/img/svg/octicon-comment-locked.svg
generated
Normal file
1
public/assets/img/svg/octicon-comment-locked.svg
generated
Normal 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 |
1
public/assets/img/svg/octicon-git-pull-request-locked.svg
generated
Normal file
1
public/assets/img/svg/octicon-git-pull-request-locked.svg
generated
Normal 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 |
1
public/assets/img/svg/octicon-issue-locked.svg
generated
Normal file
1
public/assets/img/svg/octicon-issue-locked.svg
generated
Normal 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 |
@ -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),
|
||||
|
||||
47
routers/web/repo/actions/view_test.go
Normal file
47
routers/web/repo/actions/view_test.go
Normal 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)
|
||||
}
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
66
routers/web/user/heatmap.go
Normal file
66
routers/web/user/heatmap.go
Normal 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)
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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() {
|
||||
|
||||
@ -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
|
||||
|
||||
@ -18,7 +18,6 @@ import (
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
unittest.MainTest(m)
|
||||
os.Exit(m.Run())
|
||||
}
|
||||
|
||||
func TestInitToken(t *testing.T) {
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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}}"
|
||||
>
|
||||
|
||||
@ -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"}}"
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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})
|
||||
|
||||
59
tests/integration/heatmap_test.go
Normal file
59
tests/integration/heatmap_test.go
Normal 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")
|
||||
})
|
||||
}
|
||||
@ -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 {
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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><i>XSS PR</i></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, "<u>XSS PR</u>", 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{
|
||||
|
||||
@ -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(),
|
||||
)
|
||||
}
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -44,6 +44,7 @@
|
||||
"stripInternal": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
"types": [
|
||||
"node",
|
||||
"webpack/module",
|
||||
"vitest/globals",
|
||||
"./web_src/js/globals.d.ts",
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -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'),
|
||||
};
|
||||
|
||||
|
||||
@ -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`);
|
||||
|
||||
@ -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';
|
||||
}
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user