mirror of
https://github.com/go-gitea/gitea.git
synced 2026-08-23 07:40:58 +00:00
Merge branch 'main' into update_flake_lock_action
This commit is contained in:
4
.github/workflows/pull-db-tests.yml
vendored
4
.github/workflows/pull-db-tests.yml
vendored
@@ -63,7 +63,6 @@ jobs:
|
||||
RACE_ENABLED: true
|
||||
TEST_TAGS: gogit
|
||||
TEST_LDAP: 1
|
||||
USE_REPO_TEST_DIR: 1
|
||||
|
||||
test-sqlite:
|
||||
if: needs.files-changed.outputs.backend == 'true' || needs.files-changed.outputs.actions == 'true'
|
||||
@@ -90,7 +89,6 @@ jobs:
|
||||
TAGS: bindata gogit sqlite sqlite_unlock_notify
|
||||
RACE_ENABLED: true
|
||||
TEST_TAGS: gogit sqlite sqlite_unlock_notify
|
||||
USE_REPO_TEST_DIR: 1
|
||||
|
||||
test-unit:
|
||||
if: needs.files-changed.outputs.backend == 'true' || needs.files-changed.outputs.actions == 'true'
|
||||
@@ -206,7 +204,6 @@ jobs:
|
||||
env:
|
||||
TAGS: bindata
|
||||
RACE_ENABLED: true
|
||||
USE_REPO_TEST_DIR: 1
|
||||
TEST_INDEXER_CODE_ES_URL: "http://elastic:changeme@elasticsearch:9200"
|
||||
|
||||
test-mssql:
|
||||
@@ -246,4 +243,3 @@ jobs:
|
||||
timeout-minutes: 50
|
||||
env:
|
||||
TAGS: bindata
|
||||
USE_REPO_TEST_DIR: 1
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -89,7 +89,6 @@ cpu.out
|
||||
/vendor
|
||||
/VERSION
|
||||
/.air
|
||||
/.go-licenses
|
||||
|
||||
# Files and folders that were previously generated
|
||||
/public/assets/img/webpack
|
||||
|
||||
@@ -18,6 +18,7 @@ linters:
|
||||
- mirror
|
||||
- modernize
|
||||
- nakedret
|
||||
- nilnil
|
||||
- nolintlint
|
||||
- perfsprint
|
||||
- revive
|
||||
@@ -66,35 +67,24 @@ linters:
|
||||
revive:
|
||||
severity: error
|
||||
rules:
|
||||
- name: atomic
|
||||
- name: bare-return
|
||||
- name: blank-imports
|
||||
- name: constant-logical-expr
|
||||
- name: context-as-argument
|
||||
- name: context-keys-type
|
||||
- name: dot-imports
|
||||
- name: duplicated-imports
|
||||
- name: empty-lines
|
||||
- name: error-naming
|
||||
- name: error-return
|
||||
- name: error-strings
|
||||
- name: errorf
|
||||
- name: exported
|
||||
- name: identical-branches
|
||||
- name: if-return
|
||||
- name: increment-decrement
|
||||
- name: indent-error-flow
|
||||
- name: modifies-value-receiver
|
||||
- name: package-comments
|
||||
- name: range
|
||||
- name: receiver-naming
|
||||
- name: redefines-builtin-id
|
||||
- name: string-of-int
|
||||
- name: superfluous-else
|
||||
- name: time-naming
|
||||
- name: unconditional-recursion
|
||||
- name: unexported-return
|
||||
- name: unreachable-code
|
||||
- name: var-declaration
|
||||
- name: var-naming
|
||||
arguments:
|
||||
@@ -132,16 +122,12 @@ linters:
|
||||
- linters:
|
||||
- dupl
|
||||
- errcheck
|
||||
- gocyclo
|
||||
- gosec
|
||||
- staticcheck
|
||||
- unparam
|
||||
path: _test\.go
|
||||
- linters:
|
||||
- dupl
|
||||
- errcheck
|
||||
- gocyclo
|
||||
- gosec
|
||||
path: models/migrations/v
|
||||
- linters:
|
||||
- forbidigo
|
||||
@@ -153,7 +139,6 @@ linters:
|
||||
- gocritic
|
||||
text: (?i)`ID' should not be capitalized
|
||||
- linters:
|
||||
- deadcode
|
||||
- unused
|
||||
text: (?i)swagger
|
||||
- linters:
|
||||
|
||||
@@ -183,7 +183,7 @@ Here's how to run the test suite:
|
||||
## Translation
|
||||
|
||||
All translation work happens on [Crowdin](https://translate.gitea.com).
|
||||
The only translation that is maintained in this repository is [the English translation](https://github.com/go-gitea/gitea/blob/main/options/locale/locale_en-US.ini).
|
||||
The only translation that is maintained in this repository is [the English translation](https://github.com/go-gitea/gitea/blob/main/options/locale/locale_en-US.json).
|
||||
It is synced regularly with Crowdin. \
|
||||
Other locales on main branch **should not** be updated manually as they will be overwritten with each sync. \
|
||||
Once a language has reached a **satisfactory percentage** of translated keys (~25%), it will be synced back into this repo and included in the next released version.
|
||||
|
||||
22
Dockerfile
22
Dockerfile
@@ -1,5 +1,12 @@
|
||||
# syntax=docker/dockerfile:1
|
||||
# Build stage
|
||||
# Build frontend on the native platform to avoid QEMU-related issues with esbuild/webpack
|
||||
FROM --platform=$BUILDPLATFORM docker.io/library/golang:1.26-alpine3.23 AS frontend-build
|
||||
RUN apk --no-cache add build-base git nodejs pnpm
|
||||
WORKDIR /src
|
||||
COPY --exclude=.git/ . .
|
||||
RUN --mount=type=cache,target=/root/.local/share/pnpm/store make frontend
|
||||
|
||||
# Build backend for each target platform
|
||||
FROM docker.io/library/golang:1.26-alpine3.23 AS build-env
|
||||
|
||||
ARG GOPROXY=direct
|
||||
@@ -12,22 +19,19 @@ ARG CGO_EXTRA_CFLAGS
|
||||
# Build deps
|
||||
RUN apk --no-cache add \
|
||||
build-base \
|
||||
git \
|
||||
nodejs \
|
||||
pnpm
|
||||
git
|
||||
|
||||
WORKDIR ${GOPATH}/src/code.gitea.io/gitea
|
||||
# Use COPY but not "mount" because some directories like "node_modules" contain platform-depended contents and these directories need to be ignored.
|
||||
# ".git" directory will be mounted later separately for getting version data.
|
||||
# TODO: in the future, maybe we can pre-build the frontend assets on one platform and share them for different platforms, the benefit is that it won't be affected by webpack plugin compatibility problems, then the working directory can be fully mounted and the COPY is not needed.
|
||||
# Use COPY instead of bind mount as read-only one breaks makefile state tracking and read-write one needs binary to be moved as it's discarded.
|
||||
# ".git" directory is mounted separately later only for version data extraction.
|
||||
COPY --exclude=.git/ . .
|
||||
COPY --from=frontend-build /src/public/assets public/assets
|
||||
|
||||
# Build gitea, .git mount is required for version data
|
||||
RUN --mount=type=cache,target=/go/pkg/mod \
|
||||
--mount=type=cache,target="/root/.cache/go-build" \
|
||||
--mount=type=cache,target=/root/.local/share/pnpm/store \
|
||||
--mount=type=bind,source=".git/",target=".git/" \
|
||||
make
|
||||
make backend
|
||||
|
||||
COPY docker/root /tmp/local
|
||||
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
# syntax=docker/dockerfile:1
|
||||
# Build stage
|
||||
# Build frontend on the native platform to avoid QEMU-related issues with esbuild/webpack
|
||||
FROM --platform=$BUILDPLATFORM docker.io/library/golang:1.26-alpine3.23 AS frontend-build
|
||||
RUN apk --no-cache add build-base git nodejs pnpm
|
||||
WORKDIR /src
|
||||
COPY --exclude=.git/ . .
|
||||
RUN --mount=type=cache,target=/root/.local/share/pnpm/store make frontend
|
||||
|
||||
# Build backend for each target platform
|
||||
FROM docker.io/library/golang:1.26-alpine3.23 AS build-env
|
||||
|
||||
ARG GOPROXY=direct
|
||||
@@ -12,20 +19,18 @@ ARG CGO_EXTRA_CFLAGS
|
||||
# Build deps
|
||||
RUN apk --no-cache add \
|
||||
build-base \
|
||||
git \
|
||||
nodejs \
|
||||
pnpm
|
||||
git
|
||||
|
||||
WORKDIR ${GOPATH}/src/code.gitea.io/gitea
|
||||
# See the comments in Dockerfile
|
||||
COPY --exclude=.git/ . .
|
||||
COPY --from=frontend-build /src/public/assets public/assets
|
||||
|
||||
# Build gitea, .git mount is required for version data
|
||||
RUN --mount=type=cache,target=/go/pkg/mod \
|
||||
--mount=type=cache,target="/root/.cache/go-build" \
|
||||
--mount=type=cache,target=/root/.local/share/pnpm/store \
|
||||
--mount=type=bind,source=".git/",target=".git/" \
|
||||
make
|
||||
make backend
|
||||
|
||||
COPY docker/rootless /tmp/local
|
||||
|
||||
|
||||
52
Makefile
52
Makefile
@@ -1,22 +1,5 @@
|
||||
ifeq ($(USE_REPO_TEST_DIR),1)
|
||||
|
||||
# This rule replaces the whole Makefile when we're trying to use /tmp repository temporary files
|
||||
location = $(CURDIR)/$(word $(words $(MAKEFILE_LIST)),$(MAKEFILE_LIST))
|
||||
self := $(location)
|
||||
|
||||
%:
|
||||
@tmpdir=`mktemp --tmpdir -d` ; \
|
||||
echo Using temporary directory $$tmpdir for test repositories ; \
|
||||
USE_REPO_TEST_DIR= $(MAKE) -f $(self) --no-print-directory REPO_TEST_DIR=$$tmpdir/ $@ ; \
|
||||
STATUS=$$? ; rm -r "$$tmpdir" ; exit $$STATUS
|
||||
|
||||
else
|
||||
|
||||
# This is the "normal" part of the Makefile
|
||||
|
||||
DIST := dist
|
||||
DIST_DIRS := $(DIST)/binaries $(DIST)/release
|
||||
IMPORT := code.gitea.io/gitea
|
||||
|
||||
# By default use go's 1.25 experimental json v2 library when building
|
||||
# TODO: remove when no longer experimental
|
||||
@@ -37,7 +20,6 @@ GXZ_PACKAGE ?= github.com/ulikunitz/xz/cmd/gxz@v0.5.15
|
||||
MISSPELL_PACKAGE ?= github.com/golangci/misspell/cmd/misspell@v0.7.0
|
||||
SWAGGER_PACKAGE ?= github.com/go-swagger/go-swagger/cmd/swagger@v0.33.1
|
||||
XGO_PACKAGE ?= src.techknowlogick.com/xgo@latest
|
||||
GO_LICENSES_PACKAGE ?= github.com/google/go-licenses@v1
|
||||
GOVULNCHECK_PACKAGE ?= golang.org/x/vuln/cmd/govulncheck@v1
|
||||
ACTIONLINT_PACKAGE ?= github.com/rhysd/actionlint/cmd/actionlint@v1.7.10
|
||||
|
||||
@@ -84,7 +66,6 @@ endif
|
||||
|
||||
EXTRA_GOFLAGS ?=
|
||||
|
||||
MAKE_VERSION := $(shell "$(MAKE)" -v | cat | head -n 1)
|
||||
MAKE_EVIDENCE_DIR := .make_evidence
|
||||
|
||||
GOTESTFLAGS ?=
|
||||
@@ -130,7 +111,7 @@ ifeq ($(VERSION),main)
|
||||
VERSION := main-nightly
|
||||
endif
|
||||
|
||||
LDFLAGS := $(LDFLAGS) -X "main.MakeVersion=$(MAKE_VERSION)" -X "main.Version=$(GITEA_VERSION)" -X "main.Tags=$(TAGS)"
|
||||
LDFLAGS := $(LDFLAGS) -X "main.Version=$(GITEA_VERSION)" -X "main.Tags=$(TAGS)"
|
||||
|
||||
LINUX_ARCHS ?= linux/amd64,linux/386,linux/arm-5,linux/arm-6,linux/arm64,linux/riscv64
|
||||
|
||||
@@ -150,7 +131,6 @@ SVG_DEST_DIR := public/assets/img/svg
|
||||
|
||||
AIR_TMP_DIR := .air
|
||||
|
||||
GO_LICENSE_TMP_DIR := .go-licenses
|
||||
GO_LICENSE_FILE := assets/go-licenses.json
|
||||
|
||||
TAGS ?=
|
||||
@@ -159,7 +139,7 @@ TAGS_EVIDENCE := $(MAKE_EVIDENCE_DIR)/tags
|
||||
|
||||
TEST_TAGS ?= $(TAGS_SPLIT) sqlite sqlite_unlock_notify
|
||||
|
||||
TAR_EXCLUDES := .git data indexers queues log node_modules $(EXECUTABLE) $(DIST) $(MAKE_EVIDENCE_DIR) $(AIR_TMP_DIR) $(GO_LICENSE_TMP_DIR)
|
||||
TAR_EXCLUDES := .git data indexers queues log node_modules $(EXECUTABLE) $(DIST) $(MAKE_EVIDENCE_DIR) $(AIR_TMP_DIR)
|
||||
|
||||
GO_DIRS := build cmd models modules routers services tests tools
|
||||
WEB_DIRS := web_src/js web_src/css
|
||||
@@ -229,7 +209,7 @@ clean: ## delete backend and integration files
|
||||
e2e*.test \
|
||||
tests/integration/gitea-integration-* \
|
||||
tests/integration/indexers-* \
|
||||
tests/mysql.ini tests/pgsql.ini tests/mssql.ini man/ \
|
||||
tests/sqlite.ini tests/mysql.ini tests/pgsql.ini tests/mssql.ini man/ \
|
||||
tests/e2e/gitea-e2e-*/ \
|
||||
tests/e2e/indexers-*/ \
|
||||
tests/e2e/reports/ tests/e2e/test-artifacts/ tests/e2e/test-snapshots/
|
||||
@@ -473,16 +453,11 @@ tidy-check: tidy
|
||||
go-licenses: $(GO_LICENSE_FILE) ## regenerate go licenses
|
||||
|
||||
$(GO_LICENSE_FILE): go.mod go.sum
|
||||
@rm -rf $(GO_LICENSE_FILE)
|
||||
$(GO) install $(GO_LICENSES_PACKAGE)
|
||||
-GOOS=linux CGO_ENABLED=1 go-licenses save . --force --save_path=$(GO_LICENSE_TMP_DIR) 2>/dev/null
|
||||
$(GO) run build/generate-go-licenses.go $(GO_LICENSE_TMP_DIR) $(GO_LICENSE_FILE)
|
||||
@rm -rf $(GO_LICENSE_TMP_DIR)
|
||||
GO=$(GO) $(GO) run build/generate-go-licenses.go $(GO_LICENSE_FILE)
|
||||
|
||||
generate-ini-sqlite:
|
||||
sed -e 's|{{REPO_TEST_DIR}}|${REPO_TEST_DIR}|g' \
|
||||
sed -e 's|{{WORK_PATH}}|$(CURDIR)/tests/$(or $(TEST_TYPE),integration)/gitea-$(or $(TEST_TYPE),integration)-sqlite|g' \
|
||||
-e 's|{{TEST_LOGGER}}|$(or $(TEST_LOGGER),test$(COMMA)file)|g' \
|
||||
-e 's|{{TEST_TYPE}}|$(or $(TEST_TYPE),integration)|g' \
|
||||
tests/sqlite.ini.tmpl > tests/sqlite.ini
|
||||
|
||||
.PHONY: test-sqlite
|
||||
@@ -501,9 +476,8 @@ generate-ini-mysql:
|
||||
-e 's|{{TEST_MYSQL_DBNAME}}|${TEST_MYSQL_DBNAME}|g' \
|
||||
-e 's|{{TEST_MYSQL_USERNAME}}|${TEST_MYSQL_USERNAME}|g' \
|
||||
-e 's|{{TEST_MYSQL_PASSWORD}}|${TEST_MYSQL_PASSWORD}|g' \
|
||||
-e 's|{{REPO_TEST_DIR}}|${REPO_TEST_DIR}|g' \
|
||||
-e 's|{{WORK_PATH}}|$(CURDIR)/tests/$(or $(TEST_TYPE),integration)/gitea-$(or $(TEST_TYPE),integration)-mysql|g' \
|
||||
-e 's|{{TEST_LOGGER}}|$(or $(TEST_LOGGER),test$(COMMA)file)|g' \
|
||||
-e 's|{{TEST_TYPE}}|$(or $(TEST_TYPE),integration)|g' \
|
||||
tests/mysql.ini.tmpl > tests/mysql.ini
|
||||
|
||||
.PHONY: test-mysql
|
||||
@@ -524,9 +498,8 @@ generate-ini-pgsql:
|
||||
-e 's|{{TEST_PGSQL_PASSWORD}}|${TEST_PGSQL_PASSWORD}|g' \
|
||||
-e 's|{{TEST_PGSQL_SCHEMA}}|${TEST_PGSQL_SCHEMA}|g' \
|
||||
-e 's|{{TEST_MINIO_ENDPOINT}}|${TEST_MINIO_ENDPOINT}|g' \
|
||||
-e 's|{{REPO_TEST_DIR}}|${REPO_TEST_DIR}|g' \
|
||||
-e 's|{{WORK_PATH}}|$(CURDIR)/tests/$(or $(TEST_TYPE),integration)/gitea-$(or $(TEST_TYPE),integration)-pgsql|g' \
|
||||
-e 's|{{TEST_LOGGER}}|$(or $(TEST_LOGGER),test$(COMMA)file)|g' \
|
||||
-e 's|{{TEST_TYPE}}|$(or $(TEST_TYPE),integration)|g' \
|
||||
tests/pgsql.ini.tmpl > tests/pgsql.ini
|
||||
|
||||
.PHONY: test-pgsql
|
||||
@@ -545,9 +518,8 @@ generate-ini-mssql:
|
||||
-e 's|{{TEST_MSSQL_DBNAME}}|${TEST_MSSQL_DBNAME}|g' \
|
||||
-e 's|{{TEST_MSSQL_USERNAME}}|${TEST_MSSQL_USERNAME}|g' \
|
||||
-e 's|{{TEST_MSSQL_PASSWORD}}|${TEST_MSSQL_PASSWORD}|g' \
|
||||
-e 's|{{REPO_TEST_DIR}}|${REPO_TEST_DIR}|g' \
|
||||
-e 's|{{WORK_PATH}}|$(CURDIR)/tests/$(or $(TEST_TYPE),integration)/gitea-$(or $(TEST_TYPE),integration)-mssql|g' \
|
||||
-e 's|{{TEST_LOGGER}}|$(or $(TEST_LOGGER),test$(COMMA)file)|g' \
|
||||
-e 's|{{TEST_TYPE}}|$(or $(TEST_TYPE),integration)|g' \
|
||||
tests/mssql.ini.tmpl > tests/mssql.ini
|
||||
|
||||
.PHONY: test-mssql
|
||||
@@ -668,7 +640,7 @@ migrations.sqlite.test: $(GO_SOURCES) generate-ini-sqlite
|
||||
GITEA_TEST_CONF=tests/sqlite.ini ./migrations.sqlite.test
|
||||
|
||||
.PHONY: migrations.individual.mysql.test
|
||||
migrations.individual.mysql.test: $(GO_SOURCES)
|
||||
migrations.individual.mysql.test: $(GO_SOURCES) generate-ini-mysql
|
||||
GITEA_TEST_CONF=tests/mysql.ini $(GO) test $(GOTESTFLAGS) -tags='$(TEST_TAGS)' -p 1 $(MIGRATE_TEST_PACKAGES)
|
||||
|
||||
.PHONY: migrations.individual.sqlite.test\#%
|
||||
@@ -676,7 +648,7 @@ migrations.individual.sqlite.test\#%: $(GO_SOURCES) generate-ini-sqlite
|
||||
GITEA_TEST_CONF=tests/sqlite.ini $(GO) test $(GOTESTFLAGS) -tags '$(TEST_TAGS)' code.gitea.io/gitea/models/migrations/$*
|
||||
|
||||
.PHONY: migrations.individual.pgsql.test
|
||||
migrations.individual.pgsql.test: $(GO_SOURCES)
|
||||
migrations.individual.pgsql.test: $(GO_SOURCES) generate-ini-pgsql
|
||||
GITEA_TEST_CONF=tests/pgsql.ini $(GO) test $(GOTESTFLAGS) -tags='$(TEST_TAGS)' -p 1 $(MIGRATE_TEST_PACKAGES)
|
||||
|
||||
.PHONY: migrations.individual.pgsql.test\#%
|
||||
@@ -819,7 +791,6 @@ deps-tools: ## install tool dependencies
|
||||
$(GO) install $(MISSPELL_PACKAGE) & \
|
||||
$(GO) install $(SWAGGER_PACKAGE) & \
|
||||
$(GO) install $(XGO_PACKAGE) & \
|
||||
$(GO) install $(GO_LICENSES_PACKAGE) & \
|
||||
$(GO) install $(GOVULNCHECK_PACKAGE) & \
|
||||
$(GO) install $(ACTIONLINT_PACKAGE) & \
|
||||
wait
|
||||
@@ -908,9 +879,6 @@ docker:
|
||||
docker build --disable-content-trust=false -t $(DOCKER_REF) .
|
||||
# support also build args docker build --build-arg GITEA_VERSION=v1.2.3 --build-arg TAGS="bindata sqlite sqlite_unlock_notify" .
|
||||
|
||||
# This endif closes the if at the top of the file
|
||||
endif
|
||||
|
||||
# Disable parallel execution because it would break some targets that don't
|
||||
# specify exact dependencies like 'backend' which does currently not depend
|
||||
# on 'frontend' to enable Node.js-less builds from source tarballs.
|
||||
|
||||
142
assets/go-licenses.json
generated
142
assets/go-licenses.json
generated
File diff suppressed because one or more lines are too long
@@ -1,115 +0,0 @@
|
||||
// Copyright 2023 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
//go:build ignore
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/modules/container"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
)
|
||||
|
||||
func main() {
|
||||
if len(os.Args) != 2 {
|
||||
println("usage: backport-locales <to-ref>")
|
||||
println("eg: backport-locales release/v1.19")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
mustNoErr := func(err error) {
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
collectInis := func(ref string) map[string]setting.ConfigProvider {
|
||||
inis := map[string]setting.ConfigProvider{}
|
||||
err := filepath.WalkDir("options/locale", func(path string, d os.DirEntry, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if d.IsDir() || !strings.HasSuffix(d.Name(), ".ini") {
|
||||
return nil
|
||||
}
|
||||
cfg, err := setting.NewConfigProviderForLocale(path)
|
||||
mustNoErr(err)
|
||||
inis[path] = cfg
|
||||
fmt.Printf("collecting: %s @ %s\n", path, ref)
|
||||
return nil
|
||||
})
|
||||
mustNoErr(err)
|
||||
return inis
|
||||
}
|
||||
|
||||
// collect new locales from current working directory
|
||||
inisNew := collectInis("HEAD")
|
||||
|
||||
// switch to the target ref, and collect the old locales
|
||||
cmd := exec.Command("git", "checkout", os.Args[1])
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
mustNoErr(cmd.Run())
|
||||
inisOld := collectInis(os.Args[1])
|
||||
|
||||
// use old en-US as the base, and copy the new translations to the old locales
|
||||
enUsOld := inisOld["options/locale/locale_en-US.ini"]
|
||||
brokenWarned := make(container.Set[string])
|
||||
for path, iniOld := range inisOld {
|
||||
if iniOld == enUsOld {
|
||||
continue
|
||||
}
|
||||
iniNew := inisNew[path]
|
||||
if iniNew == nil {
|
||||
continue
|
||||
}
|
||||
for _, secEnUS := range enUsOld.Sections() {
|
||||
secOld := iniOld.Section(secEnUS.Name())
|
||||
secNew := iniNew.Section(secEnUS.Name())
|
||||
for _, keyEnUs := range secEnUS.Keys() {
|
||||
if secNew.HasKey(keyEnUs.Name()) {
|
||||
oldStr := secOld.Key(keyEnUs.Name()).String()
|
||||
newStr := secNew.Key(keyEnUs.Name()).String()
|
||||
broken := oldStr != "" && strings.Count(oldStr, "%") != strings.Count(newStr, "%")
|
||||
broken = broken || strings.Contains(oldStr, "\n") || strings.Contains(oldStr, "\n")
|
||||
if broken {
|
||||
brokenWarned.Add(secOld.Name() + "." + keyEnUs.Name())
|
||||
fmt.Println("----")
|
||||
fmt.Printf("WARNING: skip broken locale: %s , [%s] %s\n", path, secEnUS.Name(), keyEnUs.Name())
|
||||
fmt.Printf("\told: %s\n", strings.ReplaceAll(oldStr, "\n", "\\n"))
|
||||
fmt.Printf("\tnew: %s\n", strings.ReplaceAll(newStr, "\n", "\\n"))
|
||||
continue
|
||||
}
|
||||
secOld.Key(keyEnUs.Name()).SetValue(newStr)
|
||||
}
|
||||
}
|
||||
}
|
||||
mustNoErr(iniOld.SaveTo(path))
|
||||
}
|
||||
|
||||
fmt.Println("========")
|
||||
|
||||
for path, iniNew := range inisNew {
|
||||
for _, sec := range iniNew.Sections() {
|
||||
for _, key := range sec.Keys() {
|
||||
str := sec.Key(key.Name()).String()
|
||||
broken := strings.Contains(str, "\n")
|
||||
broken = broken || strings.HasPrefix(str, "`") != strings.HasSuffix(str, "`")
|
||||
broken = broken || strings.HasPrefix(str, "\"`")
|
||||
broken = broken || strings.HasPrefix(str, "`\"")
|
||||
broken = broken || strings.Count(str, `"`)%2 == 1
|
||||
broken = broken || strings.Count(str, "`")%2 == 1
|
||||
if broken && !brokenWarned.Contains(sec.Name()+"."+key.Name()) {
|
||||
fmt.Printf("WARNING: found broken locale: %s , [%s] %s\n", path, sec.Name(), key.Name())
|
||||
fmt.Printf("\tstr: %s\n", strings.ReplaceAll(str, "\n", "\\n"))
|
||||
fmt.Println("----")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,99 +8,219 @@ package main
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"slices"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/modules/container"
|
||||
)
|
||||
|
||||
// regexp is based on go-license, excluding README and NOTICE
|
||||
// https://github.com/google/go-licenses/blob/master/licenses/find.go
|
||||
var licenseRe = regexp.MustCompile(`^(?i)((UN)?LICEN(S|C)E|COPYING).*$`)
|
||||
|
||||
// primaryLicenseRe matches exact primary license filenames without suffixes.
|
||||
// When a directory has both primary and variant files (e.g. LICENSE and
|
||||
// LICENSE.docs), only the primary files are kept.
|
||||
var primaryLicenseRe = regexp.MustCompile(`^(?i)(LICEN[SC]E|COPYING)$`)
|
||||
|
||||
// ignoredNames are LicenseEntry.Name values to exclude from the output.
|
||||
var ignoredNames = map[string]bool{
|
||||
"code.gitea.io/gitea": true,
|
||||
"code.gitea.io/gitea/options/license": true,
|
||||
}
|
||||
|
||||
var excludedExt = map[string]bool{
|
||||
".gitignore": true,
|
||||
".go": true,
|
||||
".mod": true,
|
||||
".sum": true,
|
||||
".toml": true,
|
||||
".yaml": true,
|
||||
".yml": true,
|
||||
}
|
||||
|
||||
type ModuleInfo struct {
|
||||
Path string
|
||||
Dir string
|
||||
PkgDirs []string // directories of packages imported from this module
|
||||
}
|
||||
|
||||
type LicenseEntry struct {
|
||||
Name string `json:"name"`
|
||||
Path string `json:"path"`
|
||||
LicenseText string `json:"licenseText"`
|
||||
}
|
||||
|
||||
func main() {
|
||||
if len(os.Args) != 3 {
|
||||
fmt.Println("usage: go run generate-go-licenses.go <base-dir> <out-json-file>")
|
||||
// getModules returns all dependency modules with their local directory paths
|
||||
// and the package directories used from each module.
|
||||
func getModules(goCmd string) []ModuleInfo {
|
||||
cmd := exec.Command(goCmd, "list", "-deps", "-f",
|
||||
"{{if .Module}}{{.Module.Path}}\t{{.Module.Dir}}\t{{.Dir}}{{end}}", "./...")
|
||||
cmd.Stderr = os.Stderr
|
||||
// Use GOOS=linux with CGO to ensure we capture all platform-specific
|
||||
// dependencies, matching the CI environment.
|
||||
cmd.Env = append(os.Environ(), "GOOS=linux", "GOARCH=amd64", "CGO_ENABLED=1")
|
||||
output, err := cmd.Output()
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "failed to run 'go list -deps': %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
base, out := os.Args[1], os.Args[2]
|
||||
|
||||
// Add ext for excluded files because license_test.go will be included for some reason.
|
||||
// And there are more files that should be excluded, check with:
|
||||
//
|
||||
// go run github.com/google/go-licenses@v1.6.0 save . --force --save_path=.go-licenses 2>/dev/null
|
||||
// find .go-licenses -type f | while read FILE; do echo "${$(basename $FILE)##*.}"; done | sort -u
|
||||
// AUTHORS
|
||||
// COPYING
|
||||
// LICENSE
|
||||
// Makefile
|
||||
// NOTICE
|
||||
// gitignore
|
||||
// go
|
||||
// md
|
||||
// mod
|
||||
// sum
|
||||
// toml
|
||||
// txt
|
||||
// yml
|
||||
//
|
||||
// It could be removed once we have a better regex.
|
||||
excludedExt := container.SetOf(".gitignore", ".go", ".mod", ".sum", ".toml", ".yml")
|
||||
|
||||
var paths []string
|
||||
err := filepath.WalkDir(base, func(path string, entry fs.DirEntry, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
var modules []ModuleInfo
|
||||
seen := make(map[string]int) // module path -> index in modules
|
||||
for _, line := range strings.Split(string(output), "\n") {
|
||||
line = strings.TrimSpace(line)
|
||||
if line == "" {
|
||||
continue
|
||||
}
|
||||
if entry.IsDir() || !licenseRe.MatchString(entry.Name()) || excludedExt.Contains(filepath.Ext(entry.Name())) {
|
||||
return nil
|
||||
parts := strings.Split(line, "\t")
|
||||
if len(parts) != 3 {
|
||||
continue
|
||||
}
|
||||
paths = append(paths, path)
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
modPath, modDir, pkgDir := parts[0], parts[1], parts[2]
|
||||
if idx, ok := seen[modPath]; ok {
|
||||
modules[idx].PkgDirs = append(modules[idx].PkgDirs, pkgDir)
|
||||
} else {
|
||||
seen[modPath] = len(modules)
|
||||
modules = append(modules, ModuleInfo{
|
||||
Path: modPath,
|
||||
Dir: modDir,
|
||||
PkgDirs: []string{pkgDir},
|
||||
})
|
||||
}
|
||||
}
|
||||
return modules
|
||||
}
|
||||
|
||||
// findLicenseFiles scans a module's root directory and its used package
|
||||
// directories for license files. It also walks up from each package directory
|
||||
// to the module root, scanning intermediate directories. Subdirectory licenses
|
||||
// are only included if their text differs from the root license(s).
|
||||
func findLicenseFiles(mod ModuleInfo) []LicenseEntry {
|
||||
var entries []LicenseEntry
|
||||
seenTexts := make(map[string]bool)
|
||||
|
||||
// First, collect root-level license files.
|
||||
entries = append(entries, scanDirForLicenses(mod.Dir, mod.Path, "")...)
|
||||
for _, e := range entries {
|
||||
seenTexts[e.LicenseText] = true
|
||||
}
|
||||
|
||||
sort.Strings(paths)
|
||||
// Then check each package directory and all intermediate parent directories
|
||||
// up to the module root for license files with unique text.
|
||||
seenDirs := map[string]bool{mod.Dir: true}
|
||||
for _, pkgDir := range mod.PkgDirs {
|
||||
for dir := pkgDir; dir != mod.Dir && strings.HasPrefix(dir, mod.Dir); dir = filepath.Dir(dir) {
|
||||
if seenDirs[dir] {
|
||||
continue
|
||||
}
|
||||
seenDirs[dir] = true
|
||||
for _, e := range scanDirForLicenses(dir, mod.Path, mod.Dir) {
|
||||
if !seenTexts[e.LicenseText] {
|
||||
seenTexts[e.LicenseText] = true
|
||||
entries = append(entries, e)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return entries
|
||||
}
|
||||
|
||||
// scanDirForLicenses reads a single directory for license files and returns entries.
|
||||
// If moduleRoot is non-empty, paths are made relative to it.
|
||||
func scanDirForLicenses(dir, modulePath, moduleRoot string) []LicenseEntry {
|
||||
dirEntries, err := os.ReadDir(dir)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
var entries []LicenseEntry
|
||||
for _, filePath := range paths {
|
||||
licenseText, err := os.ReadFile(filePath)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
for _, entry := range dirEntries {
|
||||
if entry.IsDir() {
|
||||
continue
|
||||
}
|
||||
|
||||
pkgPath := filepath.ToSlash(filePath)
|
||||
pkgPath = strings.TrimPrefix(pkgPath, base+"/")
|
||||
pkgName := path.Dir(pkgPath)
|
||||
|
||||
// There might be a bug somewhere in go-licenses that sometimes interprets the
|
||||
// root package as "." and sometimes as "code.gitea.io/gitea". Workaround by
|
||||
// removing both of them for the sake of stable output.
|
||||
if pkgName == "." || pkgName == "code.gitea.io/gitea" {
|
||||
name := entry.Name()
|
||||
if !licenseRe.MatchString(name) {
|
||||
continue
|
||||
}
|
||||
if excludedExt[strings.ToLower(filepath.Ext(name))] {
|
||||
continue
|
||||
}
|
||||
|
||||
content, err := os.ReadFile(filepath.Join(dir, name))
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
entryName := modulePath
|
||||
entryPath := modulePath + "/" + name
|
||||
if moduleRoot != "" {
|
||||
rel, _ := filepath.Rel(moduleRoot, dir)
|
||||
if rel != "." {
|
||||
relSlash := filepath.ToSlash(rel)
|
||||
entryName = modulePath + "/" + relSlash
|
||||
entryPath = modulePath + "/" + relSlash + "/" + name
|
||||
}
|
||||
}
|
||||
|
||||
entries = append(entries, LicenseEntry{
|
||||
Name: pkgName,
|
||||
Path: pkgPath,
|
||||
LicenseText: string(licenseText),
|
||||
Name: entryName,
|
||||
Path: entryPath,
|
||||
LicenseText: string(content),
|
||||
})
|
||||
}
|
||||
|
||||
// When multiple license files exist, prefer primary files (e.g. LICENSE)
|
||||
// over variants with suffixes (e.g. LICENSE.docs, LICENSE-2.0.txt).
|
||||
// If no primary file exists, keep only the first variant.
|
||||
if len(entries) > 1 {
|
||||
var primary []LicenseEntry
|
||||
for _, e := range entries {
|
||||
fileName := e.Path[strings.LastIndex(e.Path, "/")+1:]
|
||||
if primaryLicenseRe.MatchString(fileName) {
|
||||
primary = append(primary, e)
|
||||
}
|
||||
}
|
||||
if len(primary) > 0 {
|
||||
return primary
|
||||
}
|
||||
return entries[:1]
|
||||
}
|
||||
|
||||
return entries
|
||||
}
|
||||
|
||||
func main() {
|
||||
if len(os.Args) != 2 {
|
||||
fmt.Println("usage: go run generate-go-licenses.go <out-json-file>")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
out := os.Args[1]
|
||||
|
||||
goCmd := "go"
|
||||
if env := os.Getenv("GO"); env != "" {
|
||||
goCmd = env
|
||||
}
|
||||
|
||||
modules := getModules(goCmd)
|
||||
|
||||
var entries []LicenseEntry
|
||||
for _, mod := range modules {
|
||||
entries = append(entries, findLicenseFiles(mod)...)
|
||||
}
|
||||
|
||||
entries = slices.DeleteFunc(entries, func(e LicenseEntry) bool {
|
||||
return ignoredNames[e.Name]
|
||||
})
|
||||
|
||||
sort.Slice(entries, func(i, j int) bool {
|
||||
return entries[i].Path < entries[j].Path
|
||||
})
|
||||
|
||||
jsonBytes, err := json.MarshalIndent(entries, "", " ")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
|
||||
@@ -163,8 +163,6 @@ func serveInstall(cmd *cli.Command) error {
|
||||
}
|
||||
|
||||
func serveInstalled(c *cli.Command) error {
|
||||
setting.InitCfgProvider(setting.CustomConf)
|
||||
setting.LoadCommonSettings()
|
||||
setting.MustInstalled()
|
||||
|
||||
showWebStartupMessage("Prepare to run web server")
|
||||
|
||||
@@ -2858,6 +2858,9 @@ LEVEL = Info
|
||||
;ABANDONED_JOB_TIMEOUT = 24h
|
||||
;; Strings committers can place inside a commit message or PR title to skip executing the corresponding actions workflow
|
||||
;SKIP_WORKFLOW_STRINGS = [skip ci],[ci skip],[no ci],[skip actions],[actions skip]
|
||||
;; Comma-separated list of workflow directories, the first one to exist
|
||||
;; in a repo is used to find Actions workflow files
|
||||
;WORKFLOW_DIRS = .gitea/workflows,.github/workflows
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
@@ -211,7 +211,7 @@ export default defineConfig([
|
||||
'@typescript-eslint/no-non-null-asserted-nullish-coalescing': [0],
|
||||
'@typescript-eslint/no-non-null-asserted-optional-chain': [2],
|
||||
'@typescript-eslint/no-non-null-assertion': [0],
|
||||
'@typescript-eslint/no-redeclare': [0],
|
||||
'@typescript-eslint/no-redeclare': [2],
|
||||
'@typescript-eslint/no-redundant-type-constituents': [2],
|
||||
'@typescript-eslint/no-require-imports': [2],
|
||||
'@typescript-eslint/no-restricted-imports': [0],
|
||||
@@ -437,7 +437,7 @@ export default defineConfig([
|
||||
'no-import-assign': [2],
|
||||
'no-inline-comments': [0],
|
||||
'no-inner-declarations': [2],
|
||||
'no-invalid-regexp': [2],
|
||||
'no-invalid-regexp': [0], // handled by regexp/no-invalid-regexp
|
||||
'no-invalid-this': [0],
|
||||
'no-irregular-whitespace': [2],
|
||||
'no-iterator': [2],
|
||||
@@ -551,7 +551,7 @@ export default defineConfig([
|
||||
'no-new-func': [0], // handled by @typescript-eslint/no-implied-eval
|
||||
'no-new-native-nonconstructor': [2],
|
||||
'no-new-object': [2],
|
||||
'no-new-symbol': [2],
|
||||
'no-new-symbol': [0], // handled by no-new-native-nonconstructor
|
||||
'no-new-wrappers': [2],
|
||||
'no-new': [0],
|
||||
'no-nonoctal-decimal-escape': [2],
|
||||
@@ -582,7 +582,7 @@ export default defineConfig([
|
||||
'no-template-curly-in-string': [2],
|
||||
'no-ternary': [0],
|
||||
'no-this-before-super': [2],
|
||||
'no-throw-literal': [2],
|
||||
'no-throw-literal': [0], // handled by @typescript-eslint/only-throw-error
|
||||
'no-undef-init': [2],
|
||||
'no-undef': [2], // it is still needed by eslint & IDE to prompt undefined names in real time
|
||||
'no-undefined': [0],
|
||||
@@ -600,7 +600,7 @@ export default defineConfig([
|
||||
'no-unused-vars': [0], // handled by @typescript-eslint/no-unused-vars
|
||||
'no-use-before-define': [0], // handled by @typescript-eslint/no-use-before-define
|
||||
'no-useless-assignment': [2],
|
||||
'no-useless-backreference': [2],
|
||||
'no-useless-backreference': [0], // handled by regexp/no-useless-backreference
|
||||
'no-useless-call': [2],
|
||||
'no-useless-catch': [2],
|
||||
'no-useless-computed-key': [2],
|
||||
@@ -608,7 +608,7 @@ export default defineConfig([
|
||||
'no-useless-constructor': [2],
|
||||
'no-useless-escape': [2],
|
||||
'no-useless-rename': [2],
|
||||
'no-useless-return': [2],
|
||||
'no-useless-return': [0], // handled by sonarjs/no-redundant-jump
|
||||
'no-var': [2],
|
||||
'no-void': [2],
|
||||
'no-warning-comments': [0],
|
||||
@@ -617,7 +617,7 @@ export default defineConfig([
|
||||
'one-var-declaration-per-line': [0],
|
||||
'one-var': [0],
|
||||
'operator-assignment': [2, 'always'],
|
||||
'operator-linebreak': [2, 'after'],
|
||||
'operator-linebreak': [0], // handled by @stylistic/operator-linebreak
|
||||
'prefer-arrow-callback': [2, {allowNamedFunctions: true, allowUnboundThis: true}],
|
||||
'prefer-const': [2, {destructuring: 'all', ignoreReadBeforeAssign: true}],
|
||||
'prefer-destructuring': [0],
|
||||
@@ -697,7 +697,7 @@ export default defineConfig([
|
||||
'regexp/prefer-question-quantifier': [2],
|
||||
'regexp/prefer-range': [2],
|
||||
'regexp/prefer-regexp-exec': [2],
|
||||
'regexp/prefer-regexp-test': [2],
|
||||
'regexp/prefer-regexp-test': [0], // handled by unicorn/prefer-regexp-test
|
||||
'regexp/prefer-result-array-groups': [0],
|
||||
'regexp/prefer-set-operation': [2],
|
||||
'regexp/prefer-star-quantifier': [2],
|
||||
@@ -727,7 +727,7 @@ export default defineConfig([
|
||||
'sonarjs/no-empty-collection': [2],
|
||||
'sonarjs/no-extra-arguments': [2],
|
||||
'sonarjs/no-gratuitous-expressions': [2],
|
||||
'sonarjs/no-identical-conditions': [2],
|
||||
'sonarjs/no-identical-conditions': [0], // handled by no-dupe-else-if
|
||||
'sonarjs/no-identical-expressions': [2],
|
||||
'sonarjs/no-identical-functions': [2, 5],
|
||||
'sonarjs/no-ignored-return': [2],
|
||||
@@ -740,7 +740,7 @@ export default defineConfig([
|
||||
'sonarjs/no-small-switch': [0],
|
||||
'sonarjs/no-unused-collection': [2],
|
||||
'sonarjs/no-use-of-empty-return-value': [2],
|
||||
'sonarjs/no-useless-catch': [2],
|
||||
'sonarjs/no-useless-catch': [0], // handled by no-useless-catch
|
||||
'sonarjs/non-existent-operator': [2],
|
||||
'sonarjs/prefer-immediate-return': [0],
|
||||
'sonarjs/prefer-object-literal': [0],
|
||||
@@ -767,6 +767,7 @@ export default defineConfig([
|
||||
'unicorn/filename-case': [0],
|
||||
'unicorn/import-index': [0],
|
||||
'unicorn/import-style': [0],
|
||||
'unicorn/isolated-functions': [2, {functions: []}],
|
||||
'unicorn/new-for-builtins': [2],
|
||||
'unicorn/no-abusive-eslint-disable': [0],
|
||||
'unicorn/no-anonymous-default-export': [0],
|
||||
@@ -806,7 +807,7 @@ export default defineConfig([
|
||||
'unicorn/no-unnecessary-await': [2],
|
||||
'unicorn/no-unnecessary-polyfills': [2],
|
||||
'unicorn/no-unreadable-array-destructuring': [0],
|
||||
'unicorn/no-unreadable-iife': [2],
|
||||
'unicorn/no-unreadable-iife': [0],
|
||||
'unicorn/no-unused-properties': [2],
|
||||
'unicorn/no-useless-collection-argument': [2],
|
||||
'unicorn/no-useless-fallback-in-spread': [2],
|
||||
@@ -819,7 +820,7 @@ export default defineConfig([
|
||||
'unicorn/number-literal-case': [0],
|
||||
'unicorn/numeric-separators-style': [0],
|
||||
'unicorn/prefer-add-event-listener': [2],
|
||||
'unicorn/prefer-array-find': [2],
|
||||
'unicorn/prefer-array-find': [0], // handled by @typescript-eslint/prefer-find
|
||||
'unicorn/prefer-array-flat': [2],
|
||||
'unicorn/prefer-array-flat-map': [2],
|
||||
'unicorn/prefer-array-index-of': [2],
|
||||
@@ -836,7 +837,7 @@ export default defineConfig([
|
||||
'unicorn/prefer-event-target': [2],
|
||||
'unicorn/prefer-export-from': [0],
|
||||
'unicorn/prefer-global-this': [0],
|
||||
'unicorn/prefer-includes': [2],
|
||||
'unicorn/prefer-includes': [0], // handled by @typescript-eslint/prefer-includes
|
||||
'unicorn/prefer-json-parse-buffer': [0],
|
||||
'unicorn/prefer-keyboard-event-key': [2],
|
||||
'unicorn/prefer-logical-operator-over-ternary': [2],
|
||||
@@ -863,7 +864,7 @@ export default defineConfig([
|
||||
'unicorn/prefer-string-raw': [0],
|
||||
'unicorn/prefer-string-replace-all': [0],
|
||||
'unicorn/prefer-string-slice': [0],
|
||||
'unicorn/prefer-string-starts-ends-with': [2],
|
||||
'unicorn/prefer-string-starts-ends-with': [0], // handled by @typescript-eslint/prefer-string-starts-ends-with
|
||||
'unicorn/prefer-string-trim-start-end': [2],
|
||||
'unicorn/prefer-structured-clone': [2],
|
||||
'unicorn/prefer-switch': [0],
|
||||
|
||||
8
main.go
8
main.go
@@ -26,9 +26,8 @@ import (
|
||||
|
||||
// these flags will be set by the build flags
|
||||
var (
|
||||
Version = "development" // program version for this build
|
||||
Tags = "" // the Golang build tags
|
||||
MakeVersion = "" // "make" program version if built with make
|
||||
Version = "development" // program version for this build
|
||||
Tags = "" // the Golang build tags
|
||||
)
|
||||
|
||||
func init() {
|
||||
@@ -50,9 +49,6 @@ func main() {
|
||||
|
||||
func formatBuiltWith() string {
|
||||
version := runtime.Version()
|
||||
if len(MakeVersion) > 0 {
|
||||
version = MakeVersion + ", " + runtime.Version()
|
||||
}
|
||||
if len(Tags) == 0 {
|
||||
return " built with " + version
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"crypto/subtle"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
auth_model "code.gitea.io/gitea/models/auth"
|
||||
@@ -20,6 +21,7 @@ import (
|
||||
|
||||
runnerv1 "code.gitea.io/actions-proto-go/runner/v1"
|
||||
lru "github.com/hashicorp/golang-lru/v2"
|
||||
"github.com/nektos/act/pkg/jobparser"
|
||||
"google.golang.org/protobuf/types/known/timestamppb"
|
||||
"xorm.io/builder"
|
||||
)
|
||||
@@ -214,6 +216,20 @@ func GetRunningTaskByToken(ctx context.Context, token string) (*ActionTask, erro
|
||||
return nil, errNotExist
|
||||
}
|
||||
|
||||
func makeTaskStepDisplayName(step *jobparser.Step, limit int) (name string) {
|
||||
if step.Name != "" {
|
||||
name = step.Name // the step has an explicit name
|
||||
} else {
|
||||
// for unnamed step, its "String()" method tries to get a display name by its "name", "uses",
|
||||
// "run" or "id" (last fallback), we add the "Run " prefix for unnamed steps for better display
|
||||
// for multi-line "run" scripts, only use the first line to match GitHub's behavior
|
||||
// https://github.com/actions/runner/blob/66800900843747f37591b077091dd2c8cf2c1796/src/Runner.Worker/Handlers/ScriptHandler.cs#L45-L58
|
||||
runStr, _, _ := strings.Cut(strings.TrimSpace(step.Run), "\n")
|
||||
name = "Run " + util.IfZero(strings.TrimSpace(runStr), step.String())
|
||||
}
|
||||
return util.EllipsisDisplayString(name, limit) // database column has a length limit
|
||||
}
|
||||
|
||||
func CreateTaskForRunner(ctx context.Context, runner *ActionRunner) (*ActionTask, bool, error) {
|
||||
ctx, committer, err := db.TxContext(ctx)
|
||||
if err != nil {
|
||||
@@ -293,9 +309,8 @@ func CreateTaskForRunner(ctx context.Context, runner *ActionRunner) (*ActionTask
|
||||
if len(workflowJob.Steps) > 0 {
|
||||
steps := make([]*ActionTaskStep, len(workflowJob.Steps))
|
||||
for i, v := range workflowJob.Steps {
|
||||
name := util.EllipsisDisplayString(v.String(), 255)
|
||||
steps[i] = &ActionTaskStep{
|
||||
Name: name,
|
||||
Name: makeTaskStepDisplayName(v, 255),
|
||||
TaskID: task.ID,
|
||||
Index: int64(i),
|
||||
RepoID: task.RepoID,
|
||||
|
||||
@@ -14,7 +14,7 @@ import (
|
||||
// ActionTaskStep represents a step of ActionTask
|
||||
type ActionTaskStep struct {
|
||||
ID int64
|
||||
Name string `xorm:"VARCHAR(255)"`
|
||||
Name string `xorm:"VARCHAR(255)"` // the step name, for display purpose only, it will be truncated if it is too long
|
||||
TaskID int64 `xorm:"index unique(task_index)"`
|
||||
Index int64 `xorm:"index unique(task_index)"`
|
||||
RepoID int64 `xorm:"index"`
|
||||
|
||||
76
models/actions/task_test.go
Normal file
76
models/actions/task_test.go
Normal file
@@ -0,0 +1,76 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package actions
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/nektos/act/pkg/jobparser"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestMakeTaskStepDisplayName(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
jobStep *jobparser.Step
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
name: "explicit name",
|
||||
jobStep: &jobparser.Step{
|
||||
Name: "Test Step",
|
||||
},
|
||||
expected: "Test Step",
|
||||
},
|
||||
{
|
||||
name: "uses step",
|
||||
jobStep: &jobparser.Step{
|
||||
Uses: "actions/checkout@v4",
|
||||
},
|
||||
expected: "Run actions/checkout@v4",
|
||||
},
|
||||
{
|
||||
name: "single-line run",
|
||||
jobStep: &jobparser.Step{
|
||||
Run: "echo hello",
|
||||
},
|
||||
expected: "Run echo hello",
|
||||
},
|
||||
{
|
||||
name: "multi-line run block scalar",
|
||||
jobStep: &jobparser.Step{
|
||||
Run: "\n echo hello \r\n echo world \n ",
|
||||
},
|
||||
expected: "Run echo hello",
|
||||
},
|
||||
{
|
||||
name: "fallback to id",
|
||||
jobStep: &jobparser.Step{
|
||||
ID: "step-id",
|
||||
},
|
||||
expected: "Run step-id",
|
||||
},
|
||||
{
|
||||
name: "very long name truncated",
|
||||
jobStep: &jobparser.Step{
|
||||
Name: strings.Repeat("a", 300),
|
||||
},
|
||||
expected: strings.Repeat("a", 252) + "…",
|
||||
},
|
||||
{
|
||||
name: "very long run truncated",
|
||||
jobStep: &jobparser.Step{
|
||||
Run: strings.Repeat("a", 300),
|
||||
},
|
||||
expected: "Run " + strings.Repeat("a", 248) + "…",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := makeTaskStepDisplayName(tt.jobStep, 255)
|
||||
assert.Equal(t, tt.expected, result)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -30,7 +30,7 @@ func (actions ActionList) getUserIDs() []int64 {
|
||||
|
||||
func (actions ActionList) LoadActUsers(ctx context.Context) (map[int64]*user_model.User, error) {
|
||||
if len(actions) == 0 {
|
||||
return nil, nil
|
||||
return nil, nil //nolint:nilnil // returns nil when there are no actions
|
||||
}
|
||||
|
||||
userIDs := actions.getUserIDs()
|
||||
|
||||
@@ -19,14 +19,14 @@ type UserHeatmapData struct {
|
||||
Contributions int64 `json:"contributions"`
|
||||
}
|
||||
|
||||
// GetUserHeatmapDataByUser returns an array of UserHeatmapData
|
||||
// GetUserHeatmapDataByUser returns an array of UserHeatmapData, it checks whether doer can access user's activity
|
||||
func GetUserHeatmapDataByUser(ctx context.Context, user, doer *user_model.User) ([]*UserHeatmapData, error) {
|
||||
return getUserHeatmapData(ctx, user, nil, doer)
|
||||
}
|
||||
|
||||
// GetUserHeatmapDataByUserTeam returns an array of UserHeatmapData
|
||||
func GetUserHeatmapDataByUserTeam(ctx context.Context, user *user_model.User, team *organization.Team, doer *user_model.User) ([]*UserHeatmapData, error) {
|
||||
return getUserHeatmapData(ctx, user, team, doer)
|
||||
// GetUserHeatmapDataByOrgTeam returns an array of UserHeatmapData, it checks whether doer can access org's activity
|
||||
func GetUserHeatmapDataByOrgTeam(ctx context.Context, org *organization.Organization, team *organization.Team, doer *user_model.User) ([]*UserHeatmapData, error) {
|
||||
return getUserHeatmapData(ctx, org.AsUser(), team, doer)
|
||||
}
|
||||
|
||||
func getUserHeatmapData(ctx context.Context, user *user_model.User, team *organization.Team, doer *user_model.User) ([]*UserHeatmapData, error) {
|
||||
@@ -71,12 +71,3 @@ func getUserHeatmapData(ctx context.Context, user *user_model.User, team *organi
|
||||
OrderBy("timestamp").
|
||||
Find(&hdata)
|
||||
}
|
||||
|
||||
// GetTotalContributionsInHeatmap returns the total number of contributions in a heatmap
|
||||
func GetTotalContributionsInHeatmap(hdata []*UserHeatmapData) int64 {
|
||||
var total int64
|
||||
for _, v := range hdata {
|
||||
total += v.Contributions
|
||||
}
|
||||
return total
|
||||
}
|
||||
|
||||
@@ -70,7 +70,7 @@ func hashAndVerify(sig *packet.Signature, payload string, k *GPGKey) (*GPGKey, e
|
||||
// We will ignore errors in verification as they don't need to be propagated up
|
||||
err = verifySign(sig, hash, k)
|
||||
if err != nil {
|
||||
return nil, nil
|
||||
return nil, nil //nolint:nilnil // verification failed, not an error
|
||||
}
|
||||
return k, nil
|
||||
}
|
||||
@@ -86,7 +86,7 @@ func hashAndVerifyWithSubKeys(sig *packet.Signature, payload string, k *GPGKey)
|
||||
return verified, err
|
||||
}
|
||||
}
|
||||
return nil, nil
|
||||
return nil, nil //nolint:nilnil // verification failed, not an error
|
||||
}
|
||||
|
||||
func HashAndVerifyWithSubKeysCommitVerification(sig *packet.Signature, payload string, k *GPGKey, committer, signer *user_model.User, email string) *CommitVerification {
|
||||
|
||||
@@ -209,7 +209,7 @@ func (app *OAuth2Application) GetGrantByUserID(ctx context.Context, userID int64
|
||||
if has, err := db.GetEngine(ctx).Where("user_id = ? AND application_id = ?", userID, app.ID).Get(grant); err != nil {
|
||||
return nil, err
|
||||
} else if !has {
|
||||
return nil, nil
|
||||
return nil, nil //nolint:nilnil // return nil to indicate that the object does not exist
|
||||
}
|
||||
return grant, nil
|
||||
}
|
||||
@@ -431,13 +431,13 @@ func GetOAuth2AuthorizationByCode(ctx context.Context, code string) (auth *OAuth
|
||||
if has, err := db.GetEngine(ctx).Where("code = ?", code).Get(auth); err != nil {
|
||||
return nil, err
|
||||
} else if !has {
|
||||
return nil, nil
|
||||
return nil, nil //nolint:nilnil // return nil to indicate that the object does not exist
|
||||
}
|
||||
auth.Grant = new(OAuth2Grant)
|
||||
if has, err := db.GetEngine(ctx).ID(auth.GrantID).Get(auth.Grant); err != nil {
|
||||
return nil, err
|
||||
} else if !has {
|
||||
return nil, nil
|
||||
return nil, nil //nolint:nilnil // return nil to indicate that the object does not exist
|
||||
}
|
||||
return auth, nil
|
||||
}
|
||||
@@ -521,7 +521,7 @@ func GetOAuth2GrantByID(ctx context.Context, id int64) (grant *OAuth2Grant, err
|
||||
if has, err := db.GetEngine(ctx).ID(id).Get(grant); err != nil {
|
||||
return nil, err
|
||||
} else if !has {
|
||||
return nil, nil
|
||||
return nil, nil //nolint:nilnil // return nil to indicate that the object does not exist
|
||||
}
|
||||
return grant, err
|
||||
}
|
||||
|
||||
@@ -98,7 +98,7 @@ func CheckCollations(x *xorm.Engine) (*CheckCollationsResult, error) {
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
return nil, nil
|
||||
return nil, nil //nolint:nilnil // return nil for unsupported database types
|
||||
}
|
||||
|
||||
if res.DatabaseCollation == "" {
|
||||
|
||||
@@ -339,7 +339,7 @@ func findFileMetaByID(ctx context.Context, metaID int64) (*dbfsMeta, error) {
|
||||
} else if ok {
|
||||
return &fileMeta, nil
|
||||
}
|
||||
return nil, nil
|
||||
return nil, nil //nolint:nilnil // return nil to indicate that the object does not exist
|
||||
}
|
||||
|
||||
func buildPath(path string) string {
|
||||
|
||||
@@ -139,26 +139,7 @@
|
||||
updated: 1683636626
|
||||
need_approval: 0
|
||||
approved_by: 0
|
||||
-
|
||||
id: 804
|
||||
title: "use a private action"
|
||||
repo_id: 60
|
||||
owner_id: 40
|
||||
workflow_id: "run.yaml"
|
||||
index: 189
|
||||
trigger_user_id: 40
|
||||
ref: "refs/heads/master"
|
||||
commit_sha: "6e64b26de7ba966d01d90ecfaf5c7f14ef203e86"
|
||||
event: "push"
|
||||
trigger_event: "push"
|
||||
is_fork_pull_request: 0
|
||||
status: 1
|
||||
started: 1683636528
|
||||
stopped: 1683636626
|
||||
created: 1683636108
|
||||
updated: 1683636626
|
||||
need_approval: 0
|
||||
approved_by: 0
|
||||
|
||||
-
|
||||
id: 805
|
||||
title: "update actions"
|
||||
|
||||
@@ -129,20 +129,7 @@
|
||||
status: 5
|
||||
started: 1683636528
|
||||
stopped: 1683636626
|
||||
-
|
||||
id: 205
|
||||
run_id: 804
|
||||
repo_id: 6
|
||||
owner_id: 10
|
||||
commit_sha: 6e64b26de7ba966d01d90ecfaf5c7f14ef203e86
|
||||
is_fork_pull_request: 0
|
||||
name: job_2
|
||||
attempt: 1
|
||||
job_id: job_2
|
||||
task_id: 48
|
||||
status: 1
|
||||
started: 1683636528
|
||||
stopped: 1683636626
|
||||
|
||||
-
|
||||
id: 206
|
||||
run_id: 805
|
||||
|
||||
@@ -177,26 +177,7 @@
|
||||
log_length: 0
|
||||
log_size: 0
|
||||
log_expired: 0
|
||||
-
|
||||
id: 55
|
||||
job_id: 205
|
||||
attempt: 1
|
||||
runner_id: 1
|
||||
status: 6 # 6 is the status code for "running"
|
||||
started: 1683636528
|
||||
stopped: 1683636626
|
||||
repo_id: 6
|
||||
owner_id: 10
|
||||
commit_sha: 6e64b26de7ba966d01d90ecfaf5c7f14ef203e86
|
||||
is_fork_pull_request: 0
|
||||
token_hash: b8d3962425466b6709b9ac51446f93260c54afe8e7b6d3686e34f991fb8a8953822b0deed86fe41a103f34bc48dbc478422b
|
||||
token_salt: ERxJGHvg3I
|
||||
token_last_eight: 182199eb
|
||||
log_filename: collaborative-owner-test/1a/49.log
|
||||
log_in_storage: 1
|
||||
log_length: 707
|
||||
log_size: 90179
|
||||
log_expired: 0
|
||||
|
||||
-
|
||||
id: 56
|
||||
attempt: 1
|
||||
|
||||
@@ -397,10 +397,16 @@ func RenameBranch(ctx context.Context, repo *repo_model.Repository, from, to str
|
||||
|
||||
if protectedBranch != nil {
|
||||
// there is a protect rule for this branch
|
||||
protectedBranch.RuleName = to
|
||||
if _, err = sess.ID(protectedBranch.ID).Cols("branch_name").Update(protectedBranch); err != nil {
|
||||
existingRule, err := GetProtectedBranchRuleByName(ctx, repo.ID, to)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if existingRule == nil || existingRule.ID == protectedBranch.ID {
|
||||
protectedBranch.RuleName = to
|
||||
if _, err = sess.ID(protectedBranch.ID).Cols("branch_name").Update(protectedBranch); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// some glob protect rules may match this branch
|
||||
protected, err := IsBranchProtected(ctx, repo.ID, from)
|
||||
|
||||
@@ -159,6 +159,53 @@ func TestRenameBranch(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestRenameBranchProtectedRuleConflict(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
|
||||
master := unittest.AssertExistsAndLoadBean(t, &git_model.Branch{RepoID: repo1.ID, Name: "master"})
|
||||
|
||||
devBranch := &git_model.Branch{
|
||||
RepoID: repo1.ID,
|
||||
Name: "dev",
|
||||
CommitID: master.CommitID,
|
||||
CommitMessage: master.CommitMessage,
|
||||
CommitTime: master.CommitTime,
|
||||
PusherID: master.PusherID,
|
||||
}
|
||||
assert.NoError(t, db.Insert(t.Context(), devBranch))
|
||||
|
||||
pbDev := git_model.ProtectedBranch{
|
||||
RepoID: repo1.ID,
|
||||
RuleName: "dev",
|
||||
CanPush: true,
|
||||
}
|
||||
assert.NoError(t, git_model.UpdateProtectBranch(t.Context(), repo1, &pbDev, git_model.WhitelistOptions{}))
|
||||
|
||||
pbMain := git_model.ProtectedBranch{
|
||||
RepoID: repo1.ID,
|
||||
RuleName: "main",
|
||||
CanPush: true,
|
||||
}
|
||||
assert.NoError(t, git_model.UpdateProtectBranch(t.Context(), repo1, &pbMain, git_model.WhitelistOptions{}))
|
||||
|
||||
assert.NoError(t, git_model.RenameBranch(t.Context(), repo1, "dev", "main", func(ctx context.Context, isDefault bool) error {
|
||||
return nil
|
||||
}))
|
||||
|
||||
unittest.AssertNotExistsBean(t, &git_model.Branch{RepoID: repo1.ID, Name: "dev"})
|
||||
unittest.AssertExistsAndLoadBean(t, &git_model.Branch{RepoID: repo1.ID, Name: "main"})
|
||||
|
||||
protectedDev, err := git_model.GetProtectedBranchRuleByName(t.Context(), repo1.ID, "dev")
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, protectedDev)
|
||||
assert.Equal(t, "dev", protectedDev.RuleName)
|
||||
|
||||
protectedMainByID, err := git_model.GetProtectedBranchRuleByID(t.Context(), repo1.ID, pbMain.ID)
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, protectedMainByID)
|
||||
assert.Equal(t, "main", protectedMainByID.RuleName)
|
||||
}
|
||||
|
||||
func TestOnlyGetDeletedBranchOnCorrectRepo(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
|
||||
@@ -130,7 +130,7 @@ func GetLFSLockByRepoID(ctx context.Context, repoID int64, page, pageSize int) (
|
||||
// GetTreePathLock returns LSF lock for the treePath
|
||||
func GetTreePathLock(ctx context.Context, repoID int64, treePath string) (*LFSLock, error) {
|
||||
if !setting.LFS.StartServer {
|
||||
return nil, nil
|
||||
return nil, nil //nolint:nilnil // return nil when LFS is not started
|
||||
}
|
||||
|
||||
locks, err := GetLFSLockByRepoID(ctx, repoID, 0, 0)
|
||||
@@ -142,7 +142,7 @@ func GetTreePathLock(ctx context.Context, repoID int64, treePath string) (*LFSLo
|
||||
return lock, nil
|
||||
}
|
||||
}
|
||||
return nil, nil
|
||||
return nil, nil //nolint:nilnil // return nil to indicate that the object does not exist
|
||||
}
|
||||
|
||||
// CountLFSLockByRepoID returns a count of all LFSLocks associated with a repository.
|
||||
|
||||
@@ -318,7 +318,7 @@ func GetProtectedBranchRuleByName(ctx context.Context, repoID int64, ruleName st
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else if !exist {
|
||||
return nil, nil
|
||||
return nil, nil //nolint:nilnil // return nil to indicate that the object does not exist
|
||||
}
|
||||
return rel, nil
|
||||
}
|
||||
@@ -329,7 +329,7 @@ func GetProtectedBranchRuleByID(ctx context.Context, repoID, ruleID int64) (*Pro
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else if !exist {
|
||||
return nil, nil
|
||||
return nil, nil //nolint:nilnil // return nil to indicate that the object does not exist
|
||||
}
|
||||
return rel, nil
|
||||
}
|
||||
|
||||
@@ -104,7 +104,7 @@ func GetProtectedTagByID(ctx context.Context, id int64) (*ProtectedTag, error) {
|
||||
return nil, err
|
||||
}
|
||||
if !has {
|
||||
return nil, nil
|
||||
return nil, nil //nolint:nilnil // return nil to indicate that the object does not exist
|
||||
}
|
||||
return tag, nil
|
||||
}
|
||||
@@ -117,7 +117,7 @@ func GetProtectedTagByNamePattern(ctx context.Context, repoID int64, pattern str
|
||||
return nil, err
|
||||
}
|
||||
if !has {
|
||||
return nil, nil
|
||||
return nil, nil //nolint:nilnil // return nil to indicate that the object does not exist
|
||||
}
|
||||
return tag, nil
|
||||
}
|
||||
|
||||
@@ -498,7 +498,7 @@ func (issue *Issue) GetLastComment(ctx context.Context) (*Comment, error) {
|
||||
return nil, err
|
||||
}
|
||||
if !exist {
|
||||
return nil, nil
|
||||
return nil, nil //nolint:nilnil // return nil to indicate that the object does not exist
|
||||
}
|
||||
return &c, nil
|
||||
}
|
||||
|
||||
@@ -384,7 +384,7 @@ func CreateReview(ctx context.Context, opts CreateReviewOptions) (*Review, error
|
||||
// GetCurrentReview returns the current pending review of reviewer for given issue
|
||||
func GetCurrentReview(ctx context.Context, reviewer *user_model.User, issue *Issue) (*Review, error) {
|
||||
if reviewer == nil {
|
||||
return nil, nil
|
||||
return nil, nil //nolint:nilnil // return nil when reviewer is nil
|
||||
}
|
||||
reviews, err := FindReviews(ctx, FindReviewOptions{
|
||||
Types: []ReviewType{ReviewTypePending},
|
||||
|
||||
@@ -10,7 +10,6 @@ import (
|
||||
"path"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
"code.gitea.io/gitea/models/unittest"
|
||||
@@ -27,18 +26,6 @@ import (
|
||||
|
||||
// FIXME: this file shouldn't be in a normal package, it should only be compiled for tests
|
||||
|
||||
func removeAllWithRetry(dir string) error {
|
||||
var err error
|
||||
for range 20 {
|
||||
err = os.RemoveAll(dir)
|
||||
if err == nil {
|
||||
break
|
||||
}
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func newXORMEngine(t *testing.T) (*xorm.Engine, error) {
|
||||
if err := db.InitEngine(t.Context()); err != nil {
|
||||
return nil, err
|
||||
@@ -213,13 +200,12 @@ func LoadTableSchemasMap(t *testing.T, x *xorm.Engine) map[string]*schemas.Table
|
||||
return tableMap
|
||||
}
|
||||
|
||||
func MainTest(m *testing.M) {
|
||||
func mainTest(m *testing.M) int {
|
||||
testlogger.Init()
|
||||
setting.SetupGiteaTestEnv()
|
||||
|
||||
tmpDataPath, cleanup, err := tempdir.OsTempDir("gitea-test").MkdirTempRandom("data")
|
||||
if err != nil {
|
||||
testlogger.Fatalf("Unable to create temporary data path %v\n", err)
|
||||
testlogger.Panicf("Unable to create temporary data path %v\n", err)
|
||||
}
|
||||
defer cleanup()
|
||||
|
||||
@@ -227,15 +213,13 @@ func MainTest(m *testing.M) {
|
||||
|
||||
unittest.InitSettingsForTesting()
|
||||
if err = git.InitFull(); err != nil {
|
||||
testlogger.Fatalf("Unable to InitFull: %v\n", err)
|
||||
testlogger.Panicf("Unable to InitFull: %v\n", err)
|
||||
}
|
||||
setting.LoadDBSetting()
|
||||
setting.InitLoggersForTest()
|
||||
|
||||
exitStatus := m.Run()
|
||||
|
||||
if err := removeAllWithRetry(setting.RepoRootPath); err != nil {
|
||||
_, _ = fmt.Fprintf(os.Stderr, "os.RemoveAll: %v\n", err)
|
||||
}
|
||||
os.Exit(exitStatus)
|
||||
return m.Run()
|
||||
}
|
||||
|
||||
func MainTest(m *testing.M) {
|
||||
os.Exit(mainTest(m))
|
||||
}
|
||||
|
||||
@@ -174,13 +174,13 @@ func GetOrgAssignees(ctx context.Context, orgID int64) (_ []*user_model.User, er
|
||||
|
||||
func loadOrganizationOwners(ctx context.Context, users user_model.UserList, orgID int64) (map[int64]*TeamUser, error) {
|
||||
if len(users) == 0 {
|
||||
return nil, nil
|
||||
return nil, nil //nolint:nilnil // return nil when there are no users
|
||||
}
|
||||
ownerTeam, err := GetOwnerTeam(ctx, orgID)
|
||||
if err != nil {
|
||||
if IsErrTeamNotExist(err) {
|
||||
log.Error("Organization does not have owner team: %d", orgID)
|
||||
return nil, nil
|
||||
return nil, nil //nolint:nilnil // return nil when owner team does not exist
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -107,7 +107,7 @@ func GetRepoArchiver(ctx context.Context, repoID int64, tp ArchiveType, commitID
|
||||
if has {
|
||||
return &archiver, nil
|
||||
}
|
||||
return nil, nil
|
||||
return nil, nil //nolint:nilnil // return nil to indicate that the object does not exist
|
||||
}
|
||||
|
||||
// ExistsRepoArchiverWithStoragePath checks if there is a RepoArchiver for a given storage path
|
||||
|
||||
@@ -49,7 +49,7 @@ func GetUserFork(ctx context.Context, repoID, userID int64) (*Repository, error)
|
||||
return nil, err
|
||||
}
|
||||
if !has {
|
||||
return nil, nil
|
||||
return nil, nil //nolint:nilnil // return nil to indicate that the object does not exist
|
||||
}
|
||||
return &forkedRepo, nil
|
||||
}
|
||||
|
||||
@@ -878,7 +878,7 @@ func IsRepositoryModelExist(ctx context.Context, u *user_model.User, repoName st
|
||||
// non-generated repositories, and TemplateRepo will be left untouched)
|
||||
func GetTemplateRepo(ctx context.Context, repo *Repository) (*Repository, error) {
|
||||
if !repo.IsGenerated() {
|
||||
return nil, nil
|
||||
return nil, nil //nolint:nilnil // return nil for non-generated repositories
|
||||
}
|
||||
|
||||
return GetRepositoryByID(ctx, repo.TemplateID)
|
||||
|
||||
@@ -257,7 +257,7 @@ func DeleteTopic(ctx context.Context, repoID int64, topicName string) (*Topic, e
|
||||
}
|
||||
if topic == nil {
|
||||
// Repo doesn't have topic, can't be removed
|
||||
return nil, nil
|
||||
return nil, nil //nolint:nilnil // return nil to indicate that the topic does not exist
|
||||
}
|
||||
|
||||
return db.WithTx2(ctx, func(ctx context.Context) (*Topic, error) {
|
||||
|
||||
@@ -169,7 +169,7 @@ func (f *fixturesLoaderInternal) Load() error {
|
||||
|
||||
func FixturesFileFullPaths(dir string, files []string) (map[string]*FixtureItem, error) {
|
||||
if files != nil && len(files) == 0 {
|
||||
return nil, nil // load nothing
|
||||
return nil, nil //nolint:nilnil // load nothing
|
||||
}
|
||||
files = slices.Clone(files)
|
||||
if len(files) == 0 {
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
package unittest_test
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
@@ -16,7 +17,7 @@ import (
|
||||
)
|
||||
|
||||
var NewFixturesLoaderVendor = func(e *xorm.Engine, opts unittest.FixturesOptions) (unittest.FixturesLoader, error) {
|
||||
return nil, nil
|
||||
return nil, nil //nolint:nilnil // no vendor fixtures loader configured
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -58,9 +59,14 @@ func NewFixturesLoaderVendorGoTestfixtures(e *xorm.Engine, opts unittest.Fixture
|
||||
}
|
||||
*/
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
setting.SetupGiteaTestEnv()
|
||||
os.Exit(m.Run())
|
||||
}
|
||||
|
||||
func prepareTestFixturesLoaders(t testing.TB) unittest.FixturesOptions {
|
||||
_ = user_model.User{}
|
||||
giteaRoot := setting.SetupGiteaTestEnv()
|
||||
giteaRoot := setting.GetGiteaTestSourceRoot()
|
||||
opts := unittest.FixturesOptions{Dir: filepath.Join(giteaRoot, "models", "fixtures"), Files: []string{
|
||||
"user.yml",
|
||||
}}
|
||||
|
||||
@@ -4,10 +4,12 @@
|
||||
package unittest
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
)
|
||||
|
||||
@@ -39,7 +41,20 @@ func SyncFile(srcPath, destPath string) error {
|
||||
// SyncDirs synchronizes files recursively from source to target directory.
|
||||
// It returns error when error occurs in underlying functions.
|
||||
func SyncDirs(srcPath, destPath string) error {
|
||||
err := os.MkdirAll(destPath, os.ModePerm)
|
||||
destPath = filepath.Clean(destPath)
|
||||
destPathAbs, err := filepath.Abs(destPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
devDataPathAbs, err := filepath.Abs(filepath.Join(setting.GetGiteaTestSourceRoot(), "data"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if strings.HasPrefix(destPathAbs+string(filepath.Separator), devDataPathAbs+string(filepath.Separator)) {
|
||||
return errors.New("destination path should not be inside Gitea data directory, otherwise your data for dev mode will be removed")
|
||||
}
|
||||
|
||||
err = os.MkdirAll(destPath, os.ModePerm)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ import (
|
||||
"code.gitea.io/gitea/modules/setting/config"
|
||||
"code.gitea.io/gitea/modules/storage"
|
||||
"code.gitea.io/gitea/modules/tempdir"
|
||||
"code.gitea.io/gitea/modules/testlogger"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
@@ -28,16 +29,10 @@ import (
|
||||
"xorm.io/xorm/names"
|
||||
)
|
||||
|
||||
var giteaRoot string
|
||||
|
||||
func fatalTestError(fmtStr string, args ...any) {
|
||||
_, _ = fmt.Fprintf(os.Stderr, fmtStr, args...)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// InitSettingsForTesting initializes config provider and load common settings for tests
|
||||
func InitSettingsForTesting() {
|
||||
setting.IsInTesting = true
|
||||
setting.SetupGiteaTestEnv()
|
||||
|
||||
log.OsExiter = func(code int) {
|
||||
if code != 0 {
|
||||
// non-zero exit code (log.Fatal) shouldn't occur during testing, if it happens, show a full stacktrace for more details
|
||||
@@ -49,8 +44,12 @@ func InitSettingsForTesting() {
|
||||
setting.CustomConf = filepath.Join(setting.CustomPath, "conf/app-unittest-tmp.ini")
|
||||
_ = os.Remove(setting.CustomConf)
|
||||
}
|
||||
setting.InitCfgProvider(setting.CustomConf)
|
||||
setting.LoadCommonSettings()
|
||||
|
||||
// init paths and config system for testing
|
||||
getTestEnv := func(key string) string {
|
||||
return ""
|
||||
}
|
||||
setting.InitWorkPathAndCommonConfig(getTestEnv, setting.ArgWorkPathAndCustomConf{CustomConf: setting.CustomConf})
|
||||
|
||||
if err := setting.PrepareAppDataPath(); err != nil {
|
||||
log.Fatal("Can not prepare APP_DATA_PATH: %v", err)
|
||||
@@ -71,16 +70,18 @@ type TestOptions struct {
|
||||
// MainTest a reusable TestMain(..) function for unit tests that need to use a
|
||||
// test database. Creates the test database, and sets necessary settings.
|
||||
func MainTest(m *testing.M, testOptsArg ...*TestOptions) {
|
||||
testOpts := util.OptionalArg(testOptsArg, &TestOptions{})
|
||||
giteaRoot = setting.SetupGiteaTestEnv()
|
||||
InitSettingsForTesting()
|
||||
os.Exit(mainTest(m, testOptsArg...))
|
||||
}
|
||||
|
||||
func mainTest(m *testing.M, testOptsArg ...*TestOptions) int {
|
||||
testOpts := util.OptionalArg(testOptsArg, &TestOptions{})
|
||||
InitSettingsForTesting()
|
||||
giteaRoot := setting.GetGiteaTestSourceRoot()
|
||||
fixturesOpts := FixturesOptions{Dir: filepath.Join(giteaRoot, "models", "fixtures"), Files: testOpts.FixtureFiles}
|
||||
if err := CreateTestEngine(fixturesOpts); err != nil {
|
||||
fatalTestError("Error creating test engine: %v\n", err)
|
||||
testlogger.Panicf("Error creating test engine: %v\n", err)
|
||||
}
|
||||
|
||||
setting.IsInTesting = true
|
||||
setting.AppURL = "https://try.gitea.io/"
|
||||
setting.Domain = "try.gitea.io"
|
||||
setting.RunUser = "runuser"
|
||||
@@ -92,20 +93,18 @@ func MainTest(m *testing.M, testOptsArg ...*TestOptions) {
|
||||
setting.Repository.DefaultBranch = "master" // many test code still assume that default branch is called "master"
|
||||
repoRootPath, cleanup1, err := tempdir.OsTempDir("gitea-test").MkdirTempRandom("repos")
|
||||
if err != nil {
|
||||
fatalTestError("TempDir: %v\n", err)
|
||||
testlogger.Panicf("TempDir: %v\n", err)
|
||||
}
|
||||
defer cleanup1()
|
||||
|
||||
setting.RepoRootPath = repoRootPath
|
||||
appDataPath, cleanup2, err := tempdir.OsTempDir("gitea-test").MkdirTempRandom("appdata")
|
||||
if err != nil {
|
||||
fatalTestError("TempDir: %v\n", err)
|
||||
testlogger.Panicf("TempDir: %v\n", err)
|
||||
}
|
||||
defer cleanup2()
|
||||
|
||||
setting.AppDataPath = appDataPath
|
||||
setting.AppWorkPath = giteaRoot
|
||||
setting.StaticRootPath = giteaRoot
|
||||
setting.GravatarSource = "https://secure.gravatar.com/avatar/"
|
||||
|
||||
setting.Attachment.Storage.Path = filepath.Join(setting.AppDataPath, "attachments")
|
||||
@@ -129,22 +128,22 @@ func MainTest(m *testing.M, testOptsArg ...*TestOptions) {
|
||||
config.SetDynGetter(system.NewDatabaseDynKeyGetter())
|
||||
|
||||
if err = cache.Init(); err != nil {
|
||||
fatalTestError("cache.Init: %v\n", err)
|
||||
testlogger.Panicf("cache.Init: %v\n", err)
|
||||
}
|
||||
if err = storage.Init(); err != nil {
|
||||
fatalTestError("storage.Init: %v\n", err)
|
||||
testlogger.Panicf("storage.Init: %v\n", err)
|
||||
}
|
||||
if err = SyncDirs(filepath.Join(giteaRoot, "tests", "gitea-repositories-meta"), setting.RepoRootPath); err != nil {
|
||||
fatalTestError("util.SyncDirs: %v\n", err)
|
||||
testlogger.Panicf("util.SyncDirs: %v\n", err)
|
||||
}
|
||||
|
||||
if err = git.InitFull(); err != nil {
|
||||
fatalTestError("git.Init: %v\n", err)
|
||||
testlogger.Panicf("git.Init: %v\n", err)
|
||||
}
|
||||
|
||||
if testOpts.SetUp != nil {
|
||||
if err := testOpts.SetUp(); err != nil {
|
||||
fatalTestError("set up failed: %v\n", err)
|
||||
testlogger.Panicf("set up failed: %v\n", err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -152,10 +151,10 @@ func MainTest(m *testing.M, testOptsArg ...*TestOptions) {
|
||||
|
||||
if testOpts.TearDown != nil {
|
||||
if err := testOpts.TearDown(); err != nil {
|
||||
fatalTestError("tear down failed: %v\n", err)
|
||||
testlogger.Panicf("tear down failed: %v\n", err)
|
||||
}
|
||||
}
|
||||
os.Exit(exitStatus)
|
||||
return exitStatus
|
||||
}
|
||||
|
||||
// FixturesOptions fixtures needs to be loaded options
|
||||
@@ -196,7 +195,6 @@ func PrepareTestDatabase() error {
|
||||
// by tests that use the above MainTest(..) function.
|
||||
func PrepareTestEnv(t testing.TB) {
|
||||
assert.NoError(t, PrepareTestDatabase())
|
||||
metaPath := filepath.Join(giteaRoot, "tests", "gitea-repositories-meta")
|
||||
metaPath := filepath.Join(setting.GetGiteaTestSourceRoot(), "tests", "gitea-repositories-meta")
|
||||
assert.NoError(t, SyncDirs(metaPath, setting.RepoRootPath))
|
||||
setting.SetupGiteaTestEnv()
|
||||
}
|
||||
|
||||
@@ -90,7 +90,7 @@ func GetBlocking(ctx context.Context, blockerID, blockeeID int64) (*Blocking, er
|
||||
return nil, err
|
||||
}
|
||||
if len(blocks) == 0 {
|
||||
return nil, nil
|
||||
return nil, nil //nolint:nilnil // return nil to indicate that the object does not exist
|
||||
}
|
||||
return blocks[0], nil
|
||||
}
|
||||
|
||||
@@ -215,7 +215,7 @@ func GetEmailAddressByID(ctx context.Context, uid, id int64) (*EmailAddress, err
|
||||
if has, err := db.GetEngine(ctx).ID(id).Get(email); err != nil {
|
||||
return nil, err
|
||||
} else if !has {
|
||||
return nil, nil
|
||||
return nil, nil //nolint:nilnil // return nil to indicate that the object does not exist
|
||||
}
|
||||
return email, nil
|
||||
}
|
||||
|
||||
@@ -48,7 +48,7 @@ func (users UserList) GetTwoFaStatus(ctx context.Context) map[int64]bool {
|
||||
|
||||
func (users UserList) loadTwoFactorStatus(ctx context.Context) (map[int64]*auth.TwoFactor, error) {
|
||||
if len(users) == 0 {
|
||||
return nil, nil
|
||||
return nil, nil //nolint:nilnil // returns nil when there are no users
|
||||
}
|
||||
|
||||
userIDs := users.GetUserIDs()
|
||||
|
||||
@@ -213,7 +213,7 @@ func (u *User) SetLastLogin() {
|
||||
|
||||
// GetPlaceholderEmail returns an noreply email
|
||||
func (u *User) GetPlaceholderEmail() string {
|
||||
return fmt.Sprintf("%s+%d@%s", u.LowerName, u.ID, setting.Service.NoReplyAddress)
|
||||
return fmt.Sprintf("%d+%s@%s", u.ID, u.LowerName, setting.Service.NoReplyAddress)
|
||||
}
|
||||
|
||||
// GetEmail returns a noreply email, if the user has set to keep his
|
||||
@@ -496,10 +496,10 @@ func (u *User) ShortName(length int) string {
|
||||
return util.EllipsisDisplayString(u.Name, length)
|
||||
}
|
||||
|
||||
// IsMailable checks if a user is eligible
|
||||
// to receive emails.
|
||||
// IsMailable checks if a user is eligible to receive emails.
|
||||
// System users like Ghost and Gitea Actions are excluded.
|
||||
func (u *User) IsMailable() bool {
|
||||
return u.IsActive
|
||||
return u.IsActive && !u.IsGiteaActions() && !u.IsGhost()
|
||||
}
|
||||
|
||||
// IsUserExist checks if given username exist,
|
||||
@@ -1193,7 +1193,7 @@ func (eum *EmailUserMap) GetByEmail(email string) *User {
|
||||
|
||||
func GetUsersByEmails(ctx context.Context, emails []string) (*EmailUserMap, error) {
|
||||
if len(emails) == 0 {
|
||||
return nil, nil
|
||||
return nil, nil //nolint:nilnil // return nil when there are no emails to look up
|
||||
}
|
||||
|
||||
needCheckEmails := make(container.Set[string])
|
||||
@@ -1279,13 +1279,15 @@ func GetUsersByEmails(ctx context.Context, emails []string) (*EmailUserMap, erro
|
||||
return &EmailUserMap{results}, nil
|
||||
}
|
||||
|
||||
// parseLocalPartToNameID attempts to unparse local-part of email that's in format user+id
|
||||
// parseLocalPartToNameID attempts to unparse local-part of email that's in format id+user
|
||||
// returns user and id if possible
|
||||
func parseLocalPartToNameID(localPart string) (string, int64) {
|
||||
var id int64
|
||||
name, idstr, hasPlus := strings.Cut(localPart, "+")
|
||||
idstr, name, hasPlus := strings.Cut(localPart, "+")
|
||||
if hasPlus {
|
||||
id, _ = strconv.ParseInt(idstr, 10, 64)
|
||||
} else {
|
||||
name = idstr
|
||||
}
|
||||
return name, id
|
||||
}
|
||||
|
||||
@@ -66,9 +66,9 @@ func TestUserEmails(t *testing.T) {
|
||||
{"UseR1@example.com", 1},
|
||||
{"user1-2@example.COM", 1},
|
||||
{"USER2@" + setting.Service.NoReplyAddress, 2},
|
||||
{"user2+2@" + setting.Service.NoReplyAddress, 2},
|
||||
{"oldUser2UsernameWhichDoesNotMatterForQuery+2@" + setting.Service.NoReplyAddress, 2},
|
||||
{"badUser+99999@" + setting.Service.NoReplyAddress, 0},
|
||||
{"2+user2@" + setting.Service.NoReplyAddress, 2},
|
||||
{"2+oldUser2UsernameWhichDoesNotMatterForQuery@" + setting.Service.NoReplyAddress, 2},
|
||||
{"99999+badUser@" + setting.Service.NoReplyAddress, 0},
|
||||
{"user4@example.com", 4},
|
||||
{"no-such", 0},
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
"code.gitea.io/gitea/modules/git"
|
||||
"code.gitea.io/gitea/modules/glob"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
api "code.gitea.io/gitea/modules/structs"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
webhook_module "code.gitea.io/gitea/modules/webhook"
|
||||
@@ -41,22 +42,30 @@ func IsWorkflow(path string) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
return strings.HasPrefix(path, ".gitea/workflows") || strings.HasPrefix(path, ".github/workflows")
|
||||
for _, workflowDir := range setting.Actions.WorkflowDirs {
|
||||
if strings.HasPrefix(path, workflowDir+"/") {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func ListWorkflows(commit *git.Commit) (string, git.Entries, error) {
|
||||
rpath := ".gitea/workflows"
|
||||
tree, err := commit.SubTree(rpath)
|
||||
if _, ok := err.(git.ErrNotExist); ok {
|
||||
rpath = ".github/workflows"
|
||||
tree, err = commit.SubTree(rpath)
|
||||
var tree *git.Tree
|
||||
var err error
|
||||
var workflowDir string
|
||||
for _, workflowDir = range setting.Actions.WorkflowDirs {
|
||||
tree, err = commit.SubTree(workflowDir)
|
||||
if err == nil {
|
||||
break
|
||||
}
|
||||
if !git.IsErrNotExist(err) {
|
||||
return "", nil, err
|
||||
}
|
||||
}
|
||||
if _, ok := err.(git.ErrNotExist); ok {
|
||||
if tree == nil {
|
||||
return "", nil, nil
|
||||
}
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
|
||||
entries, err := tree.ListEntriesRecursiveFast()
|
||||
if err != nil {
|
||||
@@ -69,7 +78,7 @@ func ListWorkflows(commit *git.Commit) (string, git.Entries, error) {
|
||||
ret = append(ret, entry)
|
||||
}
|
||||
}
|
||||
return rpath, ret, nil
|
||||
return workflowDir, ret, nil
|
||||
}
|
||||
|
||||
func GetContentFromEntry(entry *git.TreeEntry) ([]byte, error) {
|
||||
|
||||
@@ -7,12 +7,83 @@ import (
|
||||
"testing"
|
||||
|
||||
"code.gitea.io/gitea/modules/git"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
api "code.gitea.io/gitea/modules/structs"
|
||||
webhook_module "code.gitea.io/gitea/modules/webhook"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestIsWorkflow(t *testing.T) {
|
||||
oldDirs := setting.Actions.WorkflowDirs
|
||||
defer func() {
|
||||
setting.Actions.WorkflowDirs = oldDirs
|
||||
}()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
dirs []string
|
||||
path string
|
||||
expected bool
|
||||
}{
|
||||
{
|
||||
name: "default with yml extension",
|
||||
dirs: []string{".gitea/workflows", ".github/workflows"},
|
||||
path: ".gitea/workflows/test.yml",
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "default with yaml extension",
|
||||
dirs: []string{".gitea/workflows", ".github/workflows"},
|
||||
path: ".github/workflows/test.yaml",
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "only gitea configured, github path rejected",
|
||||
dirs: []string{".gitea/workflows"},
|
||||
path: ".github/workflows/test.yml",
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "only github configured, gitea path rejected",
|
||||
dirs: []string{".github/workflows"},
|
||||
path: ".gitea/workflows/test.yml",
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "custom workflow dir",
|
||||
dirs: []string{".custom/workflows"},
|
||||
path: ".custom/workflows/deploy.yml",
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "non-workflow file",
|
||||
dirs: []string{".gitea/workflows", ".github/workflows"},
|
||||
path: ".gitea/workflows/readme.md",
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "directory boundary",
|
||||
dirs: []string{".gitea/workflows"},
|
||||
path: ".gitea/workflows2/test.yml",
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "unrelated path",
|
||||
dirs: []string{".gitea/workflows", ".github/workflows"},
|
||||
path: "src/main.go",
|
||||
expected: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
setting.Actions.WorkflowDirs = tt.dirs
|
||||
assert.Equal(t, tt.expected, IsWorkflow(tt.path))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDetectMatched(t *testing.T) {
|
||||
testCases := []struct {
|
||||
desc string
|
||||
|
||||
@@ -37,6 +37,10 @@ type CommitSignature struct {
|
||||
|
||||
// Message returns the commit message. Same as retrieving CommitMessage directly.
|
||||
func (c *Commit) Message() string {
|
||||
// FIXME: GIT-COMMIT-MESSAGE-ENCODING: this logic is not right
|
||||
// * When need to use commit message in templates/database, it should be valid UTF-8
|
||||
// * When need to get the original commit message, it should just use "c.CommitMessage"
|
||||
// It's not easy to refactor at the moment, many templates need to be updated and tested
|
||||
return c.CommitMessage
|
||||
}
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ func (c *Commit) GetSubModules() (*ObjectCache[*SubModule], error) {
|
||||
entry, err := c.GetTreeEntryByPath(".gitmodules")
|
||||
if err != nil {
|
||||
if _, ok := err.(ErrNotExist); ok {
|
||||
return nil, nil
|
||||
return nil, nil //nolint:nilnil // return nil to indicate that the submodule does not exist
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
@@ -48,5 +48,5 @@ func (c *Commit) GetSubModule(entryName string) (*SubModule, error) {
|
||||
return module, nil
|
||||
}
|
||||
}
|
||||
return nil, nil
|
||||
return nil, nil //nolint:nilnil // return nil to indicate that the submodule does not exist
|
||||
}
|
||||
|
||||
@@ -100,7 +100,7 @@ func (p *Parser) Err() error {
|
||||
func (p *Parser) parseRef(refBlock string) (map[string]string, error) {
|
||||
if refBlock == "" {
|
||||
// must be at EOF
|
||||
return nil, nil
|
||||
return nil, nil //nolint:nilnil // return nil to signal EOF
|
||||
}
|
||||
|
||||
fieldValues := make(map[string]string)
|
||||
|
||||
@@ -17,6 +17,7 @@ import (
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/tempdir"
|
||||
"code.gitea.io/gitea/modules/testlogger"
|
||||
|
||||
"github.com/hashicorp/go-version"
|
||||
)
|
||||
@@ -185,21 +186,19 @@ func InitFull() (err error) {
|
||||
// RunGitTests helps to init the git module and run tests.
|
||||
// FIXME: GIT-PACKAGE-DEPENDENCY: the dependency is not right, setting.Git.HomePath is initialized in this package but used in gitcmd package
|
||||
func RunGitTests(m interface{ Run() int }) {
|
||||
fatalf := func(exitCode int, format string, args ...any) {
|
||||
_, _ = fmt.Fprintf(os.Stderr, format, args...)
|
||||
os.Exit(exitCode)
|
||||
}
|
||||
os.Exit(runGitTests(m))
|
||||
}
|
||||
|
||||
func runGitTests(m interface{ Run() int }) int {
|
||||
gitHomePath, cleanup, err := tempdir.OsTempDir("gitea-test").MkdirTempRandom("git-home")
|
||||
if err != nil {
|
||||
fatalf(1, "unable to create temp dir: %s", err.Error())
|
||||
testlogger.Panicf("unable to create temp dir: %s", err.Error())
|
||||
}
|
||||
defer cleanup()
|
||||
|
||||
setting.Git.HomePath = gitHomePath
|
||||
if err = InitFull(); err != nil {
|
||||
fatalf(1, "failed to call Init: %s", err.Error())
|
||||
}
|
||||
if exitCode := m.Run(); exitCode != 0 {
|
||||
fatalf(exitCode, "run test failed, ExitCode=%d", exitCode)
|
||||
testlogger.Panicf("failed to call Init: %s", err.Error())
|
||||
}
|
||||
return m.Run()
|
||||
}
|
||||
|
||||
@@ -12,23 +12,27 @@ import (
|
||||
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/tempdir"
|
||||
"code.gitea.io/gitea/modules/testlogger"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
func testMain(m *testing.M) int {
|
||||
// FIXME: GIT-PACKAGE-DEPENDENCY: the dependency is not right.
|
||||
// "setting.Git.HomePath" is initialized in "git" package but really used in "gitcmd" package
|
||||
gitHomePath, cleanup, err := tempdir.OsTempDir("gitea-test").MkdirTempRandom("git-home")
|
||||
if err != nil {
|
||||
_, _ = fmt.Fprintf(os.Stderr, "unable to create temp dir: %v", err)
|
||||
os.Exit(1)
|
||||
testlogger.Panicf("failed to create temp dir: %v", err)
|
||||
}
|
||||
defer cleanup()
|
||||
|
||||
setting.Git.HomePath = gitHomePath
|
||||
os.Exit(m.Run())
|
||||
return m.Run()
|
||||
}
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
os.Exit(testMain(m))
|
||||
}
|
||||
|
||||
func TestRunWithContextStd(t *testing.T) {
|
||||
|
||||
@@ -108,7 +108,7 @@ func GetLanguageStats(repo *git_module.Repository, commitID string) (map[string]
|
||||
if (!isVendored.Has() && analyze.IsVendor(f.Name)) ||
|
||||
enry.IsDotFile(f.Name) ||
|
||||
(!isDocumentation.Has() && enry.IsDocumentation(f.Name)) ||
|
||||
enry.IsConfiguration(f.Name) {
|
||||
(!isDetectable.Has() && enry.IsConfiguration(f.Name)) {
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -132,7 +132,7 @@ func GetLanguageStats(repo *git.Repository, commitID string) (map[string]int64,
|
||||
if (!isVendored.Has() && analyze.IsVendor(f.Name())) ||
|
||||
enry.IsDotFile(f.Name()) ||
|
||||
(!isDocumentation.Has() && enry.IsDocumentation(f.Name())) ||
|
||||
enry.IsConfiguration(f.Name()) {
|
||||
(!isDetectable.Has() && enry.IsConfiguration(f.Name())) {
|
||||
continue
|
||||
}
|
||||
|
||||
|
||||
@@ -55,12 +55,12 @@ func (c *LastCommitCache) Put(ref, entryPath, commitID string) error {
|
||||
// Get gets the last commit information by commit id and entry path
|
||||
func (c *LastCommitCache) Get(ref, entryPath string) (*Commit, error) {
|
||||
if c == nil || c.cache == nil {
|
||||
return nil, nil
|
||||
return nil, nil //nolint:nilnil // return nil when cache is not available
|
||||
}
|
||||
|
||||
commitID, ok := c.cache.Get(getCacheKey(c.repoPath, ref, entryPath))
|
||||
if !ok || commitID == "" {
|
||||
return nil, nil
|
||||
return nil, nil //nolint:nilnil // return nil when cache miss
|
||||
}
|
||||
|
||||
log.Debug("LastCommitCache hit level 1: [%s:%s:%s]", ref, entryPath, commitID)
|
||||
|
||||
@@ -106,7 +106,7 @@ func (g *LogNameStatusRepoParser) Next(treepath string, paths2ids map[string]int
|
||||
case bufio.ErrBufferFull:
|
||||
g.buffull = true
|
||||
case io.EOF:
|
||||
return nil, nil
|
||||
return nil, nil //nolint:nilnil // return nil to signal EOF
|
||||
default:
|
||||
return nil, err
|
||||
}
|
||||
@@ -121,7 +121,7 @@ func (g *LogNameStatusRepoParser) Next(treepath string, paths2ids map[string]int
|
||||
case bufio.ErrBufferFull:
|
||||
g.buffull = true
|
||||
case io.EOF:
|
||||
return nil, nil
|
||||
return nil, nil //nolint:nilnil // return nil to signal EOF
|
||||
default:
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -13,12 +13,13 @@ import (
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
// resolve repository path relative to the test directory
|
||||
testRootDir := setting.SetupGiteaTestEnv()
|
||||
setting.SetupGiteaTestEnv()
|
||||
giteaRoot := setting.GetGiteaTestSourceRoot()
|
||||
repoPath = func(repo Repository) string {
|
||||
if filepath.IsAbs(repo.RelativePath()) {
|
||||
return repo.RelativePath() // for testing purpose only
|
||||
}
|
||||
return filepath.Join(testRootDir, "modules/git/tests/repos", repo.RelativePath())
|
||||
return filepath.Join(giteaRoot, "modules/git/tests/repos", repo.RelativePath())
|
||||
}
|
||||
git.RunGitTests(m)
|
||||
}
|
||||
|
||||
@@ -290,11 +290,11 @@ func getActiveListenersToUnlink() []bool {
|
||||
func getNotifySocket() (*net.UnixConn, error) {
|
||||
if err := getProvidedFDs(); err != nil {
|
||||
// This error will be logged elsewhere
|
||||
return nil, nil
|
||||
return nil, nil //nolint:nilnil // return nil when no provided FDs are available
|
||||
}
|
||||
|
||||
if notifySocketAddr == "" {
|
||||
return nil, nil
|
||||
return nil, nil //nolint:nilnil // return nil when notify socket is not configured
|
||||
}
|
||||
|
||||
socketAddr := &net.UnixAddr{
|
||||
|
||||
@@ -8,7 +8,6 @@ import (
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
@@ -20,10 +19,6 @@ const (
|
||||
dummyToken = "10000000-aaaa-bbbb-cccc-000000000001"
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
os.Exit(m.Run())
|
||||
}
|
||||
|
||||
type mockTransport struct{}
|
||||
|
||||
func (mockTransport) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||
|
||||
@@ -21,7 +21,8 @@ const mapKeyLowerPrefix = "lower/"
|
||||
// chromaLexers is fully managed by us to do fast lookup for chroma lexers by file name or language name
|
||||
// Don't use lexers.Get because it is very slow in many cases (iterate all rules, filepath glob match, etc.)
|
||||
var chromaLexers = sync.OnceValue(func() (ret struct {
|
||||
conflictingExtLangMap map[string]string
|
||||
conflictingExtLangMap map[string]string
|
||||
conflictingAliasLangMap map[string]string
|
||||
|
||||
lowerNameMap map[string]chroma.Lexer // lexer name (lang name) in lower-case
|
||||
fileBaseMap map[string]chroma.Lexer
|
||||
@@ -36,9 +37,9 @@ var chromaLexers = sync.OnceValue(func() (ret struct {
|
||||
ret.fileBaseMap = make(map[string]chroma.Lexer)
|
||||
ret.fileExtMap = make(map[string]chroma.Lexer)
|
||||
|
||||
// Chroma has overlaps in file extension for different languages,
|
||||
// Chroma has conflicts in file extension for different languages,
|
||||
// When we need to do fast render, there is no way to detect the language by content,
|
||||
// So we can only choose some default languages for the overlapped file extensions.
|
||||
// So we can only choose some default languages for the conflicted file extensions.
|
||||
ret.conflictingExtLangMap = map[string]string{
|
||||
".as": "ActionScript 3", // ActionScript
|
||||
".asm": "NASM", // TASM, NASM, RGBDS Assembly, Z80 Assembly
|
||||
@@ -71,12 +72,17 @@ var chromaLexers = sync.OnceValue(func() (ret struct {
|
||||
".v": "V", // verilog
|
||||
".xslt": "HTML", // XML
|
||||
}
|
||||
// use widely used language names as the default mapping to resolve name alias conflict
|
||||
ret.conflictingAliasLangMap = map[string]string{
|
||||
"hcl": "HCL", // Terraform
|
||||
"v": "V", // verilog
|
||||
}
|
||||
|
||||
isPlainPattern := func(key string) bool {
|
||||
return !strings.ContainsAny(key, "*?[]") // only support simple patterns
|
||||
}
|
||||
|
||||
setMapWithLowerKey := func(m map[string]chroma.Lexer, key string, lexer chroma.Lexer) {
|
||||
setFileNameMapWithLowerKey := func(m map[string]chroma.Lexer, key string, lexer chroma.Lexer) {
|
||||
if _, conflict := m[key]; conflict {
|
||||
panic("duplicate key in lexer map: " + key + ", need to add it to conflictingExtLangMap")
|
||||
}
|
||||
@@ -87,7 +93,7 @@ var chromaLexers = sync.OnceValue(func() (ret struct {
|
||||
processFileName := func(fileName string, lexer chroma.Lexer) bool {
|
||||
if isPlainPattern(fileName) {
|
||||
// full base name match
|
||||
setMapWithLowerKey(ret.fileBaseMap, fileName, lexer)
|
||||
setFileNameMapWithLowerKey(ret.fileBaseMap, fileName, lexer)
|
||||
return true
|
||||
}
|
||||
if strings.HasPrefix(fileName, "*") {
|
||||
@@ -96,7 +102,7 @@ var chromaLexers = sync.OnceValue(func() (ret struct {
|
||||
if isPlainPattern(fileExt) {
|
||||
presetName := ret.conflictingExtLangMap[fileExt]
|
||||
if presetName == "" || lexer.Config().Name == presetName {
|
||||
setMapWithLowerKey(ret.fileExtMap, fileExt, lexer)
|
||||
setFileNameMapWithLowerKey(ret.fileExtMap, fileExt, lexer)
|
||||
}
|
||||
return true
|
||||
}
|
||||
@@ -134,13 +140,30 @@ var chromaLexers = sync.OnceValue(func() (ret struct {
|
||||
return patterns
|
||||
}
|
||||
|
||||
// add lexers to our map, for fast lookup
|
||||
processLexerNameAliases := func(lexer chroma.Lexer) {
|
||||
cfg := lexer.Config()
|
||||
lowerName := strings.ToLower(cfg.Name)
|
||||
if _, conflicted := ret.lowerNameMap[lowerName]; conflicted {
|
||||
panic("duplicate language name in lexer map: " + lowerName)
|
||||
}
|
||||
ret.lowerNameMap[lowerName] = lexer
|
||||
|
||||
for _, name := range cfg.Aliases {
|
||||
lowerName := strings.ToLower(name)
|
||||
if overriddenName, overridden := ret.conflictingAliasLangMap[lowerName]; overridden && overriddenName != cfg.Name {
|
||||
continue
|
||||
}
|
||||
if existingLexer, conflict := ret.lowerNameMap[lowerName]; conflict && existingLexer.Config().Name != cfg.Name {
|
||||
panic("duplicate alias in lexer map: " + name + ", conflict between " + existingLexer.Config().Name + " and " + cfg.Name)
|
||||
}
|
||||
ret.lowerNameMap[lowerName] = lexer
|
||||
}
|
||||
}
|
||||
|
||||
// the main loop: build our lookup maps for lexers
|
||||
for _, lexer := range lexers.GlobalLexerRegistry.Lexers {
|
||||
cfg := lexer.Config()
|
||||
ret.lowerNameMap[strings.ToLower(lexer.Config().Name)] = lexer
|
||||
for _, alias := range cfg.Aliases {
|
||||
ret.lowerNameMap[strings.ToLower(alias)] = lexer
|
||||
}
|
||||
processLexerNameAliases(lexer)
|
||||
for _, s := range expandGlobPatterns(cfg.Filenames) {
|
||||
if !processFileName(s, lexer) {
|
||||
panic("unsupported file name pattern in lexer: " + s)
|
||||
@@ -153,7 +176,12 @@ var chromaLexers = sync.OnceValue(func() (ret struct {
|
||||
}
|
||||
}
|
||||
|
||||
// final check: make sure the default ext-lang mapping is correct, nothing is missing
|
||||
// final check: make sure the default overriding mapping is correct, nothing is missing
|
||||
for lowerName, lexerName := range ret.conflictingAliasLangMap {
|
||||
if lexer, ok := ret.lowerNameMap[lowerName]; !ok || lexer.Config().Name != lexerName {
|
||||
panic("missing default name-lang mapping for: " + lowerName)
|
||||
}
|
||||
}
|
||||
for ext, lexerName := range ret.conflictingExtLangMap {
|
||||
if lexer, ok := ret.fileExtMap[ext]; !ok || lexer.Config().Name != lexerName {
|
||||
panic("missing default ext-lang mapping for: " + ext)
|
||||
|
||||
@@ -45,7 +45,7 @@ func BenchmarkRenderCodeByLexer(b *testing.B) {
|
||||
lexer := DetectChromaLexerByFileName("a.sql", "")
|
||||
b.StartTimer()
|
||||
for b.Loop() {
|
||||
// Really slow .......
|
||||
// Really slow ....... the regexp2 used by Chroma takes most of the time
|
||||
// BenchmarkRenderCodeByLexer-12 22 47159038 ns/op
|
||||
RenderCodeByLexer(lexer, code)
|
||||
}
|
||||
@@ -55,13 +55,14 @@ func TestDetectChromaLexer(t *testing.T) {
|
||||
globalVars().highlightMapping[".my-html"] = "HTML"
|
||||
t.Cleanup(func() { delete(globalVars().highlightMapping, ".my-html") })
|
||||
|
||||
cases := []struct {
|
||||
casesWithContent := []struct {
|
||||
fileName string
|
||||
language string
|
||||
content string
|
||||
expected string
|
||||
}{
|
||||
{"test.py", "", "", "Python"},
|
||||
{"test.v", "", "", "V"},
|
||||
{"test.v", "any-lang-name", "", "V"},
|
||||
|
||||
{"any-file", "javascript", "", "JavaScript"},
|
||||
{"any-file", "", "/* vim: set filetype=python */", "Python"},
|
||||
@@ -80,11 +81,36 @@ func TestDetectChromaLexer(t *testing.T) {
|
||||
{"a.sql", "", "", "SQL"},
|
||||
{"dhcpd.conf", "", "", "ISCdhcpd"},
|
||||
{".env.my-production", "", "", "Bash"},
|
||||
|
||||
{"a.hcl", "", "", "HCL"}, // not the same as Chroma, enry detects "*.hcl" as "HCL"
|
||||
{"a.hcl", "HCL", "", "HCL"},
|
||||
{"a.hcl", "Terraform", "", "Terraform"},
|
||||
}
|
||||
for _, c := range cases {
|
||||
for _, c := range casesWithContent {
|
||||
lexer := detectChromaLexerWithAnalyze(c.fileName, c.language, []byte(c.content))
|
||||
if assert.NotNil(t, lexer, "case: %+v", c) {
|
||||
assert.Equal(t, c.expected, lexer.Config().Name, "case: %+v", c)
|
||||
}
|
||||
}
|
||||
|
||||
casesNameLang := []struct {
|
||||
fileName string
|
||||
language string
|
||||
expected string
|
||||
byLang bool
|
||||
}{
|
||||
{"a.v", "", "V", false},
|
||||
{"a.v", "V", "V", true},
|
||||
{"a.v", "verilog", "verilog", true},
|
||||
{"a.v", "any-lang-name", "V", false},
|
||||
|
||||
{"a.hcl", "", "Terraform", false}, // not the same as enry
|
||||
{"a.hcl", "HCL", "HCL", true},
|
||||
{"a.hcl", "Terraform", "Terraform", true},
|
||||
}
|
||||
for _, c := range casesNameLang {
|
||||
lexer, byLang := detectChromaLexerByFileName(c.fileName, c.language)
|
||||
assert.Equal(t, c.expected, lexer.Config().Name, "case: %+v", c)
|
||||
assert.Equal(t, c.byLang, byLang, "case: %+v", c)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/modules/base"
|
||||
"code.gitea.io/gitea/modules/httplib"
|
||||
"code.gitea.io/gitea/modules/references"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
|
||||
@@ -121,6 +122,11 @@ func fullHashPatternProcessor(ctx *RenderContext, node *html.Node) {
|
||||
if ret.QueryHash != "" {
|
||||
text += " (" + ret.QueryHash + ")"
|
||||
}
|
||||
// only turn commit links to the current instance into hash link
|
||||
if !httplib.IsCurrentGiteaSiteURL(ctx, ret.FullURL) {
|
||||
node = node.NextSibling
|
||||
continue
|
||||
}
|
||||
replaceContent(node, ret.PosStart, ret.PosEnd, createCodeLink(ret.FullURL, text, "commit"))
|
||||
node = node.NextSibling.NextSibling
|
||||
}
|
||||
@@ -167,6 +173,12 @@ func comparePatternProcessor(ctx *RenderContext, node *html.Node) {
|
||||
}
|
||||
}
|
||||
|
||||
// only turn compare links to the current instance into hash link
|
||||
if !httplib.IsCurrentGiteaSiteURL(ctx, urlFull) {
|
||||
node = node.NextSibling
|
||||
continue
|
||||
}
|
||||
|
||||
text := text1 + textDots + text2
|
||||
if hash != "" {
|
||||
text += " (" + hash + ")"
|
||||
|
||||
@@ -299,7 +299,7 @@ func TestRender_AutoLink(t *testing.T) {
|
||||
|
||||
// render other commit URLs
|
||||
tmp = "https://external-link.gitea.io/go-gitea/gitea/commit/d8a994ef243349f321568f9e36d5c3f444b99cae#diff-2"
|
||||
test(tmp, "<a href=\""+tmp+"\" class=\"commit\"><code>d8a994ef24 (diff-2)</code></a>")
|
||||
test(tmp, "<a href=\""+tmp+"\">"+tmp+"</a>")
|
||||
}
|
||||
|
||||
func TestRender_FullIssueURLs(t *testing.T) {
|
||||
|
||||
@@ -71,6 +71,7 @@ func TestRender_Commits(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestRender_CrossReferences(t *testing.T) {
|
||||
defer testModule.MockVariableValue(&setting.AppURL, markup.TestAppURL)()
|
||||
defer testModule.MockVariableValue(&markup.RenderBehaviorForTesting.DisableAdditionalAttributes, true)()
|
||||
test := func(input, expected string) {
|
||||
rctx := markup.NewTestRenderContext(markup.TestAppURL, localMetas).WithRelativePath("a.md")
|
||||
@@ -98,17 +99,17 @@ func TestRender_CrossReferences(t *testing.T) {
|
||||
util.URLJoin(markup.TestAppURL, "gogitea", "some-repo-name", "issues", "12345"),
|
||||
`<p><a href="`+util.URLJoin(markup.TestAppURL, "gogitea", "some-repo-name", "issues", "12345")+`" class="ref-issue" rel="nofollow">gogitea/some-repo-name#12345</a></p>`)
|
||||
|
||||
inputURL := "https://host/a/b/commit/0123456789012345678901234567890123456789/foo.txt?a=b#L2-L3"
|
||||
inputURL := setting.AppURL + "a/b/commit/0123456789012345678901234567890123456789/foo.txt?a=b#L2-L3"
|
||||
test(
|
||||
inputURL,
|
||||
`<p><a href="`+inputURL+`" rel="nofollow"><code>0123456789/foo.txt (L2-L3)</code></a></p>`)
|
||||
|
||||
inputURL = "https://example.com/repo/owner/archive/0123456789012345678901234567890123456789.tar.gz"
|
||||
inputURL = setting.AppURL + "repo/owner/archive/0123456789012345678901234567890123456789.tar.gz"
|
||||
test(
|
||||
inputURL,
|
||||
`<p><a href="`+inputURL+`" rel="nofollow"><code>0123456789.tar.gz</code></a></p>`)
|
||||
|
||||
inputURL = "https://example.com/owner/repo/commit/0123456789012345678901234567890123456789.patch?key=val"
|
||||
inputURL = setting.AppURL + "owner/repo/commit/0123456789012345678901234567890123456789.patch?key=val"
|
||||
test(
|
||||
inputURL,
|
||||
`<p><a href="`+inputURL+`" rel="nofollow"><code>0123456789.patch</code></a></p>`)
|
||||
@@ -575,13 +576,15 @@ func TestFuzz(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestIssue18471(t *testing.T) {
|
||||
data := `http://domain/org/repo/compare/783b039...da951ce`
|
||||
defer testModule.MockVariableValue(&setting.AppURL, markup.TestAppURL)()
|
||||
|
||||
data := markup.TestAppURL + `org/repo/compare/783b039...da951ce`
|
||||
|
||||
var res strings.Builder
|
||||
err := markup.PostProcessDefault(markup.NewTestRenderContext(localMetas), strings.NewReader(data), &res)
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, `<a href="http://domain/org/repo/compare/783b039...da951ce" class="compare"><code>783b039...da951ce</code></a>`, res.String())
|
||||
assert.Equal(t, `<a href="`+markup.TestAppURL+`org/repo/compare/783b039...da951ce" class="compare"><code>783b039...da951ce</code></a>`, res.String())
|
||||
}
|
||||
|
||||
func TestIsFullURL(t *testing.T) {
|
||||
|
||||
@@ -483,6 +483,9 @@ foo: bar
|
||||
}
|
||||
|
||||
func TestRenderLinks(t *testing.T) {
|
||||
defer test.MockVariableValue(&setting.AppURL, AppURL)()
|
||||
defer test.MockVariableValue(&markup.RenderBehaviorForTesting.DisableAdditionalAttributes, true)()
|
||||
|
||||
input := ` space @mention-user${SPACE}${SPACE}
|
||||
/just/a/path.bin
|
||||
https://example.com/file.bin
|
||||
@@ -520,9 +523,9 @@ mail@domain.com
|
||||
<a href="https://example.com/image.jpg" target="_blank" rel="nofollow noopener"><img src="https://example.com/image.jpg" alt="remote image"/></a>
|
||||
<a href="/image.jpg" rel="nofollow"><img src="/image.jpg" title="local image" alt="local image"/></a>
|
||||
<a href="https://example.com/image.jpg" rel="nofollow"><img src="https://example.com/image.jpg" title="remote link" alt="remote link"/></a>
|
||||
<a href="https://example.com/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash" rel="nofollow"><code>88fc37a3c0...12fc37a3c0 (hash)</code></a>
|
||||
<a href="https://example.com/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash" rel="nofollow">https://example.com/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash</a>
|
||||
com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb pare
|
||||
<a href="https://example.com/user/repo/commit/88fc37a3c0a4dda553bdcfc80c178a58247f42fb" rel="nofollow"><code>88fc37a3c0</code></a>
|
||||
<a href="https://example.com/user/repo/commit/88fc37a3c0a4dda553bdcfc80c178a58247f42fb" rel="nofollow">https://example.com/user/repo/commit/88fc37a3c0a4dda553bdcfc80c178a58247f42fb</a>
|
||||
com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb mit
|
||||
<span class="emoji" aria-label="thumbs up">👍</span>
|
||||
<a href="mailto:mail@domain.com" rel="nofollow">mail@domain.com</a>
|
||||
@@ -530,10 +533,21 @@ com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb mit
|
||||
#123
|
||||
space</p>
|
||||
`
|
||||
defer test.MockVariableValue(&markup.RenderBehaviorForTesting.DisableAdditionalAttributes, true)()
|
||||
result, err := markdown.RenderString(markup.NewTestRenderContext(localMetas), input)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, expected, string(result))
|
||||
|
||||
t.Run("LocalCommitAndCompare", func(t *testing.T) {
|
||||
input := `http://localhost:3000/user/repo/commit/88fc37a3c0a4dda553bdcfc80c178a58247f42fb
|
||||
http://localhost:3000/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash`
|
||||
|
||||
expected := `<p><a href="http://localhost:3000/user/repo/commit/88fc37a3c0a4dda553bdcfc80c178a58247f42fb" rel="nofollow"><code>88fc37a3c0</code></a>
|
||||
<a href="http://localhost:3000/user/repo/compare/88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb#hash" rel="nofollow"><code>88fc37a3c0...12fc37a3c0 (hash)</code></a></p>
|
||||
`
|
||||
result, err := markdown.RenderString(markup.NewTestRenderContext(localMetas), input)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, expected, string(result))
|
||||
})
|
||||
}
|
||||
|
||||
func TestMarkdownLink(t *testing.T) {
|
||||
|
||||
@@ -37,7 +37,7 @@ func (o *Option[T]) UnmarshalYAML(value *yaml.Node) error {
|
||||
|
||||
func (o Option[T]) MarshalYAML() (any, error) {
|
||||
if !o.Has() {
|
||||
return nil, nil
|
||||
return nil, nil //nolint:nilnil // return nil to indicate no value to marshal
|
||||
}
|
||||
|
||||
value := new(yaml.Node)
|
||||
|
||||
@@ -65,7 +65,6 @@ func (o *VirtualSessionProvider) Read(sid string) (session.RawStore, error) {
|
||||
return nil, fmt.Errorf("check if '%s' exist failed: %w", sid, err)
|
||||
}
|
||||
kv := make(map[any]any)
|
||||
kv["_old_uid"] = "0"
|
||||
return NewVirtualStore(o, sid, kv), nil
|
||||
}
|
||||
|
||||
@@ -160,7 +159,7 @@ func (s *VirtualStore) Release() error {
|
||||
// Now need to lock the provider
|
||||
s.p.lock.Lock()
|
||||
defer s.p.lock.Unlock()
|
||||
if oldUID, ok := s.data["_old_uid"]; (ok && (oldUID != "0" || len(s.data) > 1)) || (!ok && len(s.data) > 0) {
|
||||
if len(s.data) > 0 {
|
||||
// Now ensure that we don't exist!
|
||||
realProvider := s.p.provider
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
package setting
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -25,10 +26,12 @@ var (
|
||||
EndlessTaskTimeout time.Duration `ini:"ENDLESS_TASK_TIMEOUT"`
|
||||
AbandonedJobTimeout time.Duration `ini:"ABANDONED_JOB_TIMEOUT"`
|
||||
SkipWorkflowStrings []string `ini:"SKIP_WORKFLOW_STRINGS"`
|
||||
WorkflowDirs []string `ini:"WORKFLOW_DIRS"`
|
||||
}{
|
||||
Enabled: true,
|
||||
DefaultActionsURL: defaultActionsURLGitHub,
|
||||
SkipWorkflowStrings: []string{"[skip ci]", "[ci skip]", "[no ci]", "[skip actions]", "[actions skip]"},
|
||||
WorkflowDirs: []string{".gitea/workflows", ".github/workflows"},
|
||||
}
|
||||
)
|
||||
|
||||
@@ -119,5 +122,20 @@ func loadActionsFrom(rootCfg ConfigProvider) error {
|
||||
return fmt.Errorf("invalid [actions] LOG_COMPRESSION: %q", Actions.LogCompression)
|
||||
}
|
||||
|
||||
workflowDirs := make([]string, 0, len(Actions.WorkflowDirs))
|
||||
for _, dir := range Actions.WorkflowDirs {
|
||||
dir = strings.TrimSpace(dir)
|
||||
if dir == "" {
|
||||
continue
|
||||
}
|
||||
dir = strings.ReplaceAll(dir, `\`, `/`)
|
||||
dir = strings.TrimRight(dir, "/")
|
||||
workflowDirs = append(workflowDirs, dir)
|
||||
}
|
||||
if len(workflowDirs) == 0 {
|
||||
return errors.New("[actions] WORKFLOW_DIRS must contain at least one entry")
|
||||
}
|
||||
Actions.WorkflowDirs = workflowDirs
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -97,6 +97,65 @@ STORAGE_TYPE = minio
|
||||
assert.Equal(t, "actions_artifacts", filepath.Base(Actions.ArtifactStorage.Path))
|
||||
}
|
||||
|
||||
func Test_WorkflowDirs(t *testing.T) {
|
||||
oldActions := Actions
|
||||
defer func() {
|
||||
Actions = oldActions
|
||||
}()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
iniStr string
|
||||
wantDirs []string
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "default",
|
||||
iniStr: `[actions]`,
|
||||
wantDirs: []string{".gitea/workflows", ".github/workflows"},
|
||||
},
|
||||
{
|
||||
name: "single dir",
|
||||
iniStr: "[actions]\nWORKFLOW_DIRS = .github/workflows",
|
||||
wantDirs: []string{".github/workflows"},
|
||||
},
|
||||
{
|
||||
name: "custom order",
|
||||
iniStr: "[actions]\nWORKFLOW_DIRS = .github/workflows,.gitea/workflows",
|
||||
wantDirs: []string{".github/workflows", ".gitea/workflows"},
|
||||
},
|
||||
{
|
||||
name: "whitespace trimming",
|
||||
iniStr: "[actions]\nWORKFLOW_DIRS = .gitea/workflows , .github/workflows ",
|
||||
wantDirs: []string{".gitea/workflows", ".github/workflows"},
|
||||
},
|
||||
{
|
||||
name: "trailing slash normalization",
|
||||
iniStr: "[actions]\nWORKFLOW_DIRS = .gitea/workflows/,.github/workflows/",
|
||||
wantDirs: []string{".gitea/workflows", ".github/workflows"},
|
||||
},
|
||||
{
|
||||
name: "only commas and whitespace",
|
||||
iniStr: "[actions]\nWORKFLOW_DIRS = , , ,",
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
cfg, err := NewConfigProviderFromData(tt.iniStr)
|
||||
require.NoError(t, err)
|
||||
err = loadActionsFrom(cfg)
|
||||
if tt.wantErr {
|
||||
require.Error(t, err)
|
||||
return
|
||||
}
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, tt.wantDirs, Actions.WorkflowDirs)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_getDefaultActionsURLForActions(t *testing.T) {
|
||||
oldActions := Actions
|
||||
oldAppURL := AppURL
|
||||
|
||||
@@ -13,7 +13,18 @@ import (
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
)
|
||||
|
||||
func SetupGiteaTestEnv() string {
|
||||
var giteaTestSourceRoot *string
|
||||
|
||||
func GetGiteaTestSourceRoot() string {
|
||||
return *giteaTestSourceRoot
|
||||
}
|
||||
|
||||
func SetupGiteaTestEnv() {
|
||||
if giteaTestSourceRoot != nil {
|
||||
return // already initialized
|
||||
}
|
||||
|
||||
IsInTesting = true
|
||||
giteaRoot := os.Getenv("GITEA_TEST_ROOT")
|
||||
if giteaRoot == "" {
|
||||
_, filename, _, _ := runtime.Caller(0)
|
||||
@@ -27,6 +38,7 @@ func SetupGiteaTestEnv() string {
|
||||
appWorkPathBuiltin = giteaRoot
|
||||
AppWorkPath = giteaRoot
|
||||
AppPath = filepath.Join(giteaRoot, "gitea") + util.Iif(IsWindows, ".exe", "")
|
||||
StaticRootPath = giteaRoot // need to load assets (options, public) from the source code directory for testing
|
||||
|
||||
// giteaConf (GITEA_CONF) must be relative because it is used in the git hooks as "$GITEA_ROOT/$GITEA_CONF"
|
||||
giteaConf := os.Getenv("GITEA_TEST_CONF")
|
||||
@@ -56,6 +68,5 @@ func SetupGiteaTestEnv() string {
|
||||
// TODO: some git repo hooks (test fixtures) still use these env variables, need to be refactored in the future
|
||||
_ = os.Setenv("GITEA_ROOT", giteaRoot)
|
||||
_ = os.Setenv("GITEA_CONF", giteaConf) // test fixture git hooks use "$GITEA_ROOT/$GITEA_CONF" in their scripts
|
||||
|
||||
return giteaRoot
|
||||
giteaTestSourceRoot = &giteaRoot
|
||||
}
|
||||
|
||||
@@ -93,14 +93,14 @@ func dateTimeFormat(format string, datetime any) template.HTML {
|
||||
attrs := []string{`weekday=""`, `year="numeric"`}
|
||||
switch format {
|
||||
case "short", "long": // date only
|
||||
attrs = append(attrs, `month="`+format+`"`, `day="numeric"`)
|
||||
return template.HTML(fmt.Sprintf(`<absolute-date %s date="%s">%s</absolute-date>`, strings.Join(attrs, " "), datetimeEscaped, textEscaped))
|
||||
attrs = append(attrs, `threshold="P0Y"`, `month="`+format+`"`, `day="numeric"`, `prefix=""`)
|
||||
case "full": // full date including time
|
||||
attrs = append(attrs, `format="datetime"`, `month="short"`, `day="numeric"`, `hour="numeric"`, `minute="numeric"`, `second="numeric"`, `data-tooltip-content`, `data-tooltip-interactive="true"`)
|
||||
return template.HTML(fmt.Sprintf(`<relative-time %s datetime="%s">%s</relative-time>`, strings.Join(attrs, " "), datetimeEscaped, textEscaped))
|
||||
default:
|
||||
panic("Unsupported format " + format)
|
||||
}
|
||||
|
||||
return template.HTML(fmt.Sprintf(`<relative-time %s datetime="%s">%s</relative-time>`, strings.Join(attrs, " "), datetimeEscaped, textEscaped))
|
||||
}
|
||||
|
||||
func timeSinceTo(then any, now time.Time) template.HTML {
|
||||
|
||||
@@ -32,10 +32,10 @@ func TestDateTime(t *testing.T) {
|
||||
assert.EqualValues(t, "-", du.AbsoluteShort(timeutil.TimeStamp(0)))
|
||||
|
||||
actual := du.AbsoluteShort(refTime)
|
||||
assert.EqualValues(t, `<absolute-date weekday="" year="numeric" month="short" day="numeric" date="2018-01-01T00:00:00Z">2018-01-01</absolute-date>`, actual)
|
||||
assert.EqualValues(t, `<relative-time weekday="" year="numeric" threshold="P0Y" month="short" day="numeric" prefix="" datetime="2018-01-01T00:00:00Z">2018-01-01</relative-time>`, actual)
|
||||
|
||||
actual = du.AbsoluteShort(refTimeStamp)
|
||||
assert.EqualValues(t, `<absolute-date weekday="" year="numeric" month="short" day="numeric" date="2017-12-31T19:00:00-05:00">2017-12-31</absolute-date>`, actual)
|
||||
assert.EqualValues(t, `<relative-time weekday="" year="numeric" threshold="P0Y" month="short" day="numeric" prefix="" datetime="2017-12-31T19:00:00-05:00">2017-12-31</relative-time>`, actual)
|
||||
|
||||
actual = du.FullTime(refTimeStamp)
|
||||
assert.EqualValues(t, `<relative-time weekday="" year="numeric" format="datetime" month="short" day="numeric" hour="numeric" minute="numeric" second="numeric" data-tooltip-content data-tooltip-interactive="true" datetime="2017-12-31T19:00:00-05:00">2017-12-31 19:00:00 -05:00</relative-time>`, actual)
|
||||
|
||||
@@ -173,7 +173,7 @@ func Init() {
|
||||
log.RegisterEventWriter("test", newTestLoggerWriter)
|
||||
}
|
||||
|
||||
func Fatalf(format string, args ...any) {
|
||||
Printf(format+"\n", args...)
|
||||
os.Exit(1)
|
||||
func Panicf(format string, args ...any) {
|
||||
// don't call os.Exit, otherwise the "defer" functions won't be executed
|
||||
panic(fmt.Sprintf(format, args...))
|
||||
}
|
||||
|
||||
@@ -32,11 +32,7 @@ func TestMain(m *testing.M) {
|
||||
// setup
|
||||
translation.InitLocales(context.Background())
|
||||
BaseDate = time.Date(2000, time.January, 1, 0, 0, 0, 0, time.UTC)
|
||||
|
||||
// run the tests
|
||||
retVal := m.Run()
|
||||
|
||||
os.Exit(retVal)
|
||||
os.Exit(m.Run())
|
||||
}
|
||||
|
||||
func TestTimeSincePro(t *testing.T) {
|
||||
|
||||
@@ -1,10 +1,24 @@
|
||||
This is free and unencumbered software released into the public domain.
|
||||
|
||||
Anyone is free to copy, modify, publish, use, compile, sell, or distribute this software, either in source code form or as a compiled binary, for any purpose, commercial or non-commercial, and by any means.
|
||||
Anyone is free to copy, modify, publish, use, compile, sell, or
|
||||
distribute this software, either in source code form or as a compiled
|
||||
binary, for any purpose, commercial or non-commercial, and by any
|
||||
means.
|
||||
|
||||
In jurisdictions that recognize copyright laws, the author or authors of this software dedicate any and all copyright interest in the software to the public domain. We make this dedication for the benefit of the public at large and to the detriment of our heirs and
|
||||
successors. We intend this dedication to be an overt act of relinquishment in perpetuity of all present and future rights to this software under copyright law.
|
||||
In jurisdictions that recognize copyright laws, the author or authors
|
||||
of this software dedicate any and all copyright interest in the
|
||||
software to the public domain. We make this dedication for the benefit
|
||||
of the public at large and to the detriment of our heirs and
|
||||
successors. We intend this dedication to be an overt act of
|
||||
relinquishment in perpetuity of all present and future rights to this
|
||||
software under copyright law.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||
IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
|
||||
OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
|
||||
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
||||
OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
For more information, please refer to <http://unlicense.org/>
|
||||
For more information, please refer to <https://unlicense.org/>
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
"link_account": "アカウント連携",
|
||||
"register": "登録",
|
||||
"version": "バージョン",
|
||||
"powered_by": "Powered by %s",
|
||||
"page": "ページ",
|
||||
"template": "テンプレート",
|
||||
"language": "言語",
|
||||
@@ -31,6 +32,7 @@
|
||||
"password": "パスワード",
|
||||
"access_token": "アクセストークン",
|
||||
"re_type": "パスワード確認",
|
||||
"captcha": "CAPTCHA",
|
||||
"twofa": "2要素認証",
|
||||
"twofa_scratch": "2要素認証スクラッチコード",
|
||||
"passcode": "パスコード",
|
||||
@@ -74,6 +76,7 @@
|
||||
"pull_requests": "プルリクエスト",
|
||||
"issues": "イシュー",
|
||||
"milestones": "マイルストーン",
|
||||
"ok": "OK",
|
||||
"cancel": "キャンセル",
|
||||
"retry": "再試行",
|
||||
"rerun": "再実行",
|
||||
@@ -130,6 +133,7 @@
|
||||
"confirm_delete_selected": "選択したすべてのアイテムを削除してよろしいですか?",
|
||||
"name": "名称",
|
||||
"value": "値",
|
||||
"readme": "Readme",
|
||||
"filter_title": "フィルター",
|
||||
"filter.clear": "フィルターをクリア",
|
||||
"filter.is_archived": "アーカイブ",
|
||||
@@ -144,6 +148,13 @@
|
||||
"filter.private": "プライベート",
|
||||
"no_results_found": "見つかりません。",
|
||||
"internal_error_skipped": "内部エラーが発生しましたがスキップされました: %s",
|
||||
"characters_spaces": "スペース",
|
||||
"characters_tabs": "タブ",
|
||||
"text_indent_style": "インデントスタイル",
|
||||
"text_indent_size": "インデントサイズ",
|
||||
"text_line_wrap": "折り返す",
|
||||
"text_line_nowrap": "折り返さない",
|
||||
"text_line_wrap_mode": "行折り返しモード",
|
||||
"search.search": "検索…",
|
||||
"search.type_tooltip": "検索タイプ",
|
||||
"search.fuzzy": "あいまい",
|
||||
@@ -183,6 +194,7 @@
|
||||
"editor.buttons.heading.tooltip": "見出し追加",
|
||||
"editor.buttons.bold.tooltip": "太字追加",
|
||||
"editor.buttons.italic.tooltip": "イタリック体追加",
|
||||
"editor.buttons.strikethrough.tooltip": "取り消し線のテキストを追加",
|
||||
"editor.buttons.quote.tooltip": "引用",
|
||||
"editor.buttons.code.tooltip": "コード追加",
|
||||
"editor.buttons.link.tooltip": "リンク追加",
|
||||
@@ -198,6 +210,8 @@
|
||||
"editor.buttons.switch_to_legacy.tooltip": "レガシーエディタを使用する",
|
||||
"editor.buttons.enable_monospace_font": "等幅フォントを有効にする",
|
||||
"editor.buttons.disable_monospace_font": "等幅フォントを無効にする",
|
||||
"filter.string.asc": "A–Z",
|
||||
"filter.string.desc": "Z–A",
|
||||
"error.occurred": "エラーが発生しました",
|
||||
"error.report_message": "Gitea のバグが疑われる場合は、<a href=\"%s\" target=\"_blank\">GitHub</a>でIssueを検索して、見つからなければ新しいIssueを作成してください。",
|
||||
"error.not_found": "ターゲットが見つかりませんでした。",
|
||||
@@ -224,6 +238,7 @@
|
||||
"install.db_name": "データベース名",
|
||||
"install.db_schema": "スキーマ",
|
||||
"install.db_schema_helper": "空の場合はデータベースのデフォルト(\"public\")となります。",
|
||||
"install.ssl_mode": "SSL",
|
||||
"install.path": "パス",
|
||||
"install.sqlite_helper": "SQLite3のデータベースファイルパス。<br>Giteaをサービスとして実行する場合は絶対パスを入力します。",
|
||||
"install.reinstall_error": "既存のGiteaデータベースへインストールしようとしています",
|
||||
@@ -401,6 +416,7 @@
|
||||
"auth.twofa_scratch_token_incorrect": "スクラッチコードが正しくありません。",
|
||||
"auth.twofa_required": "リポジトリにアクセスするには2段階認証を設定するか、再度ログインしてください。",
|
||||
"auth.login_userpass": "サインイン",
|
||||
"auth.login_openid": "OpenID",
|
||||
"auth.oauth_signup_tab": "新規アカウント登録",
|
||||
"auth.oauth_signup_title": "新規アカウントの仕上げ",
|
||||
"auth.oauth_signup_submit": "アカウント登録完了",
|
||||
@@ -505,6 +521,7 @@
|
||||
"form.Password": "パスワード",
|
||||
"form.Retype": "パスワード確認",
|
||||
"form.SSHTitle": "SSHキー名",
|
||||
"form.HttpsUrl": "HTTPS URL",
|
||||
"form.PayloadUrl": "ペイロードのURL",
|
||||
"form.TeamName": "チーム名",
|
||||
"form.AuthName": "承認名",
|
||||
@@ -648,6 +665,7 @@
|
||||
"settings.twofa": "2要素認証 (TOTP)",
|
||||
"settings.account_link": "連携アカウント",
|
||||
"settings.organization": "組織",
|
||||
"settings.uid": "UID",
|
||||
"settings.webauthn": "2要素認証 (セキュリティキー)",
|
||||
"settings.public_profile": "公開プロフィール",
|
||||
"settings.biography_placeholder": "自己紹介してください!(Markdownを使うことができます)",
|
||||
@@ -740,6 +758,7 @@
|
||||
"settings.add_email": "メールアドレスを追加",
|
||||
"settings.add_openid": "OpenID URIを追加する",
|
||||
"settings.add_email_confirmation_sent": "\"%s\" に確認メールを送信しました。 %s以内に受信トレイを確認し、メールアドレス確認を行ってください。",
|
||||
"settings.email_primary_not_found": "選択したメールアドレスが見つかりませんでした。",
|
||||
"settings.add_email_success": "新しいメールアドレスを追加しました。",
|
||||
"settings.email_preference_set_success": "メール設定を保存しました。",
|
||||
"settings.add_openid_success": "新しいOpenIDアドレスを追加しました。",
|
||||
@@ -966,6 +985,7 @@
|
||||
"repo.fork.blocked_user": "リポジトリのオーナーがあなたをブロックしているため、リポジトリをフォークできません。",
|
||||
"repo.use_template": "このテンプレートを使用",
|
||||
"repo.open_with_editor": "%s で開く",
|
||||
"repo.download_directory_as": "%sとしてディレクトリをダウンロード",
|
||||
"repo.download_zip": "ZIPファイルをダウンロード",
|
||||
"repo.download_tar": "TAR.GZファイルをダウンロード",
|
||||
"repo.download_bundle": "バンドルをダウンロード",
|
||||
@@ -985,6 +1005,7 @@
|
||||
"repo.multiple_licenses": "複数のライセンス",
|
||||
"repo.object_format": "オブジェクトのフォーマット",
|
||||
"repo.object_format_helper": "リポジトリのオブジェクトフォーマット。後で変更することはできません。SHA1 は最も互換性があります。",
|
||||
"repo.readme": "README",
|
||||
"repo.readme_helper": "READMEファイル テンプレートを選択してください。",
|
||||
"repo.readme_helper_desc": "プロジェクトについての説明をひととおり書く場所です。",
|
||||
"repo.auto_init": "リポジトリの初期設定 (.gitignore、ライセンスファイル、READMEファイルの追加)",
|
||||
@@ -997,6 +1018,7 @@
|
||||
"repo.default_branch": "デフォルトブランチ",
|
||||
"repo.default_branch_label": "デフォルト",
|
||||
"repo.default_branch_helper": "デフォルトブランチはプルリクエストとコードコミットのベースブランチとなります。",
|
||||
"repo.mirror_prune": "Prune",
|
||||
"repo.mirror_prune_desc": "不要になった古いリモートトラッキング参照を削除",
|
||||
"repo.mirror_interval": "ミラー間隔 (有効な時間の単位は'h'、'm'、's')。 定期的な同期を無効にする場合は0。(最小間隔: %s)",
|
||||
"repo.mirror_interval_invalid": "ミラー間隔が不正です。",
|
||||
@@ -1006,6 +1028,7 @@
|
||||
"repo.mirror_address_desc": "必要な資格情報は「認証」セクションに設定してください。",
|
||||
"repo.mirror_address_url_invalid": "入力したURLは無効です。 URLの構成要素はすべて正しくエスケープしてください。",
|
||||
"repo.mirror_address_protocol_invalid": "入力したURLは無効です。 ミラーできるのは、http(s):// または git:// からだけです。",
|
||||
"repo.mirror_lfs": "Large File Storage (LFS)",
|
||||
"repo.mirror_lfs_desc": "LFS データのミラーリングを有効にする。",
|
||||
"repo.mirror_lfs_endpoint": "LFS エンドポイント",
|
||||
"repo.mirror_lfs_endpoint_desc": "同期するときは、クローンURLをもとに<a target=\"_blank\" rel=\"noopener noreferrer\" href=\"%s\">LFSサーバーを決定</a>しようとします。 リポジトリのLFSデータがほかの場所に保存されている場合は、独自のエンドポイントを指定することができます。",
|
||||
@@ -1047,6 +1070,7 @@
|
||||
"repo.desc.template": "テンプレート",
|
||||
"repo.desc.internal": "内部",
|
||||
"repo.desc.archived": "アーカイブ",
|
||||
"repo.desc.sha256": "SHA256",
|
||||
"repo.template.items": "テンプレート項目",
|
||||
"repo.template.git_content": "Gitコンテンツ (デフォルトブランチ)",
|
||||
"repo.template.git_hooks": "Gitフック",
|
||||
@@ -1075,6 +1099,7 @@
|
||||
"repo.migrate_options_lfs_endpoint.description.local": "ローカルサーバーのパスもサポートされています。",
|
||||
"repo.migrate_options_lfs_endpoint.placeholder": "空にするとエンドポイントはクローン URL から決定されます。",
|
||||
"repo.migrate_items": "移行する項目",
|
||||
"repo.migrate_items_wiki": "Wiki",
|
||||
"repo.migrate_items_milestones": "マイルストーン",
|
||||
"repo.migrate_items_labels": "ラベル",
|
||||
"repo.migrate_items_issues": "イシュー",
|
||||
@@ -1124,6 +1149,7 @@
|
||||
"repo.migration_status": "移行状況",
|
||||
"repo.mirror_from": "ミラー元",
|
||||
"repo.forked_from": "フォーク元",
|
||||
"repo.generated_from": "generated from",
|
||||
"repo.fork_from_self": "自分が所有しているリポジトリはフォークできません。",
|
||||
"repo.fork_guest_user": "リポジトリをフォークするにはサインインしてください。",
|
||||
"repo.watch_guest_user": "リポジトリをウォッチするにはサインインしてください。",
|
||||
@@ -1157,6 +1183,7 @@
|
||||
"repo.pulls": "プルリクエスト",
|
||||
"repo.projects": "プロジェクト",
|
||||
"repo.packages": "パッケージ",
|
||||
"repo.actions": "Actions",
|
||||
"repo.labels": "ラベル",
|
||||
"repo.org_labels_desc": "組織で定義されているラベル (組織の<strong>すべてのリポジトリ</strong>で使用可能なもの)",
|
||||
"repo.org_labels_desc_manage": "編集",
|
||||
@@ -1167,8 +1194,11 @@
|
||||
"repo.release": "リリース",
|
||||
"repo.releases": "リリース",
|
||||
"repo.tag": "タグ",
|
||||
"repo.git_tag": "Gitタグ",
|
||||
"repo.released_this": "がこれをリリース",
|
||||
"repo.tagged_this": "がタグ付け",
|
||||
"repo.file.title": "%s at %s",
|
||||
"repo.file_raw": "Raw",
|
||||
"repo.file_history": "履歴",
|
||||
"repo.file_view_source": "ソースを表示",
|
||||
"repo.file_view_rendered": "レンダリング表示",
|
||||
@@ -1204,6 +1234,7 @@
|
||||
"repo.commit.contained_in_default_branch": "このコミットはデフォルトブランチに含まれています",
|
||||
"repo.commit.load_referencing_branches_and_tags": "このコミットを参照しているブランチやタグを取得",
|
||||
"repo.commit.merged_in_pr": "このコミットはプルリクエスト %s でマージされました。",
|
||||
"repo.blame": "Blame",
|
||||
"repo.download_file": "ファイルをダウンロード",
|
||||
"repo.normal_view": "通常表示",
|
||||
"repo.line": "行",
|
||||
@@ -1467,6 +1498,7 @@
|
||||
"repo.issues.filter_sort.feweststars": "スターが少ない順",
|
||||
"repo.issues.filter_sort.mostforks": "フォークが多い順",
|
||||
"repo.issues.filter_sort.fewestforks": "フォークが少ない順",
|
||||
"repo.issues.quick_goto": "イシューへ移動",
|
||||
"repo.issues.action_open": "オープン",
|
||||
"repo.issues.action_close": "クローズ",
|
||||
"repo.issues.action_label": "ラベル",
|
||||
@@ -1627,6 +1659,7 @@
|
||||
"repo.issues.push_commits_n": "が %d コミット追加 %s",
|
||||
"repo.issues.force_push_codes": "が %[1]s を強制プッシュ ( <a class=\"ui sha\" href=\"%[3]s\"><code>%[2]s</code></a> から <a class=\"ui sha\" href=\"%[5]s\"><code>%[4]s</code></a> へ ) %[6]s",
|
||||
"repo.issues.force_push_compare": "比較",
|
||||
"repo.issues.due_date_form": "yyyy-mm-dd",
|
||||
"repo.issues.due_date_form_add": "期日の追加",
|
||||
"repo.issues.due_date_form_edit": "変更",
|
||||
"repo.issues.due_date_form_remove": "削除",
|
||||
@@ -1678,6 +1711,7 @@
|
||||
"repo.issues.review.content.empty": "修正を指示するコメントを残す必要があります。",
|
||||
"repo.issues.review.reject": "が変更を要請 %s",
|
||||
"repo.issues.review.wait": "にレビュー依頼 %s",
|
||||
"repo.issues.review.codeowners_rules": "CODEOWNERSルール",
|
||||
"repo.issues.review.add_review_request": "が %s にレビューを依頼 %s",
|
||||
"repo.issues.review.remove_review_request": "が %s へのレビュー依頼を取り消し %s",
|
||||
"repo.issues.review.remove_review_request_self": "がレビューを辞退 %s",
|
||||
@@ -1713,17 +1747,20 @@
|
||||
"repo.issues.reference_link": "リファレンス: %s",
|
||||
"repo.compare.compare_base": "基準",
|
||||
"repo.compare.compare_head": "比較",
|
||||
"repo.compare.title": "変更の比較",
|
||||
"repo.compare.description": "ふたつのブランチまたはタグを選び、変更された内容を確認、あるいは新しいプルリクエストを開始してください。",
|
||||
"repo.pulls.desc": "プルリクエストとコードレビューの有効化。",
|
||||
"repo.pulls.new": "新しいプルリクエスト",
|
||||
"repo.pulls.new.description": "この比較における変更点について議論し、レビューします。",
|
||||
"repo.pulls.new.blocked_user": "リポジトリのオーナーがあなたをブロックしているため、プルリクエストを作成できません。",
|
||||
"repo.pulls.new.must_collaborator": "プルリクエストを作成するには、共同作業者である必要があります。",
|
||||
"repo.pulls.new.already_existed": "これらのブランチのプルリクエストはすでに存在します",
|
||||
"repo.pulls.edit.already_changed": "プルリクエストの変更を保存できません。 他のユーザーによって内容がすでに変更されているようです。 変更を上書きしないようにするため、ページを更新してからもう一度編集してください。",
|
||||
"repo.pulls.view": "プルリクエストを表示",
|
||||
"repo.pulls.compare_changes": "新規プルリクエスト",
|
||||
"repo.pulls.allow_edits_from_maintainers": "メンテナーからの編集を許可する",
|
||||
"repo.pulls.allow_edits_from_maintainers_desc": "ベースブランチへの書き込みアクセス権を持つユーザーは、このブランチにプッシュすることもできます",
|
||||
"repo.pulls.allow_edits_from_maintainers_err": "更新に失敗しました",
|
||||
"repo.pulls.compare_changes_desc": "マージ先ブランチとプル元ブランチを選択。",
|
||||
"repo.pulls.has_viewed_file": "閲覧済",
|
||||
"repo.pulls.has_changed_since_last_review": "前回のレビュー後に変更あり",
|
||||
"repo.pulls.viewed_files_label": "%[1]d / %[2]d ファイル閲覧済み",
|
||||
@@ -1749,6 +1786,8 @@
|
||||
"repo.pulls.title_desc": "が <code>%[2]s</code> から <code id=\"branch_target\">%[3]s</code> への %[1]d コミットのマージを希望しています",
|
||||
"repo.pulls.merged_title_desc": "が %[1]d 個のコミットを <code>%[2]s</code> から <code>%[3]s</code> へマージ %[4]s",
|
||||
"repo.pulls.change_target_branch_at": "がターゲットブランチを <b>%s</b> から <b>%s</b> に変更 %s",
|
||||
"repo.pulls.marked_as_work_in_progress_at": "がこのプルリクエストを作業中(WIP)とマーク %s",
|
||||
"repo.pulls.marked_as_ready_for_review_at": "がこのプルリクエストをレビュー可とマーク %s",
|
||||
"repo.pulls.tab_conversation": "会話",
|
||||
"repo.pulls.tab_commits": "コミット",
|
||||
"repo.pulls.tab_files": "変更されたファイル",
|
||||
@@ -1767,6 +1806,7 @@
|
||||
"repo.pulls.remove_prefix": "先頭の <strong>%s</strong> を除去",
|
||||
"repo.pulls.data_broken": "このプルリクエストは、フォークの情報が見つからないため壊れています。",
|
||||
"repo.pulls.files_conflicted": "このプルリクエストは、ターゲットブランチと競合する変更を含んでいます。",
|
||||
"repo.pulls.files_conflicted_no_listed_files": "(競合するファイルはありません)",
|
||||
"repo.pulls.is_checking": "マージのコンフリクトを確認中…",
|
||||
"repo.pulls.is_ancestor": "このブランチは既にターゲットブランチに含まれています。マージするものはありません。",
|
||||
"repo.pulls.is_empty": "このブランチの変更は既にターゲットブランチにあります。これは空のコミットになります。",
|
||||
@@ -1821,7 +1861,8 @@
|
||||
"repo.pulls.status_checking": "いくつかのステータスチェックが待機中です",
|
||||
"repo.pulls.status_checks_success": "ステータスチェックはすべて成功しました",
|
||||
"repo.pulls.status_checks_warning": "ステータスチェックにより警告が出ています",
|
||||
"repo.pulls.status_checks_failure": "失敗したステータスチェックがあります",
|
||||
"repo.pulls.status_checks_failure_required": "必須チェックに失敗しています",
|
||||
"repo.pulls.status_checks_failure_optional": "必須ではないチェックが失敗しています",
|
||||
"repo.pulls.status_checks_error": "ステータスチェックによりエラーが出ています",
|
||||
"repo.pulls.status_checks_requested": "必須",
|
||||
"repo.pulls.status_checks_details": "詳細",
|
||||
@@ -1911,6 +1952,7 @@
|
||||
"repo.signing.wont_sign.not_signed_in": "サインインしていません。",
|
||||
"repo.ext_wiki": "外部Wikiへのアクセス",
|
||||
"repo.ext_wiki.desc": "外部Wikiへのリンク。",
|
||||
"repo.wiki": "Wiki",
|
||||
"repo.wiki.welcome": "Wikiへようこそ。",
|
||||
"repo.wiki.welcome_desc": "Wikiを使って共同作業者とドキュメンテーションの作成と共有ができます。",
|
||||
"repo.wiki.desc": "共同作業者とのドキュメンテーションの作成と共有。",
|
||||
@@ -1937,6 +1979,7 @@
|
||||
"repo.wiki.page_name_desc": "この Wiki ページの名前を入力してください。いくつかの特別な名前として 'Home', '_Sidebar' と '_Footer' があります。",
|
||||
"repo.wiki.original_git_entry_tooltip": "フレンドリーリンクを使用する代わりにオリジナルのGitファイルを表示します。",
|
||||
"repo.activity": "アクティビティ",
|
||||
"repo.activity.navbar.pulse": "Pulse",
|
||||
"repo.activity.navbar.code_frequency": "コード更新頻度",
|
||||
"repo.activity.navbar.contributors": "貢献者",
|
||||
"repo.activity.navbar.recent_commits": "最近のコミット",
|
||||
@@ -2089,6 +2132,8 @@
|
||||
"repo.settings.pulls.ignore_whitespace": "空白文字のコンフリクトを無視する",
|
||||
"repo.settings.pulls.enable_autodetect_manual_merge": "手動マージの自動検出を有効にする (注意: 特殊なケースでは判定ミスが発生する場合があります)",
|
||||
"repo.settings.pulls.allow_rebase_update": "リベースでプルリクエストのブランチの更新を可能にする",
|
||||
"repo.settings.pulls.default_target_branch": "新しいプルリクエストのデフォルトのターゲットブランチ",
|
||||
"repo.settings.pulls.default_target_branch_default": "デフォルトブランチ (%s)",
|
||||
"repo.settings.pulls.default_delete_branch_after_merge": "デフォルトでプルリクエストのブランチをマージ後に削除する",
|
||||
"repo.settings.pulls.default_allow_edits_from_maintainers": "デフォルトでメンテナからの編集を許可する",
|
||||
"repo.settings.releases_desc": "リリースを有効にする",
|
||||
@@ -2210,6 +2255,8 @@
|
||||
"repo.settings.add_webhook_desc": "GiteaはターゲットURLに、指定したContent Typeで<code>POST</code>リクエストを送ります。 詳細は<a target=\"_blank\" rel=\"noopener noreferrer\" href=\"%s\">Webhookガイド</a>へ。",
|
||||
"repo.settings.payload_url": "ターゲットURL",
|
||||
"repo.settings.http_method": "HTTPメソッド",
|
||||
"repo.settings.content_type": "POST Content Type",
|
||||
"repo.settings.secret": "シークレット",
|
||||
"repo.settings.webhook_secret_desc": "Webhookサーバーがsecretの使用をサポートしている場合は、webhookのマニュアルに従いここにsecretを入力できます。",
|
||||
"repo.settings.slack_username": "ユーザー名",
|
||||
"repo.settings.slack_icon_url": "アイコンのURL",
|
||||
@@ -2227,6 +2274,7 @@
|
||||
"repo.settings.event_delete_desc": "ブランチやタグが削除されたとき。",
|
||||
"repo.settings.event_fork": "フォーク",
|
||||
"repo.settings.event_fork_desc": "リポジトリがフォークされたとき。",
|
||||
"repo.settings.event_wiki": "Wiki",
|
||||
"repo.settings.event_wiki_desc": "Wikiページが作成・名前変更・編集・削除されたとき。",
|
||||
"repo.settings.event_statuses": "ステータス",
|
||||
"repo.settings.event_statuses_desc": "APIによってコミットのステータスが更新されたとき。",
|
||||
@@ -2292,6 +2340,19 @@
|
||||
"repo.settings.slack_domain": "ドメイン",
|
||||
"repo.settings.slack_channel": "チャンネル",
|
||||
"repo.settings.add_web_hook_desc": "<a target=\"_blank\" rel=\"noreferrer\" href=\"%s\">%s</a> をリポジトリと組み合わせます。",
|
||||
"repo.settings.web_hook_name_gitea": "Gitea",
|
||||
"repo.settings.web_hook_name_gogs": "Gogs",
|
||||
"repo.settings.web_hook_name_slack": "Slack",
|
||||
"repo.settings.web_hook_name_discord": "Discord",
|
||||
"repo.settings.web_hook_name_dingtalk": "DingTalk",
|
||||
"repo.settings.web_hook_name_telegram": "Telegram",
|
||||
"repo.settings.web_hook_name_matrix": "Matrix",
|
||||
"repo.settings.web_hook_name_msteams": "Microsoft Teams",
|
||||
"repo.settings.web_hook_name_feishu_or_larksuite": "Feishu / Lark Suite",
|
||||
"repo.settings.web_hook_name_feishu": "Feishu",
|
||||
"repo.settings.web_hook_name_larksuite": "Lark Suite",
|
||||
"repo.settings.web_hook_name_wechatwork": "WeCom (Wechat Work)",
|
||||
"repo.settings.web_hook_name_packagist": "Packagist",
|
||||
"repo.settings.packagist_username": "Packagist ユーザー名",
|
||||
"repo.settings.packagist_api_token": "API トークン",
|
||||
"repo.settings.packagist_package_url": "Packagist パッケージ URL",
|
||||
@@ -2385,7 +2446,8 @@
|
||||
"repo.settings.block_outdated_branch_desc": "baseブランチがheadブランチより進んでいる場合、マージできないようにします。",
|
||||
"repo.settings.block_admin_merge_override": "管理者もブランチ保護のルールに従う",
|
||||
"repo.settings.block_admin_merge_override_desc": "管理者はブランチ保護のルールに従う必要があり、回避することはできません。",
|
||||
"repo.settings.default_branch_desc": "プルリクエストやコミット表示のデフォルトのブランチを選択:",
|
||||
"repo.settings.default_branch_desc": "コミット表示のデフォルトのブランチを選択します。",
|
||||
"repo.settings.default_target_branch_desc": "プルリクエストでは、リポジトリ拡張設定の「プルリクエスト」セクションで設定することで、別のデフォルトターゲットブランチを使用できます。",
|
||||
"repo.settings.merge_style_desc": "マージ スタイル",
|
||||
"repo.settings.default_merge_style_desc": "デフォルトのマージスタイル",
|
||||
"repo.settings.choose_branch": "ブランチを選択…",
|
||||
@@ -2437,6 +2499,7 @@
|
||||
"repo.settings.unarchive.success": "リポジトリのアーカイブを解除しました。",
|
||||
"repo.settings.unarchive.error": "リポジトリのアーカイブ解除でエラーが発生しました。 詳細はログを確認してください。",
|
||||
"repo.settings.update_avatar_success": "リポジトリのアバターを更新しました。",
|
||||
"repo.settings.lfs": "LFS",
|
||||
"repo.settings.lfs_filelist": "このリポジトリに含まれているLFSファイル",
|
||||
"repo.settings.lfs_no_lfs_files": "このリポジトリにLFSファイルはありません",
|
||||
"repo.settings.lfs_findcommits": "コミットを検索",
|
||||
@@ -2455,6 +2518,8 @@
|
||||
"repo.settings.lfs_lock_file_no_exist": "ロックしたファイルがデフォルトブランチにありません",
|
||||
"repo.settings.lfs_force_unlock": "強制ロック解除",
|
||||
"repo.settings.lfs_pointers.found": "%d件のblobポインタ — 登録済 %d件、未登録 %d件 (実体ファイルなし %d件)",
|
||||
"repo.settings.lfs_pointers.sha": "Blob SHA",
|
||||
"repo.settings.lfs_pointers.oid": "OID",
|
||||
"repo.settings.lfs_pointers.inRepo": "Repo内",
|
||||
"repo.settings.lfs_pointers.exists": "実ファイルあり",
|
||||
"repo.settings.lfs_pointers.accessible": "アクセス可",
|
||||
@@ -2468,6 +2533,7 @@
|
||||
"repo.diff.browse_source": "ソースを参照",
|
||||
"repo.diff.parent": "親",
|
||||
"repo.diff.commit": "コミット",
|
||||
"repo.diff.git-notes": "Notes",
|
||||
"repo.diff.data_not_available": "差分はありません",
|
||||
"repo.diff.options_button": "差分オプション",
|
||||
"repo.diff.download_patch": "Patchファイルをダウンロード",
|
||||
@@ -2494,6 +2560,8 @@
|
||||
"repo.diff.too_many_files": "変更されたファイルが多すぎるため、一部のファイルは表示されません",
|
||||
"repo.diff.show_more": "さらに表示",
|
||||
"repo.diff.load": "差分を読み込み",
|
||||
"repo.diff.generated": "生成ファイル",
|
||||
"repo.diff.vendored": "ベンダーファイル",
|
||||
"repo.diff.comment.add_line_comment": "行コメントを追加",
|
||||
"repo.diff.comment.placeholder": "コメントを残す",
|
||||
"repo.diff.comment.add_single_comment": "単独のコメントを追加",
|
||||
@@ -2508,6 +2576,7 @@
|
||||
"repo.diff.review.self_reject": "プルリクエストの作成者は自分のプルリクエストで変更要請できません",
|
||||
"repo.diff.review.reject": "変更要請",
|
||||
"repo.diff.review.self_approve": "プルリクエストの作成者は自分のプルリクエストを承認できません",
|
||||
"repo.diff.committed_by": "committed by",
|
||||
"repo.diff.protected": "保護されているファイル",
|
||||
"repo.diff.image.side_by_side": "並べて表示",
|
||||
"repo.diff.image.swipe": "スワイプ",
|
||||
@@ -2566,6 +2635,13 @@
|
||||
"repo.release.add_tag": "タグのみ作成",
|
||||
"repo.release.releases_for": "%s のリリース",
|
||||
"repo.release.tags_for": "%s のタグ",
|
||||
"repo.release.notes": "リリースノート",
|
||||
"repo.release.generate_notes": "リリースノートを生成",
|
||||
"repo.release.generate_notes_desc": "マージされたプルリクエストと変更履歴のリンクを自動的に追加します。",
|
||||
"repo.release.previous_tag": "前回のタグ",
|
||||
"repo.release.generate_notes_tag_not_found": "このリポジトリにタグ \"%s\" は存在しません。",
|
||||
"repo.release.generate_notes_target_not_found": "リリースターゲット \"%s\" が見つかりません。",
|
||||
"repo.release.generate_notes_missing_tag": "リリースノートを生成するタグ名を入力してください。",
|
||||
"repo.branch.name": "ブランチ名",
|
||||
"repo.branch.already_exists": "ブランチ \"%s\" は既に存在します。",
|
||||
"repo.branch.delete_head": "削除",
|
||||
@@ -2585,7 +2661,7 @@
|
||||
"repo.branch.restore_success": "ブランチ \"%s\" を復元しました。",
|
||||
"repo.branch.restore_failed": "ブランチ \"%s\" の復元に失敗しました。",
|
||||
"repo.branch.protected_deletion_failed": "ブランチ \"%s\" は保護されています。 削除できません。",
|
||||
"repo.branch.default_deletion_failed": "ブランチ \"%s\" はデフォルトブランチです。 削除できません。",
|
||||
"repo.branch.default_deletion_failed": "ブランチ \"%s\" は、デフォルトブランチまたはプルリクエストのターゲットブランチです。 削除できません。",
|
||||
"repo.branch.default_branch_not_exist": "デフォルトブランチ \"%s\" がありません。",
|
||||
"repo.branch.restore": "ブランチ \"%s\" の復元",
|
||||
"repo.branch.download": "ブランチ \"%s\" をダウンロード",
|
||||
@@ -2602,7 +2678,7 @@
|
||||
"repo.branch.new_branch_from": "\"%s\" から新しいブランチを作成",
|
||||
"repo.branch.renamed": "ブランチ %s は %s にリネームされました。",
|
||||
"repo.branch.rename_default_or_protected_branch_error": "デフォルトブランチや保護ブランチのリネームが可能なのは管理者だけです。",
|
||||
"repo.branch.rename_protected_branch_failed": "このブランチはglobベースの保護ルールに従って保護されています。",
|
||||
"repo.branch.rename_protected_branch_failed": "ブランチ保護ルールにより、ブランチ名の変更は失敗しました。",
|
||||
"repo.branch.commits_divergence_from": "コミットの乖離: %[3]s より %[1]d 件遅れ %[2]d 件先行",
|
||||
"repo.branch.commits_no_divergence": "%[1]s ブランチと一致",
|
||||
"repo.tag.create_tag": "タグ %s を作成",
|
||||
@@ -2809,6 +2885,7 @@
|
||||
"admin.dashboard.task.finished": "タスク: %[2]s が開始したタスク %[1]s が完了",
|
||||
"admin.dashboard.task.unknown": "不明なタスクです: %[1]s",
|
||||
"admin.dashboard.cron.started": "Cronを開始しました: %[1]s",
|
||||
"admin.dashboard.cron.process": "Cron: %[1]s",
|
||||
"admin.dashboard.cron.cancelled": "Cron: %[1]s をキャンセル: %[3]s",
|
||||
"admin.dashboard.cron.error": "Cronでエラー: %s: %[3]s",
|
||||
"admin.dashboard.cron.finished": "Cron: %[1]s が完了",
|
||||
@@ -2886,7 +2963,9 @@
|
||||
"admin.users.admin": "管理者",
|
||||
"admin.users.restricted": "制限あり",
|
||||
"admin.users.reserved": "予約済み",
|
||||
"admin.users.bot": "Bot",
|
||||
"admin.users.remote": "リモート",
|
||||
"admin.users.2fa": "2FA",
|
||||
"admin.users.repos": "リポジトリ",
|
||||
"admin.users.created": "作成日",
|
||||
"admin.users.last_login": "前回のサインイン",
|
||||
@@ -3008,6 +3087,7 @@
|
||||
"admin.auths.attribute_mail": "メールアドレス",
|
||||
"admin.auths.attribute_ssh_public_key": "SSH公開鍵",
|
||||
"admin.auths.attribute_avatar": "アバター",
|
||||
"admin.auths.ssh_keys_are_verified": "LDAPからのSSHキーを検証済みとする",
|
||||
"admin.auths.attributes_in_bind": "バインドDNのコンテクストから属性を取得する",
|
||||
"admin.auths.allow_deactivate_all": "サーチ結果が空のときは全ユーザーを非アクティブ化",
|
||||
"admin.auths.use_paged_search": "ページ分割検索を使用",
|
||||
@@ -3141,6 +3221,7 @@
|
||||
"admin.config.db_name": "データベース名",
|
||||
"admin.config.db_user": "ユーザー名",
|
||||
"admin.config.db_schema": "スキーマ",
|
||||
"admin.config.db_ssl_mode": "SSL",
|
||||
"admin.config.db_path": "パス",
|
||||
"admin.config.service_config": "サービス設定",
|
||||
"admin.config.register_email_confirm": "登録にはメールによる確認が必要",
|
||||
@@ -3179,6 +3260,7 @@
|
||||
"admin.config.mailer_sendmail_path": "Sendmailのパス",
|
||||
"admin.config.mailer_sendmail_args": "Sendmailの追加引数",
|
||||
"admin.config.mailer_sendmail_timeout": "Sendmail のタイムアウト",
|
||||
"admin.config.mailer_use_dummy": "Dummy",
|
||||
"admin.config.test_email_placeholder": "メールアドレス (例 test@example.com)",
|
||||
"admin.config.send_test_mail": "テストメールを送信",
|
||||
"admin.config.send_test_mail_submit": "送信",
|
||||
@@ -3217,8 +3299,6 @@
|
||||
"admin.config.git_gc_args": "GC引数",
|
||||
"admin.config.git_migrate_timeout": "移行タイムアウト",
|
||||
"admin.config.git_mirror_timeout": "ミラー更新タイムアウト",
|
||||
"admin.config.git_clone_timeout": "クローン操作のタイムアウト",
|
||||
"admin.config.git_pull_timeout": "プル操作のタイムアウト",
|
||||
"admin.config.git_gc_timeout": "GC操作のタイムアウト",
|
||||
"admin.config.log_config": "ログ設定",
|
||||
"admin.config.logger_name_fmt": "ロガー: %s",
|
||||
@@ -3393,6 +3473,7 @@
|
||||
"packages.assets": "アセット",
|
||||
"packages.versions": "バージョン",
|
||||
"packages.versions.view_all": "すべて表示",
|
||||
"packages.dependency.id": "ID",
|
||||
"packages.dependency.version": "バージョン",
|
||||
"packages.search_in_external_registry": "%s で検索",
|
||||
"packages.alpine.registry": "あなたの <code>/etc/apk/repositories</code> ファイルにURLを追加して、このレジストリをセットアップします:",
|
||||
@@ -3402,10 +3483,12 @@
|
||||
"packages.alpine.repository": "リポジトリ情報",
|
||||
"packages.alpine.repository.branches": "ブランチ",
|
||||
"packages.alpine.repository.repositories": "リポジトリ",
|
||||
"packages.alpine.repository.architectures": "Architectures",
|
||||
"packages.arch.registry": "<code>/etc/pacman.conf</code> にリポジトリとアーキテクチャを含めてサーバーを追加します:",
|
||||
"packages.arch.install": "pacmanでパッケージを同期します:",
|
||||
"packages.arch.repository": "リポジトリ情報",
|
||||
"packages.arch.repository.repositories": "リポジトリ",
|
||||
"packages.arch.repository.architectures": "Architectures",
|
||||
"packages.cargo.registry": "Cargo 設定ファイルでこのレジストリをセットアップします。(例 <code>~/.cargo/config.toml</code>):",
|
||||
"packages.cargo.install": "Cargo を使用してパッケージをインストールするには、次のコマンドを実行します:",
|
||||
"packages.chef.registry": "あなたの <code>~/.chef/config.rb</code> ファイルに、このレジストリをセットアップします:",
|
||||
@@ -3435,6 +3518,9 @@
|
||||
"packages.debian.registry.info": "$distribution と $component は下にあるリストから選んでください。",
|
||||
"packages.debian.install": "パッケージをインストールするには、次のコマンドを実行します:",
|
||||
"packages.debian.repository": "リポジトリ情報",
|
||||
"packages.debian.repository.distributions": "Distributions",
|
||||
"packages.debian.repository.components": "Components",
|
||||
"packages.debian.repository.architectures": "Architectures",
|
||||
"packages.generic.download": "コマンドラインでパッケージをダウンロードします:",
|
||||
"packages.go.install": "コマンドラインでパッケージをインストール:",
|
||||
"packages.helm.registry": "このレジストリをコマンドラインからセットアップします:",
|
||||
@@ -3463,6 +3549,7 @@
|
||||
"packages.rpm.distros.suse": "SUSE系ディストリビューションの場合",
|
||||
"packages.rpm.install": "パッケージをインストールするには、次のコマンドを実行します:",
|
||||
"packages.rpm.repository": "リポジトリ情報",
|
||||
"packages.rpm.repository.architectures": "Architectures",
|
||||
"packages.rpm.repository.multiple_groups": "このパッケージは複数のグループで利用可能です。",
|
||||
"packages.rubygems.install": "gem を使用してパッケージをインストールするには、次のコマンドを実行します:",
|
||||
"packages.rubygems.install2": "または Gemfile に追加します:",
|
||||
@@ -3536,6 +3623,7 @@
|
||||
"secrets.deletion.success": "シークレットを削除しました。",
|
||||
"secrets.deletion.failed": "シークレットの削除に失敗しました。",
|
||||
"secrets.management": "シークレット管理",
|
||||
"actions.actions": "Actions",
|
||||
"actions.unit.desc": "Actionsの管理",
|
||||
"actions.status.unknown": "不明",
|
||||
"actions.status.waiting": "待機中",
|
||||
@@ -3550,6 +3638,7 @@
|
||||
"actions.runners.new": "新しいランナーを作成",
|
||||
"actions.runners.new_notice": "ランナーの開始方法",
|
||||
"actions.runners.status": "ステータス",
|
||||
"actions.runners.id": "ID",
|
||||
"actions.runners.name": "名称",
|
||||
"actions.runners.owner_type": "タイプ",
|
||||
"actions.runners.description": "説明",
|
||||
@@ -3584,6 +3673,7 @@
|
||||
"actions.runs.all_workflows": "すべてのワークフロー",
|
||||
"actions.runs.commit": "コミット",
|
||||
"actions.runs.scheduled": "スケジュール済み",
|
||||
"actions.runs.pushed_by": "pushed by",
|
||||
"actions.runs.invalid_workflow_helper": "ワークフロー設定ファイルは無効です。あなたの設定ファイルを確認してください: %s",
|
||||
"actions.runs.no_matching_online_runner_helper": "ラベルに一致するオンラインのランナーが見つかりません: %s",
|
||||
"actions.runs.no_job_without_needs": "ワークフローには依存関係のないジョブが少なくとも1つ含まれている必要があります。",
|
||||
@@ -3648,6 +3738,7 @@
|
||||
"projects.type-3.display_name": "組織プロジェクト",
|
||||
"projects.enter_fullscreen": "フルスクリーン",
|
||||
"projects.exit_fullscreen": "フルスクリーンを終了",
|
||||
"git.filemode.changed_filemode": "%[1]s → %[2]s",
|
||||
"git.filemode.directory": "ディレクトリ",
|
||||
"git.filemode.normal_file": "ノーマルファイル",
|
||||
"git.filemode.executable_file": "実行可能ファイル",
|
||||
|
||||
@@ -148,6 +148,13 @@
|
||||
"filter.private": "私有",
|
||||
"no_results_found": "未找到结果",
|
||||
"internal_error_skipped": "发生内部错误,但已跳过: %s",
|
||||
"characters_spaces": "空格",
|
||||
"characters_tabs": "制表符",
|
||||
"text_indent_style": "缩进风格",
|
||||
"text_indent_size": "缩进大小",
|
||||
"text_line_wrap": "换行",
|
||||
"text_line_nowrap": "无换行",
|
||||
"text_line_wrap_mode": "换行模式",
|
||||
"search.search": "搜索…",
|
||||
"search.type_tooltip": "搜索类型",
|
||||
"search.fuzzy": "模糊",
|
||||
@@ -751,6 +758,7 @@
|
||||
"settings.add_email": "新增邮箱地址",
|
||||
"settings.add_openid": "添加 OpenID URI",
|
||||
"settings.add_email_confirmation_sent": "一封确认邮件已经发送至「%s」,请检查您的收件箱并在 %s 内完成确认注册操作。",
|
||||
"settings.email_primary_not_found": "找不到选定的电子邮件地址。",
|
||||
"settings.add_email_success": "新邮箱地址已添加。",
|
||||
"settings.email_preference_set_success": "邮件首选项已成功设置。",
|
||||
"settings.add_openid_success": "新的 OpenID 地址已添加。",
|
||||
@@ -1635,7 +1643,7 @@
|
||||
"repo.issues.cancel_tracking": "取消",
|
||||
"repo.issues.cancel_tracking_history": "取消时间跟踪 %s",
|
||||
"repo.issues.del_time": "删除此时间跟踪日志",
|
||||
"repo.issues.add_time_history": "于 %[2]s 添加计时 <b>%[1]</b>",
|
||||
"repo.issues.add_time_history": "于 %[2]s 添加计时 <b>%[1]s</b>",
|
||||
"repo.issues.del_time_history": "已删除时间 %s",
|
||||
"repo.issues.add_time_manually": "手动添加时间",
|
||||
"repo.issues.add_time_hours": "小时",
|
||||
@@ -1778,6 +1786,8 @@
|
||||
"repo.pulls.title_desc": "请求将 %[1]d 次代码提交从 <code>%[2]s</code> 合并至 <code id=\"branch_target\">%[3]s</code>",
|
||||
"repo.pulls.merged_title_desc": "于 %[4]s 将 %[1]d 次代码提交从 <code>%[2]s</code>合并至 <code>%[3]s</code>",
|
||||
"repo.pulls.change_target_branch_at": "将目标分支从 <b>%s</b> 更改为 <b>%s</b> %s",
|
||||
"repo.pulls.marked_as_work_in_progress_at": "已将合并请求标记为进行中 %s",
|
||||
"repo.pulls.marked_as_ready_for_review_at": "已将合并请求标记为准备评审 %s",
|
||||
"repo.pulls.tab_conversation": "对话内容",
|
||||
"repo.pulls.tab_commits": "代码提交",
|
||||
"repo.pulls.tab_files": "文件变动",
|
||||
@@ -2122,6 +2132,8 @@
|
||||
"repo.settings.pulls.ignore_whitespace": "忽略空白冲突",
|
||||
"repo.settings.pulls.enable_autodetect_manual_merge": "启用自动检查手动合并(注意:在某些特殊情况下可能会出现误判)",
|
||||
"repo.settings.pulls.allow_rebase_update": "允许通过变基更新合并请求分支",
|
||||
"repo.settings.pulls.default_target_branch": "新合并请求的默认目标分支",
|
||||
"repo.settings.pulls.default_target_branch_default": "默认分支(%s)",
|
||||
"repo.settings.pulls.default_delete_branch_after_merge": "默认合并后删除合并请求分支",
|
||||
"repo.settings.pulls.default_allow_edits_from_maintainers": "默认允许维护者编辑",
|
||||
"repo.settings.releases_desc": "启用仓库发布",
|
||||
@@ -2434,7 +2446,8 @@
|
||||
"repo.settings.block_outdated_branch_desc": "当头部分支落后基础分支时,不能合并。",
|
||||
"repo.settings.block_admin_merge_override": "管理员须遵守分支保护规则",
|
||||
"repo.settings.block_admin_merge_override_desc": "管理员须遵守分支保护规则,不能规避该规则。",
|
||||
"repo.settings.default_branch_desc": "请选择一个默认的分支用于合并请求和提交:",
|
||||
"repo.settings.default_branch_desc": "选择一个默认分支用于提交代码。",
|
||||
"repo.settings.default_target_branch_desc": "如果在仓库高级设置的合并请求部分中进行了设置,则合并请求可以使用不同的默认目标分支。",
|
||||
"repo.settings.merge_style_desc": "合并方式",
|
||||
"repo.settings.default_merge_style_desc": "默认合并风格",
|
||||
"repo.settings.choose_branch": "选择一个分支…",
|
||||
@@ -2548,7 +2561,7 @@
|
||||
"repo.diff.show_more": "显示更多",
|
||||
"repo.diff.load": "加载差异",
|
||||
"repo.diff.generated": "自动生成",
|
||||
"repo.diff.vendored": "vendored",
|
||||
"repo.diff.vendored": "第三方依赖",
|
||||
"repo.diff.comment.add_line_comment": "添加行内评论",
|
||||
"repo.diff.comment.placeholder": "留下评论",
|
||||
"repo.diff.comment.add_single_comment": "添加单条评论",
|
||||
@@ -2648,7 +2661,7 @@
|
||||
"repo.branch.restore_success": "分支「%s」已还原。",
|
||||
"repo.branch.restore_failed": "分支「%s」还原失败。",
|
||||
"repo.branch.protected_deletion_failed": "不能删除受保护的分支「%s」。",
|
||||
"repo.branch.default_deletion_failed": "不能删除默认分支「%s」。",
|
||||
"repo.branch.default_deletion_failed": "分支「%s」是默认分支或合并请求目标分支,无法删除。",
|
||||
"repo.branch.default_branch_not_exist": "默认分支「%s」不存在。",
|
||||
"repo.branch.restore": "还原分支「%s」",
|
||||
"repo.branch.download": "下载分支「%s」",
|
||||
@@ -2672,7 +2685,7 @@
|
||||
"repo.tag.create_tag_operation": "创建 Git 标签",
|
||||
"repo.tag.confirm_create_tag": "创建 Git 标签",
|
||||
"repo.tag.create_tag_from": "基于「%s」创建新 Git 标签",
|
||||
"repo.tag.create_success": "Git 标签「%s」已存在。",
|
||||
"repo.tag.create_success": "Git 标签「%s」创建成功。",
|
||||
"repo.topic.manage_topics": "管理主题",
|
||||
"repo.topic.done": "保存",
|
||||
"repo.topic.count_prompt": "您最多选择25个主题",
|
||||
|
||||
42
package.json
42
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"type": "module",
|
||||
"packageManager": "pnpm@10.29.2",
|
||||
"packageManager": "pnpm@10.30.0",
|
||||
"engines": {
|
||||
"node": ">= 22.6.0",
|
||||
"pnpm": ">= 10.0.0"
|
||||
@@ -16,20 +16,20 @@
|
||||
"@github/text-expander-element": "2.9.4",
|
||||
"@mcaptcha/vanilla-glue": "0.1.0-alpha-3",
|
||||
"@mermaid-js/layout-elk": "0.2.0",
|
||||
"@primer/octicons": "19.21.2",
|
||||
"@primer/octicons": "19.22.0",
|
||||
"@resvg/resvg-wasm": "2.6.2",
|
||||
"@silverwind/vue3-calendar-heatmap": "2.1.1",
|
||||
"@techknowlogick/license-checker-webpack-plugin": "0.3.0",
|
||||
"add-asset-webpack-plugin": "3.1.1",
|
||||
"ansi_up": "6.0.6",
|
||||
"asciinema-player": "3.14.0",
|
||||
"asciinema-player": "3.14.15",
|
||||
"chart.js": "4.5.1",
|
||||
"chartjs-adapter-dayjs-4": "1.0.4",
|
||||
"chartjs-plugin-zoom": "2.2.0",
|
||||
"clippie": "4.1.10",
|
||||
"compare-versions": "6.1.1",
|
||||
"cropperjs": "1.6.2",
|
||||
"css-loader": "7.1.3",
|
||||
"css-loader": "7.1.4",
|
||||
"dayjs": "1.11.19",
|
||||
"dropzone": "6.0.0-beta.2",
|
||||
"easymde": "2.20.0",
|
||||
@@ -39,7 +39,7 @@
|
||||
"jquery": "4.0.0",
|
||||
"js-yaml": "4.1.1",
|
||||
"katex": "0.16.28",
|
||||
"mermaid": "11.12.2",
|
||||
"mermaid": "11.12.3",
|
||||
"mini-css-extract-plugin": "2.10.0",
|
||||
"monaco-editor": "0.55.1",
|
||||
"monaco-editor-webpack-plugin": "7.1.1",
|
||||
@@ -47,9 +47,9 @@
|
||||
"pdfobject": "2.3.1",
|
||||
"perfect-debounce": "2.1.0",
|
||||
"postcss": "8.5.6",
|
||||
"postcss-loader": "8.2.0",
|
||||
"sortablejs": "1.15.6",
|
||||
"swagger-ui-dist": "5.31.0",
|
||||
"postcss-loader": "8.2.1",
|
||||
"sortablejs": "1.15.7",
|
||||
"swagger-ui-dist": "5.31.1",
|
||||
"tailwindcss": "3.4.17",
|
||||
"throttle-debounce": "5.0.2",
|
||||
"tinycolor2": "1.6.0",
|
||||
@@ -62,7 +62,7 @@
|
||||
"vue-bar-graph": "2.2.0",
|
||||
"vue-chartjs": "5.3.3",
|
||||
"vue-loader": "17.4.2",
|
||||
"webpack": "5.105.0",
|
||||
"webpack": "5.105.2",
|
||||
"webpack-cli": "6.0.1",
|
||||
"wrap-ansi": "9.0.2"
|
||||
},
|
||||
@@ -77,15 +77,16 @@
|
||||
"@types/jquery": "3.5.33",
|
||||
"@types/js-yaml": "4.0.9",
|
||||
"@types/katex": "0.16.8",
|
||||
"@types/node": "25.2.3",
|
||||
"@types/pdfobject": "2.2.5",
|
||||
"@types/sortablejs": "1.15.9",
|
||||
"@types/swagger-ui-dist": "3.30.6",
|
||||
"@types/throttle-debounce": "5.0.2",
|
||||
"@types/tinycolor2": "1.4.6",
|
||||
"@types/toastify-js": "1.12.4",
|
||||
"@typescript-eslint/parser": "8.55.0",
|
||||
"@typescript-eslint/parser": "8.56.0",
|
||||
"@vitejs/plugin-vue": "6.0.4",
|
||||
"@vitest/eslint-plugin": "1.6.7",
|
||||
"@vitest/eslint-plugin": "1.6.9",
|
||||
"eslint": "9.39.2",
|
||||
"eslint-import-resolver-typescript": "4.4.4",
|
||||
"eslint-plugin-array-func": "5.1.0",
|
||||
@@ -93,35 +94,32 @@
|
||||
"eslint-plugin-import-x": "4.16.1",
|
||||
"eslint-plugin-playwright": "2.5.1",
|
||||
"eslint-plugin-regexp": "3.0.0",
|
||||
"eslint-plugin-sonarjs": "3.0.6",
|
||||
"eslint-plugin-unicorn": "62.0.0",
|
||||
"eslint-plugin-vue": "10.7.0",
|
||||
"eslint-plugin-sonarjs": "3.0.7",
|
||||
"eslint-plugin-unicorn": "63.0.0",
|
||||
"eslint-plugin-vue": "10.8.0",
|
||||
"eslint-plugin-vue-scoped-css": "2.12.0",
|
||||
"eslint-plugin-wc": "3.1.0",
|
||||
"globals": "17.3.0",
|
||||
"happy-dom": "20.6.0",
|
||||
"happy-dom": "20.6.1",
|
||||
"jiti": "2.6.1",
|
||||
"markdownlint-cli": "0.47.0",
|
||||
"material-icon-theme": "5.31.0",
|
||||
"nolyfill": "1.0.44",
|
||||
"postcss-html": "1.8.1",
|
||||
"spectral-cli-bundle": "1.0.4",
|
||||
"stylelint": "17.1.1",
|
||||
"spectral-cli-bundle": "1.0.7",
|
||||
"stylelint": "17.3.0",
|
||||
"stylelint-config-recommended": "18.0.0",
|
||||
"stylelint-declaration-block-no-ignored-properties": "3.0.0",
|
||||
"stylelint-declaration-strict-value": "1.10.11",
|
||||
"stylelint-value-no-unknown-custom-properties": "6.1.1",
|
||||
"svgo": "4.0.0",
|
||||
"typescript": "5.9.3",
|
||||
"typescript-eslint": "8.55.0",
|
||||
"updates": "17.4.0",
|
||||
"typescript-eslint": "8.56.0",
|
||||
"updates": "17.5.7",
|
||||
"vite-string-plugin": "2.0.1",
|
||||
"vitest": "4.0.18",
|
||||
"vue-tsc": "3.2.4"
|
||||
},
|
||||
"browserslist": [
|
||||
"defaults"
|
||||
],
|
||||
"pnpm": {
|
||||
"overrides": {
|
||||
"array-includes": "npm:@nolyfill/array-includes@^1",
|
||||
|
||||
807
pnpm-lock.yaml
generated
807
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 |
@@ -32,14 +32,14 @@ func (a *Auth) Verify(req *http.Request, w http.ResponseWriter, store auth.DataS
|
||||
}
|
||||
|
||||
if packageMeta == nil || packageMeta.UserID == 0 {
|
||||
return nil, nil
|
||||
return nil, nil //nolint:nilnil // the auth method is not applicable
|
||||
}
|
||||
|
||||
var u *user_model.User
|
||||
switch packageMeta.UserID {
|
||||
case user_model.GhostUserID:
|
||||
if !a.AllowGhostUser {
|
||||
return nil, nil
|
||||
return nil, nil //nolint:nilnil // the auth method is not applicable
|
||||
}
|
||||
u = user_model.NewGhostUser()
|
||||
case user_model.ActionsUserID:
|
||||
|
||||
@@ -61,7 +61,7 @@ func (a *Auth) Verify(req *http.Request, w http.ResponseWriter, store auth.DataS
|
||||
return nil, err
|
||||
}
|
||||
if u == nil {
|
||||
return nil, nil
|
||||
return nil, nil //nolint:nilnil // the auth method is not applicable
|
||||
}
|
||||
|
||||
pub, err := getUserPublicKey(req.Context(), u)
|
||||
@@ -88,7 +88,7 @@ func (a *Auth) Verify(req *http.Request, w http.ResponseWriter, store auth.DataS
|
||||
func getUserFromRequest(req *http.Request) (*user_model.User, error) {
|
||||
username := req.Header.Get("X-Ops-Userid")
|
||||
if username == "" {
|
||||
return nil, nil
|
||||
return nil, nil //nolint:nilnil // the auth method is not applicable
|
||||
}
|
||||
|
||||
return user_model.GetUserByName(req.Context(), username)
|
||||
|
||||
@@ -300,7 +300,7 @@ func formFileOptionalReadCloser(ctx *context.Context, formKey string) (io.ReadCl
|
||||
|
||||
content := ctx.Req.FormValue(formKey)
|
||||
if content == "" {
|
||||
return nil, nil
|
||||
return nil, nil //nolint:nilnil // return nil to indicate that the content does not exist
|
||||
}
|
||||
return io.NopCloser(strings.NewReader(content)), nil
|
||||
}
|
||||
|
||||
@@ -161,7 +161,7 @@ func GetHeadOwnerAndRepo(ctx context.Context, baseRepo *repo_model.Repository, c
|
||||
|
||||
func findHeadRepoFromRootBase(ctx context.Context, baseRepo *repo_model.Repository, headUserID int64, traverseLevel int) (*repo_model.Repository, error) {
|
||||
if traverseLevel == 0 {
|
||||
return nil, nil
|
||||
return nil, nil //nolint:nilnil // return nil to indicate that the object does not exist
|
||||
}
|
||||
// test if we are lucky
|
||||
repo, err := repo_model.GetUserFork(ctx, baseRepo.ID, headUserID)
|
||||
@@ -185,5 +185,5 @@ func findHeadRepoFromRootBase(ctx context.Context, baseRepo *repo_model.Reposito
|
||||
return forked, nil
|
||||
}
|
||||
}
|
||||
return nil, nil
|
||||
return nil, nil //nolint:nilnil // return nil to indicate that the object does not exist
|
||||
}
|
||||
|
||||
@@ -27,6 +27,7 @@ import (
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/storage"
|
||||
"code.gitea.io/gitea/modules/templates"
|
||||
"code.gitea.io/gitea/modules/translation"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
"code.gitea.io/gitea/modules/web"
|
||||
"code.gitea.io/gitea/routers/common"
|
||||
@@ -302,7 +303,7 @@ func ViewPost(ctx *context_module.Context) {
|
||||
resp.State.CurrentJob.Steps = make([]*ViewJobStep, 0) // marshal to '[]' instead fo 'null' in json
|
||||
resp.Logs.StepsLog = make([]*ViewStepLog, 0) // marshal to '[]' instead fo 'null' in json
|
||||
if task != nil {
|
||||
steps, logs, err := convertToViewModel(ctx, req.LogCursors, task)
|
||||
steps, logs, err := convertToViewModel(ctx, ctx.Locale, req.LogCursors, task)
|
||||
if err != nil {
|
||||
ctx.ServerError("convertToViewModel", err)
|
||||
return
|
||||
@@ -314,7 +315,7 @@ func ViewPost(ctx *context_module.Context) {
|
||||
ctx.JSON(http.StatusOK, resp)
|
||||
}
|
||||
|
||||
func convertToViewModel(ctx *context_module.Context, cursors []LogCursor, task *actions_model.ActionTask) ([]*ViewJobStep, []*ViewStepLog, error) {
|
||||
func convertToViewModel(ctx context.Context, locale translation.Locale, cursors []LogCursor, task *actions_model.ActionTask) ([]*ViewJobStep, []*ViewStepLog, error) {
|
||||
var viewJobs []*ViewJobStep
|
||||
var logs []*ViewStepLog
|
||||
|
||||
@@ -344,7 +345,7 @@ func convertToViewModel(ctx *context_module.Context, cursors []LogCursor, task *
|
||||
Lines: []*ViewStepLogLine{
|
||||
{
|
||||
Index: 1,
|
||||
Message: ctx.Locale.TrString("actions.runs.expire_log_message"),
|
||||
Message: locale.TrString("actions.runs.expire_log_message"),
|
||||
// Timestamp doesn't mean anything when the log is expired.
|
||||
// Set it to the task's updated time since it's probably the time when the log has expired.
|
||||
Timestamp: float64(task.Updated.AsTime().UnixNano()) / float64(time.Second),
|
||||
|
||||
47
routers/web/repo/actions/view_test.go
Normal file
47
routers/web/repo/actions/view_test.go
Normal file
@@ -0,0 +1,47 @@
|
||||
// Copyright 2025 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package actions
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
actions_model "code.gitea.io/gitea/models/actions"
|
||||
"code.gitea.io/gitea/modules/timeutil"
|
||||
"code.gitea.io/gitea/modules/translation"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestConvertToViewModel(t *testing.T) {
|
||||
task := &actions_model.ActionTask{
|
||||
Status: actions_model.StatusSuccess,
|
||||
Steps: []*actions_model.ActionTaskStep{
|
||||
{Name: "Run step-name", Index: 0, Status: actions_model.StatusSuccess, LogLength: 1, Started: timeutil.TimeStamp(1), Stopped: timeutil.TimeStamp(5)},
|
||||
},
|
||||
Stopped: timeutil.TimeStamp(20),
|
||||
}
|
||||
|
||||
viewJobSteps, _, err := convertToViewModel(t.Context(), translation.MockLocale{}, nil, task)
|
||||
require.NoError(t, err)
|
||||
|
||||
expectedViewJobs := []*ViewJobStep{
|
||||
{
|
||||
Summary: "Set up job",
|
||||
Duration: "0s",
|
||||
Status: "success",
|
||||
},
|
||||
{
|
||||
Summary: "Run step-name",
|
||||
Duration: "4s",
|
||||
Status: "success",
|
||||
},
|
||||
{
|
||||
Summary: "Complete job",
|
||||
Duration: "15s",
|
||||
Status: "success",
|
||||
},
|
||||
}
|
||||
assert.Equal(t, expectedViewJobs, viewJobSteps)
|
||||
}
|
||||
@@ -7,7 +7,6 @@ import (
|
||||
gocontext "context"
|
||||
"encoding/csv"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
@@ -426,6 +425,36 @@ func ParseCompareInfo(ctx *context.Context) *git_service.CompareInfo {
|
||||
return compareInfo
|
||||
}
|
||||
|
||||
func prepareNewPullRequestTitleContent(ci *git_service.CompareInfo, commits []*git_model.SignCommitWithStatuses) (title, content string) {
|
||||
title = ci.HeadRef.ShortName()
|
||||
|
||||
if len(commits) > 0 {
|
||||
// the "commits" are from "ShowPrettyFormatLogToList", which is ordered from newest to oldest, here take the oldest one
|
||||
c := commits[len(commits)-1]
|
||||
title = strings.TrimSpace(c.UserCommit.Summary())
|
||||
}
|
||||
|
||||
if len(commits) == 1 {
|
||||
// FIXME: GIT-COMMIT-MESSAGE-ENCODING: try to convert the encoding for commit message explicitly, ideally it should be done by a git commit struct method
|
||||
c := commits[0]
|
||||
_, content, _ = strings.Cut(strings.TrimSpace(c.UserCommit.CommitMessage), "\n")
|
||||
content = strings.TrimSpace(content)
|
||||
content = string(charset.ToUTF8([]byte(content), charset.ConvertOpts{}))
|
||||
}
|
||||
|
||||
var titleTrailer string
|
||||
// TODO: 255 doesn't seem to be a good limit for title, just keep the old behavior
|
||||
title, titleTrailer = util.EllipsisDisplayStringX(title, 255)
|
||||
if titleTrailer != "" {
|
||||
if content != "" {
|
||||
content = titleTrailer + "\n\n" + content
|
||||
} else {
|
||||
content = titleTrailer + "\n"
|
||||
}
|
||||
}
|
||||
return title, content
|
||||
}
|
||||
|
||||
// PrepareCompareDiff renders compare diff page
|
||||
func PrepareCompareDiff(
|
||||
ctx *context.Context,
|
||||
@@ -539,30 +568,7 @@ func PrepareCompareDiff(
|
||||
ctx.Data["Commits"] = commits
|
||||
ctx.Data["CommitCount"] = len(commits)
|
||||
|
||||
title := ci.HeadRef.ShortName()
|
||||
if len(commits) == 1 {
|
||||
c := commits[0]
|
||||
title = strings.TrimSpace(c.UserCommit.Summary())
|
||||
|
||||
body := strings.Split(strings.TrimSpace(c.UserCommit.Message()), "\n")
|
||||
if len(body) > 1 {
|
||||
ctx.Data["content"] = strings.Join(body[1:], "\n")
|
||||
}
|
||||
}
|
||||
|
||||
if len(title) > 255 {
|
||||
var trailer string
|
||||
title, trailer = util.EllipsisDisplayStringX(title, 255)
|
||||
if len(trailer) > 0 {
|
||||
if ctx.Data["content"] != nil {
|
||||
ctx.Data["content"] = fmt.Sprintf("%s\n\n%s", trailer, ctx.Data["content"])
|
||||
} else {
|
||||
ctx.Data["content"] = trailer + "\n"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ctx.Data["title"] = title
|
||||
ctx.Data["title"], ctx.Data["content"] = prepareNewPullRequestTitleContent(ci, commits)
|
||||
ctx.Data["Username"] = ci.HeadRepo.OwnerName
|
||||
ctx.Data["Reponame"] = ci.HeadRepo.Name
|
||||
|
||||
|
||||
@@ -4,9 +4,16 @@
|
||||
package repo
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
"unicode/utf8"
|
||||
|
||||
asymkey_model "code.gitea.io/gitea/models/asymkey"
|
||||
git_model "code.gitea.io/gitea/models/git"
|
||||
issues_model "code.gitea.io/gitea/models/issues"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/git"
|
||||
git_service "code.gitea.io/gitea/services/git"
|
||||
"code.gitea.io/gitea/services/gitdiff"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
@@ -38,3 +45,47 @@ func TestAttachCommentsToLines(t *testing.T) {
|
||||
assert.Equal(t, int64(300), section.Lines[1].Comments[0].ID)
|
||||
assert.Equal(t, int64(301), section.Lines[1].Comments[1].ID)
|
||||
}
|
||||
|
||||
func TestNewPullRequestTitleContent(t *testing.T) {
|
||||
ci := &git_service.CompareInfo{HeadRef: "refs/heads/head-branch"}
|
||||
|
||||
mockCommit := func(msg string) *git_model.SignCommitWithStatuses {
|
||||
return &git_model.SignCommitWithStatuses{
|
||||
SignCommit: &asymkey_model.SignCommit{
|
||||
UserCommit: &user_model.UserCommit{
|
||||
Commit: &git.Commit{
|
||||
CommitMessage: msg,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
title, content := prepareNewPullRequestTitleContent(ci, nil)
|
||||
assert.Equal(t, "head-branch", title)
|
||||
assert.Empty(t, content)
|
||||
|
||||
title, content = prepareNewPullRequestTitleContent(ci, []*git_model.SignCommitWithStatuses{mockCommit("title-only")})
|
||||
assert.Equal(t, "title-only", title)
|
||||
assert.Empty(t, content)
|
||||
|
||||
title, content = prepareNewPullRequestTitleContent(ci, []*git_model.SignCommitWithStatuses{mockCommit("title-" + strings.Repeat("a", 255))})
|
||||
assert.Equal(t, "title-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa…", title)
|
||||
assert.Equal(t, "…aaaaaaaaa\n", content)
|
||||
|
||||
title, content = prepareNewPullRequestTitleContent(ci, []*git_model.SignCommitWithStatuses{mockCommit("title\nbody")})
|
||||
assert.Equal(t, "title", title)
|
||||
assert.Equal(t, "body", content)
|
||||
|
||||
title, content = prepareNewPullRequestTitleContent(ci, []*git_model.SignCommitWithStatuses{mockCommit("a\xf0\xf0\xf0\nb\xf0\xf0\xf0")})
|
||||
assert.Equal(t, "a?", title) // FIXME: GIT-COMMIT-MESSAGE-ENCODING: "title" doesn't use the same charset converting logic as "content"
|
||||
assert.Equal(t, "b"+string(utf8.RuneError)+string(utf8.RuneError), content)
|
||||
|
||||
title, content = prepareNewPullRequestTitleContent(ci, []*git_model.SignCommitWithStatuses{
|
||||
// ordered from newest to oldest
|
||||
mockCommit("title2\nbody2"),
|
||||
mockCommit("title1\nbody1"),
|
||||
})
|
||||
assert.Equal(t, "title1", title)
|
||||
assert.Empty(t, content)
|
||||
}
|
||||
|
||||
@@ -46,7 +46,7 @@ func getSecretsCtx(ctx *context.Context) (*secretsCtx, error) {
|
||||
if ctx.Data["PageIsOrgSettings"] == true {
|
||||
if _, err := shared_user.RenderUserOrgHeader(ctx); err != nil {
|
||||
ctx.ServerError("RenderUserOrgHeader", err)
|
||||
return nil, nil
|
||||
return nil, nil //nolint:nilnil // error is already handled by ctx.ServerError
|
||||
}
|
||||
return &secretsCtx{
|
||||
OwnerID: ctx.ContextUser.ID,
|
||||
|
||||
@@ -59,7 +59,7 @@ func getRunnersCtx(ctx *context.Context) (*runnersCtx, error) {
|
||||
if ctx.Data["PageIsOrgSettings"] == true {
|
||||
if _, err := shared_user.RenderUserOrgHeader(ctx); err != nil {
|
||||
ctx.ServerError("RenderUserOrgHeader", err)
|
||||
return nil, nil
|
||||
return nil, nil //nolint:nilnil // error is already handled by ctx.ServerError
|
||||
}
|
||||
return &runnersCtx{
|
||||
RepoID: 0,
|
||||
|
||||
@@ -51,7 +51,7 @@ func getVariablesCtx(ctx *context.Context) (*variablesCtx, error) {
|
||||
if ctx.Data["PageIsOrgSettings"] == true {
|
||||
if _, err := shared_user.RenderUserOrgHeader(ctx); err != nil {
|
||||
ctx.ServerError("RenderUserOrgHeader", err)
|
||||
return nil, nil
|
||||
return nil, nil //nolint:nilnil // error is already handled by ctx.ServerError
|
||||
}
|
||||
return &variablesCtx{
|
||||
OwnerID: ctx.ContextUser.ID,
|
||||
|
||||
66
routers/web/user/heatmap.go
Normal file
66
routers/web/user/heatmap.go
Normal file
@@ -0,0 +1,66 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package user
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
activities_model "code.gitea.io/gitea/models/activities"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
)
|
||||
|
||||
func prepareHeatmapURL(ctx *context.Context) {
|
||||
ctx.Data["EnableHeatmap"] = setting.Service.EnableUserHeatmap
|
||||
if !setting.Service.EnableUserHeatmap {
|
||||
return
|
||||
}
|
||||
|
||||
if ctx.Org.Organization == nil {
|
||||
// for individual user
|
||||
ctx.Data["HeatmapURL"] = ctx.Doer.HomeLink() + "/-/heatmap"
|
||||
return
|
||||
}
|
||||
|
||||
// for org or team
|
||||
heatmapURL := ctx.Org.Organization.OrganisationLink() + "/dashboard/-/heatmap"
|
||||
if ctx.Org.Team != nil {
|
||||
heatmapURL += "/" + url.PathEscape(ctx.Org.Team.LowerName)
|
||||
}
|
||||
ctx.Data["HeatmapURL"] = heatmapURL
|
||||
}
|
||||
|
||||
func writeHeatmapJSON(ctx *context.Context, hdata []*activities_model.UserHeatmapData) {
|
||||
data := make([][2]int64, len(hdata))
|
||||
var total int64
|
||||
for i, v := range hdata {
|
||||
data[i] = [2]int64{int64(v.Timestamp), v.Contributions}
|
||||
total += v.Contributions
|
||||
}
|
||||
ctx.JSON(http.StatusOK, map[string]any{
|
||||
"heatmapData": data,
|
||||
"totalContributions": total,
|
||||
})
|
||||
}
|
||||
|
||||
// DashboardHeatmap returns heatmap data as JSON, for the individual user, organization or team dashboard.
|
||||
func DashboardHeatmap(ctx *context.Context) {
|
||||
if !setting.Service.EnableUserHeatmap {
|
||||
ctx.NotFound(nil)
|
||||
return
|
||||
}
|
||||
var data []*activities_model.UserHeatmapData
|
||||
var err error
|
||||
if ctx.Org.Organization == nil {
|
||||
data, err = activities_model.GetUserHeatmapDataByUser(ctx, ctx.ContextUser, ctx.Doer)
|
||||
} else {
|
||||
data, err = activities_model.GetUserHeatmapDataByOrgTeam(ctx, ctx.Org.Organization, ctx.Org.Team, ctx.Doer)
|
||||
}
|
||||
if err != nil {
|
||||
ctx.ServerError("GetUserHeatmapData", err)
|
||||
return
|
||||
}
|
||||
writeHeatmapJSON(ctx, data)
|
||||
}
|
||||
@@ -54,8 +54,8 @@ const (
|
||||
tplProfile templates.TplName = "user/profile"
|
||||
)
|
||||
|
||||
// getDashboardContextUser finds out which context user dashboard is being viewed as .
|
||||
func getDashboardContextUser(ctx *context.Context) *user_model.User {
|
||||
// prepareDashboardContextUserOrgTeams finds out which context user dashboard is being viewed as .
|
||||
func prepareDashboardContextUserOrgTeams(ctx *context.Context) *user_model.User {
|
||||
ctxUser := ctx.Doer
|
||||
orgName := ctx.PathParam("org")
|
||||
if len(orgName) > 0 {
|
||||
@@ -76,7 +76,7 @@ func getDashboardContextUser(ctx *context.Context) *user_model.User {
|
||||
|
||||
// Dashboard render the dashboard page
|
||||
func Dashboard(ctx *context.Context) {
|
||||
ctxUser := getDashboardContextUser(ctx)
|
||||
ctxUser := prepareDashboardContextUserOrgTeams(ctx)
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
@@ -109,15 +109,7 @@ func Dashboard(ctx *context.Context) {
|
||||
"uid": uid,
|
||||
}
|
||||
|
||||
if setting.Service.EnableUserHeatmap {
|
||||
data, err := activities_model.GetUserHeatmapDataByUserTeam(ctx, ctxUser, ctx.Org.Team, ctx.Doer)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetUserHeatmapDataByUserTeam", err)
|
||||
return
|
||||
}
|
||||
ctx.Data["HeatmapData"] = data
|
||||
ctx.Data["HeatmapTotalContributions"] = activities_model.GetTotalContributionsInHeatmap(data)
|
||||
}
|
||||
prepareHeatmapURL(ctx)
|
||||
|
||||
feeds, count, err := feed_service.GetFeedsForDashboard(ctx, activities_model.GetFeedsOptions{
|
||||
RequestedUser: ctxUser,
|
||||
@@ -156,7 +148,7 @@ func Milestones(ctx *context.Context) {
|
||||
ctx.Data["Title"] = ctx.Tr("milestones")
|
||||
ctx.Data["PageIsMilestonesDashboard"] = true
|
||||
|
||||
ctxUser := getDashboardContextUser(ctx)
|
||||
ctxUser := prepareDashboardContextUserOrgTeams(ctx)
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
@@ -371,7 +363,7 @@ func buildIssueOverview(ctx *context.Context, unitType unit.Type) {
|
||||
// Return with NotFound or ServerError if unsuccessful.
|
||||
// ----------------------------------------------------
|
||||
|
||||
ctxUser := getDashboardContextUser(ctx)
|
||||
ctxUser := prepareDashboardContextUserOrgTeams(ctx)
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -103,6 +103,7 @@ func prepareUserProfileTabData(ctx *context.Context, profileDbRepo *repo_model.R
|
||||
repos []*repo_model.Repository
|
||||
count int64
|
||||
total int
|
||||
curRows int
|
||||
orderBy db.SearchOrderBy
|
||||
)
|
||||
|
||||
@@ -161,21 +162,15 @@ func prepareUserProfileTabData(ctx *context.Context, profileDbRepo *repo_model.R
|
||||
ctx.Data["Cards"] = following
|
||||
total = int(numFollowing)
|
||||
case "activity":
|
||||
// prepare heatmap data
|
||||
if setting.Service.EnableUserHeatmap {
|
||||
data, err := activities_model.GetUserHeatmapDataByUser(ctx, ctx.ContextUser, ctx.Doer)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetUserHeatmapDataByUser", err)
|
||||
return
|
||||
}
|
||||
ctx.Data["HeatmapData"] = data
|
||||
ctx.Data["HeatmapTotalContributions"] = activities_model.GetTotalContributionsInHeatmap(data)
|
||||
if setting.Service.EnableUserHeatmap && activities_model.ActivityReadable(ctx.ContextUser, ctx.Doer) {
|
||||
ctx.Data["EnableHeatmap"] = true
|
||||
ctx.Data["HeatmapURL"] = ctx.ContextUser.HomeLink() + "/-/heatmap"
|
||||
}
|
||||
|
||||
date := ctx.FormString("date")
|
||||
pagingNum = setting.UI.FeedPagingNum
|
||||
showPrivate := ctx.IsSigned && (ctx.Doer.IsAdmin || ctx.Doer.ID == ctx.ContextUser.ID)
|
||||
items, count, err := feed_service.GetFeeds(ctx, activities_model.GetFeedsOptions{
|
||||
items, feedCount, err := feed_service.GetFeedsForDashboard(ctx, activities_model.GetFeedsOptions{
|
||||
RequestedUser: ctx.ContextUser,
|
||||
Actor: ctx.Doer,
|
||||
IncludePrivate: showPrivate,
|
||||
@@ -193,8 +188,8 @@ func prepareUserProfileTabData(ctx *context.Context, profileDbRepo *repo_model.R
|
||||
}
|
||||
ctx.Data["Feeds"] = items
|
||||
ctx.Data["Date"] = date
|
||||
|
||||
total = int(count)
|
||||
curRows = len(items)
|
||||
total = feedCount
|
||||
case "stars":
|
||||
ctx.Data["PageIsProfileStarList"] = true
|
||||
ctx.Data["ShowRepoOwnerOnList"] = true
|
||||
@@ -316,6 +311,9 @@ func prepareUserProfileTabData(ctx *context.Context, profileDbRepo *repo_model.R
|
||||
}
|
||||
|
||||
pager := context.NewPagination(total, pagingNum, page, 5)
|
||||
if tab == "activity" {
|
||||
pager.WithCurRows(curRows)
|
||||
}
|
||||
pager.AddParamFromRequest(ctx.Req)
|
||||
ctx.Data["Page"] = pager
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user