mirror of
https://github.com/go-gitea/gitea.git
synced 2026-05-11 22:15:38 +02:00
Merge branch 'main' into aipolicy
This commit is contained in:
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
|
RACE_ENABLED: true
|
||||||
TEST_TAGS: gogit
|
TEST_TAGS: gogit
|
||||||
TEST_LDAP: 1
|
TEST_LDAP: 1
|
||||||
USE_REPO_TEST_DIR: 1
|
|
||||||
|
|
||||||
test-sqlite:
|
test-sqlite:
|
||||||
if: needs.files-changed.outputs.backend == 'true' || needs.files-changed.outputs.actions == 'true'
|
if: needs.files-changed.outputs.backend == 'true' || needs.files-changed.outputs.actions == 'true'
|
||||||
@ -90,7 +89,6 @@ jobs:
|
|||||||
TAGS: bindata gogit sqlite sqlite_unlock_notify
|
TAGS: bindata gogit sqlite sqlite_unlock_notify
|
||||||
RACE_ENABLED: true
|
RACE_ENABLED: true
|
||||||
TEST_TAGS: gogit sqlite sqlite_unlock_notify
|
TEST_TAGS: gogit sqlite sqlite_unlock_notify
|
||||||
USE_REPO_TEST_DIR: 1
|
|
||||||
|
|
||||||
test-unit:
|
test-unit:
|
||||||
if: needs.files-changed.outputs.backend == 'true' || needs.files-changed.outputs.actions == 'true'
|
if: needs.files-changed.outputs.backend == 'true' || needs.files-changed.outputs.actions == 'true'
|
||||||
@ -206,7 +204,6 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
TAGS: bindata
|
TAGS: bindata
|
||||||
RACE_ENABLED: true
|
RACE_ENABLED: true
|
||||||
USE_REPO_TEST_DIR: 1
|
|
||||||
TEST_INDEXER_CODE_ES_URL: "http://elastic:changeme@elasticsearch:9200"
|
TEST_INDEXER_CODE_ES_URL: "http://elastic:changeme@elasticsearch:9200"
|
||||||
|
|
||||||
test-mssql:
|
test-mssql:
|
||||||
@ -246,4 +243,3 @@ jobs:
|
|||||||
timeout-minutes: 50
|
timeout-minutes: 50
|
||||||
env:
|
env:
|
||||||
TAGS: bindata
|
TAGS: bindata
|
||||||
USE_REPO_TEST_DIR: 1
|
|
||||||
|
|||||||
1
.gitignore
vendored
1
.gitignore
vendored
@ -89,7 +89,6 @@ cpu.out
|
|||||||
/vendor
|
/vendor
|
||||||
/VERSION
|
/VERSION
|
||||||
/.air
|
/.air
|
||||||
/.go-licenses
|
|
||||||
|
|
||||||
# Files and folders that were previously generated
|
# Files and folders that were previously generated
|
||||||
/public/assets/img/webpack
|
/public/assets/img/webpack
|
||||||
|
|||||||
@ -67,35 +67,24 @@ linters:
|
|||||||
revive:
|
revive:
|
||||||
severity: error
|
severity: error
|
||||||
rules:
|
rules:
|
||||||
- name: atomic
|
|
||||||
- name: bare-return
|
|
||||||
- name: blank-imports
|
- name: blank-imports
|
||||||
- name: constant-logical-expr
|
- name: constant-logical-expr
|
||||||
- name: context-as-argument
|
- name: context-as-argument
|
||||||
- name: context-keys-type
|
- name: context-keys-type
|
||||||
- name: dot-imports
|
- name: dot-imports
|
||||||
- name: duplicated-imports
|
|
||||||
- name: empty-lines
|
- name: empty-lines
|
||||||
- name: error-naming
|
|
||||||
- name: error-return
|
- name: error-return
|
||||||
- name: error-strings
|
- name: error-strings
|
||||||
- name: errorf
|
|
||||||
- name: exported
|
- name: exported
|
||||||
- name: identical-branches
|
- name: identical-branches
|
||||||
- name: if-return
|
- name: if-return
|
||||||
- name: increment-decrement
|
- name: increment-decrement
|
||||||
- name: indent-error-flow
|
|
||||||
- name: modifies-value-receiver
|
- name: modifies-value-receiver
|
||||||
- name: package-comments
|
- name: package-comments
|
||||||
- name: range
|
|
||||||
- name: receiver-naming
|
|
||||||
- name: redefines-builtin-id
|
- name: redefines-builtin-id
|
||||||
- name: string-of-int
|
|
||||||
- name: superfluous-else
|
- name: superfluous-else
|
||||||
- name: time-naming
|
- name: time-naming
|
||||||
- name: unconditional-recursion
|
|
||||||
- name: unexported-return
|
- name: unexported-return
|
||||||
- name: unreachable-code
|
|
||||||
- name: var-declaration
|
- name: var-declaration
|
||||||
- name: var-naming
|
- name: var-naming
|
||||||
arguments:
|
arguments:
|
||||||
@ -133,16 +122,12 @@ linters:
|
|||||||
- linters:
|
- linters:
|
||||||
- dupl
|
- dupl
|
||||||
- errcheck
|
- errcheck
|
||||||
- gocyclo
|
|
||||||
- gosec
|
|
||||||
- staticcheck
|
- staticcheck
|
||||||
- unparam
|
- unparam
|
||||||
path: _test\.go
|
path: _test\.go
|
||||||
- linters:
|
- linters:
|
||||||
- dupl
|
- dupl
|
||||||
- errcheck
|
- errcheck
|
||||||
- gocyclo
|
|
||||||
- gosec
|
|
||||||
path: models/migrations/v
|
path: models/migrations/v
|
||||||
- linters:
|
- linters:
|
||||||
- forbidigo
|
- forbidigo
|
||||||
@ -154,7 +139,6 @@ linters:
|
|||||||
- gocritic
|
- gocritic
|
||||||
text: (?i)`ID' should not be capitalized
|
text: (?i)`ID' should not be capitalized
|
||||||
- linters:
|
- linters:
|
||||||
- deadcode
|
|
||||||
- unused
|
- unused
|
||||||
text: (?i)swagger
|
text: (?i)swagger
|
||||||
- linters:
|
- linters:
|
||||||
|
|||||||
@ -197,7 +197,7 @@ Here's how to run the test suite:
|
|||||||
## Translation
|
## Translation
|
||||||
|
|
||||||
All translation work happens on [Crowdin](https://translate.gitea.com).
|
All translation work happens on [Crowdin](https://translate.gitea.com).
|
||||||
The only translation that is maintained in this repository is [the English translation](https://github.com/go-gitea/gitea/blob/main/options/locale/locale_en-US.ini).
|
The only translation that is maintained in this repository is [the English translation](https://github.com/go-gitea/gitea/blob/main/options/locale/locale_en-US.json).
|
||||||
It is synced regularly with Crowdin. \
|
It is synced regularly with Crowdin. \
|
||||||
Other locales on main branch **should not** be updated manually as they will be overwritten with each sync. \
|
Other locales on main branch **should not** be updated manually as they will be overwritten with each sync. \
|
||||||
Once a language has reached a **satisfactory percentage** of translated keys (~25%), it will be synced back into this repo and included in the next released version.
|
Once a language has reached a **satisfactory percentage** of translated keys (~25%), it will be synced back into this repo and included in the next released version.
|
||||||
|
|||||||
22
Dockerfile
22
Dockerfile
@ -1,5 +1,12 @@
|
|||||||
# syntax=docker/dockerfile:1
|
# syntax=docker/dockerfile:1
|
||||||
# Build stage
|
# Build frontend on the native platform to avoid QEMU-related issues with esbuild/webpack
|
||||||
|
FROM --platform=$BUILDPLATFORM docker.io/library/golang:1.26-alpine3.23 AS frontend-build
|
||||||
|
RUN apk --no-cache add build-base git nodejs pnpm
|
||||||
|
WORKDIR /src
|
||||||
|
COPY --exclude=.git/ . .
|
||||||
|
RUN --mount=type=cache,target=/root/.local/share/pnpm/store make frontend
|
||||||
|
|
||||||
|
# Build backend for each target platform
|
||||||
FROM docker.io/library/golang:1.26-alpine3.23 AS build-env
|
FROM docker.io/library/golang:1.26-alpine3.23 AS build-env
|
||||||
|
|
||||||
ARG GOPROXY=direct
|
ARG GOPROXY=direct
|
||||||
@ -12,22 +19,19 @@ ARG CGO_EXTRA_CFLAGS
|
|||||||
# Build deps
|
# Build deps
|
||||||
RUN apk --no-cache add \
|
RUN apk --no-cache add \
|
||||||
build-base \
|
build-base \
|
||||||
git \
|
git
|
||||||
nodejs \
|
|
||||||
pnpm
|
|
||||||
|
|
||||||
WORKDIR ${GOPATH}/src/code.gitea.io/gitea
|
WORKDIR ${GOPATH}/src/code.gitea.io/gitea
|
||||||
# Use COPY but not "mount" because some directories like "node_modules" contain platform-depended contents and these directories need to be ignored.
|
# Use COPY instead of bind mount as read-only one breaks makefile state tracking and read-write one needs binary to be moved as it's discarded.
|
||||||
# ".git" directory will be mounted later separately for getting version data.
|
# ".git" directory is mounted separately later only for version data extraction.
|
||||||
# TODO: in the future, maybe we can pre-build the frontend assets on one platform and share them for different platforms, the benefit is that it won't be affected by webpack plugin compatibility problems, then the working directory can be fully mounted and the COPY is not needed.
|
|
||||||
COPY --exclude=.git/ . .
|
COPY --exclude=.git/ . .
|
||||||
|
COPY --from=frontend-build /src/public/assets public/assets
|
||||||
|
|
||||||
# Build gitea, .git mount is required for version data
|
# Build gitea, .git mount is required for version data
|
||||||
RUN --mount=type=cache,target=/go/pkg/mod \
|
RUN --mount=type=cache,target=/go/pkg/mod \
|
||||||
--mount=type=cache,target="/root/.cache/go-build" \
|
--mount=type=cache,target="/root/.cache/go-build" \
|
||||||
--mount=type=cache,target=/root/.local/share/pnpm/store \
|
|
||||||
--mount=type=bind,source=".git/",target=".git/" \
|
--mount=type=bind,source=".git/",target=".git/" \
|
||||||
make
|
make backend
|
||||||
|
|
||||||
COPY docker/root /tmp/local
|
COPY docker/root /tmp/local
|
||||||
|
|
||||||
|
|||||||
@ -1,5 +1,12 @@
|
|||||||
# syntax=docker/dockerfile:1
|
# syntax=docker/dockerfile:1
|
||||||
# Build stage
|
# Build frontend on the native platform to avoid QEMU-related issues with esbuild/webpack
|
||||||
|
FROM --platform=$BUILDPLATFORM docker.io/library/golang:1.26-alpine3.23 AS frontend-build
|
||||||
|
RUN apk --no-cache add build-base git nodejs pnpm
|
||||||
|
WORKDIR /src
|
||||||
|
COPY --exclude=.git/ . .
|
||||||
|
RUN --mount=type=cache,target=/root/.local/share/pnpm/store make frontend
|
||||||
|
|
||||||
|
# Build backend for each target platform
|
||||||
FROM docker.io/library/golang:1.26-alpine3.23 AS build-env
|
FROM docker.io/library/golang:1.26-alpine3.23 AS build-env
|
||||||
|
|
||||||
ARG GOPROXY=direct
|
ARG GOPROXY=direct
|
||||||
@ -12,20 +19,18 @@ ARG CGO_EXTRA_CFLAGS
|
|||||||
# Build deps
|
# Build deps
|
||||||
RUN apk --no-cache add \
|
RUN apk --no-cache add \
|
||||||
build-base \
|
build-base \
|
||||||
git \
|
git
|
||||||
nodejs \
|
|
||||||
pnpm
|
|
||||||
|
|
||||||
WORKDIR ${GOPATH}/src/code.gitea.io/gitea
|
WORKDIR ${GOPATH}/src/code.gitea.io/gitea
|
||||||
# See the comments in Dockerfile
|
# See the comments in Dockerfile
|
||||||
COPY --exclude=.git/ . .
|
COPY --exclude=.git/ . .
|
||||||
|
COPY --from=frontend-build /src/public/assets public/assets
|
||||||
|
|
||||||
# Build gitea, .git mount is required for version data
|
# Build gitea, .git mount is required for version data
|
||||||
RUN --mount=type=cache,target=/go/pkg/mod \
|
RUN --mount=type=cache,target=/go/pkg/mod \
|
||||||
--mount=type=cache,target="/root/.cache/go-build" \
|
--mount=type=cache,target="/root/.cache/go-build" \
|
||||||
--mount=type=cache,target=/root/.local/share/pnpm/store \
|
|
||||||
--mount=type=bind,source=".git/",target=".git/" \
|
--mount=type=bind,source=".git/",target=".git/" \
|
||||||
make
|
make backend
|
||||||
|
|
||||||
COPY docker/rootless /tmp/local
|
COPY docker/rootless /tmp/local
|
||||||
|
|
||||||
|
|||||||
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 := dist
|
||||||
DIST_DIRS := $(DIST)/binaries $(DIST)/release
|
DIST_DIRS := $(DIST)/binaries $(DIST)/release
|
||||||
IMPORT := code.gitea.io/gitea
|
|
||||||
|
|
||||||
# By default use go's 1.25 experimental json v2 library when building
|
# By default use go's 1.25 experimental json v2 library when building
|
||||||
# TODO: remove when no longer experimental
|
# TODO: remove when no longer experimental
|
||||||
@ -37,7 +20,6 @@ GXZ_PACKAGE ?= github.com/ulikunitz/xz/cmd/gxz@v0.5.15
|
|||||||
MISSPELL_PACKAGE ?= github.com/golangci/misspell/cmd/misspell@v0.7.0
|
MISSPELL_PACKAGE ?= github.com/golangci/misspell/cmd/misspell@v0.7.0
|
||||||
SWAGGER_PACKAGE ?= github.com/go-swagger/go-swagger/cmd/swagger@v0.33.1
|
SWAGGER_PACKAGE ?= github.com/go-swagger/go-swagger/cmd/swagger@v0.33.1
|
||||||
XGO_PACKAGE ?= src.techknowlogick.com/xgo@latest
|
XGO_PACKAGE ?= src.techknowlogick.com/xgo@latest
|
||||||
GO_LICENSES_PACKAGE ?= github.com/google/go-licenses@v1
|
|
||||||
GOVULNCHECK_PACKAGE ?= golang.org/x/vuln/cmd/govulncheck@v1
|
GOVULNCHECK_PACKAGE ?= golang.org/x/vuln/cmd/govulncheck@v1
|
||||||
ACTIONLINT_PACKAGE ?= github.com/rhysd/actionlint/cmd/actionlint@v1.7.10
|
ACTIONLINT_PACKAGE ?= github.com/rhysd/actionlint/cmd/actionlint@v1.7.10
|
||||||
|
|
||||||
@ -84,7 +66,6 @@ endif
|
|||||||
|
|
||||||
EXTRA_GOFLAGS ?=
|
EXTRA_GOFLAGS ?=
|
||||||
|
|
||||||
MAKE_VERSION := $(shell "$(MAKE)" -v | cat | head -n 1)
|
|
||||||
MAKE_EVIDENCE_DIR := .make_evidence
|
MAKE_EVIDENCE_DIR := .make_evidence
|
||||||
|
|
||||||
GOTESTFLAGS ?=
|
GOTESTFLAGS ?=
|
||||||
@ -130,7 +111,7 @@ ifeq ($(VERSION),main)
|
|||||||
VERSION := main-nightly
|
VERSION := main-nightly
|
||||||
endif
|
endif
|
||||||
|
|
||||||
LDFLAGS := $(LDFLAGS) -X "main.MakeVersion=$(MAKE_VERSION)" -X "main.Version=$(GITEA_VERSION)" -X "main.Tags=$(TAGS)"
|
LDFLAGS := $(LDFLAGS) -X "main.Version=$(GITEA_VERSION)" -X "main.Tags=$(TAGS)"
|
||||||
|
|
||||||
LINUX_ARCHS ?= linux/amd64,linux/386,linux/arm-5,linux/arm-6,linux/arm64,linux/riscv64
|
LINUX_ARCHS ?= linux/amd64,linux/386,linux/arm-5,linux/arm-6,linux/arm64,linux/riscv64
|
||||||
|
|
||||||
@ -150,7 +131,6 @@ SVG_DEST_DIR := public/assets/img/svg
|
|||||||
|
|
||||||
AIR_TMP_DIR := .air
|
AIR_TMP_DIR := .air
|
||||||
|
|
||||||
GO_LICENSE_TMP_DIR := .go-licenses
|
|
||||||
GO_LICENSE_FILE := assets/go-licenses.json
|
GO_LICENSE_FILE := assets/go-licenses.json
|
||||||
|
|
||||||
TAGS ?=
|
TAGS ?=
|
||||||
@ -159,7 +139,7 @@ TAGS_EVIDENCE := $(MAKE_EVIDENCE_DIR)/tags
|
|||||||
|
|
||||||
TEST_TAGS ?= $(TAGS_SPLIT) sqlite sqlite_unlock_notify
|
TEST_TAGS ?= $(TAGS_SPLIT) sqlite sqlite_unlock_notify
|
||||||
|
|
||||||
TAR_EXCLUDES := .git data indexers queues log node_modules $(EXECUTABLE) $(DIST) $(MAKE_EVIDENCE_DIR) $(AIR_TMP_DIR) $(GO_LICENSE_TMP_DIR)
|
TAR_EXCLUDES := .git data indexers queues log node_modules $(EXECUTABLE) $(DIST) $(MAKE_EVIDENCE_DIR) $(AIR_TMP_DIR)
|
||||||
|
|
||||||
GO_DIRS := build cmd models modules routers services tests tools
|
GO_DIRS := build cmd models modules routers services tests tools
|
||||||
WEB_DIRS := web_src/js web_src/css
|
WEB_DIRS := web_src/js web_src/css
|
||||||
@ -229,7 +209,7 @@ clean: ## delete backend and integration files
|
|||||||
e2e*.test \
|
e2e*.test \
|
||||||
tests/integration/gitea-integration-* \
|
tests/integration/gitea-integration-* \
|
||||||
tests/integration/indexers-* \
|
tests/integration/indexers-* \
|
||||||
tests/mysql.ini tests/pgsql.ini tests/mssql.ini man/ \
|
tests/sqlite.ini tests/mysql.ini tests/pgsql.ini tests/mssql.ini man/ \
|
||||||
tests/e2e/gitea-e2e-*/ \
|
tests/e2e/gitea-e2e-*/ \
|
||||||
tests/e2e/indexers-*/ \
|
tests/e2e/indexers-*/ \
|
||||||
tests/e2e/reports/ tests/e2e/test-artifacts/ tests/e2e/test-snapshots/
|
tests/e2e/reports/ tests/e2e/test-artifacts/ tests/e2e/test-snapshots/
|
||||||
@ -473,16 +453,11 @@ tidy-check: tidy
|
|||||||
go-licenses: $(GO_LICENSE_FILE) ## regenerate go licenses
|
go-licenses: $(GO_LICENSE_FILE) ## regenerate go licenses
|
||||||
|
|
||||||
$(GO_LICENSE_FILE): go.mod go.sum
|
$(GO_LICENSE_FILE): go.mod go.sum
|
||||||
@rm -rf $(GO_LICENSE_FILE)
|
GO=$(GO) $(GO) run build/generate-go-licenses.go $(GO_LICENSE_FILE)
|
||||||
$(GO) install $(GO_LICENSES_PACKAGE)
|
|
||||||
-GOOS=linux CGO_ENABLED=1 go-licenses save . --force --save_path=$(GO_LICENSE_TMP_DIR) 2>/dev/null
|
|
||||||
$(GO) run build/generate-go-licenses.go $(GO_LICENSE_TMP_DIR) $(GO_LICENSE_FILE)
|
|
||||||
@rm -rf $(GO_LICENSE_TMP_DIR)
|
|
||||||
|
|
||||||
generate-ini-sqlite:
|
generate-ini-sqlite:
|
||||||
sed -e 's|{{REPO_TEST_DIR}}|${REPO_TEST_DIR}|g' \
|
sed -e 's|{{WORK_PATH}}|$(CURDIR)/tests/$(or $(TEST_TYPE),integration)/gitea-$(or $(TEST_TYPE),integration)-sqlite|g' \
|
||||||
-e 's|{{TEST_LOGGER}}|$(or $(TEST_LOGGER),test$(COMMA)file)|g' \
|
-e 's|{{TEST_LOGGER}}|$(or $(TEST_LOGGER),test$(COMMA)file)|g' \
|
||||||
-e 's|{{TEST_TYPE}}|$(or $(TEST_TYPE),integration)|g' \
|
|
||||||
tests/sqlite.ini.tmpl > tests/sqlite.ini
|
tests/sqlite.ini.tmpl > tests/sqlite.ini
|
||||||
|
|
||||||
.PHONY: test-sqlite
|
.PHONY: test-sqlite
|
||||||
@ -501,9 +476,8 @@ generate-ini-mysql:
|
|||||||
-e 's|{{TEST_MYSQL_DBNAME}}|${TEST_MYSQL_DBNAME}|g' \
|
-e 's|{{TEST_MYSQL_DBNAME}}|${TEST_MYSQL_DBNAME}|g' \
|
||||||
-e 's|{{TEST_MYSQL_USERNAME}}|${TEST_MYSQL_USERNAME}|g' \
|
-e 's|{{TEST_MYSQL_USERNAME}}|${TEST_MYSQL_USERNAME}|g' \
|
||||||
-e 's|{{TEST_MYSQL_PASSWORD}}|${TEST_MYSQL_PASSWORD}|g' \
|
-e 's|{{TEST_MYSQL_PASSWORD}}|${TEST_MYSQL_PASSWORD}|g' \
|
||||||
-e 's|{{REPO_TEST_DIR}}|${REPO_TEST_DIR}|g' \
|
-e 's|{{WORK_PATH}}|$(CURDIR)/tests/$(or $(TEST_TYPE),integration)/gitea-$(or $(TEST_TYPE),integration)-mysql|g' \
|
||||||
-e 's|{{TEST_LOGGER}}|$(or $(TEST_LOGGER),test$(COMMA)file)|g' \
|
-e 's|{{TEST_LOGGER}}|$(or $(TEST_LOGGER),test$(COMMA)file)|g' \
|
||||||
-e 's|{{TEST_TYPE}}|$(or $(TEST_TYPE),integration)|g' \
|
|
||||||
tests/mysql.ini.tmpl > tests/mysql.ini
|
tests/mysql.ini.tmpl > tests/mysql.ini
|
||||||
|
|
||||||
.PHONY: test-mysql
|
.PHONY: test-mysql
|
||||||
@ -524,9 +498,8 @@ generate-ini-pgsql:
|
|||||||
-e 's|{{TEST_PGSQL_PASSWORD}}|${TEST_PGSQL_PASSWORD}|g' \
|
-e 's|{{TEST_PGSQL_PASSWORD}}|${TEST_PGSQL_PASSWORD}|g' \
|
||||||
-e 's|{{TEST_PGSQL_SCHEMA}}|${TEST_PGSQL_SCHEMA}|g' \
|
-e 's|{{TEST_PGSQL_SCHEMA}}|${TEST_PGSQL_SCHEMA}|g' \
|
||||||
-e 's|{{TEST_MINIO_ENDPOINT}}|${TEST_MINIO_ENDPOINT}|g' \
|
-e 's|{{TEST_MINIO_ENDPOINT}}|${TEST_MINIO_ENDPOINT}|g' \
|
||||||
-e 's|{{REPO_TEST_DIR}}|${REPO_TEST_DIR}|g' \
|
-e 's|{{WORK_PATH}}|$(CURDIR)/tests/$(or $(TEST_TYPE),integration)/gitea-$(or $(TEST_TYPE),integration)-pgsql|g' \
|
||||||
-e 's|{{TEST_LOGGER}}|$(or $(TEST_LOGGER),test$(COMMA)file)|g' \
|
-e 's|{{TEST_LOGGER}}|$(or $(TEST_LOGGER),test$(COMMA)file)|g' \
|
||||||
-e 's|{{TEST_TYPE}}|$(or $(TEST_TYPE),integration)|g' \
|
|
||||||
tests/pgsql.ini.tmpl > tests/pgsql.ini
|
tests/pgsql.ini.tmpl > tests/pgsql.ini
|
||||||
|
|
||||||
.PHONY: test-pgsql
|
.PHONY: test-pgsql
|
||||||
@ -545,9 +518,8 @@ generate-ini-mssql:
|
|||||||
-e 's|{{TEST_MSSQL_DBNAME}}|${TEST_MSSQL_DBNAME}|g' \
|
-e 's|{{TEST_MSSQL_DBNAME}}|${TEST_MSSQL_DBNAME}|g' \
|
||||||
-e 's|{{TEST_MSSQL_USERNAME}}|${TEST_MSSQL_USERNAME}|g' \
|
-e 's|{{TEST_MSSQL_USERNAME}}|${TEST_MSSQL_USERNAME}|g' \
|
||||||
-e 's|{{TEST_MSSQL_PASSWORD}}|${TEST_MSSQL_PASSWORD}|g' \
|
-e 's|{{TEST_MSSQL_PASSWORD}}|${TEST_MSSQL_PASSWORD}|g' \
|
||||||
-e 's|{{REPO_TEST_DIR}}|${REPO_TEST_DIR}|g' \
|
-e 's|{{WORK_PATH}}|$(CURDIR)/tests/$(or $(TEST_TYPE),integration)/gitea-$(or $(TEST_TYPE),integration)-mssql|g' \
|
||||||
-e 's|{{TEST_LOGGER}}|$(or $(TEST_LOGGER),test$(COMMA)file)|g' \
|
-e 's|{{TEST_LOGGER}}|$(or $(TEST_LOGGER),test$(COMMA)file)|g' \
|
||||||
-e 's|{{TEST_TYPE}}|$(or $(TEST_TYPE),integration)|g' \
|
|
||||||
tests/mssql.ini.tmpl > tests/mssql.ini
|
tests/mssql.ini.tmpl > tests/mssql.ini
|
||||||
|
|
||||||
.PHONY: test-mssql
|
.PHONY: test-mssql
|
||||||
@ -668,7 +640,7 @@ migrations.sqlite.test: $(GO_SOURCES) generate-ini-sqlite
|
|||||||
GITEA_TEST_CONF=tests/sqlite.ini ./migrations.sqlite.test
|
GITEA_TEST_CONF=tests/sqlite.ini ./migrations.sqlite.test
|
||||||
|
|
||||||
.PHONY: migrations.individual.mysql.test
|
.PHONY: migrations.individual.mysql.test
|
||||||
migrations.individual.mysql.test: $(GO_SOURCES)
|
migrations.individual.mysql.test: $(GO_SOURCES) generate-ini-mysql
|
||||||
GITEA_TEST_CONF=tests/mysql.ini $(GO) test $(GOTESTFLAGS) -tags='$(TEST_TAGS)' -p 1 $(MIGRATE_TEST_PACKAGES)
|
GITEA_TEST_CONF=tests/mysql.ini $(GO) test $(GOTESTFLAGS) -tags='$(TEST_TAGS)' -p 1 $(MIGRATE_TEST_PACKAGES)
|
||||||
|
|
||||||
.PHONY: migrations.individual.sqlite.test\#%
|
.PHONY: migrations.individual.sqlite.test\#%
|
||||||
@ -676,7 +648,7 @@ migrations.individual.sqlite.test\#%: $(GO_SOURCES) generate-ini-sqlite
|
|||||||
GITEA_TEST_CONF=tests/sqlite.ini $(GO) test $(GOTESTFLAGS) -tags '$(TEST_TAGS)' code.gitea.io/gitea/models/migrations/$*
|
GITEA_TEST_CONF=tests/sqlite.ini $(GO) test $(GOTESTFLAGS) -tags '$(TEST_TAGS)' code.gitea.io/gitea/models/migrations/$*
|
||||||
|
|
||||||
.PHONY: migrations.individual.pgsql.test
|
.PHONY: migrations.individual.pgsql.test
|
||||||
migrations.individual.pgsql.test: $(GO_SOURCES)
|
migrations.individual.pgsql.test: $(GO_SOURCES) generate-ini-pgsql
|
||||||
GITEA_TEST_CONF=tests/pgsql.ini $(GO) test $(GOTESTFLAGS) -tags='$(TEST_TAGS)' -p 1 $(MIGRATE_TEST_PACKAGES)
|
GITEA_TEST_CONF=tests/pgsql.ini $(GO) test $(GOTESTFLAGS) -tags='$(TEST_TAGS)' -p 1 $(MIGRATE_TEST_PACKAGES)
|
||||||
|
|
||||||
.PHONY: migrations.individual.pgsql.test\#%
|
.PHONY: migrations.individual.pgsql.test\#%
|
||||||
@ -741,7 +713,7 @@ generate-go: $(TAGS_PREREQ)
|
|||||||
|
|
||||||
.PHONY: security-check
|
.PHONY: security-check
|
||||||
security-check:
|
security-check:
|
||||||
GOEXPERIMENT= go run $(GOVULNCHECK_PACKAGE) -show color ./...
|
GOEXPERIMENT= go run $(GOVULNCHECK_PACKAGE) -show color ./... || true
|
||||||
|
|
||||||
$(EXECUTABLE): $(GO_SOURCES) $(TAGS_PREREQ)
|
$(EXECUTABLE): $(GO_SOURCES) $(TAGS_PREREQ)
|
||||||
ifneq ($(and $(STATIC),$(findstring pam,$(TAGS))),)
|
ifneq ($(and $(STATIC),$(findstring pam,$(TAGS))),)
|
||||||
@ -819,7 +791,6 @@ deps-tools: ## install tool dependencies
|
|||||||
$(GO) install $(MISSPELL_PACKAGE) & \
|
$(GO) install $(MISSPELL_PACKAGE) & \
|
||||||
$(GO) install $(SWAGGER_PACKAGE) & \
|
$(GO) install $(SWAGGER_PACKAGE) & \
|
||||||
$(GO) install $(XGO_PACKAGE) & \
|
$(GO) install $(XGO_PACKAGE) & \
|
||||||
$(GO) install $(GO_LICENSES_PACKAGE) & \
|
|
||||||
$(GO) install $(GOVULNCHECK_PACKAGE) & \
|
$(GO) install $(GOVULNCHECK_PACKAGE) & \
|
||||||
$(GO) install $(ACTIONLINT_PACKAGE) & \
|
$(GO) install $(ACTIONLINT_PACKAGE) & \
|
||||||
wait
|
wait
|
||||||
@ -908,9 +879,6 @@ docker:
|
|||||||
docker build --disable-content-trust=false -t $(DOCKER_REF) .
|
docker build --disable-content-trust=false -t $(DOCKER_REF) .
|
||||||
# support also build args docker build --build-arg GITEA_VERSION=v1.2.3 --build-arg TAGS="bindata sqlite sqlite_unlock_notify" .
|
# support also build args docker build --build-arg GITEA_VERSION=v1.2.3 --build-arg TAGS="bindata sqlite sqlite_unlock_notify" .
|
||||||
|
|
||||||
# This endif closes the if at the top of the file
|
|
||||||
endif
|
|
||||||
|
|
||||||
# Disable parallel execution because it would break some targets that don't
|
# Disable parallel execution because it would break some targets that don't
|
||||||
# specify exact dependencies like 'backend' which does currently not depend
|
# specify exact dependencies like 'backend' which does currently not depend
|
||||||
# on 'frontend' to enable Node.js-less builds from source tarballs.
|
# on 'frontend' to enable Node.js-less builds from source tarballs.
|
||||||
|
|||||||
142
assets/go-licenses.json
generated
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 (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/fs"
|
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"os/exec"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"regexp"
|
"regexp"
|
||||||
|
"slices"
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"code.gitea.io/gitea/modules/container"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// regexp is based on go-license, excluding README and NOTICE
|
// regexp is based on go-license, excluding README and NOTICE
|
||||||
// https://github.com/google/go-licenses/blob/master/licenses/find.go
|
// https://github.com/google/go-licenses/blob/master/licenses/find.go
|
||||||
var licenseRe = regexp.MustCompile(`^(?i)((UN)?LICEN(S|C)E|COPYING).*$`)
|
var licenseRe = regexp.MustCompile(`^(?i)((UN)?LICEN(S|C)E|COPYING).*$`)
|
||||||
|
|
||||||
|
// primaryLicenseRe matches exact primary license filenames without suffixes.
|
||||||
|
// When a directory has both primary and variant files (e.g. LICENSE and
|
||||||
|
// LICENSE.docs), only the primary files are kept.
|
||||||
|
var primaryLicenseRe = regexp.MustCompile(`^(?i)(LICEN[SC]E|COPYING)$`)
|
||||||
|
|
||||||
|
// ignoredNames are LicenseEntry.Name values to exclude from the output.
|
||||||
|
var ignoredNames = map[string]bool{
|
||||||
|
"code.gitea.io/gitea": true,
|
||||||
|
"code.gitea.io/gitea/options/license": true,
|
||||||
|
}
|
||||||
|
|
||||||
|
var excludedExt = map[string]bool{
|
||||||
|
".gitignore": true,
|
||||||
|
".go": true,
|
||||||
|
".mod": true,
|
||||||
|
".sum": true,
|
||||||
|
".toml": true,
|
||||||
|
".yaml": true,
|
||||||
|
".yml": true,
|
||||||
|
}
|
||||||
|
|
||||||
|
type ModuleInfo struct {
|
||||||
|
Path string
|
||||||
|
Dir string
|
||||||
|
PkgDirs []string // directories of packages imported from this module
|
||||||
|
}
|
||||||
|
|
||||||
type LicenseEntry struct {
|
type LicenseEntry struct {
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Path string `json:"path"`
|
Path string `json:"path"`
|
||||||
LicenseText string `json:"licenseText"`
|
LicenseText string `json:"licenseText"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func main() {
|
// getModules returns all dependency modules with their local directory paths
|
||||||
if len(os.Args) != 3 {
|
// and the package directories used from each module.
|
||||||
fmt.Println("usage: go run generate-go-licenses.go <base-dir> <out-json-file>")
|
func getModules(goCmd string) []ModuleInfo {
|
||||||
|
cmd := exec.Command(goCmd, "list", "-deps", "-f",
|
||||||
|
"{{if .Module}}{{.Module.Path}}\t{{.Module.Dir}}\t{{.Dir}}{{end}}", "./...")
|
||||||
|
cmd.Stderr = os.Stderr
|
||||||
|
// Use GOOS=linux with CGO to ensure we capture all platform-specific
|
||||||
|
// dependencies, matching the CI environment.
|
||||||
|
cmd.Env = append(os.Environ(), "GOOS=linux", "GOARCH=amd64", "CGO_ENABLED=1")
|
||||||
|
output, err := cmd.Output()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "failed to run 'go list -deps': %v\n", err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
base, out := os.Args[1], os.Args[2]
|
var modules []ModuleInfo
|
||||||
|
seen := make(map[string]int) // module path -> index in modules
|
||||||
// Add ext for excluded files because license_test.go will be included for some reason.
|
for _, line := range strings.Split(string(output), "\n") {
|
||||||
// And there are more files that should be excluded, check with:
|
line = strings.TrimSpace(line)
|
||||||
//
|
if line == "" {
|
||||||
// go run github.com/google/go-licenses@v1.6.0 save . --force --save_path=.go-licenses 2>/dev/null
|
continue
|
||||||
// find .go-licenses -type f | while read FILE; do echo "${$(basename $FILE)##*.}"; done | sort -u
|
|
||||||
// AUTHORS
|
|
||||||
// COPYING
|
|
||||||
// LICENSE
|
|
||||||
// Makefile
|
|
||||||
// NOTICE
|
|
||||||
// gitignore
|
|
||||||
// go
|
|
||||||
// md
|
|
||||||
// mod
|
|
||||||
// sum
|
|
||||||
// toml
|
|
||||||
// txt
|
|
||||||
// yml
|
|
||||||
//
|
|
||||||
// It could be removed once we have a better regex.
|
|
||||||
excludedExt := container.SetOf(".gitignore", ".go", ".mod", ".sum", ".toml", ".yml")
|
|
||||||
|
|
||||||
var paths []string
|
|
||||||
err := filepath.WalkDir(base, func(path string, entry fs.DirEntry, err error) error {
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
if entry.IsDir() || !licenseRe.MatchString(entry.Name()) || excludedExt.Contains(filepath.Ext(entry.Name())) {
|
parts := strings.Split(line, "\t")
|
||||||
return nil
|
if len(parts) != 3 {
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
paths = append(paths, path)
|
modPath, modDir, pkgDir := parts[0], parts[1], parts[2]
|
||||||
return nil
|
if idx, ok := seen[modPath]; ok {
|
||||||
})
|
modules[idx].PkgDirs = append(modules[idx].PkgDirs, pkgDir)
|
||||||
if err != nil {
|
} else {
|
||||||
panic(err)
|
seen[modPath] = len(modules)
|
||||||
|
modules = append(modules, ModuleInfo{
|
||||||
|
Path: modPath,
|
||||||
|
Dir: modDir,
|
||||||
|
PkgDirs: []string{pkgDir},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return modules
|
||||||
|
}
|
||||||
|
|
||||||
|
// findLicenseFiles scans a module's root directory and its used package
|
||||||
|
// directories for license files. It also walks up from each package directory
|
||||||
|
// to the module root, scanning intermediate directories. Subdirectory licenses
|
||||||
|
// are only included if their text differs from the root license(s).
|
||||||
|
func findLicenseFiles(mod ModuleInfo) []LicenseEntry {
|
||||||
|
var entries []LicenseEntry
|
||||||
|
seenTexts := make(map[string]bool)
|
||||||
|
|
||||||
|
// First, collect root-level license files.
|
||||||
|
entries = append(entries, scanDirForLicenses(mod.Dir, mod.Path, "")...)
|
||||||
|
for _, e := range entries {
|
||||||
|
seenTexts[e.LicenseText] = true
|
||||||
}
|
}
|
||||||
|
|
||||||
sort.Strings(paths)
|
// Then check each package directory and all intermediate parent directories
|
||||||
|
// up to the module root for license files with unique text.
|
||||||
|
seenDirs := map[string]bool{mod.Dir: true}
|
||||||
|
for _, pkgDir := range mod.PkgDirs {
|
||||||
|
for dir := pkgDir; dir != mod.Dir && strings.HasPrefix(dir, mod.Dir); dir = filepath.Dir(dir) {
|
||||||
|
if seenDirs[dir] {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
seenDirs[dir] = true
|
||||||
|
for _, e := range scanDirForLicenses(dir, mod.Path, mod.Dir) {
|
||||||
|
if !seenTexts[e.LicenseText] {
|
||||||
|
seenTexts[e.LicenseText] = true
|
||||||
|
entries = append(entries, e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return entries
|
||||||
|
}
|
||||||
|
|
||||||
|
// scanDirForLicenses reads a single directory for license files and returns entries.
|
||||||
|
// If moduleRoot is non-empty, paths are made relative to it.
|
||||||
|
func scanDirForLicenses(dir, modulePath, moduleRoot string) []LicenseEntry {
|
||||||
|
dirEntries, err := os.ReadDir(dir)
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
var entries []LicenseEntry
|
var entries []LicenseEntry
|
||||||
for _, filePath := range paths {
|
for _, entry := range dirEntries {
|
||||||
licenseText, err := os.ReadFile(filePath)
|
if entry.IsDir() {
|
||||||
if err != nil {
|
continue
|
||||||
panic(err)
|
|
||||||
}
|
}
|
||||||
|
name := entry.Name()
|
||||||
pkgPath := filepath.ToSlash(filePath)
|
if !licenseRe.MatchString(name) {
|
||||||
pkgPath = strings.TrimPrefix(pkgPath, base+"/")
|
continue
|
||||||
pkgName := path.Dir(pkgPath)
|
}
|
||||||
|
if excludedExt[strings.ToLower(filepath.Ext(name))] {
|
||||||
// There might be a bug somewhere in go-licenses that sometimes interprets the
|
|
||||||
// root package as "." and sometimes as "code.gitea.io/gitea". Workaround by
|
|
||||||
// removing both of them for the sake of stable output.
|
|
||||||
if pkgName == "." || pkgName == "code.gitea.io/gitea" {
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
content, err := os.ReadFile(filepath.Join(dir, name))
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
entryName := modulePath
|
||||||
|
entryPath := modulePath + "/" + name
|
||||||
|
if moduleRoot != "" {
|
||||||
|
rel, _ := filepath.Rel(moduleRoot, dir)
|
||||||
|
if rel != "." {
|
||||||
|
relSlash := filepath.ToSlash(rel)
|
||||||
|
entryName = modulePath + "/" + relSlash
|
||||||
|
entryPath = modulePath + "/" + relSlash + "/" + name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
entries = append(entries, LicenseEntry{
|
entries = append(entries, LicenseEntry{
|
||||||
Name: pkgName,
|
Name: entryName,
|
||||||
Path: pkgPath,
|
Path: entryPath,
|
||||||
LicenseText: string(licenseText),
|
LicenseText: string(content),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// When multiple license files exist, prefer primary files (e.g. LICENSE)
|
||||||
|
// over variants with suffixes (e.g. LICENSE.docs, LICENSE-2.0.txt).
|
||||||
|
// If no primary file exists, keep only the first variant.
|
||||||
|
if len(entries) > 1 {
|
||||||
|
var primary []LicenseEntry
|
||||||
|
for _, e := range entries {
|
||||||
|
fileName := e.Path[strings.LastIndex(e.Path, "/")+1:]
|
||||||
|
if primaryLicenseRe.MatchString(fileName) {
|
||||||
|
primary = append(primary, e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(primary) > 0 {
|
||||||
|
return primary
|
||||||
|
}
|
||||||
|
return entries[:1]
|
||||||
|
}
|
||||||
|
|
||||||
|
return entries
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
if len(os.Args) != 2 {
|
||||||
|
fmt.Println("usage: go run generate-go-licenses.go <out-json-file>")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
out := os.Args[1]
|
||||||
|
|
||||||
|
goCmd := "go"
|
||||||
|
if env := os.Getenv("GO"); env != "" {
|
||||||
|
goCmd = env
|
||||||
|
}
|
||||||
|
|
||||||
|
modules := getModules(goCmd)
|
||||||
|
|
||||||
|
var entries []LicenseEntry
|
||||||
|
for _, mod := range modules {
|
||||||
|
entries = append(entries, findLicenseFiles(mod)...)
|
||||||
|
}
|
||||||
|
|
||||||
|
entries = slices.DeleteFunc(entries, func(e LicenseEntry) bool {
|
||||||
|
return ignoredNames[e.Name]
|
||||||
|
})
|
||||||
|
|
||||||
|
sort.Slice(entries, func(i, j int) bool {
|
||||||
|
return entries[i].Path < entries[j].Path
|
||||||
|
})
|
||||||
|
|
||||||
jsonBytes, err := json.MarshalIndent(entries, "", " ")
|
jsonBytes, err := json.MarshalIndent(entries, "", " ")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
|
|||||||
@ -2858,6 +2858,9 @@ LEVEL = Info
|
|||||||
;ABANDONED_JOB_TIMEOUT = 24h
|
;ABANDONED_JOB_TIMEOUT = 24h
|
||||||
;; Strings committers can place inside a commit message or PR title to skip executing the corresponding actions workflow
|
;; Strings committers can place inside a commit message or PR title to skip executing the corresponding actions workflow
|
||||||
;SKIP_WORKFLOW_STRINGS = [skip ci],[ci skip],[no ci],[skip actions],[actions skip]
|
;SKIP_WORKFLOW_STRINGS = [skip ci],[ci skip],[no ci],[skip actions],[actions skip]
|
||||||
|
;; Comma-separated list of workflow directories, the first one to exist
|
||||||
|
;; in a repo is used to find Actions workflow files
|
||||||
|
;WORKFLOW_DIRS = .gitea/workflows,.github/workflows
|
||||||
|
|
||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
|
|||||||
@ -211,7 +211,7 @@ export default defineConfig([
|
|||||||
'@typescript-eslint/no-non-null-asserted-nullish-coalescing': [0],
|
'@typescript-eslint/no-non-null-asserted-nullish-coalescing': [0],
|
||||||
'@typescript-eslint/no-non-null-asserted-optional-chain': [2],
|
'@typescript-eslint/no-non-null-asserted-optional-chain': [2],
|
||||||
'@typescript-eslint/no-non-null-assertion': [0],
|
'@typescript-eslint/no-non-null-assertion': [0],
|
||||||
'@typescript-eslint/no-redeclare': [0],
|
'@typescript-eslint/no-redeclare': [2],
|
||||||
'@typescript-eslint/no-redundant-type-constituents': [2],
|
'@typescript-eslint/no-redundant-type-constituents': [2],
|
||||||
'@typescript-eslint/no-require-imports': [2],
|
'@typescript-eslint/no-require-imports': [2],
|
||||||
'@typescript-eslint/no-restricted-imports': [0],
|
'@typescript-eslint/no-restricted-imports': [0],
|
||||||
@ -437,7 +437,7 @@ export default defineConfig([
|
|||||||
'no-import-assign': [2],
|
'no-import-assign': [2],
|
||||||
'no-inline-comments': [0],
|
'no-inline-comments': [0],
|
||||||
'no-inner-declarations': [2],
|
'no-inner-declarations': [2],
|
||||||
'no-invalid-regexp': [2],
|
'no-invalid-regexp': [0], // handled by regexp/no-invalid-regexp
|
||||||
'no-invalid-this': [0],
|
'no-invalid-this': [0],
|
||||||
'no-irregular-whitespace': [2],
|
'no-irregular-whitespace': [2],
|
||||||
'no-iterator': [2],
|
'no-iterator': [2],
|
||||||
@ -551,7 +551,7 @@ export default defineConfig([
|
|||||||
'no-new-func': [0], // handled by @typescript-eslint/no-implied-eval
|
'no-new-func': [0], // handled by @typescript-eslint/no-implied-eval
|
||||||
'no-new-native-nonconstructor': [2],
|
'no-new-native-nonconstructor': [2],
|
||||||
'no-new-object': [2],
|
'no-new-object': [2],
|
||||||
'no-new-symbol': [2],
|
'no-new-symbol': [0], // handled by no-new-native-nonconstructor
|
||||||
'no-new-wrappers': [2],
|
'no-new-wrappers': [2],
|
||||||
'no-new': [0],
|
'no-new': [0],
|
||||||
'no-nonoctal-decimal-escape': [2],
|
'no-nonoctal-decimal-escape': [2],
|
||||||
@ -582,7 +582,7 @@ export default defineConfig([
|
|||||||
'no-template-curly-in-string': [2],
|
'no-template-curly-in-string': [2],
|
||||||
'no-ternary': [0],
|
'no-ternary': [0],
|
||||||
'no-this-before-super': [2],
|
'no-this-before-super': [2],
|
||||||
'no-throw-literal': [2],
|
'no-throw-literal': [0], // handled by @typescript-eslint/only-throw-error
|
||||||
'no-undef-init': [2],
|
'no-undef-init': [2],
|
||||||
'no-undef': [2], // it is still needed by eslint & IDE to prompt undefined names in real time
|
'no-undef': [2], // it is still needed by eslint & IDE to prompt undefined names in real time
|
||||||
'no-undefined': [0],
|
'no-undefined': [0],
|
||||||
@ -600,7 +600,7 @@ export default defineConfig([
|
|||||||
'no-unused-vars': [0], // handled by @typescript-eslint/no-unused-vars
|
'no-unused-vars': [0], // handled by @typescript-eslint/no-unused-vars
|
||||||
'no-use-before-define': [0], // handled by @typescript-eslint/no-use-before-define
|
'no-use-before-define': [0], // handled by @typescript-eslint/no-use-before-define
|
||||||
'no-useless-assignment': [2],
|
'no-useless-assignment': [2],
|
||||||
'no-useless-backreference': [2],
|
'no-useless-backreference': [0], // handled by regexp/no-useless-backreference
|
||||||
'no-useless-call': [2],
|
'no-useless-call': [2],
|
||||||
'no-useless-catch': [2],
|
'no-useless-catch': [2],
|
||||||
'no-useless-computed-key': [2],
|
'no-useless-computed-key': [2],
|
||||||
@ -608,7 +608,7 @@ export default defineConfig([
|
|||||||
'no-useless-constructor': [2],
|
'no-useless-constructor': [2],
|
||||||
'no-useless-escape': [2],
|
'no-useless-escape': [2],
|
||||||
'no-useless-rename': [2],
|
'no-useless-rename': [2],
|
||||||
'no-useless-return': [2],
|
'no-useless-return': [0], // handled by sonarjs/no-redundant-jump
|
||||||
'no-var': [2],
|
'no-var': [2],
|
||||||
'no-void': [2],
|
'no-void': [2],
|
||||||
'no-warning-comments': [0],
|
'no-warning-comments': [0],
|
||||||
@ -617,7 +617,7 @@ export default defineConfig([
|
|||||||
'one-var-declaration-per-line': [0],
|
'one-var-declaration-per-line': [0],
|
||||||
'one-var': [0],
|
'one-var': [0],
|
||||||
'operator-assignment': [2, 'always'],
|
'operator-assignment': [2, 'always'],
|
||||||
'operator-linebreak': [2, 'after'],
|
'operator-linebreak': [0], // handled by @stylistic/operator-linebreak
|
||||||
'prefer-arrow-callback': [2, {allowNamedFunctions: true, allowUnboundThis: true}],
|
'prefer-arrow-callback': [2, {allowNamedFunctions: true, allowUnboundThis: true}],
|
||||||
'prefer-const': [2, {destructuring: 'all', ignoreReadBeforeAssign: true}],
|
'prefer-const': [2, {destructuring: 'all', ignoreReadBeforeAssign: true}],
|
||||||
'prefer-destructuring': [0],
|
'prefer-destructuring': [0],
|
||||||
@ -697,7 +697,7 @@ export default defineConfig([
|
|||||||
'regexp/prefer-question-quantifier': [2],
|
'regexp/prefer-question-quantifier': [2],
|
||||||
'regexp/prefer-range': [2],
|
'regexp/prefer-range': [2],
|
||||||
'regexp/prefer-regexp-exec': [2],
|
'regexp/prefer-regexp-exec': [2],
|
||||||
'regexp/prefer-regexp-test': [2],
|
'regexp/prefer-regexp-test': [0], // handled by unicorn/prefer-regexp-test
|
||||||
'regexp/prefer-result-array-groups': [0],
|
'regexp/prefer-result-array-groups': [0],
|
||||||
'regexp/prefer-set-operation': [2],
|
'regexp/prefer-set-operation': [2],
|
||||||
'regexp/prefer-star-quantifier': [2],
|
'regexp/prefer-star-quantifier': [2],
|
||||||
@ -727,7 +727,7 @@ export default defineConfig([
|
|||||||
'sonarjs/no-empty-collection': [2],
|
'sonarjs/no-empty-collection': [2],
|
||||||
'sonarjs/no-extra-arguments': [2],
|
'sonarjs/no-extra-arguments': [2],
|
||||||
'sonarjs/no-gratuitous-expressions': [2],
|
'sonarjs/no-gratuitous-expressions': [2],
|
||||||
'sonarjs/no-identical-conditions': [2],
|
'sonarjs/no-identical-conditions': [0], // handled by no-dupe-else-if
|
||||||
'sonarjs/no-identical-expressions': [2],
|
'sonarjs/no-identical-expressions': [2],
|
||||||
'sonarjs/no-identical-functions': [2, 5],
|
'sonarjs/no-identical-functions': [2, 5],
|
||||||
'sonarjs/no-ignored-return': [2],
|
'sonarjs/no-ignored-return': [2],
|
||||||
@ -740,7 +740,7 @@ export default defineConfig([
|
|||||||
'sonarjs/no-small-switch': [0],
|
'sonarjs/no-small-switch': [0],
|
||||||
'sonarjs/no-unused-collection': [2],
|
'sonarjs/no-unused-collection': [2],
|
||||||
'sonarjs/no-use-of-empty-return-value': [2],
|
'sonarjs/no-use-of-empty-return-value': [2],
|
||||||
'sonarjs/no-useless-catch': [2],
|
'sonarjs/no-useless-catch': [0], // handled by no-useless-catch
|
||||||
'sonarjs/non-existent-operator': [2],
|
'sonarjs/non-existent-operator': [2],
|
||||||
'sonarjs/prefer-immediate-return': [0],
|
'sonarjs/prefer-immediate-return': [0],
|
||||||
'sonarjs/prefer-object-literal': [0],
|
'sonarjs/prefer-object-literal': [0],
|
||||||
@ -767,6 +767,7 @@ export default defineConfig([
|
|||||||
'unicorn/filename-case': [0],
|
'unicorn/filename-case': [0],
|
||||||
'unicorn/import-index': [0],
|
'unicorn/import-index': [0],
|
||||||
'unicorn/import-style': [0],
|
'unicorn/import-style': [0],
|
||||||
|
'unicorn/isolated-functions': [2, {functions: []}],
|
||||||
'unicorn/new-for-builtins': [2],
|
'unicorn/new-for-builtins': [2],
|
||||||
'unicorn/no-abusive-eslint-disable': [0],
|
'unicorn/no-abusive-eslint-disable': [0],
|
||||||
'unicorn/no-anonymous-default-export': [0],
|
'unicorn/no-anonymous-default-export': [0],
|
||||||
@ -806,7 +807,7 @@ export default defineConfig([
|
|||||||
'unicorn/no-unnecessary-await': [2],
|
'unicorn/no-unnecessary-await': [2],
|
||||||
'unicorn/no-unnecessary-polyfills': [2],
|
'unicorn/no-unnecessary-polyfills': [2],
|
||||||
'unicorn/no-unreadable-array-destructuring': [0],
|
'unicorn/no-unreadable-array-destructuring': [0],
|
||||||
'unicorn/no-unreadable-iife': [2],
|
'unicorn/no-unreadable-iife': [0],
|
||||||
'unicorn/no-unused-properties': [2],
|
'unicorn/no-unused-properties': [2],
|
||||||
'unicorn/no-useless-collection-argument': [2],
|
'unicorn/no-useless-collection-argument': [2],
|
||||||
'unicorn/no-useless-fallback-in-spread': [2],
|
'unicorn/no-useless-fallback-in-spread': [2],
|
||||||
@ -819,7 +820,7 @@ export default defineConfig([
|
|||||||
'unicorn/number-literal-case': [0],
|
'unicorn/number-literal-case': [0],
|
||||||
'unicorn/numeric-separators-style': [0],
|
'unicorn/numeric-separators-style': [0],
|
||||||
'unicorn/prefer-add-event-listener': [2],
|
'unicorn/prefer-add-event-listener': [2],
|
||||||
'unicorn/prefer-array-find': [2],
|
'unicorn/prefer-array-find': [0], // handled by @typescript-eslint/prefer-find
|
||||||
'unicorn/prefer-array-flat': [2],
|
'unicorn/prefer-array-flat': [2],
|
||||||
'unicorn/prefer-array-flat-map': [2],
|
'unicorn/prefer-array-flat-map': [2],
|
||||||
'unicorn/prefer-array-index-of': [2],
|
'unicorn/prefer-array-index-of': [2],
|
||||||
@ -836,7 +837,7 @@ export default defineConfig([
|
|||||||
'unicorn/prefer-event-target': [2],
|
'unicorn/prefer-event-target': [2],
|
||||||
'unicorn/prefer-export-from': [0],
|
'unicorn/prefer-export-from': [0],
|
||||||
'unicorn/prefer-global-this': [0],
|
'unicorn/prefer-global-this': [0],
|
||||||
'unicorn/prefer-includes': [2],
|
'unicorn/prefer-includes': [0], // handled by @typescript-eslint/prefer-includes
|
||||||
'unicorn/prefer-json-parse-buffer': [0],
|
'unicorn/prefer-json-parse-buffer': [0],
|
||||||
'unicorn/prefer-keyboard-event-key': [2],
|
'unicorn/prefer-keyboard-event-key': [2],
|
||||||
'unicorn/prefer-logical-operator-over-ternary': [2],
|
'unicorn/prefer-logical-operator-over-ternary': [2],
|
||||||
@ -863,7 +864,7 @@ export default defineConfig([
|
|||||||
'unicorn/prefer-string-raw': [0],
|
'unicorn/prefer-string-raw': [0],
|
||||||
'unicorn/prefer-string-replace-all': [0],
|
'unicorn/prefer-string-replace-all': [0],
|
||||||
'unicorn/prefer-string-slice': [0],
|
'unicorn/prefer-string-slice': [0],
|
||||||
'unicorn/prefer-string-starts-ends-with': [2],
|
'unicorn/prefer-string-starts-ends-with': [0], // handled by @typescript-eslint/prefer-string-starts-ends-with
|
||||||
'unicorn/prefer-string-trim-start-end': [2],
|
'unicorn/prefer-string-trim-start-end': [2],
|
||||||
'unicorn/prefer-structured-clone': [2],
|
'unicorn/prefer-structured-clone': [2],
|
||||||
'unicorn/prefer-switch': [0],
|
'unicorn/prefer-switch': [0],
|
||||||
|
|||||||
6
flake.lock
generated
6
flake.lock
generated
@ -2,11 +2,11 @@
|
|||||||
"nodes": {
|
"nodes": {
|
||||||
"nixpkgs": {
|
"nixpkgs": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1760038930,
|
"lastModified": 1771369470,
|
||||||
"narHash": "sha256-Oncbh0UmHjSlxO7ErQDM3KM0A5/Znfofj2BSzlHLeVw=",
|
"narHash": "sha256-0NBlEBKkN3lufyvFegY4TYv5mCNHbi5OmBDrzihbBMQ=",
|
||||||
"owner": "nixos",
|
"owner": "nixos",
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"rev": "0b4defa2584313f3b781240b29d61f6f9f7e0df3",
|
"rev": "0182a361324364ae3f436a63005877674cf45efb",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
|
|||||||
2
go.mod
2
go.mod
@ -53,7 +53,7 @@ require (
|
|||||||
github.com/go-co-op/gocron v1.37.0
|
github.com/go-co-op/gocron v1.37.0
|
||||||
github.com/go-enry/go-enry/v2 v2.9.4
|
github.com/go-enry/go-enry/v2 v2.9.4
|
||||||
github.com/go-git/go-billy/v5 v5.7.0
|
github.com/go-git/go-billy/v5 v5.7.0
|
||||||
github.com/go-git/go-git/v5 v5.16.4
|
github.com/go-git/go-git/v5 v5.16.5
|
||||||
github.com/go-ldap/ldap/v3 v3.4.12
|
github.com/go-ldap/ldap/v3 v3.4.12
|
||||||
github.com/go-redsync/redsync/v4 v4.15.0
|
github.com/go-redsync/redsync/v4 v4.15.0
|
||||||
github.com/go-sql-driver/mysql v1.9.3
|
github.com/go-sql-driver/mysql v1.9.3
|
||||||
|
|||||||
4
go.sum
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-billy/v5 v5.7.0/go.mod h1:/1IUejTKH8xipsAcdfcSAlUlo2J7lkYV8GTKxAT/L3E=
|
||||||
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4=
|
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4=
|
||||||
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII=
|
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII=
|
||||||
github.com/go-git/go-git/v5 v5.16.4 h1:7ajIEZHZJULcyJebDLo99bGgS0jRrOxzZG4uCk2Yb2Y=
|
github.com/go-git/go-git/v5 v5.16.5 h1:mdkuqblwr57kVfXri5TTH+nMFLNUxIj9Z7F5ykFbw5s=
|
||||||
github.com/go-git/go-git/v5 v5.16.4/go.mod h1:4Ge4alE/5gPs30F2H1esi2gPd69R0C39lolkucHBOp8=
|
github.com/go-git/go-git/v5 v5.16.5/go.mod h1:QOMLpNf1qxuSY4StA/ArOdfFR2TrKEjJiye2kel2m+M=
|
||||||
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
|
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
|
||||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
||||||
github.com/go-ini/ini v1.67.0 h1:z6ZrTEZqSWOTyH2FlglNbNgARyHG8oLW9gMELqKr06A=
|
github.com/go-ini/ini v1.67.0 h1:z6ZrTEZqSWOTyH2FlglNbNgARyHG8oLW9gMELqKr06A=
|
||||||
|
|||||||
8
main.go
8
main.go
@ -26,9 +26,8 @@ import (
|
|||||||
|
|
||||||
// these flags will be set by the build flags
|
// these flags will be set by the build flags
|
||||||
var (
|
var (
|
||||||
Version = "development" // program version for this build
|
Version = "development" // program version for this build
|
||||||
Tags = "" // the Golang build tags
|
Tags = "" // the Golang build tags
|
||||||
MakeVersion = "" // "make" program version if built with make
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
@ -50,9 +49,6 @@ func main() {
|
|||||||
|
|
||||||
func formatBuiltWith() string {
|
func formatBuiltWith() string {
|
||||||
version := runtime.Version()
|
version := runtime.Version()
|
||||||
if len(MakeVersion) > 0 {
|
|
||||||
version = MakeVersion + ", " + runtime.Version()
|
|
||||||
}
|
|
||||||
if len(Tags) == 0 {
|
if len(Tags) == 0 {
|
||||||
return " built with " + version
|
return " built with " + version
|
||||||
}
|
}
|
||||||
|
|||||||
@ -168,7 +168,7 @@ func (run *ActionRun) GetPushEventPayload() (*api.PushPayload, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (run *ActionRun) GetPullRequestEventPayload() (*api.PullRequestPayload, error) {
|
func (run *ActionRun) GetPullRequestEventPayload() (*api.PullRequestPayload, error) {
|
||||||
if run.Event.IsPullRequest() {
|
if run.Event.IsPullRequest() || run.Event.IsPullRequestReview() {
|
||||||
var payload api.PullRequestPayload
|
var payload api.PullRequestPayload
|
||||||
if err := json.Unmarshal([]byte(run.EventPayload), &payload); err != nil {
|
if err := json.Unmarshal([]byte(run.EventPayload), &payload); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|||||||
@ -8,6 +8,7 @@ import (
|
|||||||
"crypto/subtle"
|
"crypto/subtle"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
auth_model "code.gitea.io/gitea/models/auth"
|
auth_model "code.gitea.io/gitea/models/auth"
|
||||||
@ -20,6 +21,7 @@ import (
|
|||||||
|
|
||||||
runnerv1 "code.gitea.io/actions-proto-go/runner/v1"
|
runnerv1 "code.gitea.io/actions-proto-go/runner/v1"
|
||||||
lru "github.com/hashicorp/golang-lru/v2"
|
lru "github.com/hashicorp/golang-lru/v2"
|
||||||
|
"github.com/nektos/act/pkg/jobparser"
|
||||||
"google.golang.org/protobuf/types/known/timestamppb"
|
"google.golang.org/protobuf/types/known/timestamppb"
|
||||||
"xorm.io/builder"
|
"xorm.io/builder"
|
||||||
)
|
)
|
||||||
@ -214,6 +216,20 @@ func GetRunningTaskByToken(ctx context.Context, token string) (*ActionTask, erro
|
|||||||
return nil, errNotExist
|
return nil, errNotExist
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func makeTaskStepDisplayName(step *jobparser.Step, limit int) (name string) {
|
||||||
|
if step.Name != "" {
|
||||||
|
name = step.Name // the step has an explicit name
|
||||||
|
} else {
|
||||||
|
// for unnamed step, its "String()" method tries to get a display name by its "name", "uses",
|
||||||
|
// "run" or "id" (last fallback), we add the "Run " prefix for unnamed steps for better display
|
||||||
|
// for multi-line "run" scripts, only use the first line to match GitHub's behavior
|
||||||
|
// https://github.com/actions/runner/blob/66800900843747f37591b077091dd2c8cf2c1796/src/Runner.Worker/Handlers/ScriptHandler.cs#L45-L58
|
||||||
|
runStr, _, _ := strings.Cut(strings.TrimSpace(step.Run), "\n")
|
||||||
|
name = "Run " + util.IfZero(strings.TrimSpace(runStr), step.String())
|
||||||
|
}
|
||||||
|
return util.EllipsisDisplayString(name, limit) // database column has a length limit
|
||||||
|
}
|
||||||
|
|
||||||
func CreateTaskForRunner(ctx context.Context, runner *ActionRunner) (*ActionTask, bool, error) {
|
func CreateTaskForRunner(ctx context.Context, runner *ActionRunner) (*ActionTask, bool, error) {
|
||||||
ctx, committer, err := db.TxContext(ctx)
|
ctx, committer, err := db.TxContext(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -293,9 +309,8 @@ func CreateTaskForRunner(ctx context.Context, runner *ActionRunner) (*ActionTask
|
|||||||
if len(workflowJob.Steps) > 0 {
|
if len(workflowJob.Steps) > 0 {
|
||||||
steps := make([]*ActionTaskStep, len(workflowJob.Steps))
|
steps := make([]*ActionTaskStep, len(workflowJob.Steps))
|
||||||
for i, v := range workflowJob.Steps {
|
for i, v := range workflowJob.Steps {
|
||||||
name := util.EllipsisDisplayString(v.String(), 255)
|
|
||||||
steps[i] = &ActionTaskStep{
|
steps[i] = &ActionTaskStep{
|
||||||
Name: name,
|
Name: makeTaskStepDisplayName(v, 255),
|
||||||
TaskID: task.ID,
|
TaskID: task.ID,
|
||||||
Index: int64(i),
|
Index: int64(i),
|
||||||
RepoID: task.RepoID,
|
RepoID: task.RepoID,
|
||||||
|
|||||||
@ -14,7 +14,7 @@ import (
|
|||||||
// ActionTaskStep represents a step of ActionTask
|
// ActionTaskStep represents a step of ActionTask
|
||||||
type ActionTaskStep struct {
|
type ActionTaskStep struct {
|
||||||
ID int64
|
ID int64
|
||||||
Name string `xorm:"VARCHAR(255)"`
|
Name string `xorm:"VARCHAR(255)"` // the step name, for display purpose only, it will be truncated if it is too long
|
||||||
TaskID int64 `xorm:"index unique(task_index)"`
|
TaskID int64 `xorm:"index unique(task_index)"`
|
||||||
Index int64 `xorm:"index unique(task_index)"`
|
Index int64 `xorm:"index unique(task_index)"`
|
||||||
RepoID int64 `xorm:"index"`
|
RepoID int64 `xorm:"index"`
|
||||||
|
|||||||
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"`
|
Contributions int64 `json:"contributions"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetUserHeatmapDataByUser returns an array of UserHeatmapData
|
// GetUserHeatmapDataByUser returns an array of UserHeatmapData, it checks whether doer can access user's activity
|
||||||
func GetUserHeatmapDataByUser(ctx context.Context, user, doer *user_model.User) ([]*UserHeatmapData, error) {
|
func GetUserHeatmapDataByUser(ctx context.Context, user, doer *user_model.User) ([]*UserHeatmapData, error) {
|
||||||
return getUserHeatmapData(ctx, user, nil, doer)
|
return getUserHeatmapData(ctx, user, nil, doer)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetUserHeatmapDataByUserTeam returns an array of UserHeatmapData
|
// GetUserHeatmapDataByOrgTeam returns an array of UserHeatmapData, it checks whether doer can access org's activity
|
||||||
func GetUserHeatmapDataByUserTeam(ctx context.Context, user *user_model.User, team *organization.Team, doer *user_model.User) ([]*UserHeatmapData, error) {
|
func GetUserHeatmapDataByOrgTeam(ctx context.Context, org *organization.Organization, team *organization.Team, doer *user_model.User) ([]*UserHeatmapData, error) {
|
||||||
return getUserHeatmapData(ctx, user, team, doer)
|
return getUserHeatmapData(ctx, org.AsUser(), team, doer)
|
||||||
}
|
}
|
||||||
|
|
||||||
func getUserHeatmapData(ctx context.Context, user *user_model.User, team *organization.Team, doer *user_model.User) ([]*UserHeatmapData, error) {
|
func getUserHeatmapData(ctx context.Context, user *user_model.User, team *organization.Team, doer *user_model.User) ([]*UserHeatmapData, error) {
|
||||||
@ -71,12 +71,3 @@ func getUserHeatmapData(ctx context.Context, user *user_model.User, team *organi
|
|||||||
OrderBy("timestamp").
|
OrderBy("timestamp").
|
||||||
Find(&hdata)
|
Find(&hdata)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetTotalContributionsInHeatmap returns the total number of contributions in a heatmap
|
|
||||||
func GetTotalContributionsInHeatmap(hdata []*UserHeatmapData) int64 {
|
|
||||||
var total int64
|
|
||||||
for _, v := range hdata {
|
|
||||||
total += v.Contributions
|
|
||||||
}
|
|
||||||
return total
|
|
||||||
}
|
|
||||||
|
|||||||
@ -139,26 +139,7 @@
|
|||||||
updated: 1683636626
|
updated: 1683636626
|
||||||
need_approval: 0
|
need_approval: 0
|
||||||
approved_by: 0
|
approved_by: 0
|
||||||
-
|
|
||||||
id: 804
|
|
||||||
title: "use a private action"
|
|
||||||
repo_id: 60
|
|
||||||
owner_id: 40
|
|
||||||
workflow_id: "run.yaml"
|
|
||||||
index: 189
|
|
||||||
trigger_user_id: 40
|
|
||||||
ref: "refs/heads/master"
|
|
||||||
commit_sha: "6e64b26de7ba966d01d90ecfaf5c7f14ef203e86"
|
|
||||||
event: "push"
|
|
||||||
trigger_event: "push"
|
|
||||||
is_fork_pull_request: 0
|
|
||||||
status: 1
|
|
||||||
started: 1683636528
|
|
||||||
stopped: 1683636626
|
|
||||||
created: 1683636108
|
|
||||||
updated: 1683636626
|
|
||||||
need_approval: 0
|
|
||||||
approved_by: 0
|
|
||||||
-
|
-
|
||||||
id: 805
|
id: 805
|
||||||
title: "update actions"
|
title: "update actions"
|
||||||
|
|||||||
@ -129,20 +129,7 @@
|
|||||||
status: 5
|
status: 5
|
||||||
started: 1683636528
|
started: 1683636528
|
||||||
stopped: 1683636626
|
stopped: 1683636626
|
||||||
-
|
|
||||||
id: 205
|
|
||||||
run_id: 804
|
|
||||||
repo_id: 6
|
|
||||||
owner_id: 10
|
|
||||||
commit_sha: 6e64b26de7ba966d01d90ecfaf5c7f14ef203e86
|
|
||||||
is_fork_pull_request: 0
|
|
||||||
name: job_2
|
|
||||||
attempt: 1
|
|
||||||
job_id: job_2
|
|
||||||
task_id: 48
|
|
||||||
status: 1
|
|
||||||
started: 1683636528
|
|
||||||
stopped: 1683636626
|
|
||||||
-
|
-
|
||||||
id: 206
|
id: 206
|
||||||
run_id: 805
|
run_id: 805
|
||||||
|
|||||||
@ -177,26 +177,7 @@
|
|||||||
log_length: 0
|
log_length: 0
|
||||||
log_size: 0
|
log_size: 0
|
||||||
log_expired: 0
|
log_expired: 0
|
||||||
-
|
|
||||||
id: 55
|
|
||||||
job_id: 205
|
|
||||||
attempt: 1
|
|
||||||
runner_id: 1
|
|
||||||
status: 6 # 6 is the status code for "running"
|
|
||||||
started: 1683636528
|
|
||||||
stopped: 1683636626
|
|
||||||
repo_id: 6
|
|
||||||
owner_id: 10
|
|
||||||
commit_sha: 6e64b26de7ba966d01d90ecfaf5c7f14ef203e86
|
|
||||||
is_fork_pull_request: 0
|
|
||||||
token_hash: b8d3962425466b6709b9ac51446f93260c54afe8e7b6d3686e34f991fb8a8953822b0deed86fe41a103f34bc48dbc478422b
|
|
||||||
token_salt: ERxJGHvg3I
|
|
||||||
token_last_eight: 182199eb
|
|
||||||
log_filename: collaborative-owner-test/1a/49.log
|
|
||||||
log_in_storage: 1
|
|
||||||
log_length: 707
|
|
||||||
log_size: 90179
|
|
||||||
log_expired: 0
|
|
||||||
-
|
-
|
||||||
id: 56
|
id: 56
|
||||||
attempt: 1
|
attempt: 1
|
||||||
|
|||||||
@ -397,10 +397,16 @@ func RenameBranch(ctx context.Context, repo *repo_model.Repository, from, to str
|
|||||||
|
|
||||||
if protectedBranch != nil {
|
if protectedBranch != nil {
|
||||||
// there is a protect rule for this branch
|
// there is a protect rule for this branch
|
||||||
protectedBranch.RuleName = to
|
existingRule, err := GetProtectedBranchRuleByName(ctx, repo.ID, to)
|
||||||
if _, err = sess.ID(protectedBranch.ID).Cols("branch_name").Update(protectedBranch); err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
if existingRule == nil || existingRule.ID == protectedBranch.ID {
|
||||||
|
protectedBranch.RuleName = to
|
||||||
|
if _, err = sess.ID(protectedBranch.ID).Cols("branch_name").Update(protectedBranch); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// some glob protect rules may match this branch
|
// some glob protect rules may match this branch
|
||||||
protected, err := IsBranchProtected(ctx, repo.ID, from)
|
protected, err := IsBranchProtected(ctx, repo.ID, from)
|
||||||
|
|||||||
@ -159,6 +159,53 @@ func TestRenameBranch(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestRenameBranchProtectedRuleConflict(t *testing.T) {
|
||||||
|
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||||
|
repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
|
||||||
|
master := unittest.AssertExistsAndLoadBean(t, &git_model.Branch{RepoID: repo1.ID, Name: "master"})
|
||||||
|
|
||||||
|
devBranch := &git_model.Branch{
|
||||||
|
RepoID: repo1.ID,
|
||||||
|
Name: "dev",
|
||||||
|
CommitID: master.CommitID,
|
||||||
|
CommitMessage: master.CommitMessage,
|
||||||
|
CommitTime: master.CommitTime,
|
||||||
|
PusherID: master.PusherID,
|
||||||
|
}
|
||||||
|
assert.NoError(t, db.Insert(t.Context(), devBranch))
|
||||||
|
|
||||||
|
pbDev := git_model.ProtectedBranch{
|
||||||
|
RepoID: repo1.ID,
|
||||||
|
RuleName: "dev",
|
||||||
|
CanPush: true,
|
||||||
|
}
|
||||||
|
assert.NoError(t, git_model.UpdateProtectBranch(t.Context(), repo1, &pbDev, git_model.WhitelistOptions{}))
|
||||||
|
|
||||||
|
pbMain := git_model.ProtectedBranch{
|
||||||
|
RepoID: repo1.ID,
|
||||||
|
RuleName: "main",
|
||||||
|
CanPush: true,
|
||||||
|
}
|
||||||
|
assert.NoError(t, git_model.UpdateProtectBranch(t.Context(), repo1, &pbMain, git_model.WhitelistOptions{}))
|
||||||
|
|
||||||
|
assert.NoError(t, git_model.RenameBranch(t.Context(), repo1, "dev", "main", func(ctx context.Context, isDefault bool) error {
|
||||||
|
return nil
|
||||||
|
}))
|
||||||
|
|
||||||
|
unittest.AssertNotExistsBean(t, &git_model.Branch{RepoID: repo1.ID, Name: "dev"})
|
||||||
|
unittest.AssertExistsAndLoadBean(t, &git_model.Branch{RepoID: repo1.ID, Name: "main"})
|
||||||
|
|
||||||
|
protectedDev, err := git_model.GetProtectedBranchRuleByName(t.Context(), repo1.ID, "dev")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.NotNil(t, protectedDev)
|
||||||
|
assert.Equal(t, "dev", protectedDev.RuleName)
|
||||||
|
|
||||||
|
protectedMainByID, err := git_model.GetProtectedBranchRuleByID(t.Context(), repo1.ID, pbMain.ID)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.NotNil(t, protectedMainByID)
|
||||||
|
assert.Equal(t, "main", protectedMainByID.RuleName)
|
||||||
|
}
|
||||||
|
|
||||||
func TestOnlyGetDeletedBranchOnCorrectRepo(t *testing.T) {
|
func TestOnlyGetDeletedBranchOnCorrectRepo(t *testing.T) {
|
||||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||||
|
|
||||||
|
|||||||
@ -10,7 +10,6 @@ import (
|
|||||||
"path"
|
"path"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
|
||||||
|
|
||||||
"code.gitea.io/gitea/models/db"
|
"code.gitea.io/gitea/models/db"
|
||||||
"code.gitea.io/gitea/models/unittest"
|
"code.gitea.io/gitea/models/unittest"
|
||||||
@ -27,18 +26,6 @@ import (
|
|||||||
|
|
||||||
// FIXME: this file shouldn't be in a normal package, it should only be compiled for tests
|
// FIXME: this file shouldn't be in a normal package, it should only be compiled for tests
|
||||||
|
|
||||||
func removeAllWithRetry(dir string) error {
|
|
||||||
var err error
|
|
||||||
for range 20 {
|
|
||||||
err = os.RemoveAll(dir)
|
|
||||||
if err == nil {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
time.Sleep(100 * time.Millisecond)
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func newXORMEngine(t *testing.T) (*xorm.Engine, error) {
|
func newXORMEngine(t *testing.T) (*xorm.Engine, error) {
|
||||||
if err := db.InitEngine(t.Context()); err != nil {
|
if err := db.InitEngine(t.Context()); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@ -213,13 +200,12 @@ func LoadTableSchemasMap(t *testing.T, x *xorm.Engine) map[string]*schemas.Table
|
|||||||
return tableMap
|
return tableMap
|
||||||
}
|
}
|
||||||
|
|
||||||
func MainTest(m *testing.M) {
|
func mainTest(m *testing.M) int {
|
||||||
testlogger.Init()
|
testlogger.Init()
|
||||||
setting.SetupGiteaTestEnv()
|
|
||||||
|
|
||||||
tmpDataPath, cleanup, err := tempdir.OsTempDir("gitea-test").MkdirTempRandom("data")
|
tmpDataPath, cleanup, err := tempdir.OsTempDir("gitea-test").MkdirTempRandom("data")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
testlogger.Fatalf("Unable to create temporary data path %v\n", err)
|
testlogger.Panicf("Unable to create temporary data path %v\n", err)
|
||||||
}
|
}
|
||||||
defer cleanup()
|
defer cleanup()
|
||||||
|
|
||||||
@ -227,15 +213,13 @@ func MainTest(m *testing.M) {
|
|||||||
|
|
||||||
unittest.InitSettingsForTesting()
|
unittest.InitSettingsForTesting()
|
||||||
if err = git.InitFull(); err != nil {
|
if err = git.InitFull(); err != nil {
|
||||||
testlogger.Fatalf("Unable to InitFull: %v\n", err)
|
testlogger.Panicf("Unable to InitFull: %v\n", err)
|
||||||
}
|
}
|
||||||
setting.LoadDBSetting()
|
setting.LoadDBSetting()
|
||||||
setting.InitLoggersForTest()
|
setting.InitLoggersForTest()
|
||||||
|
return m.Run()
|
||||||
exitStatus := m.Run()
|
}
|
||||||
|
|
||||||
if err := removeAllWithRetry(setting.RepoRootPath); err != nil {
|
func MainTest(m *testing.M) {
|
||||||
_, _ = fmt.Fprintf(os.Stderr, "os.RemoveAll: %v\n", err)
|
os.Exit(mainTest(m))
|
||||||
}
|
|
||||||
os.Exit(exitStatus)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,6 +4,7 @@
|
|||||||
package unittest_test
|
package unittest_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
@ -58,9 +59,14 @@ func NewFixturesLoaderVendorGoTestfixtures(e *xorm.Engine, opts unittest.Fixture
|
|||||||
}
|
}
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
func TestMain(m *testing.M) {
|
||||||
|
setting.SetupGiteaTestEnv()
|
||||||
|
os.Exit(m.Run())
|
||||||
|
}
|
||||||
|
|
||||||
func prepareTestFixturesLoaders(t testing.TB) unittest.FixturesOptions {
|
func prepareTestFixturesLoaders(t testing.TB) unittest.FixturesOptions {
|
||||||
_ = user_model.User{}
|
_ = user_model.User{}
|
||||||
giteaRoot := setting.SetupGiteaTestEnv()
|
giteaRoot := setting.GetGiteaTestSourceRoot()
|
||||||
opts := unittest.FixturesOptions{Dir: filepath.Join(giteaRoot, "models", "fixtures"), Files: []string{
|
opts := unittest.FixturesOptions{Dir: filepath.Join(giteaRoot, "models", "fixtures"), Files: []string{
|
||||||
"user.yml",
|
"user.yml",
|
||||||
}}
|
}}
|
||||||
|
|||||||
@ -4,10 +4,12 @@
|
|||||||
package unittest
|
package unittest
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"code.gitea.io/gitea/modules/setting"
|
||||||
"code.gitea.io/gitea/modules/util"
|
"code.gitea.io/gitea/modules/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -39,7 +41,20 @@ func SyncFile(srcPath, destPath string) error {
|
|||||||
// SyncDirs synchronizes files recursively from source to target directory.
|
// SyncDirs synchronizes files recursively from source to target directory.
|
||||||
// It returns error when error occurs in underlying functions.
|
// It returns error when error occurs in underlying functions.
|
||||||
func SyncDirs(srcPath, destPath string) error {
|
func SyncDirs(srcPath, destPath string) error {
|
||||||
err := os.MkdirAll(destPath, os.ModePerm)
|
destPath = filepath.Clean(destPath)
|
||||||
|
destPathAbs, err := filepath.Abs(destPath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
devDataPathAbs, err := filepath.Abs(filepath.Join(setting.GetGiteaTestSourceRoot(), "data"))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if strings.HasPrefix(destPathAbs+string(filepath.Separator), devDataPathAbs+string(filepath.Separator)) {
|
||||||
|
return errors.New("destination path should not be inside Gitea data directory, otherwise your data for dev mode will be removed")
|
||||||
|
}
|
||||||
|
|
||||||
|
err = os.MkdirAll(destPath, os.ModePerm)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@ -21,6 +21,7 @@ import (
|
|||||||
"code.gitea.io/gitea/modules/setting/config"
|
"code.gitea.io/gitea/modules/setting/config"
|
||||||
"code.gitea.io/gitea/modules/storage"
|
"code.gitea.io/gitea/modules/storage"
|
||||||
"code.gitea.io/gitea/modules/tempdir"
|
"code.gitea.io/gitea/modules/tempdir"
|
||||||
|
"code.gitea.io/gitea/modules/testlogger"
|
||||||
"code.gitea.io/gitea/modules/util"
|
"code.gitea.io/gitea/modules/util"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
@ -28,16 +29,10 @@ import (
|
|||||||
"xorm.io/xorm/names"
|
"xorm.io/xorm/names"
|
||||||
)
|
)
|
||||||
|
|
||||||
var giteaRoot string
|
|
||||||
|
|
||||||
func fatalTestError(fmtStr string, args ...any) {
|
|
||||||
_, _ = fmt.Fprintf(os.Stderr, fmtStr, args...)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
// InitSettingsForTesting initializes config provider and load common settings for tests
|
// InitSettingsForTesting initializes config provider and load common settings for tests
|
||||||
func InitSettingsForTesting() {
|
func InitSettingsForTesting() {
|
||||||
setting.IsInTesting = true
|
setting.SetupGiteaTestEnv()
|
||||||
|
|
||||||
log.OsExiter = func(code int) {
|
log.OsExiter = func(code int) {
|
||||||
if code != 0 {
|
if code != 0 {
|
||||||
// non-zero exit code (log.Fatal) shouldn't occur during testing, if it happens, show a full stacktrace for more details
|
// non-zero exit code (log.Fatal) shouldn't occur during testing, if it happens, show a full stacktrace for more details
|
||||||
@ -49,8 +44,12 @@ func InitSettingsForTesting() {
|
|||||||
setting.CustomConf = filepath.Join(setting.CustomPath, "conf/app-unittest-tmp.ini")
|
setting.CustomConf = filepath.Join(setting.CustomPath, "conf/app-unittest-tmp.ini")
|
||||||
_ = os.Remove(setting.CustomConf)
|
_ = os.Remove(setting.CustomConf)
|
||||||
}
|
}
|
||||||
setting.InitCfgProvider(setting.CustomConf)
|
|
||||||
setting.LoadCommonSettings()
|
// init paths and config system for testing
|
||||||
|
getTestEnv := func(key string) string {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
setting.InitWorkPathAndCommonConfig(getTestEnv, setting.ArgWorkPathAndCustomConf{CustomConf: setting.CustomConf})
|
||||||
|
|
||||||
if err := setting.PrepareAppDataPath(); err != nil {
|
if err := setting.PrepareAppDataPath(); err != nil {
|
||||||
log.Fatal("Can not prepare APP_DATA_PATH: %v", err)
|
log.Fatal("Can not prepare APP_DATA_PATH: %v", err)
|
||||||
@ -71,16 +70,18 @@ type TestOptions struct {
|
|||||||
// MainTest a reusable TestMain(..) function for unit tests that need to use a
|
// MainTest a reusable TestMain(..) function for unit tests that need to use a
|
||||||
// test database. Creates the test database, and sets necessary settings.
|
// test database. Creates the test database, and sets necessary settings.
|
||||||
func MainTest(m *testing.M, testOptsArg ...*TestOptions) {
|
func MainTest(m *testing.M, testOptsArg ...*TestOptions) {
|
||||||
testOpts := util.OptionalArg(testOptsArg, &TestOptions{})
|
os.Exit(mainTest(m, testOptsArg...))
|
||||||
giteaRoot = setting.SetupGiteaTestEnv()
|
}
|
||||||
InitSettingsForTesting()
|
|
||||||
|
|
||||||
|
func mainTest(m *testing.M, testOptsArg ...*TestOptions) int {
|
||||||
|
testOpts := util.OptionalArg(testOptsArg, &TestOptions{})
|
||||||
|
InitSettingsForTesting()
|
||||||
|
giteaRoot := setting.GetGiteaTestSourceRoot()
|
||||||
fixturesOpts := FixturesOptions{Dir: filepath.Join(giteaRoot, "models", "fixtures"), Files: testOpts.FixtureFiles}
|
fixturesOpts := FixturesOptions{Dir: filepath.Join(giteaRoot, "models", "fixtures"), Files: testOpts.FixtureFiles}
|
||||||
if err := CreateTestEngine(fixturesOpts); err != nil {
|
if err := CreateTestEngine(fixturesOpts); err != nil {
|
||||||
fatalTestError("Error creating test engine: %v\n", err)
|
testlogger.Panicf("Error creating test engine: %v\n", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
setting.IsInTesting = true
|
|
||||||
setting.AppURL = "https://try.gitea.io/"
|
setting.AppURL = "https://try.gitea.io/"
|
||||||
setting.Domain = "try.gitea.io"
|
setting.Domain = "try.gitea.io"
|
||||||
setting.RunUser = "runuser"
|
setting.RunUser = "runuser"
|
||||||
@ -92,20 +93,18 @@ func MainTest(m *testing.M, testOptsArg ...*TestOptions) {
|
|||||||
setting.Repository.DefaultBranch = "master" // many test code still assume that default branch is called "master"
|
setting.Repository.DefaultBranch = "master" // many test code still assume that default branch is called "master"
|
||||||
repoRootPath, cleanup1, err := tempdir.OsTempDir("gitea-test").MkdirTempRandom("repos")
|
repoRootPath, cleanup1, err := tempdir.OsTempDir("gitea-test").MkdirTempRandom("repos")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fatalTestError("TempDir: %v\n", err)
|
testlogger.Panicf("TempDir: %v\n", err)
|
||||||
}
|
}
|
||||||
defer cleanup1()
|
defer cleanup1()
|
||||||
|
|
||||||
setting.RepoRootPath = repoRootPath
|
setting.RepoRootPath = repoRootPath
|
||||||
appDataPath, cleanup2, err := tempdir.OsTempDir("gitea-test").MkdirTempRandom("appdata")
|
appDataPath, cleanup2, err := tempdir.OsTempDir("gitea-test").MkdirTempRandom("appdata")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fatalTestError("TempDir: %v\n", err)
|
testlogger.Panicf("TempDir: %v\n", err)
|
||||||
}
|
}
|
||||||
defer cleanup2()
|
defer cleanup2()
|
||||||
|
|
||||||
setting.AppDataPath = appDataPath
|
setting.AppDataPath = appDataPath
|
||||||
setting.AppWorkPath = giteaRoot
|
|
||||||
setting.StaticRootPath = giteaRoot
|
|
||||||
setting.GravatarSource = "https://secure.gravatar.com/avatar/"
|
setting.GravatarSource = "https://secure.gravatar.com/avatar/"
|
||||||
|
|
||||||
setting.Attachment.Storage.Path = filepath.Join(setting.AppDataPath, "attachments")
|
setting.Attachment.Storage.Path = filepath.Join(setting.AppDataPath, "attachments")
|
||||||
@ -129,22 +128,22 @@ func MainTest(m *testing.M, testOptsArg ...*TestOptions) {
|
|||||||
config.SetDynGetter(system.NewDatabaseDynKeyGetter())
|
config.SetDynGetter(system.NewDatabaseDynKeyGetter())
|
||||||
|
|
||||||
if err = cache.Init(); err != nil {
|
if err = cache.Init(); err != nil {
|
||||||
fatalTestError("cache.Init: %v\n", err)
|
testlogger.Panicf("cache.Init: %v\n", err)
|
||||||
}
|
}
|
||||||
if err = storage.Init(); err != nil {
|
if err = storage.Init(); err != nil {
|
||||||
fatalTestError("storage.Init: %v\n", err)
|
testlogger.Panicf("storage.Init: %v\n", err)
|
||||||
}
|
}
|
||||||
if err = SyncDirs(filepath.Join(giteaRoot, "tests", "gitea-repositories-meta"), setting.RepoRootPath); err != nil {
|
if err = SyncDirs(filepath.Join(giteaRoot, "tests", "gitea-repositories-meta"), setting.RepoRootPath); err != nil {
|
||||||
fatalTestError("util.SyncDirs: %v\n", err)
|
testlogger.Panicf("util.SyncDirs: %v\n", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = git.InitFull(); err != nil {
|
if err = git.InitFull(); err != nil {
|
||||||
fatalTestError("git.Init: %v\n", err)
|
testlogger.Panicf("git.Init: %v\n", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if testOpts.SetUp != nil {
|
if testOpts.SetUp != nil {
|
||||||
if err := testOpts.SetUp(); err != nil {
|
if err := testOpts.SetUp(); err != nil {
|
||||||
fatalTestError("set up failed: %v\n", err)
|
testlogger.Panicf("set up failed: %v\n", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -152,10 +151,10 @@ func MainTest(m *testing.M, testOptsArg ...*TestOptions) {
|
|||||||
|
|
||||||
if testOpts.TearDown != nil {
|
if testOpts.TearDown != nil {
|
||||||
if err := testOpts.TearDown(); err != nil {
|
if err := testOpts.TearDown(); err != nil {
|
||||||
fatalTestError("tear down failed: %v\n", err)
|
testlogger.Panicf("tear down failed: %v\n", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
os.Exit(exitStatus)
|
return exitStatus
|
||||||
}
|
}
|
||||||
|
|
||||||
// FixturesOptions fixtures needs to be loaded options
|
// FixturesOptions fixtures needs to be loaded options
|
||||||
@ -196,7 +195,6 @@ func PrepareTestDatabase() error {
|
|||||||
// by tests that use the above MainTest(..) function.
|
// by tests that use the above MainTest(..) function.
|
||||||
func PrepareTestEnv(t testing.TB) {
|
func PrepareTestEnv(t testing.TB) {
|
||||||
assert.NoError(t, PrepareTestDatabase())
|
assert.NoError(t, PrepareTestDatabase())
|
||||||
metaPath := filepath.Join(giteaRoot, "tests", "gitea-repositories-meta")
|
metaPath := filepath.Join(setting.GetGiteaTestSourceRoot(), "tests", "gitea-repositories-meta")
|
||||||
assert.NoError(t, SyncDirs(metaPath, setting.RepoRootPath))
|
assert.NoError(t, SyncDirs(metaPath, setting.RepoRootPath))
|
||||||
setting.SetupGiteaTestEnv()
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -11,6 +11,7 @@ import (
|
|||||||
"code.gitea.io/gitea/modules/git"
|
"code.gitea.io/gitea/modules/git"
|
||||||
"code.gitea.io/gitea/modules/glob"
|
"code.gitea.io/gitea/modules/glob"
|
||||||
"code.gitea.io/gitea/modules/log"
|
"code.gitea.io/gitea/modules/log"
|
||||||
|
"code.gitea.io/gitea/modules/setting"
|
||||||
api "code.gitea.io/gitea/modules/structs"
|
api "code.gitea.io/gitea/modules/structs"
|
||||||
"code.gitea.io/gitea/modules/util"
|
"code.gitea.io/gitea/modules/util"
|
||||||
webhook_module "code.gitea.io/gitea/modules/webhook"
|
webhook_module "code.gitea.io/gitea/modules/webhook"
|
||||||
@ -41,22 +42,30 @@ func IsWorkflow(path string) bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
return strings.HasPrefix(path, ".gitea/workflows") || strings.HasPrefix(path, ".github/workflows")
|
for _, workflowDir := range setting.Actions.WorkflowDirs {
|
||||||
|
if strings.HasPrefix(path, workflowDir+"/") {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
func ListWorkflows(commit *git.Commit) (string, git.Entries, error) {
|
func ListWorkflows(commit *git.Commit) (string, git.Entries, error) {
|
||||||
rpath := ".gitea/workflows"
|
var tree *git.Tree
|
||||||
tree, err := commit.SubTree(rpath)
|
var err error
|
||||||
if _, ok := err.(git.ErrNotExist); ok {
|
var workflowDir string
|
||||||
rpath = ".github/workflows"
|
for _, workflowDir = range setting.Actions.WorkflowDirs {
|
||||||
tree, err = commit.SubTree(rpath)
|
tree, err = commit.SubTree(workflowDir)
|
||||||
|
if err == nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if !git.IsErrNotExist(err) {
|
||||||
|
return "", nil, err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if _, ok := err.(git.ErrNotExist); ok {
|
if tree == nil {
|
||||||
return "", nil, nil
|
return "", nil, nil
|
||||||
}
|
}
|
||||||
if err != nil {
|
|
||||||
return "", nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
entries, err := tree.ListEntriesRecursiveFast()
|
entries, err := tree.ListEntriesRecursiveFast()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -69,7 +78,7 @@ func ListWorkflows(commit *git.Commit) (string, git.Entries, error) {
|
|||||||
ret = append(ret, entry)
|
ret = append(ret, entry)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return rpath, ret, nil
|
return workflowDir, ret, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetContentFromEntry(entry *git.TreeEntry) ([]byte, error) {
|
func GetContentFromEntry(entry *git.TreeEntry) ([]byte, error) {
|
||||||
|
|||||||
@ -7,12 +7,83 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"code.gitea.io/gitea/modules/git"
|
"code.gitea.io/gitea/modules/git"
|
||||||
|
"code.gitea.io/gitea/modules/setting"
|
||||||
api "code.gitea.io/gitea/modules/structs"
|
api "code.gitea.io/gitea/modules/structs"
|
||||||
webhook_module "code.gitea.io/gitea/modules/webhook"
|
webhook_module "code.gitea.io/gitea/modules/webhook"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func TestIsWorkflow(t *testing.T) {
|
||||||
|
oldDirs := setting.Actions.WorkflowDirs
|
||||||
|
defer func() {
|
||||||
|
setting.Actions.WorkflowDirs = oldDirs
|
||||||
|
}()
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
dirs []string
|
||||||
|
path string
|
||||||
|
expected bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "default with yml extension",
|
||||||
|
dirs: []string{".gitea/workflows", ".github/workflows"},
|
||||||
|
path: ".gitea/workflows/test.yml",
|
||||||
|
expected: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "default with yaml extension",
|
||||||
|
dirs: []string{".gitea/workflows", ".github/workflows"},
|
||||||
|
path: ".github/workflows/test.yaml",
|
||||||
|
expected: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "only gitea configured, github path rejected",
|
||||||
|
dirs: []string{".gitea/workflows"},
|
||||||
|
path: ".github/workflows/test.yml",
|
||||||
|
expected: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "only github configured, gitea path rejected",
|
||||||
|
dirs: []string{".github/workflows"},
|
||||||
|
path: ".gitea/workflows/test.yml",
|
||||||
|
expected: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "custom workflow dir",
|
||||||
|
dirs: []string{".custom/workflows"},
|
||||||
|
path: ".custom/workflows/deploy.yml",
|
||||||
|
expected: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "non-workflow file",
|
||||||
|
dirs: []string{".gitea/workflows", ".github/workflows"},
|
||||||
|
path: ".gitea/workflows/readme.md",
|
||||||
|
expected: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "directory boundary",
|
||||||
|
dirs: []string{".gitea/workflows"},
|
||||||
|
path: ".gitea/workflows2/test.yml",
|
||||||
|
expected: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "unrelated path",
|
||||||
|
dirs: []string{".gitea/workflows", ".github/workflows"},
|
||||||
|
path: "src/main.go",
|
||||||
|
expected: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
setting.Actions.WorkflowDirs = tt.dirs
|
||||||
|
assert.Equal(t, tt.expected, IsWorkflow(tt.path))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestDetectMatched(t *testing.T) {
|
func TestDetectMatched(t *testing.T) {
|
||||||
testCases := []struct {
|
testCases := []struct {
|
||||||
desc string
|
desc string
|
||||||
|
|||||||
@ -37,6 +37,10 @@ type CommitSignature struct {
|
|||||||
|
|
||||||
// Message returns the commit message. Same as retrieving CommitMessage directly.
|
// Message returns the commit message. Same as retrieving CommitMessage directly.
|
||||||
func (c *Commit) Message() string {
|
func (c *Commit) Message() string {
|
||||||
|
// FIXME: GIT-COMMIT-MESSAGE-ENCODING: this logic is not right
|
||||||
|
// * When need to use commit message in templates/database, it should be valid UTF-8
|
||||||
|
// * When need to get the original commit message, it should just use "c.CommitMessage"
|
||||||
|
// It's not easy to refactor at the moment, many templates need to be updated and tested
|
||||||
return c.CommitMessage
|
return c.CommitMessage
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -17,6 +17,7 @@ import (
|
|||||||
"code.gitea.io/gitea/modules/log"
|
"code.gitea.io/gitea/modules/log"
|
||||||
"code.gitea.io/gitea/modules/setting"
|
"code.gitea.io/gitea/modules/setting"
|
||||||
"code.gitea.io/gitea/modules/tempdir"
|
"code.gitea.io/gitea/modules/tempdir"
|
||||||
|
"code.gitea.io/gitea/modules/testlogger"
|
||||||
|
|
||||||
"github.com/hashicorp/go-version"
|
"github.com/hashicorp/go-version"
|
||||||
)
|
)
|
||||||
@ -185,21 +186,19 @@ func InitFull() (err error) {
|
|||||||
// RunGitTests helps to init the git module and run tests.
|
// RunGitTests helps to init the git module and run tests.
|
||||||
// FIXME: GIT-PACKAGE-DEPENDENCY: the dependency is not right, setting.Git.HomePath is initialized in this package but used in gitcmd package
|
// FIXME: GIT-PACKAGE-DEPENDENCY: the dependency is not right, setting.Git.HomePath is initialized in this package but used in gitcmd package
|
||||||
func RunGitTests(m interface{ Run() int }) {
|
func RunGitTests(m interface{ Run() int }) {
|
||||||
fatalf := func(exitCode int, format string, args ...any) {
|
os.Exit(runGitTests(m))
|
||||||
_, _ = fmt.Fprintf(os.Stderr, format, args...)
|
}
|
||||||
os.Exit(exitCode)
|
|
||||||
}
|
func runGitTests(m interface{ Run() int }) int {
|
||||||
gitHomePath, cleanup, err := tempdir.OsTempDir("gitea-test").MkdirTempRandom("git-home")
|
gitHomePath, cleanup, err := tempdir.OsTempDir("gitea-test").MkdirTempRandom("git-home")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fatalf(1, "unable to create temp dir: %s", err.Error())
|
testlogger.Panicf("unable to create temp dir: %s", err.Error())
|
||||||
}
|
}
|
||||||
defer cleanup()
|
defer cleanup()
|
||||||
|
|
||||||
setting.Git.HomePath = gitHomePath
|
setting.Git.HomePath = gitHomePath
|
||||||
if err = InitFull(); err != nil {
|
if err = InitFull(); err != nil {
|
||||||
fatalf(1, "failed to call Init: %s", err.Error())
|
testlogger.Panicf("failed to call Init: %s", err.Error())
|
||||||
}
|
|
||||||
if exitCode := m.Run(); exitCode != 0 {
|
|
||||||
fatalf(exitCode, "run test failed, ExitCode=%d", exitCode)
|
|
||||||
}
|
}
|
||||||
|
return m.Run()
|
||||||
}
|
}
|
||||||
|
|||||||
@ -12,23 +12,27 @@ import (
|
|||||||
|
|
||||||
"code.gitea.io/gitea/modules/setting"
|
"code.gitea.io/gitea/modules/setting"
|
||||||
"code.gitea.io/gitea/modules/tempdir"
|
"code.gitea.io/gitea/modules/tempdir"
|
||||||
|
"code.gitea.io/gitea/modules/testlogger"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestMain(m *testing.M) {
|
func testMain(m *testing.M) int {
|
||||||
// FIXME: GIT-PACKAGE-DEPENDENCY: the dependency is not right.
|
// FIXME: GIT-PACKAGE-DEPENDENCY: the dependency is not right.
|
||||||
// "setting.Git.HomePath" is initialized in "git" package but really used in "gitcmd" package
|
// "setting.Git.HomePath" is initialized in "git" package but really used in "gitcmd" package
|
||||||
gitHomePath, cleanup, err := tempdir.OsTempDir("gitea-test").MkdirTempRandom("git-home")
|
gitHomePath, cleanup, err := tempdir.OsTempDir("gitea-test").MkdirTempRandom("git-home")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
_, _ = fmt.Fprintf(os.Stderr, "unable to create temp dir: %v", err)
|
testlogger.Panicf("failed to create temp dir: %v", err)
|
||||||
os.Exit(1)
|
|
||||||
}
|
}
|
||||||
defer cleanup()
|
defer cleanup()
|
||||||
|
|
||||||
setting.Git.HomePath = gitHomePath
|
setting.Git.HomePath = gitHomePath
|
||||||
os.Exit(m.Run())
|
return m.Run()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMain(m *testing.M) {
|
||||||
|
os.Exit(testMain(m))
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestRunWithContextStd(t *testing.T) {
|
func TestRunWithContextStd(t *testing.T) {
|
||||||
|
|||||||
@ -13,12 +13,13 @@ import (
|
|||||||
|
|
||||||
func TestMain(m *testing.M) {
|
func TestMain(m *testing.M) {
|
||||||
// resolve repository path relative to the test directory
|
// resolve repository path relative to the test directory
|
||||||
testRootDir := setting.SetupGiteaTestEnv()
|
setting.SetupGiteaTestEnv()
|
||||||
|
giteaRoot := setting.GetGiteaTestSourceRoot()
|
||||||
repoPath = func(repo Repository) string {
|
repoPath = func(repo Repository) string {
|
||||||
if filepath.IsAbs(repo.RelativePath()) {
|
if filepath.IsAbs(repo.RelativePath()) {
|
||||||
return repo.RelativePath() // for testing purpose only
|
return repo.RelativePath() // for testing purpose only
|
||||||
}
|
}
|
||||||
return filepath.Join(testRootDir, "modules/git/tests/repos", repo.RelativePath())
|
return filepath.Join(giteaRoot, "modules/git/tests/repos", repo.RelativePath())
|
||||||
}
|
}
|
||||||
git.RunGitTests(m)
|
git.RunGitTests(m)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -8,7 +8,6 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
@ -20,10 +19,6 @@ const (
|
|||||||
dummyToken = "10000000-aaaa-bbbb-cccc-000000000001"
|
dummyToken = "10000000-aaaa-bbbb-cccc-000000000001"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestMain(m *testing.M) {
|
|
||||||
os.Exit(m.Run())
|
|
||||||
}
|
|
||||||
|
|
||||||
type mockTransport struct{}
|
type mockTransport struct{}
|
||||||
|
|
||||||
func (mockTransport) RoundTrip(req *http.Request) (*http.Response, error) {
|
func (mockTransport) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||||
|
|||||||
@ -65,7 +65,6 @@ func (o *VirtualSessionProvider) Read(sid string) (session.RawStore, error) {
|
|||||||
return nil, fmt.Errorf("check if '%s' exist failed: %w", sid, err)
|
return nil, fmt.Errorf("check if '%s' exist failed: %w", sid, err)
|
||||||
}
|
}
|
||||||
kv := make(map[any]any)
|
kv := make(map[any]any)
|
||||||
kv["_old_uid"] = "0"
|
|
||||||
return NewVirtualStore(o, sid, kv), nil
|
return NewVirtualStore(o, sid, kv), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -160,7 +159,7 @@ func (s *VirtualStore) Release() error {
|
|||||||
// Now need to lock the provider
|
// Now need to lock the provider
|
||||||
s.p.lock.Lock()
|
s.p.lock.Lock()
|
||||||
defer s.p.lock.Unlock()
|
defer s.p.lock.Unlock()
|
||||||
if oldUID, ok := s.data["_old_uid"]; (ok && (oldUID != "0" || len(s.data) > 1)) || (!ok && len(s.data) > 0) {
|
if len(s.data) > 0 {
|
||||||
// Now ensure that we don't exist!
|
// Now ensure that we don't exist!
|
||||||
realProvider := s.p.provider
|
realProvider := s.p.provider
|
||||||
|
|
||||||
|
|||||||
@ -4,6 +4,7 @@
|
|||||||
package setting
|
package setting
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
@ -25,10 +26,12 @@ var (
|
|||||||
EndlessTaskTimeout time.Duration `ini:"ENDLESS_TASK_TIMEOUT"`
|
EndlessTaskTimeout time.Duration `ini:"ENDLESS_TASK_TIMEOUT"`
|
||||||
AbandonedJobTimeout time.Duration `ini:"ABANDONED_JOB_TIMEOUT"`
|
AbandonedJobTimeout time.Duration `ini:"ABANDONED_JOB_TIMEOUT"`
|
||||||
SkipWorkflowStrings []string `ini:"SKIP_WORKFLOW_STRINGS"`
|
SkipWorkflowStrings []string `ini:"SKIP_WORKFLOW_STRINGS"`
|
||||||
|
WorkflowDirs []string `ini:"WORKFLOW_DIRS"`
|
||||||
}{
|
}{
|
||||||
Enabled: true,
|
Enabled: true,
|
||||||
DefaultActionsURL: defaultActionsURLGitHub,
|
DefaultActionsURL: defaultActionsURLGitHub,
|
||||||
SkipWorkflowStrings: []string{"[skip ci]", "[ci skip]", "[no ci]", "[skip actions]", "[actions skip]"},
|
SkipWorkflowStrings: []string{"[skip ci]", "[ci skip]", "[no ci]", "[skip actions]", "[actions skip]"},
|
||||||
|
WorkflowDirs: []string{".gitea/workflows", ".github/workflows"},
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -119,5 +122,20 @@ func loadActionsFrom(rootCfg ConfigProvider) error {
|
|||||||
return fmt.Errorf("invalid [actions] LOG_COMPRESSION: %q", Actions.LogCompression)
|
return fmt.Errorf("invalid [actions] LOG_COMPRESSION: %q", Actions.LogCompression)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
workflowDirs := make([]string, 0, len(Actions.WorkflowDirs))
|
||||||
|
for _, dir := range Actions.WorkflowDirs {
|
||||||
|
dir = strings.TrimSpace(dir)
|
||||||
|
if dir == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
dir = strings.ReplaceAll(dir, `\`, `/`)
|
||||||
|
dir = strings.TrimRight(dir, "/")
|
||||||
|
workflowDirs = append(workflowDirs, dir)
|
||||||
|
}
|
||||||
|
if len(workflowDirs) == 0 {
|
||||||
|
return errors.New("[actions] WORKFLOW_DIRS must contain at least one entry")
|
||||||
|
}
|
||||||
|
Actions.WorkflowDirs = workflowDirs
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@ -97,6 +97,65 @@ STORAGE_TYPE = minio
|
|||||||
assert.Equal(t, "actions_artifacts", filepath.Base(Actions.ArtifactStorage.Path))
|
assert.Equal(t, "actions_artifacts", filepath.Base(Actions.ArtifactStorage.Path))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func Test_WorkflowDirs(t *testing.T) {
|
||||||
|
oldActions := Actions
|
||||||
|
defer func() {
|
||||||
|
Actions = oldActions
|
||||||
|
}()
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
iniStr string
|
||||||
|
wantDirs []string
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "default",
|
||||||
|
iniStr: `[actions]`,
|
||||||
|
wantDirs: []string{".gitea/workflows", ".github/workflows"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "single dir",
|
||||||
|
iniStr: "[actions]\nWORKFLOW_DIRS = .github/workflows",
|
||||||
|
wantDirs: []string{".github/workflows"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "custom order",
|
||||||
|
iniStr: "[actions]\nWORKFLOW_DIRS = .github/workflows,.gitea/workflows",
|
||||||
|
wantDirs: []string{".github/workflows", ".gitea/workflows"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "whitespace trimming",
|
||||||
|
iniStr: "[actions]\nWORKFLOW_DIRS = .gitea/workflows , .github/workflows ",
|
||||||
|
wantDirs: []string{".gitea/workflows", ".github/workflows"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "trailing slash normalization",
|
||||||
|
iniStr: "[actions]\nWORKFLOW_DIRS = .gitea/workflows/,.github/workflows/",
|
||||||
|
wantDirs: []string{".gitea/workflows", ".github/workflows"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "only commas and whitespace",
|
||||||
|
iniStr: "[actions]\nWORKFLOW_DIRS = , , ,",
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
cfg, err := NewConfigProviderFromData(tt.iniStr)
|
||||||
|
require.NoError(t, err)
|
||||||
|
err = loadActionsFrom(cfg)
|
||||||
|
if tt.wantErr {
|
||||||
|
require.Error(t, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, tt.wantDirs, Actions.WorkflowDirs)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func Test_getDefaultActionsURLForActions(t *testing.T) {
|
func Test_getDefaultActionsURLForActions(t *testing.T) {
|
||||||
oldActions := Actions
|
oldActions := Actions
|
||||||
oldAppURL := AppURL
|
oldAppURL := AppURL
|
||||||
|
|||||||
@ -13,7 +13,18 @@ import (
|
|||||||
"code.gitea.io/gitea/modules/util"
|
"code.gitea.io/gitea/modules/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
func SetupGiteaTestEnv() string {
|
var giteaTestSourceRoot *string
|
||||||
|
|
||||||
|
func GetGiteaTestSourceRoot() string {
|
||||||
|
return *giteaTestSourceRoot
|
||||||
|
}
|
||||||
|
|
||||||
|
func SetupGiteaTestEnv() {
|
||||||
|
if giteaTestSourceRoot != nil {
|
||||||
|
return // already initialized
|
||||||
|
}
|
||||||
|
|
||||||
|
IsInTesting = true
|
||||||
giteaRoot := os.Getenv("GITEA_TEST_ROOT")
|
giteaRoot := os.Getenv("GITEA_TEST_ROOT")
|
||||||
if giteaRoot == "" {
|
if giteaRoot == "" {
|
||||||
_, filename, _, _ := runtime.Caller(0)
|
_, filename, _, _ := runtime.Caller(0)
|
||||||
@ -27,6 +38,7 @@ func SetupGiteaTestEnv() string {
|
|||||||
appWorkPathBuiltin = giteaRoot
|
appWorkPathBuiltin = giteaRoot
|
||||||
AppWorkPath = giteaRoot
|
AppWorkPath = giteaRoot
|
||||||
AppPath = filepath.Join(giteaRoot, "gitea") + util.Iif(IsWindows, ".exe", "")
|
AppPath = filepath.Join(giteaRoot, "gitea") + util.Iif(IsWindows, ".exe", "")
|
||||||
|
StaticRootPath = giteaRoot // need to load assets (options, public) from the source code directory for testing
|
||||||
|
|
||||||
// giteaConf (GITEA_CONF) must be relative because it is used in the git hooks as "$GITEA_ROOT/$GITEA_CONF"
|
// giteaConf (GITEA_CONF) must be relative because it is used in the git hooks as "$GITEA_ROOT/$GITEA_CONF"
|
||||||
giteaConf := os.Getenv("GITEA_TEST_CONF")
|
giteaConf := os.Getenv("GITEA_TEST_CONF")
|
||||||
@ -56,6 +68,5 @@ func SetupGiteaTestEnv() string {
|
|||||||
// TODO: some git repo hooks (test fixtures) still use these env variables, need to be refactored in the future
|
// TODO: some git repo hooks (test fixtures) still use these env variables, need to be refactored in the future
|
||||||
_ = os.Setenv("GITEA_ROOT", giteaRoot)
|
_ = os.Setenv("GITEA_ROOT", giteaRoot)
|
||||||
_ = os.Setenv("GITEA_CONF", giteaConf) // test fixture git hooks use "$GITEA_ROOT/$GITEA_CONF" in their scripts
|
_ = os.Setenv("GITEA_CONF", giteaConf) // test fixture git hooks use "$GITEA_ROOT/$GITEA_CONF" in their scripts
|
||||||
|
giteaTestSourceRoot = &giteaRoot
|
||||||
return giteaRoot
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -173,7 +173,7 @@ func Init() {
|
|||||||
log.RegisterEventWriter("test", newTestLoggerWriter)
|
log.RegisterEventWriter("test", newTestLoggerWriter)
|
||||||
}
|
}
|
||||||
|
|
||||||
func Fatalf(format string, args ...any) {
|
func Panicf(format string, args ...any) {
|
||||||
Printf(format+"\n", args...)
|
// don't call os.Exit, otherwise the "defer" functions won't be executed
|
||||||
os.Exit(1)
|
panic(fmt.Sprintf(format, args...))
|
||||||
}
|
}
|
||||||
|
|||||||
@ -32,11 +32,7 @@ func TestMain(m *testing.M) {
|
|||||||
// setup
|
// setup
|
||||||
translation.InitLocales(context.Background())
|
translation.InitLocales(context.Background())
|
||||||
BaseDate = time.Date(2000, time.January, 1, 0, 0, 0, 0, time.UTC)
|
BaseDate = time.Date(2000, time.January, 1, 0, 0, 0, 0, time.UTC)
|
||||||
|
os.Exit(m.Run())
|
||||||
// run the tests
|
|
||||||
retVal := m.Run()
|
|
||||||
|
|
||||||
os.Exit(retVal)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestTimeSincePro(t *testing.T) {
|
func TestTimeSincePro(t *testing.T) {
|
||||||
|
|||||||
@ -98,6 +98,20 @@ func (h HookEventType) IsPullRequest() bool {
|
|||||||
return h.Event() == "pull_request"
|
return h.Event() == "pull_request"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// IsPullRequestReview returns true for pull request review events
|
||||||
|
// (approved, rejected, comment). These events use the same PullRequestPayload
|
||||||
|
// as regular pull_request events.
|
||||||
|
func (h HookEventType) IsPullRequestReview() bool {
|
||||||
|
switch h {
|
||||||
|
case HookEventPullRequestReviewApproved,
|
||||||
|
HookEventPullRequestReviewRejected,
|
||||||
|
HookEventPullRequestReviewComment:
|
||||||
|
return true
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// HookType is the type of a webhook
|
// HookType is the type of a webhook
|
||||||
type HookType = string
|
type HookType = string
|
||||||
|
|
||||||
|
|||||||
45
package.json
45
package.json
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"packageManager": "pnpm@10.29.2",
|
"packageManager": "pnpm@10.30.0",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 22.6.0",
|
"node": ">= 22.6.0",
|
||||||
"pnpm": ">= 10.0.0"
|
"pnpm": ">= 10.0.0"
|
||||||
@ -16,20 +16,20 @@
|
|||||||
"@github/text-expander-element": "2.9.4",
|
"@github/text-expander-element": "2.9.4",
|
||||||
"@mcaptcha/vanilla-glue": "0.1.0-alpha-3",
|
"@mcaptcha/vanilla-glue": "0.1.0-alpha-3",
|
||||||
"@mermaid-js/layout-elk": "0.2.0",
|
"@mermaid-js/layout-elk": "0.2.0",
|
||||||
"@primer/octicons": "19.21.2",
|
"@primer/octicons": "19.22.0",
|
||||||
"@resvg/resvg-wasm": "2.6.2",
|
"@resvg/resvg-wasm": "2.6.2",
|
||||||
"@silverwind/vue3-calendar-heatmap": "2.1.1",
|
"@silverwind/vue3-calendar-heatmap": "2.1.1",
|
||||||
"@techknowlogick/license-checker-webpack-plugin": "0.3.0",
|
"@techknowlogick/license-checker-webpack-plugin": "0.3.0",
|
||||||
"add-asset-webpack-plugin": "3.1.1",
|
"add-asset-webpack-plugin": "3.1.1",
|
||||||
"ansi_up": "6.0.6",
|
"ansi_up": "6.0.6",
|
||||||
"asciinema-player": "3.14.0",
|
"asciinema-player": "3.14.15",
|
||||||
"chart.js": "4.5.1",
|
"chart.js": "4.5.1",
|
||||||
"chartjs-adapter-dayjs-4": "1.0.4",
|
"chartjs-adapter-dayjs-4": "1.0.4",
|
||||||
"chartjs-plugin-zoom": "2.2.0",
|
"chartjs-plugin-zoom": "2.2.0",
|
||||||
"clippie": "4.1.10",
|
"clippie": "4.1.10",
|
||||||
"compare-versions": "6.1.1",
|
"compare-versions": "6.1.1",
|
||||||
"cropperjs": "1.6.2",
|
"cropperjs": "1.6.2",
|
||||||
"css-loader": "7.1.3",
|
"css-loader": "7.1.4",
|
||||||
"dayjs": "1.11.19",
|
"dayjs": "1.11.19",
|
||||||
"dropzone": "6.0.0-beta.2",
|
"dropzone": "6.0.0-beta.2",
|
||||||
"easymde": "2.20.0",
|
"easymde": "2.20.0",
|
||||||
@ -39,7 +39,7 @@
|
|||||||
"jquery": "4.0.0",
|
"jquery": "4.0.0",
|
||||||
"js-yaml": "4.1.1",
|
"js-yaml": "4.1.1",
|
||||||
"katex": "0.16.28",
|
"katex": "0.16.28",
|
||||||
"mermaid": "11.12.2",
|
"mermaid": "11.12.3",
|
||||||
"mini-css-extract-plugin": "2.10.0",
|
"mini-css-extract-plugin": "2.10.0",
|
||||||
"monaco-editor": "0.55.1",
|
"monaco-editor": "0.55.1",
|
||||||
"monaco-editor-webpack-plugin": "7.1.1",
|
"monaco-editor-webpack-plugin": "7.1.1",
|
||||||
@ -47,12 +47,12 @@
|
|||||||
"pdfobject": "2.3.1",
|
"pdfobject": "2.3.1",
|
||||||
"perfect-debounce": "2.1.0",
|
"perfect-debounce": "2.1.0",
|
||||||
"postcss": "8.5.6",
|
"postcss": "8.5.6",
|
||||||
"postcss-loader": "8.2.0",
|
"postcss-loader": "8.2.1",
|
||||||
"sortablejs": "1.15.6",
|
"sortablejs": "1.15.7",
|
||||||
"swagger-ui-dist": "5.31.0",
|
"swagger-ui-dist": "5.31.1",
|
||||||
"tailwindcss": "3.4.17",
|
"tailwindcss": "3.4.17",
|
||||||
"throttle-debounce": "5.0.2",
|
"throttle-debounce": "5.0.2",
|
||||||
"tinycolor2": "1.6.0",
|
"colord": "2.9.3",
|
||||||
"tippy.js": "6.3.7",
|
"tippy.js": "6.3.7",
|
||||||
"toastify-js": "1.12.0",
|
"toastify-js": "1.12.0",
|
||||||
"tributejs": "5.1.3",
|
"tributejs": "5.1.3",
|
||||||
@ -62,7 +62,7 @@
|
|||||||
"vue-bar-graph": "2.2.0",
|
"vue-bar-graph": "2.2.0",
|
||||||
"vue-chartjs": "5.3.3",
|
"vue-chartjs": "5.3.3",
|
||||||
"vue-loader": "17.4.2",
|
"vue-loader": "17.4.2",
|
||||||
"webpack": "5.105.0",
|
"webpack": "5.105.2",
|
||||||
"webpack-cli": "6.0.1",
|
"webpack-cli": "6.0.1",
|
||||||
"wrap-ansi": "9.0.2"
|
"wrap-ansi": "9.0.2"
|
||||||
},
|
},
|
||||||
@ -77,15 +77,15 @@
|
|||||||
"@types/jquery": "3.5.33",
|
"@types/jquery": "3.5.33",
|
||||||
"@types/js-yaml": "4.0.9",
|
"@types/js-yaml": "4.0.9",
|
||||||
"@types/katex": "0.16.8",
|
"@types/katex": "0.16.8",
|
||||||
|
"@types/node": "25.2.3",
|
||||||
"@types/pdfobject": "2.2.5",
|
"@types/pdfobject": "2.2.5",
|
||||||
"@types/sortablejs": "1.15.9",
|
"@types/sortablejs": "1.15.9",
|
||||||
"@types/swagger-ui-dist": "3.30.6",
|
"@types/swagger-ui-dist": "3.30.6",
|
||||||
"@types/throttle-debounce": "5.0.2",
|
"@types/throttle-debounce": "5.0.2",
|
||||||
"@types/tinycolor2": "1.4.6",
|
|
||||||
"@types/toastify-js": "1.12.4",
|
"@types/toastify-js": "1.12.4",
|
||||||
"@typescript-eslint/parser": "8.55.0",
|
"@typescript-eslint/parser": "8.56.0",
|
||||||
"@vitejs/plugin-vue": "6.0.4",
|
"@vitejs/plugin-vue": "6.0.4",
|
||||||
"@vitest/eslint-plugin": "1.6.7",
|
"@vitest/eslint-plugin": "1.6.9",
|
||||||
"eslint": "9.39.2",
|
"eslint": "9.39.2",
|
||||||
"eslint-import-resolver-typescript": "4.4.4",
|
"eslint-import-resolver-typescript": "4.4.4",
|
||||||
"eslint-plugin-array-func": "5.1.0",
|
"eslint-plugin-array-func": "5.1.0",
|
||||||
@ -93,35 +93,32 @@
|
|||||||
"eslint-plugin-import-x": "4.16.1",
|
"eslint-plugin-import-x": "4.16.1",
|
||||||
"eslint-plugin-playwright": "2.5.1",
|
"eslint-plugin-playwright": "2.5.1",
|
||||||
"eslint-plugin-regexp": "3.0.0",
|
"eslint-plugin-regexp": "3.0.0",
|
||||||
"eslint-plugin-sonarjs": "3.0.6",
|
"eslint-plugin-sonarjs": "3.0.7",
|
||||||
"eslint-plugin-unicorn": "62.0.0",
|
"eslint-plugin-unicorn": "63.0.0",
|
||||||
"eslint-plugin-vue": "10.7.0",
|
"eslint-plugin-vue": "10.8.0",
|
||||||
"eslint-plugin-vue-scoped-css": "2.12.0",
|
"eslint-plugin-vue-scoped-css": "2.12.0",
|
||||||
"eslint-plugin-wc": "3.1.0",
|
"eslint-plugin-wc": "3.1.0",
|
||||||
"globals": "17.3.0",
|
"globals": "17.3.0",
|
||||||
"happy-dom": "20.6.0",
|
"happy-dom": "20.6.1",
|
||||||
"jiti": "2.6.1",
|
"jiti": "2.6.1",
|
||||||
"markdownlint-cli": "0.47.0",
|
"markdownlint-cli": "0.47.0",
|
||||||
"material-icon-theme": "5.31.0",
|
"material-icon-theme": "5.31.0",
|
||||||
"nolyfill": "1.0.44",
|
"nolyfill": "1.0.44",
|
||||||
"postcss-html": "1.8.1",
|
"postcss-html": "1.8.1",
|
||||||
"spectral-cli-bundle": "1.0.4",
|
"spectral-cli-bundle": "1.0.7",
|
||||||
"stylelint": "17.1.1",
|
"stylelint": "17.3.0",
|
||||||
"stylelint-config-recommended": "18.0.0",
|
"stylelint-config-recommended": "18.0.0",
|
||||||
"stylelint-declaration-block-no-ignored-properties": "3.0.0",
|
"stylelint-declaration-block-no-ignored-properties": "3.0.0",
|
||||||
"stylelint-declaration-strict-value": "1.10.11",
|
"stylelint-declaration-strict-value": "1.10.11",
|
||||||
"stylelint-value-no-unknown-custom-properties": "6.1.1",
|
"stylelint-value-no-unknown-custom-properties": "6.1.1",
|
||||||
"svgo": "4.0.0",
|
"svgo": "4.0.0",
|
||||||
"typescript": "5.9.3",
|
"typescript": "5.9.3",
|
||||||
"typescript-eslint": "8.55.0",
|
"typescript-eslint": "8.56.0",
|
||||||
"updates": "17.4.0",
|
"updates": "17.5.7",
|
||||||
"vite-string-plugin": "2.0.1",
|
"vite-string-plugin": "2.0.1",
|
||||||
"vitest": "4.0.18",
|
"vitest": "4.0.18",
|
||||||
"vue-tsc": "3.2.4"
|
"vue-tsc": "3.2.4"
|
||||||
},
|
},
|
||||||
"browserslist": [
|
|
||||||
"defaults"
|
|
||||||
],
|
|
||||||
"pnpm": {
|
"pnpm": {
|
||||||
"overrides": {
|
"overrides": {
|
||||||
"array-includes": "npm:@nolyfill/array-includes@^1",
|
"array-includes": "npm:@nolyfill/array-includes@^1",
|
||||||
|
|||||||
826
pnpm-lock.yaml
generated
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/log"
|
||||||
"code.gitea.io/gitea/modules/storage"
|
"code.gitea.io/gitea/modules/storage"
|
||||||
"code.gitea.io/gitea/modules/templates"
|
"code.gitea.io/gitea/modules/templates"
|
||||||
|
"code.gitea.io/gitea/modules/translation"
|
||||||
"code.gitea.io/gitea/modules/util"
|
"code.gitea.io/gitea/modules/util"
|
||||||
"code.gitea.io/gitea/modules/web"
|
"code.gitea.io/gitea/modules/web"
|
||||||
"code.gitea.io/gitea/routers/common"
|
"code.gitea.io/gitea/routers/common"
|
||||||
@ -302,7 +303,7 @@ func ViewPost(ctx *context_module.Context) {
|
|||||||
resp.State.CurrentJob.Steps = make([]*ViewJobStep, 0) // marshal to '[]' instead fo 'null' in json
|
resp.State.CurrentJob.Steps = make([]*ViewJobStep, 0) // marshal to '[]' instead fo 'null' in json
|
||||||
resp.Logs.StepsLog = make([]*ViewStepLog, 0) // marshal to '[]' instead fo 'null' in json
|
resp.Logs.StepsLog = make([]*ViewStepLog, 0) // marshal to '[]' instead fo 'null' in json
|
||||||
if task != nil {
|
if task != nil {
|
||||||
steps, logs, err := convertToViewModel(ctx, req.LogCursors, task)
|
steps, logs, err := convertToViewModel(ctx, ctx.Locale, req.LogCursors, task)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.ServerError("convertToViewModel", err)
|
ctx.ServerError("convertToViewModel", err)
|
||||||
return
|
return
|
||||||
@ -314,7 +315,7 @@ func ViewPost(ctx *context_module.Context) {
|
|||||||
ctx.JSON(http.StatusOK, resp)
|
ctx.JSON(http.StatusOK, resp)
|
||||||
}
|
}
|
||||||
|
|
||||||
func convertToViewModel(ctx *context_module.Context, cursors []LogCursor, task *actions_model.ActionTask) ([]*ViewJobStep, []*ViewStepLog, error) {
|
func convertToViewModel(ctx context.Context, locale translation.Locale, cursors []LogCursor, task *actions_model.ActionTask) ([]*ViewJobStep, []*ViewStepLog, error) {
|
||||||
var viewJobs []*ViewJobStep
|
var viewJobs []*ViewJobStep
|
||||||
var logs []*ViewStepLog
|
var logs []*ViewStepLog
|
||||||
|
|
||||||
@ -344,7 +345,7 @@ func convertToViewModel(ctx *context_module.Context, cursors []LogCursor, task *
|
|||||||
Lines: []*ViewStepLogLine{
|
Lines: []*ViewStepLogLine{
|
||||||
{
|
{
|
||||||
Index: 1,
|
Index: 1,
|
||||||
Message: ctx.Locale.TrString("actions.runs.expire_log_message"),
|
Message: locale.TrString("actions.runs.expire_log_message"),
|
||||||
// Timestamp doesn't mean anything when the log is expired.
|
// Timestamp doesn't mean anything when the log is expired.
|
||||||
// Set it to the task's updated time since it's probably the time when the log has expired.
|
// Set it to the task's updated time since it's probably the time when the log has expired.
|
||||||
Timestamp: float64(task.Updated.AsTime().UnixNano()) / float64(time.Second),
|
Timestamp: float64(task.Updated.AsTime().UnixNano()) / float64(time.Second),
|
||||||
|
|||||||
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"
|
gocontext "context"
|
||||||
"encoding/csv"
|
"encoding/csv"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
@ -426,6 +425,36 @@ func ParseCompareInfo(ctx *context.Context) *git_service.CompareInfo {
|
|||||||
return compareInfo
|
return compareInfo
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func prepareNewPullRequestTitleContent(ci *git_service.CompareInfo, commits []*git_model.SignCommitWithStatuses) (title, content string) {
|
||||||
|
title = ci.HeadRef.ShortName()
|
||||||
|
|
||||||
|
if len(commits) > 0 {
|
||||||
|
// the "commits" are from "ShowPrettyFormatLogToList", which is ordered from newest to oldest, here take the oldest one
|
||||||
|
c := commits[len(commits)-1]
|
||||||
|
title = strings.TrimSpace(c.UserCommit.Summary())
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(commits) == 1 {
|
||||||
|
// FIXME: GIT-COMMIT-MESSAGE-ENCODING: try to convert the encoding for commit message explicitly, ideally it should be done by a git commit struct method
|
||||||
|
c := commits[0]
|
||||||
|
_, content, _ = strings.Cut(strings.TrimSpace(c.UserCommit.CommitMessage), "\n")
|
||||||
|
content = strings.TrimSpace(content)
|
||||||
|
content = string(charset.ToUTF8([]byte(content), charset.ConvertOpts{}))
|
||||||
|
}
|
||||||
|
|
||||||
|
var titleTrailer string
|
||||||
|
// TODO: 255 doesn't seem to be a good limit for title, just keep the old behavior
|
||||||
|
title, titleTrailer = util.EllipsisDisplayStringX(title, 255)
|
||||||
|
if titleTrailer != "" {
|
||||||
|
if content != "" {
|
||||||
|
content = titleTrailer + "\n\n" + content
|
||||||
|
} else {
|
||||||
|
content = titleTrailer + "\n"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return title, content
|
||||||
|
}
|
||||||
|
|
||||||
// PrepareCompareDiff renders compare diff page
|
// PrepareCompareDiff renders compare diff page
|
||||||
func PrepareCompareDiff(
|
func PrepareCompareDiff(
|
||||||
ctx *context.Context,
|
ctx *context.Context,
|
||||||
@ -539,30 +568,7 @@ func PrepareCompareDiff(
|
|||||||
ctx.Data["Commits"] = commits
|
ctx.Data["Commits"] = commits
|
||||||
ctx.Data["CommitCount"] = len(commits)
|
ctx.Data["CommitCount"] = len(commits)
|
||||||
|
|
||||||
title := ci.HeadRef.ShortName()
|
ctx.Data["title"], ctx.Data["content"] = prepareNewPullRequestTitleContent(ci, commits)
|
||||||
if len(commits) == 1 {
|
|
||||||
c := commits[0]
|
|
||||||
title = strings.TrimSpace(c.UserCommit.Summary())
|
|
||||||
|
|
||||||
body := strings.Split(strings.TrimSpace(c.UserCommit.Message()), "\n")
|
|
||||||
if len(body) > 1 {
|
|
||||||
ctx.Data["content"] = strings.Join(body[1:], "\n")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(title) > 255 {
|
|
||||||
var trailer string
|
|
||||||
title, trailer = util.EllipsisDisplayStringX(title, 255)
|
|
||||||
if len(trailer) > 0 {
|
|
||||||
if ctx.Data["content"] != nil {
|
|
||||||
ctx.Data["content"] = fmt.Sprintf("%s\n\n%s", trailer, ctx.Data["content"])
|
|
||||||
} else {
|
|
||||||
ctx.Data["content"] = trailer + "\n"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx.Data["title"] = title
|
|
||||||
ctx.Data["Username"] = ci.HeadRepo.OwnerName
|
ctx.Data["Username"] = ci.HeadRepo.OwnerName
|
||||||
ctx.Data["Reponame"] = ci.HeadRepo.Name
|
ctx.Data["Reponame"] = ci.HeadRepo.Name
|
||||||
|
|
||||||
|
|||||||
@ -4,9 +4,16 @@
|
|||||||
package repo
|
package repo
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
"unicode/utf8"
|
||||||
|
|
||||||
|
asymkey_model "code.gitea.io/gitea/models/asymkey"
|
||||||
|
git_model "code.gitea.io/gitea/models/git"
|
||||||
issues_model "code.gitea.io/gitea/models/issues"
|
issues_model "code.gitea.io/gitea/models/issues"
|
||||||
|
user_model "code.gitea.io/gitea/models/user"
|
||||||
|
"code.gitea.io/gitea/modules/git"
|
||||||
|
git_service "code.gitea.io/gitea/services/git"
|
||||||
"code.gitea.io/gitea/services/gitdiff"
|
"code.gitea.io/gitea/services/gitdiff"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
@ -38,3 +45,47 @@ func TestAttachCommentsToLines(t *testing.T) {
|
|||||||
assert.Equal(t, int64(300), section.Lines[1].Comments[0].ID)
|
assert.Equal(t, int64(300), section.Lines[1].Comments[0].ID)
|
||||||
assert.Equal(t, int64(301), section.Lines[1].Comments[1].ID)
|
assert.Equal(t, int64(301), section.Lines[1].Comments[1].ID)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestNewPullRequestTitleContent(t *testing.T) {
|
||||||
|
ci := &git_service.CompareInfo{HeadRef: "refs/heads/head-branch"}
|
||||||
|
|
||||||
|
mockCommit := func(msg string) *git_model.SignCommitWithStatuses {
|
||||||
|
return &git_model.SignCommitWithStatuses{
|
||||||
|
SignCommit: &asymkey_model.SignCommit{
|
||||||
|
UserCommit: &user_model.UserCommit{
|
||||||
|
Commit: &git.Commit{
|
||||||
|
CommitMessage: msg,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
title, content := prepareNewPullRequestTitleContent(ci, nil)
|
||||||
|
assert.Equal(t, "head-branch", title)
|
||||||
|
assert.Empty(t, content)
|
||||||
|
|
||||||
|
title, content = prepareNewPullRequestTitleContent(ci, []*git_model.SignCommitWithStatuses{mockCommit("title-only")})
|
||||||
|
assert.Equal(t, "title-only", title)
|
||||||
|
assert.Empty(t, content)
|
||||||
|
|
||||||
|
title, content = prepareNewPullRequestTitleContent(ci, []*git_model.SignCommitWithStatuses{mockCommit("title-" + strings.Repeat("a", 255))})
|
||||||
|
assert.Equal(t, "title-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa…", title)
|
||||||
|
assert.Equal(t, "…aaaaaaaaa\n", content)
|
||||||
|
|
||||||
|
title, content = prepareNewPullRequestTitleContent(ci, []*git_model.SignCommitWithStatuses{mockCommit("title\nbody")})
|
||||||
|
assert.Equal(t, "title", title)
|
||||||
|
assert.Equal(t, "body", content)
|
||||||
|
|
||||||
|
title, content = prepareNewPullRequestTitleContent(ci, []*git_model.SignCommitWithStatuses{mockCommit("a\xf0\xf0\xf0\nb\xf0\xf0\xf0")})
|
||||||
|
assert.Equal(t, "a?", title) // FIXME: GIT-COMMIT-MESSAGE-ENCODING: "title" doesn't use the same charset converting logic as "content"
|
||||||
|
assert.Equal(t, "b"+string(utf8.RuneError)+string(utf8.RuneError), content)
|
||||||
|
|
||||||
|
title, content = prepareNewPullRequestTitleContent(ci, []*git_model.SignCommitWithStatuses{
|
||||||
|
// ordered from newest to oldest
|
||||||
|
mockCommit("title2\nbody2"),
|
||||||
|
mockCommit("title1\nbody1"),
|
||||||
|
})
|
||||||
|
assert.Equal(t, "title1", title)
|
||||||
|
assert.Empty(t, content)
|
||||||
|
}
|
||||||
|
|||||||
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"
|
tplProfile templates.TplName = "user/profile"
|
||||||
)
|
)
|
||||||
|
|
||||||
// getDashboardContextUser finds out which context user dashboard is being viewed as .
|
// prepareDashboardContextUserOrgTeams finds out which context user dashboard is being viewed as .
|
||||||
func getDashboardContextUser(ctx *context.Context) *user_model.User {
|
func prepareDashboardContextUserOrgTeams(ctx *context.Context) *user_model.User {
|
||||||
ctxUser := ctx.Doer
|
ctxUser := ctx.Doer
|
||||||
orgName := ctx.PathParam("org")
|
orgName := ctx.PathParam("org")
|
||||||
if len(orgName) > 0 {
|
if len(orgName) > 0 {
|
||||||
@ -76,7 +76,7 @@ func getDashboardContextUser(ctx *context.Context) *user_model.User {
|
|||||||
|
|
||||||
// Dashboard render the dashboard page
|
// Dashboard render the dashboard page
|
||||||
func Dashboard(ctx *context.Context) {
|
func Dashboard(ctx *context.Context) {
|
||||||
ctxUser := getDashboardContextUser(ctx)
|
ctxUser := prepareDashboardContextUserOrgTeams(ctx)
|
||||||
if ctx.Written() {
|
if ctx.Written() {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -109,15 +109,7 @@ func Dashboard(ctx *context.Context) {
|
|||||||
"uid": uid,
|
"uid": uid,
|
||||||
}
|
}
|
||||||
|
|
||||||
if setting.Service.EnableUserHeatmap {
|
prepareHeatmapURL(ctx)
|
||||||
data, err := activities_model.GetUserHeatmapDataByUserTeam(ctx, ctxUser, ctx.Org.Team, ctx.Doer)
|
|
||||||
if err != nil {
|
|
||||||
ctx.ServerError("GetUserHeatmapDataByUserTeam", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
ctx.Data["HeatmapData"] = data
|
|
||||||
ctx.Data["HeatmapTotalContributions"] = activities_model.GetTotalContributionsInHeatmap(data)
|
|
||||||
}
|
|
||||||
|
|
||||||
feeds, count, err := feed_service.GetFeedsForDashboard(ctx, activities_model.GetFeedsOptions{
|
feeds, count, err := feed_service.GetFeedsForDashboard(ctx, activities_model.GetFeedsOptions{
|
||||||
RequestedUser: ctxUser,
|
RequestedUser: ctxUser,
|
||||||
@ -156,7 +148,7 @@ func Milestones(ctx *context.Context) {
|
|||||||
ctx.Data["Title"] = ctx.Tr("milestones")
|
ctx.Data["Title"] = ctx.Tr("milestones")
|
||||||
ctx.Data["PageIsMilestonesDashboard"] = true
|
ctx.Data["PageIsMilestonesDashboard"] = true
|
||||||
|
|
||||||
ctxUser := getDashboardContextUser(ctx)
|
ctxUser := prepareDashboardContextUserOrgTeams(ctx)
|
||||||
if ctx.Written() {
|
if ctx.Written() {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -371,7 +363,7 @@ func buildIssueOverview(ctx *context.Context, unitType unit.Type) {
|
|||||||
// Return with NotFound or ServerError if unsuccessful.
|
// Return with NotFound or ServerError if unsuccessful.
|
||||||
// ----------------------------------------------------
|
// ----------------------------------------------------
|
||||||
|
|
||||||
ctxUser := getDashboardContextUser(ctx)
|
ctxUser := prepareDashboardContextUserOrgTeams(ctx)
|
||||||
if ctx.Written() {
|
if ctx.Written() {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@ -103,6 +103,7 @@ func prepareUserProfileTabData(ctx *context.Context, profileDbRepo *repo_model.R
|
|||||||
repos []*repo_model.Repository
|
repos []*repo_model.Repository
|
||||||
count int64
|
count int64
|
||||||
total int
|
total int
|
||||||
|
curRows int
|
||||||
orderBy db.SearchOrderBy
|
orderBy db.SearchOrderBy
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -161,21 +162,15 @@ func prepareUserProfileTabData(ctx *context.Context, profileDbRepo *repo_model.R
|
|||||||
ctx.Data["Cards"] = following
|
ctx.Data["Cards"] = following
|
||||||
total = int(numFollowing)
|
total = int(numFollowing)
|
||||||
case "activity":
|
case "activity":
|
||||||
// prepare heatmap data
|
if setting.Service.EnableUserHeatmap && activities_model.ActivityReadable(ctx.ContextUser, ctx.Doer) {
|
||||||
if setting.Service.EnableUserHeatmap {
|
ctx.Data["EnableHeatmap"] = true
|
||||||
data, err := activities_model.GetUserHeatmapDataByUser(ctx, ctx.ContextUser, ctx.Doer)
|
ctx.Data["HeatmapURL"] = ctx.ContextUser.HomeLink() + "/-/heatmap"
|
||||||
if err != nil {
|
|
||||||
ctx.ServerError("GetUserHeatmapDataByUser", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
ctx.Data["HeatmapData"] = data
|
|
||||||
ctx.Data["HeatmapTotalContributions"] = activities_model.GetTotalContributionsInHeatmap(data)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
date := ctx.FormString("date")
|
date := ctx.FormString("date")
|
||||||
pagingNum = setting.UI.FeedPagingNum
|
pagingNum = setting.UI.FeedPagingNum
|
||||||
showPrivate := ctx.IsSigned && (ctx.Doer.IsAdmin || ctx.Doer.ID == ctx.ContextUser.ID)
|
showPrivate := ctx.IsSigned && (ctx.Doer.IsAdmin || ctx.Doer.ID == ctx.ContextUser.ID)
|
||||||
items, count, err := feed_service.GetFeeds(ctx, activities_model.GetFeedsOptions{
|
items, feedCount, err := feed_service.GetFeedsForDashboard(ctx, activities_model.GetFeedsOptions{
|
||||||
RequestedUser: ctx.ContextUser,
|
RequestedUser: ctx.ContextUser,
|
||||||
Actor: ctx.Doer,
|
Actor: ctx.Doer,
|
||||||
IncludePrivate: showPrivate,
|
IncludePrivate: showPrivate,
|
||||||
@ -193,8 +188,8 @@ func prepareUserProfileTabData(ctx *context.Context, profileDbRepo *repo_model.R
|
|||||||
}
|
}
|
||||||
ctx.Data["Feeds"] = items
|
ctx.Data["Feeds"] = items
|
||||||
ctx.Data["Date"] = date
|
ctx.Data["Date"] = date
|
||||||
|
curRows = len(items)
|
||||||
total = int(count)
|
total = feedCount
|
||||||
case "stars":
|
case "stars":
|
||||||
ctx.Data["PageIsProfileStarList"] = true
|
ctx.Data["PageIsProfileStarList"] = true
|
||||||
ctx.Data["ShowRepoOwnerOnList"] = true
|
ctx.Data["ShowRepoOwnerOnList"] = true
|
||||||
@ -316,6 +311,9 @@ func prepareUserProfileTabData(ctx *context.Context, profileDbRepo *repo_model.R
|
|||||||
}
|
}
|
||||||
|
|
||||||
pager := context.NewPagination(total, pagingNum, page, 5)
|
pager := context.NewPagination(total, pagingNum, page, 5)
|
||||||
|
if tab == "activity" {
|
||||||
|
pager.WithCurRows(curRows)
|
||||||
|
}
|
||||||
pager.AddParamFromRequest(ctx.Req)
|
pager.AddParamFromRequest(ctx.Req)
|
||||||
ctx.Data["Page"] = pager
|
ctx.Data["Page"] = pager
|
||||||
}
|
}
|
||||||
|
|||||||
@ -888,6 +888,8 @@ func registerWebRoutes(m *web.Router) {
|
|||||||
m.Group("/{org}", func() {
|
m.Group("/{org}", func() {
|
||||||
m.Get("/dashboard", user.Dashboard)
|
m.Get("/dashboard", user.Dashboard)
|
||||||
m.Get("/dashboard/{team}", user.Dashboard)
|
m.Get("/dashboard/{team}", user.Dashboard)
|
||||||
|
m.Get("/dashboard/-/heatmap", user.DashboardHeatmap)
|
||||||
|
m.Get("/dashboard/-/heatmap/{team}", user.DashboardHeatmap)
|
||||||
m.Get("/issues", user.Issues)
|
m.Get("/issues", user.Issues)
|
||||||
m.Get("/issues/{team}", user.Issues)
|
m.Get("/issues/{team}", user.Issues)
|
||||||
m.Get("/pulls", user.Pulls)
|
m.Get("/pulls", user.Pulls)
|
||||||
@ -1024,6 +1026,7 @@ func registerWebRoutes(m *web.Router) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
m.Get("/repositories", org.Repositories)
|
m.Get("/repositories", org.Repositories)
|
||||||
|
m.Get("/heatmap", user.DashboardHeatmap)
|
||||||
|
|
||||||
m.Group("/projects", func() {
|
m.Group("/projects", func() {
|
||||||
m.Group("", func() {
|
m.Group("", func() {
|
||||||
|
|||||||
@ -115,6 +115,21 @@ func getCommitStatusEventNameAndCommitID(run *actions_model.ActionRun) (event, c
|
|||||||
return "", "", errors.New("head of pull request is missing in event payload")
|
return "", "", errors.New("head of pull request is missing in event payload")
|
||||||
}
|
}
|
||||||
commitID = payload.PullRequest.Head.Sha
|
commitID = payload.PullRequest.Head.Sha
|
||||||
|
case // pull_request_review events share the same PullRequestPayload as pull_request
|
||||||
|
webhook_module.HookEventPullRequestReviewApproved,
|
||||||
|
webhook_module.HookEventPullRequestReviewRejected,
|
||||||
|
webhook_module.HookEventPullRequestReviewComment:
|
||||||
|
event = run.TriggerEvent
|
||||||
|
payload, err := run.GetPullRequestEventPayload()
|
||||||
|
if err != nil {
|
||||||
|
return "", "", fmt.Errorf("GetPullRequestEventPayload: %w", err)
|
||||||
|
}
|
||||||
|
if payload.PullRequest == nil {
|
||||||
|
return "", "", errors.New("pull request is missing in event payload")
|
||||||
|
} else if payload.PullRequest.Head == nil {
|
||||||
|
return "", "", errors.New("head of pull request is missing in event payload")
|
||||||
|
}
|
||||||
|
commitID = payload.PullRequest.Head.Sha
|
||||||
case webhook_module.HookEventRelease:
|
case webhook_module.HookEventRelease:
|
||||||
event = string(run.Event)
|
event = string(run.Event)
|
||||||
commitID = run.CommitSHA
|
commitID = run.CommitSHA
|
||||||
|
|||||||
@ -18,7 +18,6 @@ import (
|
|||||||
|
|
||||||
func TestMain(m *testing.M) {
|
func TestMain(m *testing.M) {
|
||||||
unittest.MainTest(m)
|
unittest.MainTest(m)
|
||||||
os.Exit(m.Run())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestInitToken(t *testing.T) {
|
func TestInitToken(t *testing.T) {
|
||||||
|
|||||||
@ -16,10 +16,14 @@ import (
|
|||||||
"code.gitea.io/gitea/modules/util"
|
"code.gitea.io/gitea/modules/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type themeCollection struct {
|
||||||
|
themeList []*ThemeMetaInfo
|
||||||
|
themeMap map[string]*ThemeMetaInfo
|
||||||
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
availableThemes []*ThemeMetaInfo
|
themeMu sync.RWMutex
|
||||||
availableThemeMap map[string]*ThemeMetaInfo
|
availableThemes *themeCollection
|
||||||
themeOnce sync.Once
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@ -129,23 +133,13 @@ func parseThemeMetaInfo(fileName, cssContent string) *ThemeMetaInfo {
|
|||||||
return themeInfo
|
return themeInfo
|
||||||
}
|
}
|
||||||
|
|
||||||
func initThemes() {
|
func loadThemesFromAssets() (themeList []*ThemeMetaInfo, themeMap map[string]*ThemeMetaInfo) {
|
||||||
availableThemes = nil
|
|
||||||
defer func() {
|
|
||||||
availableThemeMap = map[string]*ThemeMetaInfo{}
|
|
||||||
for _, theme := range availableThemes {
|
|
||||||
availableThemeMap[theme.InternalName] = theme
|
|
||||||
}
|
|
||||||
if availableThemeMap[setting.UI.DefaultTheme] == nil {
|
|
||||||
setting.LogStartupProblem(1, log.ERROR, "Default theme %q is not available, please correct the '[ui].DEFAULT_THEME' setting in the config file", setting.UI.DefaultTheme)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
cssFiles, err := public.AssetFS().ListFiles("assets/css")
|
cssFiles, err := public.AssetFS().ListFiles("assets/css")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error("Failed to list themes: %v", err)
|
log.Error("Failed to list themes: %v", err)
|
||||||
availableThemes = []*ThemeMetaInfo{defaultThemeMetaInfoByInternalName(setting.UI.DefaultTheme)}
|
return nil, nil
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var foundThemes []*ThemeMetaInfo
|
var foundThemes []*ThemeMetaInfo
|
||||||
for _, fileName := range cssFiles {
|
for _, fileName := range cssFiles {
|
||||||
if strings.HasPrefix(fileName, fileNamePrefix) && strings.HasSuffix(fileName, fileNameSuffix) {
|
if strings.HasPrefix(fileName, fileNamePrefix) && strings.HasSuffix(fileName, fileNameSuffix) {
|
||||||
@ -157,39 +151,84 @@ func initThemes() {
|
|||||||
foundThemes = append(foundThemes, parseThemeMetaInfo(fileName, util.UnsafeBytesToString(content)))
|
foundThemes = append(foundThemes, parseThemeMetaInfo(fileName, util.UnsafeBytesToString(content)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
themeList = foundThemes
|
||||||
if len(setting.UI.Themes) > 0 {
|
if len(setting.UI.Themes) > 0 {
|
||||||
|
themeList = nil // only allow the themes specified in the setting
|
||||||
allowedThemes := container.SetOf(setting.UI.Themes...)
|
allowedThemes := container.SetOf(setting.UI.Themes...)
|
||||||
for _, theme := range foundThemes {
|
for _, theme := range foundThemes {
|
||||||
if allowedThemes.Contains(theme.InternalName) {
|
if allowedThemes.Contains(theme.InternalName) {
|
||||||
availableThemes = append(availableThemes, theme)
|
themeList = append(themeList, theme)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
availableThemes = foundThemes
|
|
||||||
}
|
}
|
||||||
sort.Slice(availableThemes, func(i, j int) bool {
|
|
||||||
if availableThemes[i].InternalName == setting.UI.DefaultTheme {
|
sort.Slice(themeList, func(i, j int) bool {
|
||||||
|
if themeList[i].InternalName == setting.UI.DefaultTheme {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
if availableThemes[i].ColorblindType != availableThemes[j].ColorblindType {
|
if themeList[i].ColorblindType != themeList[j].ColorblindType {
|
||||||
return availableThemes[i].ColorblindType < availableThemes[j].ColorblindType
|
return themeList[i].ColorblindType < themeList[j].ColorblindType
|
||||||
}
|
}
|
||||||
return availableThemes[i].DisplayName < availableThemes[j].DisplayName
|
return themeList[i].DisplayName < themeList[j].DisplayName
|
||||||
})
|
})
|
||||||
if len(availableThemes) == 0 {
|
|
||||||
setting.LogStartupProblem(1, log.ERROR, "No theme candidate in asset files, but Gitea requires there should be at least one usable theme")
|
themeMap = map[string]*ThemeMetaInfo{}
|
||||||
availableThemes = []*ThemeMetaInfo{defaultThemeMetaInfoByInternalName(setting.UI.DefaultTheme)}
|
for _, theme := range themeList {
|
||||||
|
themeMap[theme.InternalName] = theme
|
||||||
}
|
}
|
||||||
|
return themeList, themeMap
|
||||||
|
}
|
||||||
|
|
||||||
|
func getAvailableThemes() (themeList []*ThemeMetaInfo, themeMap map[string]*ThemeMetaInfo) {
|
||||||
|
themeMu.RLock()
|
||||||
|
if availableThemes != nil {
|
||||||
|
themeList, themeMap = availableThemes.themeList, availableThemes.themeMap
|
||||||
|
}
|
||||||
|
themeMu.RUnlock()
|
||||||
|
if len(themeList) != 0 {
|
||||||
|
return themeList, themeMap
|
||||||
|
}
|
||||||
|
|
||||||
|
themeMu.Lock()
|
||||||
|
defer themeMu.Unlock()
|
||||||
|
// no need to double-check "availableThemes.themeList" since the loading isn't really slow, to keep code simple
|
||||||
|
themeList, themeMap = loadThemesFromAssets()
|
||||||
|
hasAvailableThemes := len(themeList) > 0
|
||||||
|
if !hasAvailableThemes {
|
||||||
|
defaultTheme := defaultThemeMetaInfoByInternalName(setting.UI.DefaultTheme)
|
||||||
|
themeList = []*ThemeMetaInfo{defaultTheme}
|
||||||
|
themeMap = map[string]*ThemeMetaInfo{setting.UI.DefaultTheme: defaultTheme}
|
||||||
|
}
|
||||||
|
|
||||||
|
if setting.IsProd {
|
||||||
|
if !hasAvailableThemes {
|
||||||
|
setting.LogStartupProblem(1, log.ERROR, "No theme candidate in asset files, but Gitea requires there should be at least one usable theme")
|
||||||
|
}
|
||||||
|
if themeMap[setting.UI.DefaultTheme] == nil {
|
||||||
|
setting.LogStartupProblem(1, log.ERROR, "Default theme %q is not available, please correct the '[ui].DEFAULT_THEME' setting in the config file", setting.UI.DefaultTheme)
|
||||||
|
}
|
||||||
|
availableThemes = &themeCollection{themeList, themeMap}
|
||||||
|
return themeList, themeMap
|
||||||
|
}
|
||||||
|
|
||||||
|
// In dev mode, only store the loaded themes if the list is not empty, in case the frontend is still being built.
|
||||||
|
// TBH, there still could be a data-race that the themes are only partially built then the list is incomplete for first time loading.
|
||||||
|
// Such edge case can be handled by checking whether the loaded themes are the same in a period or there is a flag file, but it is an over-kill, so, no.
|
||||||
|
if hasAvailableThemes {
|
||||||
|
availableThemes = &themeCollection{themeList, themeMap}
|
||||||
|
}
|
||||||
|
return themeList, themeMap
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetAvailableThemes() []*ThemeMetaInfo {
|
func GetAvailableThemes() []*ThemeMetaInfo {
|
||||||
themeOnce.Do(initThemes)
|
themes, _ := getAvailableThemes()
|
||||||
return availableThemes
|
return themes
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetThemeMetaInfo(internalName string) *ThemeMetaInfo {
|
func GetThemeMetaInfo(internalName string) *ThemeMetaInfo {
|
||||||
themeOnce.Do(initThemes)
|
_, themeMap := getAvailableThemes()
|
||||||
return availableThemeMap[internalName]
|
return themeMap[internalName]
|
||||||
}
|
}
|
||||||
|
|
||||||
// GuaranteeGetThemeMetaInfo guarantees to return a non-nil ThemeMetaInfo,
|
// GuaranteeGetThemeMetaInfo guarantees to return a non-nil ThemeMetaInfo,
|
||||||
|
|||||||
@ -6,7 +6,7 @@
|
|||||||
<div class=" tw-mr-4 not-mobile">{{ctx.AvatarUtils.Avatar .SignedUser 40}}</div>
|
<div class=" tw-mr-4 not-mobile">{{ctx.AvatarUtils.Avatar .SignedUser 40}}</div>
|
||||||
<div class="ui segment content tw-my-0 avatar-content-left-arrow">
|
<div class="ui segment content tw-my-0 avatar-content-left-arrow">
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<input name="title" data-global-init="initInputAutoFocusEnd" id="issue_title" required maxlength="255" autocomplete="off"
|
<input name="title" data-global-init="autoFocusEnd" id="issue_title" required maxlength="255" autocomplete="off"
|
||||||
placeholder="{{ctx.Locale.Tr "repo.milestones.title"}}"
|
placeholder="{{ctx.Locale.Tr "repo.milestones.title"}}"
|
||||||
value="{{if .TitleQuery}}{{.TitleQuery}}{{else if .IssueTemplateTitle}}{{.IssueTemplateTitle}}{{else}}{{.title}}{{end}}"
|
value="{{if .TitleQuery}}{{.TitleQuery}}{{else if .IssueTemplateTitle}}{{.IssueTemplateTitle}}{{else}}{{.title}}{{end}}"
|
||||||
>
|
>
|
||||||
|
|||||||
@ -1,8 +1,8 @@
|
|||||||
{{if .HeatmapData}}
|
{{if .EnableHeatmap}}
|
||||||
<div class="activity-heatmap-container">
|
<div class="activity-heatmap-container">
|
||||||
<div id="user-heatmap" class="is-loading"
|
<div id="user-heatmap" class="is-loading"
|
||||||
data-heatmap-data="{{JsonUtils.EncodeToString .HeatmapData}}"
|
data-heatmap-url="{{.HeatmapURL}}"
|
||||||
data-locale-total-contributions="{{ctx.Locale.Tr "heatmap.number_of_contributions_in_the_last_12_months" (ctx.Locale.PrettyNumber .HeatmapTotalContributions)}}"
|
data-locale-total-contributions="{{ctx.Locale.Tr "heatmap.number_of_contributions_in_the_last_12_months" "%s"}}"
|
||||||
data-locale-no-contributions="{{ctx.Locale.Tr "heatmap.no_contributions"}}"
|
data-locale-no-contributions="{{ctx.Locale.Tr "heatmap.no_contributions"}}"
|
||||||
data-locale-more="{{ctx.Locale.Tr "heatmap.more"}}"
|
data-locale-more="{{ctx.Locale.Tr "heatmap.more"}}"
|
||||||
data-locale-less="{{ctx.Locale.Tr "heatmap.less"}}"
|
data-locale-less="{{ctx.Locale.Tr "heatmap.less"}}"
|
||||||
|
|||||||
@ -37,7 +37,7 @@ func TestMain(m *testing.M) {
|
|||||||
graceful.InitManager(managerCtx)
|
graceful.InitManager(managerCtx)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
tests.InitTest(false)
|
tests.InitTest()
|
||||||
testE2eWebRoutes = routers.NormalRoutes()
|
testE2eWebRoutes = routers.NormalRoutes()
|
||||||
|
|
||||||
err := unittest.InitFixtures(
|
err := unittest.InitFixtures(
|
||||||
|
|||||||
@ -9,8 +9,8 @@ import (
|
|||||||
"net/url"
|
"net/url"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
actions_model "code.gitea.io/gitea/models/actions"
|
|
||||||
auth_model "code.gitea.io/gitea/models/auth"
|
auth_model "code.gitea.io/gitea/models/auth"
|
||||||
|
repo_model "code.gitea.io/gitea/models/repo"
|
||||||
"code.gitea.io/gitea/models/unittest"
|
"code.gitea.io/gitea/models/unittest"
|
||||||
user_model "code.gitea.io/gitea/models/user"
|
user_model "code.gitea.io/gitea/models/user"
|
||||||
|
|
||||||
@ -19,30 +19,52 @@ import (
|
|||||||
|
|
||||||
func TestActionsCollaborativeOwner(t *testing.T) {
|
func TestActionsCollaborativeOwner(t *testing.T) {
|
||||||
onGiteaRun(t, func(t *testing.T, u *url.URL) {
|
onGiteaRun(t, func(t *testing.T, u *url.URL) {
|
||||||
// user2 is the owner of "reusable_workflow" repo
|
// user2 is the owner of the private "reusable_workflow" repo
|
||||||
user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
|
user2Session := loginUser(t, "user2")
|
||||||
user2Session := loginUser(t, user2.Name)
|
|
||||||
user2Token := getTokenForLoggedInUser(t, user2Session, auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser)
|
user2Token := getTokenForLoggedInUser(t, user2Session, auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser)
|
||||||
repo := createActionsTestRepo(t, user2Token, "reusable_workflow", true)
|
apiReusableWorkflowRepo := createActionsTestRepo(t, user2Token, "reusable_workflow", true)
|
||||||
|
reusableWorkflowRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: apiReusableWorkflowRepo.ID})
|
||||||
|
|
||||||
// a private repo(id=6) of user10 will try to clone "reusable_workflow" repo
|
// user4 is the owner of the private caller repo
|
||||||
user10 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 10})
|
user4 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4})
|
||||||
// task id is 55 and its repo_id=6
|
user4Session := loginUser(t, user4.Name)
|
||||||
task := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionTask{ID: 55, RepoID: 6})
|
user4Token := getTokenForLoggedInUser(t, user4Session, auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser)
|
||||||
taskToken := "674f727a81ed2f195bccab036cccf86a182199eb"
|
apiCallerRepo := createActionsTestRepo(t, user4Token, "caller_workflow", true)
|
||||||
tokenHash := auth_model.HashToken(taskToken, task.TokenSalt)
|
callerRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: apiCallerRepo.ID})
|
||||||
assert.Equal(t, task.TokenHash, tokenHash)
|
|
||||||
|
|
||||||
|
// create a mock runner for caller
|
||||||
|
runner := newMockRunner()
|
||||||
|
runner.registerAsRepoRunner(t, callerRepo.OwnerName, callerRepo.Name, "mock-runner", []string{"ubuntu-latest"}, false)
|
||||||
|
|
||||||
|
// init the workflow for caller
|
||||||
|
wfTreePath := ".gitea/workflows/test_collaborative_owner.yml"
|
||||||
|
wfFileContent := `name: Test Collaborative Owner
|
||||||
|
on: push
|
||||||
|
jobs:
|
||||||
|
job1:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- run: echo 'test collaborative owner'
|
||||||
|
`
|
||||||
|
opts := getWorkflowCreateFileOptions(user4, callerRepo.DefaultBranch, "create "+wfTreePath, wfFileContent)
|
||||||
|
createWorkflowFile(t, user4Token, callerRepo.OwnerName, callerRepo.Name, wfTreePath, opts)
|
||||||
|
|
||||||
|
// fetch the task and get its token
|
||||||
|
task := runner.fetchTask(t)
|
||||||
|
taskToken := task.Secrets["GITEA_TOKEN"]
|
||||||
|
assert.NotEmpty(t, taskToken)
|
||||||
|
|
||||||
|
// prepare for clone
|
||||||
dstPath := t.TempDir()
|
dstPath := t.TempDir()
|
||||||
u.Path = fmt.Sprintf("%s/%s.git", repo.Owner.UserName, repo.Name)
|
u.Path = fmt.Sprintf("%s/%s.git", "user2", "reusable_workflow")
|
||||||
u.User = url.UserPassword("gitea-actions", taskToken)
|
u.User = url.UserPassword("gitea-actions", taskToken)
|
||||||
|
|
||||||
// the git clone will fail
|
// the git clone will fail
|
||||||
doGitCloneFail(u)(t)
|
doGitCloneFail(u)(t)
|
||||||
|
|
||||||
// add user10 to the list of collaborative owners
|
// add user10 to the list of collaborative owners
|
||||||
req := NewRequestWithValues(t, "POST", fmt.Sprintf("/%s/%s/settings/actions/general/collaborative_owner/add", repo.Owner.UserName, repo.Name), map[string]string{
|
req := NewRequestWithValues(t, "POST", fmt.Sprintf("/%s/%s/settings/actions/general/collaborative_owner/add", reusableWorkflowRepo.OwnerName, reusableWorkflowRepo.Name), map[string]string{
|
||||||
"collaborative_owner": user10.Name,
|
"collaborative_owner": user4.Name,
|
||||||
})
|
})
|
||||||
user2Session.MakeRequest(t, req, http.StatusOK)
|
user2Session.MakeRequest(t, req, http.StatusOK)
|
||||||
|
|
||||||
@ -50,7 +72,7 @@ func TestActionsCollaborativeOwner(t *testing.T) {
|
|||||||
doGitClone(dstPath, u)(t)
|
doGitClone(dstPath, u)(t)
|
||||||
|
|
||||||
// remove user10 from the list of collaborative owners
|
// remove user10 from the list of collaborative owners
|
||||||
req = NewRequest(t, "POST", fmt.Sprintf("/%s/%s/settings/actions/general/collaborative_owner/delete?id=%d", repo.Owner.UserName, repo.Name, user10.ID))
|
req = NewRequest(t, "POST", fmt.Sprintf("/%s/%s/settings/actions/general/collaborative_owner/delete?id=%d", reusableWorkflowRepo.OwnerName, reusableWorkflowRepo.Name, user4.ID))
|
||||||
user2Session.MakeRequest(t, req, http.StatusOK)
|
user2Session.MakeRequest(t, req, http.StatusOK)
|
||||||
|
|
||||||
// the git clone will fail
|
// the git clone will fail
|
||||||
|
|||||||
@ -691,6 +691,144 @@ func insertFakeStatus(t *testing.T, repo *repo_model.Repository, sha, targetURL,
|
|||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestPullRequestReviewCommitStatusEvent(t *testing.T) {
|
||||||
|
onGiteaRun(t, func(t *testing.T, u *url.URL) {
|
||||||
|
user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) // owner of the repo
|
||||||
|
user4 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4}) // reviewer
|
||||||
|
|
||||||
|
// create a repo
|
||||||
|
repo, err := repo_service.CreateRepository(t.Context(), user2, user2, repo_service.CreateRepoOptions{
|
||||||
|
Name: "repo-pull-request-review",
|
||||||
|
Description: "test pull-request-review commit status",
|
||||||
|
AutoInit: true,
|
||||||
|
Gitignores: "Go",
|
||||||
|
License: "MIT",
|
||||||
|
Readme: "Default",
|
||||||
|
DefaultBranch: "main",
|
||||||
|
IsPrivate: false,
|
||||||
|
})
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.NotEmpty(t, repo)
|
||||||
|
|
||||||
|
// add user4 as collaborator so they can review
|
||||||
|
ctx := NewAPITestContext(t, repo.OwnerName, repo.Name, auth_model.AccessTokenScopeWriteRepository)
|
||||||
|
t.Run("AddUser4AsCollaboratorWithWriteAccess", doAPIAddCollaborator(ctx, "user4", perm.AccessModeWrite))
|
||||||
|
|
||||||
|
// add workflow file that triggers on pull_request_review
|
||||||
|
addWorkflow, err := files_service.ChangeRepoFiles(t.Context(), repo, user2, &files_service.ChangeRepoFilesOptions{
|
||||||
|
Files: []*files_service.ChangeRepoFile{
|
||||||
|
{
|
||||||
|
Operation: "create",
|
||||||
|
TreePath: ".gitea/workflows/pr-review.yml",
|
||||||
|
ContentReader: strings.NewReader(`name: test
|
||||||
|
on:
|
||||||
|
pull_request_review:
|
||||||
|
types: [submitted]
|
||||||
|
jobs:
|
||||||
|
test:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- run: echo helloworld
|
||||||
|
`),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Message: "add workflow",
|
||||||
|
OldBranch: "main",
|
||||||
|
NewBranch: "main",
|
||||||
|
Author: &files_service.IdentityOptions{
|
||||||
|
GitUserName: user2.Name,
|
||||||
|
GitUserEmail: user2.Email,
|
||||||
|
},
|
||||||
|
Committer: &files_service.IdentityOptions{
|
||||||
|
GitUserName: user2.Name,
|
||||||
|
GitUserEmail: user2.Email,
|
||||||
|
},
|
||||||
|
Dates: &files_service.CommitDateOptions{
|
||||||
|
Author: time.Now(),
|
||||||
|
Committer: time.Now(),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.NotEmpty(t, addWorkflow)
|
||||||
|
|
||||||
|
// create a branch and a PR
|
||||||
|
testBranch := "test-review-branch"
|
||||||
|
err = repo_service.CreateNewBranch(t.Context(), user2, repo, "main", testBranch)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
// add a file on the test branch so the PR has changes
|
||||||
|
addFileResp, err := files_service.ChangeRepoFiles(t.Context(), repo, user2, &files_service.ChangeRepoFilesOptions{
|
||||||
|
Files: []*files_service.ChangeRepoFile{
|
||||||
|
{
|
||||||
|
Operation: "create",
|
||||||
|
TreePath: "test.txt",
|
||||||
|
ContentReader: strings.NewReader("test content"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Message: "add test file",
|
||||||
|
OldBranch: testBranch,
|
||||||
|
NewBranch: testBranch,
|
||||||
|
Author: &files_service.IdentityOptions{
|
||||||
|
GitUserName: user2.Name,
|
||||||
|
GitUserEmail: user2.Email,
|
||||||
|
},
|
||||||
|
Committer: &files_service.IdentityOptions{
|
||||||
|
GitUserName: user2.Name,
|
||||||
|
GitUserEmail: user2.Email,
|
||||||
|
},
|
||||||
|
Dates: &files_service.CommitDateOptions{
|
||||||
|
Author: time.Now(),
|
||||||
|
Committer: time.Now(),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.NotEmpty(t, addFileResp)
|
||||||
|
sha := addFileResp.Commit.SHA
|
||||||
|
|
||||||
|
pullIssue := &issues_model.Issue{
|
||||||
|
RepoID: repo.ID,
|
||||||
|
Title: "A test PR for review",
|
||||||
|
PosterID: user2.ID,
|
||||||
|
Poster: user2,
|
||||||
|
IsPull: true,
|
||||||
|
}
|
||||||
|
pullRequest := &issues_model.PullRequest{
|
||||||
|
HeadRepoID: repo.ID,
|
||||||
|
BaseRepoID: repo.ID,
|
||||||
|
HeadBranch: testBranch,
|
||||||
|
BaseBranch: "main",
|
||||||
|
HeadRepo: repo,
|
||||||
|
BaseRepo: repo,
|
||||||
|
Type: issues_model.PullRequestGitea,
|
||||||
|
}
|
||||||
|
prOpts := &pull_service.NewPullRequestOptions{Repo: repo, Issue: pullIssue, PullRequest: pullRequest}
|
||||||
|
err = pull_service.NewPullRequest(t.Context(), prOpts)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
// submit an approval review as user4
|
||||||
|
gitRepo, err := gitrepo.OpenRepository(t.Context(), repo)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
defer gitRepo.Close()
|
||||||
|
|
||||||
|
_, _, err = pull_service.SubmitReview(t.Context(), user4, gitRepo, pullIssue, issues_model.ReviewTypeApprove, "lgtm", sha, nil)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
// verify that a commit status was created for the review event
|
||||||
|
assert.Eventually(t, func() bool {
|
||||||
|
latestCommitStatuses, err := git_model.GetLatestCommitStatus(t.Context(), repo.ID, sha, db.ListOptionsAll)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
if len(latestCommitStatuses) == 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if latestCommitStatuses[0].State == commitstatus.CommitStatusPending {
|
||||||
|
insertFakeStatus(t, repo, sha, latestCommitStatuses[0].TargetURL, latestCommitStatuses[0].Context)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}, 1*time.Second, 100*time.Millisecond)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func TestWorkflowDispatchPublicApi(t *testing.T) {
|
func TestWorkflowDispatchPublicApi(t *testing.T) {
|
||||||
onGiteaRun(t, func(t *testing.T, u *url.URL) {
|
onGiteaRun(t, func(t *testing.T, u *url.URL) {
|
||||||
user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
|
user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
|
||||||
|
|||||||
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.
|
// Copyright 2017 The Gitea Authors. All rights reserved.
|
||||||
// SPDX-License-Identifier: MIT
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
//nolint:forbidigo // use of print functions is allowed in tests
|
|
||||||
package integration
|
package integration
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@ -27,6 +26,7 @@ import (
|
|||||||
"code.gitea.io/gitea/modules/json"
|
"code.gitea.io/gitea/modules/json"
|
||||||
"code.gitea.io/gitea/modules/log"
|
"code.gitea.io/gitea/modules/log"
|
||||||
"code.gitea.io/gitea/modules/setting"
|
"code.gitea.io/gitea/modules/setting"
|
||||||
|
"code.gitea.io/gitea/modules/testlogger"
|
||||||
"code.gitea.io/gitea/modules/util"
|
"code.gitea.io/gitea/modules/util"
|
||||||
"code.gitea.io/gitea/modules/web"
|
"code.gitea.io/gitea/modules/web"
|
||||||
"code.gitea.io/gitea/modules/web/middleware"
|
"code.gitea.io/gitea/modules/web/middleware"
|
||||||
@ -79,14 +79,14 @@ func NewNilResponseHashSumRecorder() *NilResponseHashSumRecorder {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestMain(m *testing.M) {
|
func testMain(m *testing.M) int {
|
||||||
defer log.GetManager().Close()
|
defer log.GetManager().Close()
|
||||||
|
|
||||||
managerCtx, cancel := context.WithCancel(context.Background())
|
managerCtx, cancel := context.WithCancel(context.Background())
|
||||||
graceful.InitManager(managerCtx)
|
graceful.InitManager(managerCtx)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
tests.InitTest(true)
|
tests.InitTest()
|
||||||
testWebRoutes = routers.NormalRoutes()
|
testWebRoutes = routers.NormalRoutes()
|
||||||
|
|
||||||
err := unittest.InitFixtures(
|
err := unittest.InitFixtures(
|
||||||
@ -95,8 +95,7 @@ func TestMain(m *testing.M) {
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("Error initializing test database: %v\n", err)
|
testlogger.Panicf("InitFixtures: %v", err)
|
||||||
os.Exit(1)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// FIXME: the console logger is deleted by mistake, so if there is any `log.Fatal`, developers won't see any error message.
|
// FIXME: the console logger is deleted by mistake, so if there is any `log.Fatal`, developers won't see any error message.
|
||||||
@ -104,15 +103,16 @@ func TestMain(m *testing.M) {
|
|||||||
exitCode := m.Run()
|
exitCode := m.Run()
|
||||||
|
|
||||||
if err = util.RemoveAll(setting.Indexer.IssuePath); err != nil {
|
if err = util.RemoveAll(setting.Indexer.IssuePath); err != nil {
|
||||||
fmt.Printf("util.RemoveAll: %v\n", err)
|
log.Error("Failed to remove indexer path: %v", err)
|
||||||
os.Exit(1)
|
|
||||||
}
|
}
|
||||||
if err = util.RemoveAll(setting.Indexer.RepoPath); err != nil {
|
if err = util.RemoveAll(setting.Indexer.RepoPath); err != nil {
|
||||||
fmt.Printf("Unable to remove repo indexer: %v\n", err)
|
log.Error("Failed to remove indexer path: %v", err)
|
||||||
os.Exit(1)
|
|
||||||
}
|
}
|
||||||
|
return exitCode
|
||||||
|
}
|
||||||
|
|
||||||
os.Exit(exitCode)
|
func TestMain(m *testing.M) {
|
||||||
|
os.Exit(testMain(m))
|
||||||
}
|
}
|
||||||
|
|
||||||
type TestSession struct {
|
type TestSession struct {
|
||||||
|
|||||||
@ -36,8 +36,6 @@ var currentEngine *xorm.Engine
|
|||||||
|
|
||||||
func initMigrationTest(t *testing.T) func() {
|
func initMigrationTest(t *testing.T) func() {
|
||||||
testlogger.Init()
|
testlogger.Init()
|
||||||
setting.SetupGiteaTestEnv()
|
|
||||||
|
|
||||||
unittest.InitSettingsForTesting()
|
unittest.InitSettingsForTesting()
|
||||||
|
|
||||||
assert.NotEmpty(t, setting.RepoRootPath)
|
assert.NotEmpty(t, setting.RepoRootPath)
|
||||||
|
|||||||
@ -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) {
|
func testUIDeleteBranch(t *testing.T, session *TestSession, ownerName, repoName, branchName string) {
|
||||||
relURL := "/" + path.Join(ownerName, repoName, "branches")
|
relURL := "/" + path.Join(ownerName, repoName, "branches")
|
||||||
req := NewRequestWithValues(t, "POST", relURL+"/delete", map[string]string{
|
req := NewRequestWithValues(t, "POST", relURL+"/delete", map[string]string{
|
||||||
|
|||||||
@ -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
|
APP_NAME = Gitea: Git with a cup of tea
|
||||||
RUN_MODE = prod
|
RUN_MODE = prod
|
||||||
|
|
||||||
@ -11,11 +12,9 @@ SSL_MODE = disable
|
|||||||
|
|
||||||
[indexer]
|
[indexer]
|
||||||
REPO_INDEXER_ENABLED = true
|
REPO_INDEXER_ENABLED = true
|
||||||
REPO_INDEXER_PATH = tests/{{TEST_TYPE}}/gitea-{{TEST_TYPE}}-mssql/indexers/repos.bleve
|
|
||||||
|
|
||||||
[queue.issue_indexer]
|
[queue.issue_indexer]
|
||||||
TYPE = level
|
TYPE = level
|
||||||
DATADIR = tests/{{TEST_TYPE}}/gitea-{{TEST_TYPE}}-mssql/indexers/issues.queue
|
|
||||||
|
|
||||||
[queue]
|
[queue]
|
||||||
TYPE = immediate
|
TYPE = immediate
|
||||||
@ -29,15 +28,6 @@ TYPE = immediate
|
|||||||
[queue.webhook_sender]
|
[queue.webhook_sender]
|
||||||
TYPE = immediate
|
TYPE = immediate
|
||||||
|
|
||||||
[repository]
|
|
||||||
ROOT = {{REPO_TEST_DIR}}tests/{{TEST_TYPE}}/gitea-{{TEST_TYPE}}-mssql/gitea-repositories
|
|
||||||
|
|
||||||
[repository.local]
|
|
||||||
LOCAL_COPY_PATH = tests/{{TEST_TYPE}}/gitea-{{TEST_TYPE}}-mssql/tmp/local-repo
|
|
||||||
|
|
||||||
[repository.upload]
|
|
||||||
TEMP_PATH = tests/{{TEST_TYPE}}/gitea-{{TEST_TYPE}}-mssql/tmp/uploads
|
|
||||||
|
|
||||||
[repository.signing]
|
[repository.signing]
|
||||||
SIGNING_KEY = none
|
SIGNING_KEY = none
|
||||||
|
|
||||||
@ -53,14 +43,13 @@ START_SSH_SERVER = true
|
|||||||
LFS_START_SERVER = true
|
LFS_START_SERVER = true
|
||||||
OFFLINE_MODE = false
|
OFFLINE_MODE = false
|
||||||
LFS_JWT_SECRET = Tv_MjmZuHqpIY6GFl12ebgkRAMt4RlWt0v4EHKSXO0w
|
LFS_JWT_SECRET = Tv_MjmZuHqpIY6GFl12ebgkRAMt4RlWt0v4EHKSXO0w
|
||||||
APP_DATA_PATH = tests/{{TEST_TYPE}}/gitea-{{TEST_TYPE}}-mssql/data
|
|
||||||
BUILTIN_SSH_SERVER_USER = git
|
BUILTIN_SSH_SERVER_USER = git
|
||||||
SSH_TRUSTED_USER_CA_KEYS = ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQCb4DC1dMFnJ6pXWo7GMxTchtzmJHYzfN6sZ9FAPFR4ijMLfGki+olvOMO5Fql1/yGnGfbELQa1S6y4shSvj/5K+zUFScmEXYf3Gcr87RqilLkyk16RS+cHNB1u87xTHbETaa3nyCJeGQRpd4IQ4NKob745mwDZ7jQBH8AZEng50Oh8y8fi8skBBBzaYp1ilgvzG740L7uex6fHV62myq0SXeCa+oJUjq326FU8y+Vsa32H8A3e7tOgXZPdt2TVNltx2S9H2WO8RMi7LfaSwARNfy1zu+bfR50r6ef8Yx5YKCMz4wWb1SHU1GS800mjOjlInLQORYRNMlSwR1+vLlVDciOqFapDSbj+YOVOawR0R1aqlSKpZkt33DuOBPx9qe6CVnIi7Z+Px/KqM+OLCzlLY/RS+LbxQpDWcfTVRiP+S5qRTcE3M3UioN/e0BE/1+MpX90IGpvVkA63ILYbKEa4bM3ASL7ChTCr6xN5XT+GpVJveFKK1cfNx9ExHI4rzYE=
|
SSH_TRUSTED_USER_CA_KEYS = ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQCb4DC1dMFnJ6pXWo7GMxTchtzmJHYzfN6sZ9FAPFR4ijMLfGki+olvOMO5Fql1/yGnGfbELQa1S6y4shSvj/5K+zUFScmEXYf3Gcr87RqilLkyk16RS+cHNB1u87xTHbETaa3nyCJeGQRpd4IQ4NKob745mwDZ7jQBH8AZEng50Oh8y8fi8skBBBzaYp1ilgvzG740L7uex6fHV62myq0SXeCa+oJUjq326FU8y+Vsa32H8A3e7tOgXZPdt2TVNltx2S9H2WO8RMi7LfaSwARNfy1zu+bfR50r6ef8Yx5YKCMz4wWb1SHU1GS800mjOjlInLQORYRNMlSwR1+vLlVDciOqFapDSbj+YOVOawR0R1aqlSKpZkt33DuOBPx9qe6CVnIi7Z+Px/KqM+OLCzlLY/RS+LbxQpDWcfTVRiP+S5qRTcE3M3UioN/e0BE/1+MpX90IGpvVkA63ILYbKEa4bM3ASL7ChTCr6xN5XT+GpVJveFKK1cfNx9ExHI4rzYE=
|
||||||
|
|
||||||
[mailer]
|
[mailer]
|
||||||
ENABLED = true
|
ENABLED = true
|
||||||
PROTOCOL = dummy
|
PROTOCOL = dummy
|
||||||
FROM = mssql-{{TEST_TYPE}}-test@gitea.io
|
FROM = mssql-integration-test@gitea.io
|
||||||
|
|
||||||
[service]
|
[service]
|
||||||
REGISTER_EMAIL_CONFIRM = false
|
REGISTER_EMAIL_CONFIRM = false
|
||||||
@ -76,16 +65,12 @@ ENABLE_NOTIFY_MAIL = true
|
|||||||
[picture]
|
[picture]
|
||||||
DISABLE_GRAVATAR = false
|
DISABLE_GRAVATAR = false
|
||||||
ENABLE_FEDERATED_AVATAR = false
|
ENABLE_FEDERATED_AVATAR = false
|
||||||
AVATAR_UPLOAD_PATH = tests/{{TEST_TYPE}}/gitea-{{TEST_TYPE}}-mssql/data/avatars
|
|
||||||
REPOSITORY_AVATAR_UPLOAD_PATH = tests/{{TEST_TYPE}}/gitea-{{TEST_TYPE}}-mssql/data/repo-avatars
|
|
||||||
|
|
||||||
[session]
|
[session]
|
||||||
PROVIDER = file
|
PROVIDER = file
|
||||||
PROVIDER_CONFIG = tests/{{TEST_TYPE}}/gitea-{{TEST_TYPE}}-mssql/data/sessions
|
|
||||||
|
|
||||||
[log]
|
[log]
|
||||||
MODE = {{TEST_LOGGER}}
|
MODE = {{TEST_LOGGER}}
|
||||||
ROOT_PATH = {{REPO_TEST_DIR}}mssql-log
|
|
||||||
ENABLE_SSH_LOG = true
|
ENABLE_SSH_LOG = true
|
||||||
logger.xorm.MODE = file
|
logger.xorm.MODE = file
|
||||||
|
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
WORK_PATH = {{WORK_PATH}}
|
||||||
APP_NAME = Gitea: Git with a cup of tea
|
APP_NAME = Gitea: Git with a cup of tea
|
||||||
RUN_MODE = prod
|
RUN_MODE = prod
|
||||||
|
|
||||||
@ -11,13 +12,11 @@ SSL_MODE = disable
|
|||||||
|
|
||||||
[indexer]
|
[indexer]
|
||||||
REPO_INDEXER_ENABLED = true
|
REPO_INDEXER_ENABLED = true
|
||||||
REPO_INDEXER_PATH = tests/{{TEST_TYPE}}/gitea-{{TEST_TYPE}}-mysql/indexers/repos.bleve
|
|
||||||
ISSUE_INDEXER_TYPE = elasticsearch
|
ISSUE_INDEXER_TYPE = elasticsearch
|
||||||
ISSUE_INDEXER_CONN_STR = http://elastic:changeme@elasticsearch:9200
|
ISSUE_INDEXER_CONN_STR = http://elastic:changeme@elasticsearch:9200
|
||||||
|
|
||||||
[queue.issue_indexer]
|
[queue.issue_indexer]
|
||||||
TYPE = level
|
TYPE = level
|
||||||
DATADIR = tests/{{TEST_TYPE}}/gitea-{{TEST_TYPE}}-mysql/indexers/issues.queue
|
|
||||||
|
|
||||||
[queue]
|
[queue]
|
||||||
TYPE = immediate
|
TYPE = immediate
|
||||||
@ -31,15 +30,6 @@ TYPE = immediate
|
|||||||
[queue.webhook_sender]
|
[queue.webhook_sender]
|
||||||
TYPE = immediate
|
TYPE = immediate
|
||||||
|
|
||||||
[repository]
|
|
||||||
ROOT = {{REPO_TEST_DIR}}tests/{{TEST_TYPE}}/gitea-{{TEST_TYPE}}-mysql/gitea-repositories
|
|
||||||
|
|
||||||
[repository.local]
|
|
||||||
LOCAL_COPY_PATH = tests/{{TEST_TYPE}}/gitea-{{TEST_TYPE}}-mysql/tmp/local-repo
|
|
||||||
|
|
||||||
[repository.upload]
|
|
||||||
TEMP_PATH = tests/{{TEST_TYPE}}/gitea-{{TEST_TYPE}}-mysql/tmp/uploads
|
|
||||||
|
|
||||||
[repository.signing]
|
[repository.signing]
|
||||||
SIGNING_KEY = none
|
SIGNING_KEY = none
|
||||||
|
|
||||||
@ -51,7 +41,6 @@ LOCAL_ROOT_URL = http://127.0.0.1:3001/
|
|||||||
DISABLE_SSH = false
|
DISABLE_SSH = false
|
||||||
SSH_LISTEN_HOST = localhost
|
SSH_LISTEN_HOST = localhost
|
||||||
SSH_PORT = 2201
|
SSH_PORT = 2201
|
||||||
APP_DATA_PATH = tests/{{TEST_TYPE}}/gitea-{{TEST_TYPE}}-mysql/data
|
|
||||||
BUILTIN_SSH_SERVER_USER = git
|
BUILTIN_SSH_SERVER_USER = git
|
||||||
START_SSH_SERVER = true
|
START_SSH_SERVER = true
|
||||||
OFFLINE_MODE = false
|
OFFLINE_MODE = false
|
||||||
@ -63,7 +52,7 @@ SSH_TRUSTED_USER_CA_KEYS = ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQCb4DC1dMFnJ6pXW
|
|||||||
[mailer]
|
[mailer]
|
||||||
ENABLED = true
|
ENABLED = true
|
||||||
PROTOCOL = dummy
|
PROTOCOL = dummy
|
||||||
FROM = mysql-{{TEST_TYPE}}-test@gitea.io
|
FROM = mysql-integration-test@gitea.io
|
||||||
|
|
||||||
[service]
|
[service]
|
||||||
REGISTER_EMAIL_CONFIRM = false
|
REGISTER_EMAIL_CONFIRM = false
|
||||||
@ -82,11 +71,9 @@ ENABLE_FEDERATED_AVATAR = false
|
|||||||
|
|
||||||
[session]
|
[session]
|
||||||
PROVIDER = file
|
PROVIDER = file
|
||||||
PROVIDER_CONFIG = tests/{{TEST_TYPE}}/gitea-{{TEST_TYPE}}-mysql/data/sessions
|
|
||||||
|
|
||||||
[log]
|
[log]
|
||||||
MODE = {{TEST_LOGGER}}
|
MODE = {{TEST_LOGGER}}
|
||||||
ROOT_PATH = {{REPO_TEST_DIR}}mysql-log
|
|
||||||
ENABLE_SSH_LOG = true
|
ENABLE_SSH_LOG = true
|
||||||
logger.xorm.MODE = file
|
logger.xorm.MODE = file
|
||||||
|
|
||||||
@ -103,9 +90,6 @@ SECRET_KEY = 9pCviYTWSb
|
|||||||
INTERNAL_TOKEN = eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYmYiOjE0OTU1NTE2MTh9.hhSVGOANkaKk3vfCd2jDOIww4pUk0xtg9JRde5UogyQ
|
INTERNAL_TOKEN = eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYmYiOjE0OTU1NTE2MTh9.hhSVGOANkaKk3vfCd2jDOIww4pUk0xtg9JRde5UogyQ
|
||||||
DISABLE_QUERY_AUTH_TOKEN = true
|
DISABLE_QUERY_AUTH_TOKEN = true
|
||||||
|
|
||||||
[lfs]
|
|
||||||
PATH = tests/{{TEST_TYPE}}/gitea-{{TEST_TYPE}}-mysql/data/lfs
|
|
||||||
|
|
||||||
[packages]
|
[packages]
|
||||||
ENABLED = true
|
ENABLED = true
|
||||||
|
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
WORK_PATH = {{WORK_PATH}}
|
||||||
APP_NAME = Gitea: Git with a cup of tea
|
APP_NAME = Gitea: Git with a cup of tea
|
||||||
RUN_MODE = prod
|
RUN_MODE = prod
|
||||||
|
|
||||||
@ -12,11 +13,9 @@ SSL_MODE = disable
|
|||||||
|
|
||||||
[indexer]
|
[indexer]
|
||||||
REPO_INDEXER_ENABLED = true
|
REPO_INDEXER_ENABLED = true
|
||||||
REPO_INDEXER_PATH = tests/{{TEST_TYPE}}/gitea-{{TEST_TYPE}}-pgsql/indexers/repos.bleve
|
|
||||||
|
|
||||||
[queue.issue_indexer]
|
[queue.issue_indexer]
|
||||||
TYPE = level
|
TYPE = level
|
||||||
DATADIR = tests/{{TEST_TYPE}}/gitea-{{TEST_TYPE}}-pgsql/indexers/issues.queue
|
|
||||||
|
|
||||||
[queue]
|
[queue]
|
||||||
TYPE = immediate
|
TYPE = immediate
|
||||||
@ -30,15 +29,6 @@ TYPE = immediate
|
|||||||
[queue.webhook_sender]
|
[queue.webhook_sender]
|
||||||
TYPE = immediate
|
TYPE = immediate
|
||||||
|
|
||||||
[repository]
|
|
||||||
ROOT = {{REPO_TEST_DIR}}tests/{{TEST_TYPE}}/gitea-{{TEST_TYPE}}-pgsql/gitea-repositories
|
|
||||||
|
|
||||||
[repository.local]
|
|
||||||
LOCAL_COPY_PATH = tests/{{TEST_TYPE}}/gitea-{{TEST_TYPE}}-pgsql/tmp/local-repo
|
|
||||||
|
|
||||||
[repository.upload]
|
|
||||||
TEMP_PATH = tests/{{TEST_TYPE}}/gitea-{{TEST_TYPE}}-pgsql/tmp/uploads
|
|
||||||
|
|
||||||
[repository.signing]
|
[repository.signing]
|
||||||
SIGNING_KEY = none
|
SIGNING_KEY = none
|
||||||
|
|
||||||
@ -54,14 +44,13 @@ START_SSH_SERVER = true
|
|||||||
LFS_START_SERVER = true
|
LFS_START_SERVER = true
|
||||||
OFFLINE_MODE = false
|
OFFLINE_MODE = false
|
||||||
LFS_JWT_SECRET = Tv_MjmZuHqpIY6GFl12ebgkRAMt4RlWt0v4EHKSXO0w
|
LFS_JWT_SECRET = Tv_MjmZuHqpIY6GFl12ebgkRAMt4RlWt0v4EHKSXO0w
|
||||||
APP_DATA_PATH = tests/{{TEST_TYPE}}/gitea-{{TEST_TYPE}}-pgsql/data
|
|
||||||
BUILTIN_SSH_SERVER_USER = git
|
BUILTIN_SSH_SERVER_USER = git
|
||||||
SSH_TRUSTED_USER_CA_KEYS = ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQCb4DC1dMFnJ6pXWo7GMxTchtzmJHYzfN6sZ9FAPFR4ijMLfGki+olvOMO5Fql1/yGnGfbELQa1S6y4shSvj/5K+zUFScmEXYf3Gcr87RqilLkyk16RS+cHNB1u87xTHbETaa3nyCJeGQRpd4IQ4NKob745mwDZ7jQBH8AZEng50Oh8y8fi8skBBBzaYp1ilgvzG740L7uex6fHV62myq0SXeCa+oJUjq326FU8y+Vsa32H8A3e7tOgXZPdt2TVNltx2S9H2WO8RMi7LfaSwARNfy1zu+bfR50r6ef8Yx5YKCMz4wWb1SHU1GS800mjOjlInLQORYRNMlSwR1+vLlVDciOqFapDSbj+YOVOawR0R1aqlSKpZkt33DuOBPx9qe6CVnIi7Z+Px/KqM+OLCzlLY/RS+LbxQpDWcfTVRiP+S5qRTcE3M3UioN/e0BE/1+MpX90IGpvVkA63ILYbKEa4bM3ASL7ChTCr6xN5XT+GpVJveFKK1cfNx9ExHI4rzYE=
|
SSH_TRUSTED_USER_CA_KEYS = ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQCb4DC1dMFnJ6pXWo7GMxTchtzmJHYzfN6sZ9FAPFR4ijMLfGki+olvOMO5Fql1/yGnGfbELQa1S6y4shSvj/5K+zUFScmEXYf3Gcr87RqilLkyk16RS+cHNB1u87xTHbETaa3nyCJeGQRpd4IQ4NKob745mwDZ7jQBH8AZEng50Oh8y8fi8skBBBzaYp1ilgvzG740L7uex6fHV62myq0SXeCa+oJUjq326FU8y+Vsa32H8A3e7tOgXZPdt2TVNltx2S9H2WO8RMi7LfaSwARNfy1zu+bfR50r6ef8Yx5YKCMz4wWb1SHU1GS800mjOjlInLQORYRNMlSwR1+vLlVDciOqFapDSbj+YOVOawR0R1aqlSKpZkt33DuOBPx9qe6CVnIi7Z+Px/KqM+OLCzlLY/RS+LbxQpDWcfTVRiP+S5qRTcE3M3UioN/e0BE/1+MpX90IGpvVkA63ILYbKEa4bM3ASL7ChTCr6xN5XT+GpVJveFKK1cfNx9ExHI4rzYE=
|
||||||
|
|
||||||
[mailer]
|
[mailer]
|
||||||
ENABLED = true
|
ENABLED = true
|
||||||
PROTOCOL = dummy
|
PROTOCOL = dummy
|
||||||
FROM = pgsql-{{TEST_TYPE}}-test@gitea.io
|
FROM = pgsql-integration-test@gitea.io
|
||||||
|
|
||||||
[service]
|
[service]
|
||||||
REGISTER_EMAIL_CONFIRM = false
|
REGISTER_EMAIL_CONFIRM = false
|
||||||
@ -77,16 +66,12 @@ ENABLE_NOTIFY_MAIL = true
|
|||||||
[picture]
|
[picture]
|
||||||
DISABLE_GRAVATAR = false
|
DISABLE_GRAVATAR = false
|
||||||
ENABLE_FEDERATED_AVATAR = false
|
ENABLE_FEDERATED_AVATAR = false
|
||||||
AVATAR_UPLOAD_PATH = tests/{{TEST_TYPE}}/gitea-{{TEST_TYPE}}-pgsql/data/avatars
|
|
||||||
REPOSITORY_AVATAR_UPLOAD_PATH = tests/{{TEST_TYPE}}/gitea-{{TEST_TYPE}}-pgsql/data/repo-avatars
|
|
||||||
|
|
||||||
[session]
|
[session]
|
||||||
PROVIDER = file
|
PROVIDER = file
|
||||||
PROVIDER_CONFIG = tests/{{TEST_TYPE}}/gitea-{{TEST_TYPE}}-pgsql/data/sessions
|
|
||||||
|
|
||||||
[log]
|
[log]
|
||||||
MODE = {{TEST_LOGGER}}
|
MODE = {{TEST_LOGGER}}
|
||||||
ROOT_PATH = {{REPO_TEST_DIR}}pgsql-log
|
|
||||||
ENABLE_SSH_LOG = true
|
ENABLE_SSH_LOG = true
|
||||||
logger.xorm.MODE = file
|
logger.xorm.MODE = file
|
||||||
|
|
||||||
|
|||||||
@ -1,17 +1,16 @@
|
|||||||
|
WORK_PATH = {{WORK_PATH}}
|
||||||
APP_NAME = Gitea: Git with a cup of tea
|
APP_NAME = Gitea: Git with a cup of tea
|
||||||
RUN_MODE = prod
|
RUN_MODE = prod
|
||||||
|
|
||||||
[database]
|
[database]
|
||||||
DB_TYPE = sqlite3
|
DB_TYPE = sqlite3
|
||||||
PATH = tests/{{TEST_TYPE}}/gitea-{{TEST_TYPE}}-sqlite/gitea.db
|
PATH = gitea.db
|
||||||
|
|
||||||
[indexer]
|
[indexer]
|
||||||
REPO_INDEXER_ENABLED = true
|
REPO_INDEXER_ENABLED = true
|
||||||
REPO_INDEXER_PATH = tests/{{TEST_TYPE}}/gitea-{{TEST_TYPE}}-sqlite/indexers/repos.bleve
|
|
||||||
|
|
||||||
[queue.issue_indexer]
|
[queue.issue_indexer]
|
||||||
TYPE = level
|
TYPE = level
|
||||||
DATADIR = tests/{{TEST_TYPE}}/gitea-{{TEST_TYPE}}-sqlite/indexers/issues.queue
|
|
||||||
|
|
||||||
[queue]
|
[queue]
|
||||||
TYPE = immediate
|
TYPE = immediate
|
||||||
@ -25,15 +24,6 @@ TYPE = immediate
|
|||||||
[queue.webhook_sender]
|
[queue.webhook_sender]
|
||||||
TYPE = immediate
|
TYPE = immediate
|
||||||
|
|
||||||
[repository]
|
|
||||||
ROOT = {{REPO_TEST_DIR}}tests/{{TEST_TYPE}}/gitea-{{TEST_TYPE}}-sqlite/gitea-repositories
|
|
||||||
|
|
||||||
[repository.local]
|
|
||||||
LOCAL_COPY_PATH = tests/{{TEST_TYPE}}/gitea-{{TEST_TYPE}}-sqlite/tmp/local-repo
|
|
||||||
|
|
||||||
[repository.upload]
|
|
||||||
TEMP_PATH = tests/{{TEST_TYPE}}/gitea-{{TEST_TYPE}}-sqlite/tmp/uploads
|
|
||||||
|
|
||||||
[repository.signing]
|
[repository.signing]
|
||||||
SIGNING_KEY = none
|
SIGNING_KEY = none
|
||||||
|
|
||||||
@ -49,18 +39,14 @@ START_SSH_SERVER = true
|
|||||||
LFS_START_SERVER = true
|
LFS_START_SERVER = true
|
||||||
OFFLINE_MODE = false
|
OFFLINE_MODE = false
|
||||||
LFS_JWT_SECRET = Tv_MjmZuHqpIY6GFl12ebgkRAMt4RlWt0v4EHKSXO0w
|
LFS_JWT_SECRET = Tv_MjmZuHqpIY6GFl12ebgkRAMt4RlWt0v4EHKSXO0w
|
||||||
APP_DATA_PATH = tests/{{TEST_TYPE}}/gitea-{{TEST_TYPE}}-sqlite/data
|
|
||||||
ENABLE_GZIP = true
|
ENABLE_GZIP = true
|
||||||
BUILTIN_SSH_SERVER_USER = git
|
BUILTIN_SSH_SERVER_USER = git
|
||||||
SSH_TRUSTED_USER_CA_KEYS = ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQCb4DC1dMFnJ6pXWo7GMxTchtzmJHYzfN6sZ9FAPFR4ijMLfGki+olvOMO5Fql1/yGnGfbELQa1S6y4shSvj/5K+zUFScmEXYf3Gcr87RqilLkyk16RS+cHNB1u87xTHbETaa3nyCJeGQRpd4IQ4NKob745mwDZ7jQBH8AZEng50Oh8y8fi8skBBBzaYp1ilgvzG740L7uex6fHV62myq0SXeCa+oJUjq326FU8y+Vsa32H8A3e7tOgXZPdt2TVNltx2S9H2WO8RMi7LfaSwARNfy1zu+bfR50r6ef8Yx5YKCMz4wWb1SHU1GS800mjOjlInLQORYRNMlSwR1+vLlVDciOqFapDSbj+YOVOawR0R1aqlSKpZkt33DuOBPx9qe6CVnIi7Z+Px/KqM+OLCzlLY/RS+LbxQpDWcfTVRiP+S5qRTcE3M3UioN/e0BE/1+MpX90IGpvVkA63ILYbKEa4bM3ASL7ChTCr6xN5XT+GpVJveFKK1cfNx9ExHI4rzYE=
|
SSH_TRUSTED_USER_CA_KEYS = ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQCb4DC1dMFnJ6pXWo7GMxTchtzmJHYzfN6sZ9FAPFR4ijMLfGki+olvOMO5Fql1/yGnGfbELQa1S6y4shSvj/5K+zUFScmEXYf3Gcr87RqilLkyk16RS+cHNB1u87xTHbETaa3nyCJeGQRpd4IQ4NKob745mwDZ7jQBH8AZEng50Oh8y8fi8skBBBzaYp1ilgvzG740L7uex6fHV62myq0SXeCa+oJUjq326FU8y+Vsa32H8A3e7tOgXZPdt2TVNltx2S9H2WO8RMi7LfaSwARNfy1zu+bfR50r6ef8Yx5YKCMz4wWb1SHU1GS800mjOjlInLQORYRNMlSwR1+vLlVDciOqFapDSbj+YOVOawR0R1aqlSKpZkt33DuOBPx9qe6CVnIi7Z+Px/KqM+OLCzlLY/RS+LbxQpDWcfTVRiP+S5qRTcE3M3UioN/e0BE/1+MpX90IGpvVkA63ILYbKEa4bM3ASL7ChTCr6xN5XT+GpVJveFKK1cfNx9ExHI4rzYE=
|
||||||
|
|
||||||
[attachment]
|
|
||||||
PATH = tests/{{TEST_TYPE}}/gitea-{{TEST_TYPE}}-sqlite/data/attachments
|
|
||||||
|
|
||||||
[mailer]
|
[mailer]
|
||||||
ENABLED = true
|
ENABLED = true
|
||||||
PROTOCOL = dummy
|
PROTOCOL = dummy
|
||||||
FROM = sqlite-{{TEST_TYPE}}-test@gitea.io
|
FROM = sqlite-integration-test@gitea.io
|
||||||
|
|
||||||
[service]
|
[service]
|
||||||
REGISTER_EMAIL_CONFIRM = false
|
REGISTER_EMAIL_CONFIRM = false
|
||||||
@ -76,16 +62,12 @@ NO_REPLY_ADDRESS = noreply.example.org
|
|||||||
[picture]
|
[picture]
|
||||||
DISABLE_GRAVATAR = false
|
DISABLE_GRAVATAR = false
|
||||||
ENABLE_FEDERATED_AVATAR = false
|
ENABLE_FEDERATED_AVATAR = false
|
||||||
AVATAR_UPLOAD_PATH = tests/{{TEST_TYPE}}/gitea-{{TEST_TYPE}}-sqlite/data/avatars
|
|
||||||
REPOSITORY_AVATAR_UPLOAD_PATH = tests/{{TEST_TYPE}}/gitea-{{TEST_TYPE}}-sqlite/data/repo-avatars
|
|
||||||
|
|
||||||
[session]
|
[session]
|
||||||
PROVIDER = file
|
PROVIDER = file
|
||||||
PROVIDER_CONFIG = tests/{{TEST_TYPE}}/gitea-{{TEST_TYPE}}-sqlite/data/sessions
|
|
||||||
|
|
||||||
[log]
|
[log]
|
||||||
MODE = {{TEST_LOGGER}}
|
MODE = {{TEST_LOGGER}}
|
||||||
ROOT_PATH = {{REPO_TEST_DIR}}sqlite-log
|
|
||||||
ENABLE_SSH_LOG = true
|
ENABLE_SSH_LOG = true
|
||||||
logger.xorm.MODE = file
|
logger.xorm.MODE = file
|
||||||
|
|
||||||
@ -105,9 +87,6 @@ DISABLE_QUERY_AUTH_TOKEN = true
|
|||||||
[oauth2]
|
[oauth2]
|
||||||
JWT_SECRET = KZb_QLUd4fYVyxetjxC4eZkrBgWM2SndOOWDNtgUUko
|
JWT_SECRET = KZb_QLUd4fYVyxetjxC4eZkrBgWM2SndOOWDNtgUUko
|
||||||
|
|
||||||
[lfs]
|
|
||||||
PATH = tests/{{TEST_TYPE}}/gitea-{{TEST_TYPE}}-sqlite/data/lfs
|
|
||||||
|
|
||||||
[packages]
|
[packages]
|
||||||
ENABLED = true
|
ENABLED = true
|
||||||
|
|
||||||
|
|||||||
@ -24,11 +24,8 @@ import (
|
|||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
func InitTest(requireGitea bool) {
|
func InitTest() {
|
||||||
testlogger.Init()
|
testlogger.Init()
|
||||||
|
|
||||||
setting.SetupGiteaTestEnv()
|
|
||||||
|
|
||||||
unittest.InitSettingsForTesting()
|
unittest.InitSettingsForTesting()
|
||||||
setting.Repository.DefaultBranch = "master" // many test code still assume that default branch is called "master"
|
setting.Repository.DefaultBranch = "master" // many test code still assume that default branch is called "master"
|
||||||
|
|
||||||
@ -38,7 +35,7 @@ func InitTest(requireGitea bool) {
|
|||||||
|
|
||||||
setting.LoadDBSetting()
|
setting.LoadDBSetting()
|
||||||
if err := storage.Init(); err != nil {
|
if err := storage.Init(); err != nil {
|
||||||
testlogger.Fatalf("Init storage failed: %v\n", err)
|
testlogger.Panicf("Init storage failed: %v\n", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
switch {
|
switch {
|
||||||
|
|||||||
@ -44,6 +44,7 @@
|
|||||||
"stripInternal": true,
|
"stripInternal": true,
|
||||||
"verbatimModuleSyntax": true,
|
"verbatimModuleSyntax": true,
|
||||||
"types": [
|
"types": [
|
||||||
|
"node",
|
||||||
"webpack/module",
|
"webpack/module",
|
||||||
"vitest/globals",
|
"vitest/globals",
|
||||||
"./web_src/js/globals.d.ts",
|
"./web_src/js/globals.d.ts",
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import tinycolor from 'tinycolor2';
|
import {colord} from 'colord';
|
||||||
import {basename, extname, isObject, isDarkTheme} from '../utils.ts';
|
import {basename, extname, isObject, isDarkTheme} from '../utils.ts';
|
||||||
import {onInputDebounce} from '../utils/dom.ts';
|
import {onInputDebounce} from '../utils/dom.ts';
|
||||||
import type MonacoNamespace from 'monaco-editor';
|
import type MonacoNamespace from 'monaco-editor';
|
||||||
@ -94,7 +94,7 @@ function updateTheme(monaco: Monaco): void {
|
|||||||
// https://github.com/microsoft/monaco-editor/issues/2427
|
// https://github.com/microsoft/monaco-editor/issues/2427
|
||||||
// also, monaco can only parse 6-digit hex colors, so we convert the colors to that format
|
// also, monaco can only parse 6-digit hex colors, so we convert the colors to that format
|
||||||
const styles = window.getComputedStyle(document.documentElement);
|
const styles = window.getComputedStyle(document.documentElement);
|
||||||
const getColor = (name: string) => tinycolor(styles.getPropertyValue(name).trim()).toString('hex6');
|
const getColor = (name: string) => colord(styles.getPropertyValue(name).trim()).alpha(1).toHex();
|
||||||
|
|
||||||
monaco.editor.defineTheme('gitea', {
|
monaco.editor.defineTheme('gitea', {
|
||||||
base: isDarkTheme() ? 'vs-dark' : 'vs',
|
base: isDarkTheme() ? 'vs-dark' : 'vs',
|
||||||
|
|||||||
@ -2,6 +2,7 @@ import {POST} from '../modules/fetch.ts';
|
|||||||
import {addDelegatedEventListener, hideElem, isElemVisible, showElem, toggleElem} from '../utils/dom.ts';
|
import {addDelegatedEventListener, hideElem, isElemVisible, showElem, toggleElem} from '../utils/dom.ts';
|
||||||
import {fomanticQuery} from '../modules/fomantic/base.ts';
|
import {fomanticQuery} from '../modules/fomantic/base.ts';
|
||||||
import {camelize} from 'vue';
|
import {camelize} from 'vue';
|
||||||
|
import {applyAutoFocus} from './common-page.ts';
|
||||||
|
|
||||||
export function initGlobalButtonClickOnEnter(): void {
|
export function initGlobalButtonClickOnEnter(): void {
|
||||||
addDelegatedEventListener(document, 'keypress', 'div.ui.button, span.ui.button', (el, e: KeyboardEvent) => {
|
addDelegatedEventListener(document, 'keypress', 'div.ui.button, span.ui.button', (el, e: KeyboardEvent) => {
|
||||||
@ -88,7 +89,7 @@ function onShowPanelClick(el: HTMLElement, e: MouseEvent) {
|
|||||||
const elems = el.classList.contains('toggle') ? toggleElem(sel) : showElem(sel);
|
const elems = el.classList.contains('toggle') ? toggleElem(sel) : showElem(sel);
|
||||||
for (const elem of elems) {
|
for (const elem of elems) {
|
||||||
if (isElemVisible(elem as HTMLElement)) {
|
if (isElemVisible(elem as HTMLElement)) {
|
||||||
elem.querySelector<HTMLElement>('[autofocus]')?.focus();
|
applyAutoFocus(elem);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -116,12 +116,30 @@ function attachInputDirAuto(el: Partial<HTMLInputElement | HTMLTextAreaElement>)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function autoFocusEnd(el: HTMLInputElement | HTMLTextAreaElement) {
|
||||||
|
el.focus();
|
||||||
|
el.setSelectionRange(el.value.length, el.value.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function applyAutoFocus(container: Element) {
|
||||||
|
// https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/autofocus
|
||||||
|
// "autofocus" behavior is defined by the standard: when a container (e.g.: dialog) becomes visible, focus the element with "autofocus" attribute
|
||||||
|
// Fomantic UI already supports it for its modal dialog, we need to cover more cases (e.g.: ".show-panel" button)
|
||||||
|
// Here is just a simple support, we don't expect more than one element that need "autofocus" appearing in the same container
|
||||||
|
container.querySelector<HTMLElement>('[autofocus]')?.focus();
|
||||||
|
// Also, apply our autoFocusEnd behavior
|
||||||
|
// TODO: GLOBAL-INIT-MULTIPLE-FUNCTIONS: use "~=" operator in case we would extend the "data-global-init" to support more functions in the future.
|
||||||
|
const el = container.querySelector<HTMLInputElement>('[data-global-init~="autoFocusEnd"]');
|
||||||
|
if (el) autoFocusEnd(el);
|
||||||
|
}
|
||||||
|
|
||||||
export function initGlobalInput() {
|
export function initGlobalInput() {
|
||||||
registerGlobalSelectorFunc('input, textarea', attachInputDirAuto);
|
registerGlobalSelectorFunc('input, textarea', attachInputDirAuto);
|
||||||
registerGlobalInitFunc('initInputAutoFocusEnd', (el: HTMLInputElement) => {
|
|
||||||
el.focus(); // expects only one such element on one page. If there are many, then the last one gets the focus.
|
// autoFocusEnd is used for autofocus an input/textarea and move the cursor to the end of the text.
|
||||||
el.setSelectionRange(el.value.length, el.value.length);
|
// It is useful for "New Issue"/"New PR" pages when the title is pre-filled with prefix text (e.g.: from template or commit message)
|
||||||
});
|
// The native "autofocus" isn't used because there is a delay between "focused (DOM rendering)" and "move cursor to end (our JS)", it causes flickers.
|
||||||
|
registerGlobalInitFunc('autoFocusEnd', autoFocusEnd);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -1,14 +1,25 @@
|
|||||||
import {createApp} from 'vue';
|
import {createApp} from 'vue';
|
||||||
import ActivityHeatmap from '../components/ActivityHeatmap.vue';
|
import ActivityHeatmap from '../components/ActivityHeatmap.vue';
|
||||||
import {translateMonth, translateDay} from '../utils.ts';
|
import {translateMonth, translateDay} from '../utils.ts';
|
||||||
|
import {GET} from '../modules/fetch.ts';
|
||||||
|
|
||||||
export function initHeatmap() {
|
type HeatmapResponse = {
|
||||||
const el = document.querySelector('#user-heatmap');
|
heatmapData: Array<[number, number]>; // [[1617235200, 2]] = [unix timestamp, count]
|
||||||
|
totalContributions: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function initHeatmap() {
|
||||||
|
const el = document.querySelector<HTMLElement>('#user-heatmap');
|
||||||
if (!el) return;
|
if (!el) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
const url = el.getAttribute('data-heatmap-url')!;
|
||||||
|
const resp = await GET(url);
|
||||||
|
if (!resp.ok) throw new Error(`Failed to load heatmap data: ${resp.status} ${resp.statusText}`);
|
||||||
|
const {heatmapData, totalContributions} = await resp.json() as HeatmapResponse;
|
||||||
|
|
||||||
const heatmap: Record<string, number> = {};
|
const heatmap: Record<string, number> = {};
|
||||||
for (const {contributions, timestamp} of JSON.parse(el.getAttribute('data-heatmap-data')!)) {
|
for (const [timestamp, contributions] of heatmapData) {
|
||||||
// Convert to user timezone and sum contributions by date
|
// Convert to user timezone and sum contributions by date
|
||||||
const dateStr = new Date(timestamp * 1000).toDateString();
|
const dateStr = new Date(timestamp * 1000).toDateString();
|
||||||
heatmap[dateStr] = (heatmap[dateStr] || 0) + contributions;
|
heatmap[dateStr] = (heatmap[dateStr] || 0) + contributions;
|
||||||
@ -18,6 +29,9 @@ export function initHeatmap() {
|
|||||||
return {date: new Date(v), count: heatmap[v]};
|
return {date: new Date(v), count: heatmap[v]};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const totalFormatted = totalContributions.toLocaleString();
|
||||||
|
const textTotalContributions = el.getAttribute('data-locale-total-contributions')!.replace('%s', totalFormatted);
|
||||||
|
|
||||||
// last heatmap tooltip localization attempt https://github.com/go-gitea/gitea/pull/24131/commits/a83761cbbae3c2e3b4bced71e680f44432073ac8
|
// last heatmap tooltip localization attempt https://github.com/go-gitea/gitea/pull/24131/commits/a83761cbbae3c2e3b4bced71e680f44432073ac8
|
||||||
const locale = {
|
const locale = {
|
||||||
heatMapLocale: {
|
heatMapLocale: {
|
||||||
@ -28,7 +42,7 @@ export function initHeatmap() {
|
|||||||
less: el.getAttribute('data-locale-less'),
|
less: el.getAttribute('data-locale-less'),
|
||||||
},
|
},
|
||||||
tooltipUnit: 'contributions',
|
tooltipUnit: 'contributions',
|
||||||
textTotalContributions: el.getAttribute('data-locale-total-contributions'),
|
textTotalContributions,
|
||||||
noDataText: el.getAttribute('data-locale-no-contributions'),
|
noDataText: el.getAttribute('data-locale-no-contributions'),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -42,6 +42,7 @@ export function registerGlobalInitFunc<T extends HTMLElement>(name: string, hand
|
|||||||
}
|
}
|
||||||
|
|
||||||
function callGlobalInitFunc(el: HTMLElement) {
|
function callGlobalInitFunc(el: HTMLElement) {
|
||||||
|
// TODO: GLOBAL-INIT-MULTIPLE-FUNCTIONS: maybe in the future we need to extend it to support multiple functions, for example: `data-global-init="func1 func2 func3"`
|
||||||
const initFunc = el.getAttribute('data-global-init')!;
|
const initFunc = el.getAttribute('data-global-init')!;
|
||||||
const func = globalInitFuncs[initFunc];
|
const func = globalInitFuncs[initFunc];
|
||||||
if (!func) throw new Error(`Global init function "${initFunc}" not found`);
|
if (!func) throw new Error(`Global init function "${initFunc}" not found`);
|
||||||
|
|||||||
@ -1,22 +1,21 @@
|
|||||||
import tinycolor from 'tinycolor2';
|
import {colord} from 'colord';
|
||||||
import type {ColorInput} from 'tinycolor2';
|
import type {AnyColor} from 'colord';
|
||||||
|
|
||||||
/** Returns relative luminance for a SRGB color - https://en.wikipedia.org/wiki/Relative_luminance */
|
/** Returns relative luminance for a SRGB color - https://en.wikipedia.org/wiki/Relative_luminance */
|
||||||
// Keep this in sync with modules/util/color.go
|
// Keep this in sync with modules/util/color.go
|
||||||
function getRelativeLuminance(color: ColorInput): number {
|
function getRelativeLuminance(color: AnyColor): number {
|
||||||
const {r, g, b} = tinycolor(color).toRgb();
|
const {r, g, b} = colord(color).toRgb();
|
||||||
return (0.2126729 * r + 0.7151522 * g + 0.072175 * b) / 255;
|
return (0.2126729 * r + 0.7151522 * g + 0.072175 * b) / 255;
|
||||||
}
|
}
|
||||||
|
|
||||||
function useLightText(backgroundColor: ColorInput): boolean {
|
function useLightText(backgroundColor: AnyColor): boolean {
|
||||||
return getRelativeLuminance(backgroundColor) < 0.453;
|
return getRelativeLuminance(backgroundColor) < 0.453;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Given a background color, returns a black or white foreground color that the highest
|
/** Given a background color, returns a black or white foreground color with the highest contrast ratio. */
|
||||||
* contrast ratio. */
|
|
||||||
// In the future, the APCA contrast function, or CSS `contrast-color` will be better.
|
// In the future, the APCA contrast function, or CSS `contrast-color` will be better.
|
||||||
// https://github.com/color-js/color.js/blob/eb7b53f7a13bb716ec8b28c7a56f052cd599acd9/src/contrast/APCA.js#L42
|
// https://github.com/color-js/color.js/blob/eb7b53f7a13bb716ec8b28c7a56f052cd599acd9/src/contrast/APCA.js#L42
|
||||||
export function contrastColor(backgroundColor: ColorInput): string {
|
export function contrastColor(backgroundColor: AnyColor): string {
|
||||||
return useLightText(backgroundColor) ? '#fff' : '#000';
|
return useLightText(backgroundColor) ? '#fff' : '#000';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user