From 07ada3666b19396eaba3575e6157845255bb15ef Mon Sep 17 00:00:00 2001 From: wxiaoguang Date: Fri, 30 Jan 2026 10:23:38 +0800 Subject: [PATCH 1/8] chore: add comments for "api/healthz", clean up test env (#36481) GITEA_UNIT_TESTS_LOG_SQL is renamed to GITEA_TEST_LOG_SQL --- Dockerfile | 1 + Dockerfile.rootless | 1 + Makefile | 70 ++++----- cmd/main.go | 2 +- cmd/main_test.go | 6 +- models/db/engine_init.go | 2 +- models/migrations/base/db.go | 118 -------------- models/migrations/base/db_test.go | 4 + models/migrations/base/main_test.go | 12 -- models/migrations/base/tests.go | 144 ++++++++++++++---- models/unittest/fixtures_test.go | 5 +- models/unittest/testdb.go | 11 +- modules/gitrepo/main_test.go | 4 +- modules/setting/config_env.go | 17 +-- modules/setting/testenv.go | 61 ++++++++ modules/test/utils.go | 20 --- modules/test/utils_test.go | 17 --- modules/testlogger/testlogger.go | 12 +- playwright.config.ts | 2 +- routers/web/healthcheck/check.go | 9 ++ services/migrations/gitea_downloader_test.go | 6 +- tests/e2e/e2e_test.go | 2 +- .../migration-test/migration_test.go | 16 +- tests/test_utils.go | 37 +---- 24 files changed, 256 insertions(+), 323 deletions(-) delete mode 100644 models/migrations/base/main_test.go create mode 100644 modules/setting/testenv.go delete mode 100644 modules/test/utils_test.go diff --git a/Dockerfile b/Dockerfile index 7cee0f32d3..cb536b0bde 100644 --- a/Dockerfile +++ b/Dockerfile @@ -76,5 +76,6 @@ ENV GITEA_CUSTOM=/data/gitea VOLUME ["/data"] +# HINT: HEALTH-CHECK-ENDPOINT: don't use HEALTHCHECK, search this hint keyword for more information ENTRYPOINT ["/usr/bin/entrypoint"] CMD ["/usr/bin/s6-svscan", "/etc/s6"] diff --git a/Dockerfile.rootless b/Dockerfile.rootless index 8a6fa587e9..e4ba3d2fba 100644 --- a/Dockerfile.rootless +++ b/Dockerfile.rootless @@ -77,5 +77,6 @@ ENV HOME="/var/lib/gitea/git" VOLUME ["/var/lib/gitea", "/etc/gitea"] WORKDIR /var/lib/gitea +# HINT: HEALTH-CHECK-ENDPOINT: don't use HEALTHCHECK, search this hint keyword for more information ENTRYPOINT ["/usr/bin/dumb-init", "--", "/usr/local/bin/docker-entrypoint.sh"] CMD [] diff --git a/Makefile b/Makefile index 49474b0c5c..0a5f174d6d 100644 --- a/Makefile +++ b/Makefile @@ -489,11 +489,11 @@ generate-ini-sqlite: .PHONY: test-sqlite test-sqlite: integrations.sqlite.test generate-ini-sqlite - GITEA_ROOT="$(CURDIR)" GITEA_CONF=tests/sqlite.ini ./integrations.sqlite.test + GITEA_TEST_CONF=tests/sqlite.ini ./integrations.sqlite.test .PHONY: test-sqlite\#% test-sqlite\#%: integrations.sqlite.test generate-ini-sqlite - GITEA_ROOT="$(CURDIR)" GITEA_CONF=tests/sqlite.ini ./integrations.sqlite.test -test.run $(subst .,/,$*) + GITEA_TEST_CONF=tests/sqlite.ini ./integrations.sqlite.test -test.run $(subst .,/,$*) .PHONY: test-sqlite-migration test-sqlite-migration: migrations.sqlite.test migrations.individual.sqlite.test @@ -510,11 +510,11 @@ generate-ini-mysql: .PHONY: test-mysql test-mysql: integrations.mysql.test generate-ini-mysql - GITEA_ROOT="$(CURDIR)" GITEA_CONF=tests/mysql.ini ./integrations.mysql.test + GITEA_TEST_CONF=tests/mysql.ini ./integrations.mysql.test .PHONY: test-mysql\#% test-mysql\#%: integrations.mysql.test generate-ini-mysql - GITEA_ROOT="$(CURDIR)" GITEA_CONF=tests/mysql.ini ./integrations.mysql.test -test.run $(subst .,/,$*) + GITEA_TEST_CONF=tests/mysql.ini ./integrations.mysql.test -test.run $(subst .,/,$*) .PHONY: test-mysql-migration test-mysql-migration: migrations.mysql.test migrations.individual.mysql.test @@ -533,11 +533,11 @@ generate-ini-pgsql: .PHONY: test-pgsql test-pgsql: integrations.pgsql.test generate-ini-pgsql - GITEA_ROOT="$(CURDIR)" GITEA_CONF=tests/pgsql.ini ./integrations.pgsql.test + GITEA_TEST_CONF=tests/pgsql.ini ./integrations.pgsql.test .PHONY: test-pgsql\#% test-pgsql\#%: integrations.pgsql.test generate-ini-pgsql - GITEA_ROOT="$(CURDIR)" GITEA_CONF=tests/pgsql.ini ./integrations.pgsql.test -test.run $(subst .,/,$*) + GITEA_TEST_CONF=tests/pgsql.ini ./integrations.pgsql.test -test.run $(subst .,/,$*) .PHONY: test-pgsql-migration test-pgsql-migration: migrations.pgsql.test migrations.individual.pgsql.test @@ -554,11 +554,11 @@ generate-ini-mssql: .PHONY: test-mssql test-mssql: integrations.mssql.test generate-ini-mssql - GITEA_ROOT="$(CURDIR)" GITEA_CONF=tests/mssql.ini ./integrations.mssql.test + GITEA_TEST_CONF=tests/mssql.ini ./integrations.mssql.test .PHONY: test-mssql\#% test-mssql\#%: integrations.mssql.test generate-ini-mssql - GITEA_ROOT="$(CURDIR)" GITEA_CONF=tests/mssql.ini ./integrations.mssql.test -test.run $(subst .,/,$*) + GITEA_TEST_CONF=tests/mssql.ini ./integrations.mssql.test -test.run $(subst .,/,$*) .PHONY: test-mssql-migration test-mssql-migration: migrations.mssql.test migrations.individual.mssql.test @@ -577,59 +577,59 @@ test-e2e: test-e2e-sqlite .PHONY: test-e2e-sqlite test-e2e-sqlite: playwright e2e.sqlite.test generate-ini-sqlite - GITEA_ROOT="$(CURDIR)" GITEA_CONF=tests/sqlite.ini ./e2e.sqlite.test + GITEA_TEST_CONF=tests/sqlite.ini ./e2e.sqlite.test .PHONY: test-e2e-sqlite\#% test-e2e-sqlite\#%: playwright e2e.sqlite.test generate-ini-sqlite - GITEA_ROOT="$(CURDIR)" GITEA_CONF=tests/sqlite.ini ./e2e.sqlite.test -test.run TestE2e/$* + GITEA_TEST_CONF=tests/sqlite.ini ./e2e.sqlite.test -test.run TestE2e/$* .PHONY: test-e2e-mysql test-e2e-mysql: playwright e2e.mysql.test generate-ini-mysql - GITEA_ROOT="$(CURDIR)" GITEA_CONF=tests/mysql.ini ./e2e.mysql.test + GITEA_TEST_CONF=tests/mysql.ini ./e2e.mysql.test .PHONY: test-e2e-mysql\#% test-e2e-mysql\#%: playwright e2e.mysql.test generate-ini-mysql - GITEA_ROOT="$(CURDIR)" GITEA_CONF=tests/mysql.ini ./e2e.mysql.test -test.run TestE2e/$* + GITEA_TEST_CONF=tests/mysql.ini ./e2e.mysql.test -test.run TestE2e/$* .PHONY: test-e2e-pgsql test-e2e-pgsql: playwright e2e.pgsql.test generate-ini-pgsql - GITEA_ROOT="$(CURDIR)" GITEA_CONF=tests/pgsql.ini ./e2e.pgsql.test + GITEA_TEST_CONF=tests/pgsql.ini ./e2e.pgsql.test .PHONY: test-e2e-pgsql\#% test-e2e-pgsql\#%: playwright e2e.pgsql.test generate-ini-pgsql - GITEA_ROOT="$(CURDIR)" GITEA_CONF=tests/pgsql.ini ./e2e.pgsql.test -test.run TestE2e/$* + GITEA_TEST_CONF=tests/pgsql.ini ./e2e.pgsql.test -test.run TestE2e/$* .PHONY: test-e2e-mssql test-e2e-mssql: playwright e2e.mssql.test generate-ini-mssql - GITEA_ROOT="$(CURDIR)" GITEA_CONF=tests/mssql.ini ./e2e.mssql.test + GITEA_TEST_CONF=tests/mssql.ini ./e2e.mssql.test .PHONY: test-e2e-mssql\#% test-e2e-mssql\#%: playwright e2e.mssql.test generate-ini-mssql - GITEA_ROOT="$(CURDIR)" GITEA_CONF=tests/mssql.ini ./e2e.mssql.test -test.run TestE2e/$* + GITEA_TEST_CONF=tests/mssql.ini ./e2e.mssql.test -test.run TestE2e/$* .PHONY: bench-sqlite bench-sqlite: integrations.sqlite.test generate-ini-sqlite - GITEA_ROOT="$(CURDIR)" GITEA_CONF=tests/sqlite.ini ./integrations.sqlite.test -test.cpuprofile=cpu.out -test.run DontRunTests -test.bench . + GITEA_TEST_CONF=tests/sqlite.ini ./integrations.sqlite.test -test.cpuprofile=cpu.out -test.run DontRunTests -test.bench . .PHONY: bench-mysql bench-mysql: integrations.mysql.test generate-ini-mysql - GITEA_ROOT="$(CURDIR)" GITEA_CONF=tests/mysql.ini ./integrations.mysql.test -test.cpuprofile=cpu.out -test.run DontRunTests -test.bench . + GITEA_TEST_CONF=tests/mysql.ini ./integrations.mysql.test -test.cpuprofile=cpu.out -test.run DontRunTests -test.bench . .PHONY: bench-mssql bench-mssql: integrations.mssql.test generate-ini-mssql - GITEA_ROOT="$(CURDIR)" GITEA_CONF=tests/mssql.ini ./integrations.mssql.test -test.cpuprofile=cpu.out -test.run DontRunTests -test.bench . + GITEA_TEST_CONF=tests/mssql.ini ./integrations.mssql.test -test.cpuprofile=cpu.out -test.run DontRunTests -test.bench . .PHONY: bench-pgsql bench-pgsql: integrations.pgsql.test generate-ini-pgsql - GITEA_ROOT="$(CURDIR)" GITEA_CONF=tests/pgsql.ini ./integrations.pgsql.test -test.cpuprofile=cpu.out -test.run DontRunTests -test.bench . + GITEA_TEST_CONF=tests/pgsql.ini ./integrations.pgsql.test -test.cpuprofile=cpu.out -test.run DontRunTests -test.bench . .PHONY: integration-test-coverage integration-test-coverage: integrations.cover.test generate-ini-mysql - GITEA_ROOT="$(CURDIR)" GITEA_CONF=tests/mysql.ini ./integrations.cover.test -test.coverprofile=integration.coverage.out + GITEA_TEST_CONF=tests/mysql.ini ./integrations.cover.test -test.coverprofile=integration.coverage.out .PHONY: integration-test-coverage-sqlite integration-test-coverage-sqlite: integrations.cover.sqlite.test generate-ini-sqlite - GITEA_ROOT="$(CURDIR)" GITEA_CONF=tests/sqlite.ini ./integrations.cover.sqlite.test -test.coverprofile=integration.coverage.out + GITEA_TEST_CONF=tests/sqlite.ini ./integrations.cover.sqlite.test -test.coverprofile=integration.coverage.out integrations.mysql.test: git-check $(GO_SOURCES) $(GO) test $(GOTESTFLAGS) -c code.gitea.io/gitea/tests/integration -o integrations.mysql.test @@ -652,54 +652,54 @@ integrations.cover.sqlite.test: git-check $(GO_SOURCES) .PHONY: migrations.mysql.test migrations.mysql.test: $(GO_SOURCES) generate-ini-mysql $(GO) test $(GOTESTFLAGS) -c code.gitea.io/gitea/tests/integration/migration-test -o migrations.mysql.test - GITEA_ROOT="$(CURDIR)" GITEA_CONF=tests/mysql.ini ./migrations.mysql.test + GITEA_TEST_CONF=tests/mysql.ini ./migrations.mysql.test .PHONY: migrations.pgsql.test migrations.pgsql.test: $(GO_SOURCES) generate-ini-pgsql $(GO) test $(GOTESTFLAGS) -c code.gitea.io/gitea/tests/integration/migration-test -o migrations.pgsql.test - GITEA_ROOT="$(CURDIR)" GITEA_CONF=tests/pgsql.ini ./migrations.pgsql.test + GITEA_TEST_CONF=tests/pgsql.ini ./migrations.pgsql.test .PHONY: migrations.mssql.test migrations.mssql.test: $(GO_SOURCES) generate-ini-mssql $(GO) test $(GOTESTFLAGS) -c code.gitea.io/gitea/tests/integration/migration-test -o migrations.mssql.test - GITEA_ROOT="$(CURDIR)" GITEA_CONF=tests/mssql.ini ./migrations.mssql.test + GITEA_TEST_CONF=tests/mssql.ini ./migrations.mssql.test .PHONY: migrations.sqlite.test migrations.sqlite.test: $(GO_SOURCES) generate-ini-sqlite $(GO) test $(GOTESTFLAGS) -c code.gitea.io/gitea/tests/integration/migration-test -o migrations.sqlite.test -tags '$(TEST_TAGS)' - GITEA_ROOT="$(CURDIR)" GITEA_CONF=tests/sqlite.ini ./migrations.sqlite.test + GITEA_TEST_CONF=tests/sqlite.ini ./migrations.sqlite.test .PHONY: migrations.individual.mysql.test migrations.individual.mysql.test: $(GO_SOURCES) - GITEA_ROOT="$(CURDIR)" GITEA_CONF=tests/mysql.ini $(GO) test $(GOTESTFLAGS) -tags='$(TEST_TAGS)' -p 1 $(MIGRATE_TEST_PACKAGES) + GITEA_TEST_CONF=tests/mysql.ini $(GO) test $(GOTESTFLAGS) -tags='$(TEST_TAGS)' -p 1 $(MIGRATE_TEST_PACKAGES) .PHONY: migrations.individual.sqlite.test\#% migrations.individual.sqlite.test\#%: $(GO_SOURCES) generate-ini-sqlite - GITEA_ROOT="$(CURDIR)" GITEA_CONF=tests/sqlite.ini $(GO) test $(GOTESTFLAGS) -tags '$(TEST_TAGS)' code.gitea.io/gitea/models/migrations/$* + GITEA_TEST_CONF=tests/sqlite.ini $(GO) test $(GOTESTFLAGS) -tags '$(TEST_TAGS)' code.gitea.io/gitea/models/migrations/$* .PHONY: migrations.individual.pgsql.test migrations.individual.pgsql.test: $(GO_SOURCES) - GITEA_ROOT="$(CURDIR)" GITEA_CONF=tests/pgsql.ini $(GO) test $(GOTESTFLAGS) -tags='$(TEST_TAGS)' -p 1 $(MIGRATE_TEST_PACKAGES) + GITEA_TEST_CONF=tests/pgsql.ini $(GO) test $(GOTESTFLAGS) -tags='$(TEST_TAGS)' -p 1 $(MIGRATE_TEST_PACKAGES) .PHONY: migrations.individual.pgsql.test\#% migrations.individual.pgsql.test\#%: $(GO_SOURCES) generate-ini-pgsql - GITEA_ROOT="$(CURDIR)" GITEA_CONF=tests/pgsql.ini $(GO) test $(GOTESTFLAGS) -tags '$(TEST_TAGS)' code.gitea.io/gitea/models/migrations/$* + GITEA_TEST_CONF=tests/pgsql.ini $(GO) test $(GOTESTFLAGS) -tags '$(TEST_TAGS)' code.gitea.io/gitea/models/migrations/$* .PHONY: migrations.individual.mssql.test migrations.individual.mssql.test: $(GO_SOURCES) generate-ini-mssql - GITEA_ROOT="$(CURDIR)" GITEA_CONF=tests/mssql.ini $(GO) test $(GOTESTFLAGS) -tags='$(TEST_TAGS)' -p 1 $(MIGRATE_TEST_PACKAGES) + GITEA_TEST_CONF=tests/mssql.ini $(GO) test $(GOTESTFLAGS) -tags='$(TEST_TAGS)' -p 1 $(MIGRATE_TEST_PACKAGES) .PHONY: migrations.individual.mssql.test\#% migrations.individual.mssql.test\#%: $(GO_SOURCES) generate-ini-mssql - GITEA_ROOT="$(CURDIR)" GITEA_CONF=tests/mssql.ini $(GO) test $(GOTESTFLAGS) -tags '$(TEST_TAGS)' code.gitea.io/gitea/models/migrations/$* + GITEA_TEST_CONF=tests/mssql.ini $(GO) test $(GOTESTFLAGS) -tags '$(TEST_TAGS)' code.gitea.io/gitea/models/migrations/$* .PHONY: migrations.individual.sqlite.test migrations.individual.sqlite.test: $(GO_SOURCES) generate-ini-sqlite - GITEA_ROOT="$(CURDIR)" GITEA_CONF=tests/sqlite.ini $(GO) test $(GOTESTFLAGS) -tags='$(TEST_TAGS)' -p 1 $(MIGRATE_TEST_PACKAGES) + GITEA_TEST_CONF=tests/sqlite.ini $(GO) test $(GOTESTFLAGS) -tags='$(TEST_TAGS)' -p 1 $(MIGRATE_TEST_PACKAGES) .PHONY: migrations.individual.sqlite.test\#% migrations.individual.sqlite.test\#%: $(GO_SOURCES) generate-ini-sqlite - GITEA_ROOT="$(CURDIR)" GITEA_CONF=tests/sqlite.ini $(GO) test $(GOTESTFLAGS) -tags '$(TEST_TAGS)' code.gitea.io/gitea/models/migrations/$* + GITEA_TEST_CONF=tests/sqlite.ini $(GO) test $(GOTESTFLAGS) -tags '$(TEST_TAGS)' code.gitea.io/gitea/models/migrations/$* e2e.mysql.test: $(GO_SOURCES) $(GO) test $(GOTESTFLAGS) -c code.gitea.io/gitea/tests/e2e -o e2e.mysql.test @@ -747,7 +747,7 @@ security-check: $(EXECUTABLE): $(GO_SOURCES) $(TAGS_PREREQ) ifneq ($(and $(STATIC),$(findstring pam,$(TAGS))),) - $(error pam support set via TAGS doesn't support static builds) + $(error pam support set via TAGS does not support static builds) endif CGO_ENABLED="$(CGO_ENABLED)" CGO_CFLAGS="$(CGO_CFLAGS)" $(GO) build $(GOFLAGS) $(EXTRA_GOFLAGS) -tags '$(TAGS)' -ldflags '-s -w $(EXTLDFLAGS) $(LDFLAGS)' -o $@ diff --git a/cmd/main.go b/cmd/main.go index 203799f02f..2ee00382d7 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -149,7 +149,7 @@ func NewMainApp(appVer AppVersion) *cli.Command { app.Commands = append(app.Commands, subCmdWithConfig...) app.Commands = append(app.Commands, subCmdStandalone...) - setting.InitGiteaEnvVars() + setting.UnsetUnnecessaryEnvVars() return app } diff --git a/cmd/main_test.go b/cmd/main_test.go index 69ea1237c6..b1f6bb3ba9 100644 --- a/cmd/main_test.go +++ b/cmd/main_test.go @@ -157,6 +157,7 @@ func TestCliCmd(t *testing.T) { for _, c := range cases { t.Run(c.cmd, func(t *testing.T) { + defer test.MockVariableValue(&setting.InstallLock, false)() app := newTestApp(cli.Command{ Action: func(ctx context.Context, cmd *cli.Command) error { _, _ = fmt.Fprint(cmd.Root().Writer, makePathOutput(setting.AppWorkPath, setting.CustomPath, setting.CustomConf)) @@ -170,7 +171,10 @@ func TestCliCmd(t *testing.T) { r, err := runTestApp(app, args...) assert.NoError(t, err, c.cmd) assert.NotEmpty(t, c.exp, c.cmd) - assert.Contains(t, r.Stdout, c.exp, c.cmd) + if !assert.Contains(t, r.Stdout, c.exp, c.cmd) { + t.Log("Full output:\n" + r.Stdout) + t.Log("Expected:\n" + c.exp) + } }) } } diff --git a/models/db/engine_init.go b/models/db/engine_init.go index f26189b805..ef5db3ff5e 100644 --- a/models/db/engine_init.go +++ b/models/db/engine_init.go @@ -57,7 +57,7 @@ func InitEngine(ctx context.Context) error { xe, err := newXORMEngine() if err != nil { if strings.Contains(err.Error(), "SQLite3 support") { - return fmt.Errorf(`sqlite3 requires: -tags sqlite,sqlite_unlock_notify%s%w`, "\n", err) + return fmt.Errorf("sqlite3 requires: -tags sqlite,sqlite_unlock_notify\n%w", err) } return fmt.Errorf("failed to connect to database: %w", err) } diff --git a/models/migrations/base/db.go b/models/migrations/base/db.go index 479a46379c..3b8f0e00a0 100644 --- a/models/migrations/base/db.go +++ b/models/migrations/base/db.go @@ -5,21 +5,14 @@ package base import ( "context" - "database/sql" "errors" "fmt" - "os" - "path" "reflect" "regexp" "strings" - "time" - "code.gitea.io/gitea/models/db" - "code.gitea.io/gitea/models/unittest" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" - "code.gitea.io/gitea/modules/util" "xorm.io/xorm" "xorm.io/xorm/schemas" @@ -515,114 +508,3 @@ func ModifyColumn(x *xorm.Engine, tableName string, col *schemas.Column) error { } return nil } - -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() (*xorm.Engine, error) { - if err := db.InitEngine(context.Background()); err != nil { - return nil, err - } - x := unittest.GetXORMEngine() - return x, nil -} - -func deleteDB() error { - switch { - case setting.Database.Type.IsSQLite3(): - if err := util.Remove(setting.Database.Path); err != nil { - return err - } - return os.MkdirAll(path.Dir(setting.Database.Path), os.ModePerm) - - case setting.Database.Type.IsMySQL(): - db, err := sql.Open("mysql", fmt.Sprintf("%s:%s@tcp(%s)/", - setting.Database.User, setting.Database.Passwd, setting.Database.Host)) - if err != nil { - return err - } - defer db.Close() - - if _, err = db.Exec("DROP DATABASE IF EXISTS " + setting.Database.Name); err != nil { - return err - } - - if _, err = db.Exec("CREATE DATABASE IF NOT EXISTS " + setting.Database.Name); err != nil { - return err - } - return nil - case setting.Database.Type.IsPostgreSQL(): - db, err := sql.Open("postgres", fmt.Sprintf("postgres://%s:%s@%s/?sslmode=%s", - setting.Database.User, setting.Database.Passwd, setting.Database.Host, setting.Database.SSLMode)) - if err != nil { - return err - } - defer db.Close() - - if _, err = db.Exec("DROP DATABASE IF EXISTS " + setting.Database.Name); err != nil { - return err - } - - if _, err = db.Exec("CREATE DATABASE " + setting.Database.Name); err != nil { - return err - } - db.Close() - - // Check if we need to setup a specific schema - if len(setting.Database.Schema) != 0 { - db, err = sql.Open("postgres", fmt.Sprintf("postgres://%s:%s@%s/%s?sslmode=%s", - setting.Database.User, setting.Database.Passwd, setting.Database.Host, setting.Database.Name, setting.Database.SSLMode)) - if err != nil { - return err - } - defer db.Close() - - schrows, err := db.Query(fmt.Sprintf("SELECT 1 FROM information_schema.schemata WHERE schema_name = '%s'", setting.Database.Schema)) - if err != nil { - return err - } - defer schrows.Close() - - if !schrows.Next() { - // Create and setup a DB schema - _, err = db.Exec("CREATE SCHEMA " + setting.Database.Schema) - if err != nil { - return err - } - } - - // Make the user's default search path the created schema; this will affect new connections - _, err = db.Exec(fmt.Sprintf(`ALTER USER "%s" SET search_path = %s`, setting.Database.User, setting.Database.Schema)) - if err != nil { - return err - } - return nil - } - case setting.Database.Type.IsMSSQL(): - host, port := setting.ParseMSSQLHostPort(setting.Database.Host) - db, err := sql.Open("mssql", fmt.Sprintf("server=%s; port=%s; database=%s; user id=%s; password=%s;", - host, port, "master", setting.Database.User, setting.Database.Passwd)) - if err != nil { - return err - } - defer db.Close() - - if _, err = db.Exec(fmt.Sprintf("DROP DATABASE IF EXISTS [%s]", setting.Database.Name)); err != nil { - return err - } - if _, err = db.Exec(fmt.Sprintf("CREATE DATABASE [%s]", setting.Database.Name)); err != nil { - return err - } - } - - return nil -} diff --git a/models/migrations/base/db_test.go b/models/migrations/base/db_test.go index 80bf00b22a..00635ca72e 100644 --- a/models/migrations/base/db_test.go +++ b/models/migrations/base/db_test.go @@ -11,6 +11,10 @@ import ( "xorm.io/xorm/names" ) +func TestMain(m *testing.M) { + MainTest(m) +} + func Test_DropTableColumns(t *testing.T) { x, deferable := PrepareTestEnv(t, 0) if x == nil || t.Failed() { diff --git a/models/migrations/base/main_test.go b/models/migrations/base/main_test.go deleted file mode 100644 index c1c789150f..0000000000 --- a/models/migrations/base/main_test.go +++ /dev/null @@ -1,12 +0,0 @@ -// Copyright 2021 The Gitea Authors. All rights reserved. -// SPDX-License-Identifier: MIT - -package base - -import ( - "testing" -) - -func TestMain(m *testing.M) { - MainTest(m) -} diff --git a/models/migrations/base/tests.go b/models/migrations/base/tests.go index 83beca8fb9..36afd35dd4 100644 --- a/models/migrations/base/tests.go +++ b/models/migrations/base/tests.go @@ -4,18 +4,21 @@ package base import ( + "database/sql" "fmt" "os" + "path" "path/filepath" - "runtime" "testing" + "time" + "code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/unittest" "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/tempdir" - "code.gitea.io/gitea/modules/test" "code.gitea.io/gitea/modules/testlogger" + "code.gitea.io/gitea/modules/util" "github.com/stretchr/testify/require" "xorm.io/xorm" @@ -24,6 +27,117 @@ 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 + } + x := unittest.GetXORMEngine() + return x, nil +} + +func deleteDB() error { + switch { + case setting.Database.Type.IsSQLite3(): + if err := util.Remove(setting.Database.Path); err != nil { + return err + } + return os.MkdirAll(path.Dir(setting.Database.Path), os.ModePerm) + + case setting.Database.Type.IsMySQL(): + db, err := sql.Open("mysql", fmt.Sprintf("%s:%s@tcp(%s)/", + setting.Database.User, setting.Database.Passwd, setting.Database.Host)) + if err != nil { + return err + } + defer db.Close() + + if _, err = db.Exec("DROP DATABASE IF EXISTS " + setting.Database.Name); err != nil { + return err + } + + if _, err = db.Exec("CREATE DATABASE IF NOT EXISTS " + setting.Database.Name); err != nil { + return err + } + return nil + case setting.Database.Type.IsPostgreSQL(): + db, err := sql.Open("postgres", fmt.Sprintf("postgres://%s:%s@%s/?sslmode=%s", + setting.Database.User, setting.Database.Passwd, setting.Database.Host, setting.Database.SSLMode)) + if err != nil { + return err + } + defer db.Close() + + if _, err = db.Exec("DROP DATABASE IF EXISTS " + setting.Database.Name); err != nil { + return err + } + + if _, err = db.Exec("CREATE DATABASE " + setting.Database.Name); err != nil { + return err + } + db.Close() + + // Check if we need to setup a specific schema + if len(setting.Database.Schema) != 0 { + db, err = sql.Open("postgres", fmt.Sprintf("postgres://%s:%s@%s/%s?sslmode=%s", + setting.Database.User, setting.Database.Passwd, setting.Database.Host, setting.Database.Name, setting.Database.SSLMode)) + if err != nil { + return err + } + defer db.Close() + + schrows, err := db.Query(fmt.Sprintf("SELECT 1 FROM information_schema.schemata WHERE schema_name = '%s'", setting.Database.Schema)) + if err != nil { + return err + } + defer schrows.Close() + + if !schrows.Next() { + // Create and setup a DB schema + _, err = db.Exec("CREATE SCHEMA " + setting.Database.Schema) + if err != nil { + return err + } + } + + // Make the user's default search path the created schema; this will affect new connections + _, err = db.Exec(fmt.Sprintf(`ALTER USER "%s" SET search_path = %s`, setting.Database.User, setting.Database.Schema)) + if err != nil { + return err + } + return nil + } + case setting.Database.Type.IsMSSQL(): + host, port := setting.ParseMSSQLHostPort(setting.Database.Host) + db, err := sql.Open("mssql", fmt.Sprintf("server=%s; port=%s; database=%s; user id=%s; password=%s;", + host, port, "master", setting.Database.User, setting.Database.Passwd)) + if err != nil { + return err + } + defer db.Close() + + if _, err = db.Exec(fmt.Sprintf("DROP DATABASE IF EXISTS [%s]", setting.Database.Name)); err != nil { + return err + } + if _, err = db.Exec(fmt.Sprintf("CREATE DATABASE [%s]", setting.Database.Name)); err != nil { + return err + } + } + + return nil +} + // PrepareTestEnv prepares the test environment and reset the database. The skip parameter should usually be 0. // Provide models to be sync'd with the database - in particular any models you expect fixtures to be loaded from. // @@ -40,7 +154,7 @@ func PrepareTestEnv(t *testing.T, skip int, syncModels ...any) (*xorm.Engine, fu return nil, deferFn } - x, err := newXORMEngine() + x, err := newXORMEngine(t) require.NoError(t, err) if x != nil { oldDefer := deferFn @@ -101,28 +215,7 @@ func LoadTableSchemasMap(t *testing.T, x *xorm.Engine) map[string]*schemas.Table func MainTest(m *testing.M) { testlogger.Init() - - giteaRoot := test.SetupGiteaRoot() - giteaBinary := "gitea" - if runtime.GOOS == "windows" { - giteaBinary += ".exe" - } - setting.AppPath = filepath.Join(giteaRoot, giteaBinary) - if _, err := os.Stat(setting.AppPath); err != nil { - testlogger.Fatalf("Could not find gitea binary at %s\n", setting.AppPath) - } - - giteaConf := os.Getenv("GITEA_CONF") - if giteaConf == "" { - giteaConf = filepath.Join(filepath.Dir(setting.AppPath), "tests/sqlite.ini") - _, _ = fmt.Fprintf(os.Stderr, "Environment variable $GITEA_CONF not set - defaulting to %s\n", giteaConf) - } - - if !filepath.IsAbs(giteaConf) { - setting.CustomConf = filepath.Join(giteaRoot, giteaConf) - } else { - setting.CustomConf = giteaConf - } + setting.SetupGiteaTestEnv() tmpDataPath, cleanup, err := tempdir.OsTempDir("gitea-test").MkdirTempRandom("data") if err != nil { @@ -130,7 +223,6 @@ func MainTest(m *testing.M) { } defer cleanup() - setting.CustomPath = filepath.Join(setting.AppWorkPath, "custom") setting.AppDataPath = tmpDataPath unittest.InitSettingsForTesting() diff --git a/models/unittest/fixtures_test.go b/models/unittest/fixtures_test.go index 8a4c5f1793..ebf209f5f4 100644 --- a/models/unittest/fixtures_test.go +++ b/models/unittest/fixtures_test.go @@ -9,7 +9,7 @@ import ( "code.gitea.io/gitea/models/unittest" user_model "code.gitea.io/gitea/models/user" - "code.gitea.io/gitea/modules/test" + "code.gitea.io/gitea/modules/setting" "github.com/stretchr/testify/require" "xorm.io/xorm" @@ -60,7 +60,8 @@ func NewFixturesLoaderVendorGoTestfixtures(e *xorm.Engine, opts unittest.Fixture func prepareTestFixturesLoaders(t testing.TB) unittest.FixturesOptions { _ = user_model.User{} - opts := unittest.FixturesOptions{Dir: filepath.Join(test.SetupGiteaRoot(), "models", "fixtures"), Files: []string{ + giteaRoot := setting.SetupGiteaTestEnv() + opts := unittest.FixturesOptions{Dir: filepath.Join(giteaRoot, "models", "fixtures"), Files: []string{ "user.yml", }} require.NoError(t, unittest.CreateTestEngine(opts)) diff --git a/models/unittest/testdb.go b/models/unittest/testdb.go index 4611a079ec..398090760e 100644 --- a/models/unittest/testdb.go +++ b/models/unittest/testdb.go @@ -21,7 +21,6 @@ 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/test" "code.gitea.io/gitea/modules/util" "github.com/stretchr/testify/assert" @@ -60,7 +59,6 @@ func InitSettingsForTesting() { _ = hash.Register("dummy", hash.NewDummyHasher) setting.PasswordHashAlgo, _ = hash.SetDefaultPasswordHashAlgorithm("dummy") - setting.InitGiteaEnvVarsForTesting() } // TestOptions represents test options @@ -74,8 +72,7 @@ type TestOptions struct { // test database. Creates the test database, and sets necessary settings. func MainTest(m *testing.M, testOptsArg ...*TestOptions) { testOpts := util.OptionalArg(testOptsArg, &TestOptions{}) - giteaRoot = test.SetupGiteaRoot() - setting.CustomPath = filepath.Join(giteaRoot, "custom") + giteaRoot = setting.SetupGiteaTestEnv() InitSettingsForTesting() fixturesOpts := FixturesOptions{Dir: filepath.Join(giteaRoot, "models", "fixtures"), Files: testOpts.FixtureFiles} @@ -172,7 +169,7 @@ func CreateTestEngine(opts FixturesOptions) error { x, err := xorm.NewEngine("sqlite3", "file::memory:?cache=shared&_txlock=immediate") if err != nil { if strings.Contains(err.Error(), "unknown driver") { - return fmt.Errorf(`sqlite3 requires: -tags sqlite,sqlite_unlock_notify%s%w`, "\n", err) + return fmt.Errorf("sqlite3 requires: -tags sqlite,sqlite_unlock_notify\n%w", err) } return err } @@ -182,7 +179,7 @@ func CreateTestEngine(opts FixturesOptions) error { if err = db.SyncAllTables(); err != nil { return err } - switch os.Getenv("GITEA_UNIT_TESTS_LOG_SQL") { + switch os.Getenv("GITEA_TEST_LOG_SQL") { case "true", "1": x.ShowSQL(true) } @@ -201,5 +198,5 @@ func PrepareTestEnv(t testing.TB) { assert.NoError(t, PrepareTestDatabase()) metaPath := filepath.Join(giteaRoot, "tests", "gitea-repositories-meta") assert.NoError(t, SyncDirs(metaPath, setting.RepoRootPath)) - test.SetupGiteaRoot() // Makes sure GITEA_ROOT is set + setting.SetupGiteaTestEnv() } diff --git a/modules/gitrepo/main_test.go b/modules/gitrepo/main_test.go index d41a1a6ad4..e47eda7bc9 100644 --- a/modules/gitrepo/main_test.go +++ b/modules/gitrepo/main_test.go @@ -8,12 +8,12 @@ import ( "testing" "code.gitea.io/gitea/modules/git" - "code.gitea.io/gitea/modules/test" + "code.gitea.io/gitea/modules/setting" ) func TestMain(m *testing.M) { // resolve repository path relative to the test directory - testRootDir := test.SetupGiteaRoot() + testRootDir := setting.SetupGiteaTestEnv() repoPath = func(repo Repository) string { if filepath.IsAbs(repo.RelativePath()) { return repo.RelativePath() // for testing purpose only diff --git a/modules/setting/config_env.go b/modules/setting/config_env.go index 4758eb72cb..e956e34439 100644 --- a/modules/setting/config_env.go +++ b/modules/setting/config_env.go @@ -167,24 +167,13 @@ func EnvironmentToConfig(cfg ConfigProvider, envs []string) (changed bool) { return changed } -// InitGiteaEnvVars initializes the environment variables for gitea -func InitGiteaEnvVars() { +func UnsetUnnecessaryEnvVars() { // Ideally Gitea should only accept the environment variables which it clearly knows instead of unsetting the ones it doesn't want, - // but the ideal behavior would be a breaking change, and it seems not bringing enough benefits to end users, - // so at the moment we could still keep "unsetting the unnecessary environments" + // but the ideal behavior would be a breaking change, and it seems not bringing enough benefits to end users. + // So at the moment we just keep "unsetting the unnecessary environment variables". // HOME is managed by Gitea, Gitea's git should use "HOME/.gitconfig". // But git would try "XDG_CONFIG_HOME/git/config" first if "HOME/.gitconfig" does not exist, // then our git.InitFull would still write to "XDG_CONFIG_HOME/git/config" if XDG_CONFIG_HOME is set. _ = os.Unsetenv("XDG_CONFIG_HOME") } - -func InitGiteaEnvVarsForTesting() { - InitGiteaEnvVars() - _ = os.Unsetenv("GIT_AUTHOR_NAME") - _ = os.Unsetenv("GIT_AUTHOR_EMAIL") - _ = os.Unsetenv("GIT_AUTHOR_DATE") - _ = os.Unsetenv("GIT_COMMITTER_NAME") - _ = os.Unsetenv("GIT_COMMITTER_EMAIL") - _ = os.Unsetenv("GIT_COMMITTER_DATE") -} diff --git a/modules/setting/testenv.go b/modules/setting/testenv.go new file mode 100644 index 0000000000..52e8912af0 --- /dev/null +++ b/modules/setting/testenv.go @@ -0,0 +1,61 @@ +// Copyright 2026 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package setting + +import ( + "fmt" + "os" + "path/filepath" + "runtime" + "strings" + + "code.gitea.io/gitea/modules/util" +) + +func SetupGiteaTestEnv() string { + giteaRoot := os.Getenv("GITEA_TEST_ROOT") + if giteaRoot == "" { + _, filename, _, _ := runtime.Caller(0) + giteaRoot = filepath.Dir(filepath.Dir(filepath.Dir(filename))) + fixturesDir := filepath.Join(giteaRoot, "models", "fixtures") + if _, err := os.Stat(fixturesDir); err != nil { + panic("in gitea source code directory, fixtures directory not found: " + fixturesDir) + } + } + + appWorkPathBuiltin = giteaRoot + AppWorkPath = giteaRoot + AppPath = filepath.Join(giteaRoot, "gitea") + util.Iif(IsWindows, ".exe", "") + + // 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") + if giteaConf == "" { + // By default, use sqlite.ini for testing, then IDE like GoLand can start the test process with debugger. + // It's easier for developers to debug bugs step by step with a debugger. + // Notice: when doing "ssh push", Gitea executes sub processes, debugger won't work for the sub processes. + giteaConf = "tests/sqlite.ini" + _, _ = fmt.Fprintf(os.Stderr, "Environment variable GITEA_TEST_CONF not set - defaulting to %s\n", giteaConf) + if !EnableSQLite3 { + _, _ = fmt.Fprintf(os.Stderr, "sqlite3 requires: -tags sqlite,sqlite_unlock_notify\n") + os.Exit(1) + } + } + // CustomConf must be absolute path to make tests pass, + CustomConf = filepath.Join(AppWorkPath, giteaConf) + + // also unset unnecessary env vars for testing (only keep "GITEA_TEST_*" ones) + UnsetUnnecessaryEnvVars() + for _, env := range os.Environ() { + if strings.HasPrefix(env, "GIT_") || (strings.HasPrefix(env, "GITEA_") && !strings.HasPrefix(env, "GITEA_TEST_")) { + k, _, _ := strings.Cut(env, "=") + _ = os.Unsetenv(k) + } + } + + // 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 +} diff --git a/modules/test/utils.go b/modules/test/utils.go index 34c11ff6b2..193fee28c8 100644 --- a/modules/test/utils.go +++ b/modules/test/utils.go @@ -9,13 +9,9 @@ import ( "io" "net/http" "net/http/httptest" - "os" - "path/filepath" - "runtime" "strings" "code.gitea.io/gitea/modules/json" - "code.gitea.io/gitea/modules/util" ) // RedirectURL returns the redirect URL of a http response. @@ -59,22 +55,6 @@ func MockVariableValue[T any](p *T, v ...T) (reset func()) { return func() { *p = old } } -// SetupGiteaRoot Sets GITEA_ROOT if it is not already set and returns the value -func SetupGiteaRoot() string { - giteaRoot := os.Getenv("GITEA_ROOT") - if giteaRoot != "" { - return giteaRoot - } - _, filename, _, _ := runtime.Caller(0) - giteaRoot = filepath.Dir(filepath.Dir(filepath.Dir(filename))) - fixturesDir := filepath.Join(giteaRoot, "models", "fixtures") - if exist, _ := util.IsDir(fixturesDir); !exist { - panic("fixtures directory not found: " + fixturesDir) - } - _ = os.Setenv("GITEA_ROOT", giteaRoot) - return giteaRoot -} - func ReadAllTarGzContent(r io.Reader) (map[string]string, error) { gzr, err := gzip.NewReader(r) if err != nil { diff --git a/modules/test/utils_test.go b/modules/test/utils_test.go deleted file mode 100644 index 0469ce97f2..0000000000 --- a/modules/test/utils_test.go +++ /dev/null @@ -1,17 +0,0 @@ -// Copyright 2024 The Gitea Authors. All rights reserved. -// SPDX-License-Identifier: MIT - -package test - -import ( - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestSetupGiteaRoot(t *testing.T) { - t.Setenv("GITEA_ROOT", "test") - assert.Equal(t, "test", SetupGiteaRoot()) - t.Setenv("GITEA_ROOT", "") - assert.NotEqual(t, "test", SetupGiteaRoot()) -} diff --git a/modules/testlogger/testlogger.go b/modules/testlogger/testlogger.go index b0f38644a7..bf08e1d6d5 100644 --- a/modules/testlogger/testlogger.go +++ b/modules/testlogger/testlogger.go @@ -118,7 +118,7 @@ func PrintCurrentTest(t testing.TB, skip ...int) func() { deferHasRun := false t.Cleanup(func() { if !deferHasRun { - Printf("!!! defer function hasn't been run but Cleanup is called\n%s", getRuntimeStackAll()) + Printf("!!! %s defer function hasn't been run but Cleanup is called, usually caused by panic", t.Name()) } }) Printf("=== %s (%s:%d)\n", log.NewColoredValue(t.Name()), strings.TrimPrefix(filename, prefix), line) @@ -171,16 +171,6 @@ func Init() { prefix = strings.TrimSuffix(filename, relFilePath) log.RegisterEventWriter("test", newTestLoggerWriter) - - duration, err := time.ParseDuration(os.Getenv("GITEA_TEST_SLOW_RUN")) - if err == nil && duration > 0 { - TestSlowRun = duration - } - - duration, err = time.ParseDuration(os.Getenv("GITEA_TEST_SLOW_FLUSH")) - if err == nil && duration > 0 { - TestSlowFlush = duration - } } func Fatalf(format string, args ...any) { diff --git a/playwright.config.ts b/playwright.config.ts index d1cd299e25..9e3396465a 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -2,7 +2,7 @@ import {devices} from '@playwright/test'; import {env} from 'node:process'; import type {PlaywrightTestConfig} from '@playwright/test'; -const BASE_URL = env.GITEA_URL?.replace?.(/\/$/g, '') || 'http://localhost:3000'; +const BASE_URL = env.GITEA_TEST_SERVER_URL?.replace?.(/\/$/g, '') || 'http://localhost:3000'; export default { testDir: './tests/e2e/', diff --git a/routers/web/healthcheck/check.go b/routers/web/healthcheck/check.go index 85f47613f0..0eea1e1ff7 100644 --- a/routers/web/healthcheck/check.go +++ b/routers/web/healthcheck/check.go @@ -64,6 +64,15 @@ type componentStatus struct { } // Check is the health check API handler +// +// HINT: HEALTH-CHECK-ENDPOINT: there is no clear definition about what "health" means. +// In most cases, end users don't need to check such endpoint, because even if database is down, +// Gitea will reover after database is up again. Sysop should monitor database and cache status directly. +// +// And keep in mind: this health check should NEVER be used as a "restart" trigger, for example: Docker's "HEALTHCHECK". +// * If Gitea is upgrading and migrating database, there will be a long time before this endpoint starts to return "pass" status. +// In this case, if the checker restarts Gitea just because it doesn't get "pass" status in short time, +// the instance will just be restarted again and again before the migation finishes and the sitution just goes worse. func Check(w http.ResponseWriter, r *http.Request) { rsp := response{ Status: pass, diff --git a/services/migrations/gitea_downloader_test.go b/services/migrations/gitea_downloader_test.go index fb985ee9d5..1a9094f6f2 100644 --- a/services/migrations/gitea_downloader_test.go +++ b/services/migrations/gitea_downloader_test.go @@ -17,10 +17,10 @@ import ( ) func TestGiteaDownloadRepo(t *testing.T) { - // Skip tests if Gitea token is not found - giteaToken := os.Getenv("GITEA_TOKEN") + // Skip tests if Gitea token is not found (TODO: this test seems stopped for long time because there is no token in CI secrets) + giteaToken := os.Getenv("GITEA_TEST_OFFICIAL_SITE_TOKEN") if giteaToken == "" { - t.Skip("skipped test because GITEA_TOKEN was not in the environment") + t.Skip("skipped test because GITEA_TEST_OFFICIAL_SITE_TOKEN was not in the environment") } resp, err := http.Get("https://gitea.com/gitea") diff --git a/tests/e2e/e2e_test.go b/tests/e2e/e2e_test.go index 4a408dbd7b..95093ffd29 100644 --- a/tests/e2e/e2e_test.go +++ b/tests/e2e/e2e_test.go @@ -94,7 +94,7 @@ func TestE2e(t *testing.T) { onGiteaRun(t, func(*testing.T, *url.URL) { cmd := exec.Command(runArgs[0], runArgs...) cmd.Env = os.Environ() - cmd.Env = append(cmd.Env, "GITEA_URL="+setting.AppURL) + cmd.Env = append(cmd.Env, "GITEA_TEST_SERVER_URL="+setting.AppURL) var stdout, stderr bytes.Buffer cmd.Stdout = &stdout diff --git a/tests/integration/migration-test/migration_test.go b/tests/integration/migration-test/migration_test.go index 2659c5c53d..ee49829127 100644 --- a/tests/integration/migration-test/migration_test.go +++ b/tests/integration/migration-test/migration_test.go @@ -25,7 +25,6 @@ import ( "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" - "code.gitea.io/gitea/modules/test" "code.gitea.io/gitea/modules/testlogger" "code.gitea.io/gitea/modules/util" "github.com/stretchr/testify/assert" @@ -37,20 +36,7 @@ var currentEngine *xorm.Engine func initMigrationTest(t *testing.T) func() { testlogger.Init() - giteaRoot := test.SetupGiteaRoot() - setting.AppPath = filepath.Join(giteaRoot, "gitea") - if _, err := os.Stat(setting.AppPath); err != nil { - testlogger.Fatalf(fmt.Sprintf("Could not find gitea binary at %s\n", setting.AppPath)) - } - - giteaConf := os.Getenv("GITEA_CONF") - if giteaConf == "" { - testlogger.Fatalf("Environment variable $GITEA_CONF not set\n") - } else if !path.IsAbs(giteaConf) { - setting.CustomConf = filepath.Join(giteaRoot, giteaConf) - } else { - setting.CustomConf = giteaConf - } + setting.SetupGiteaTestEnv() unittest.InitSettingsForTesting() diff --git a/tests/test_utils.go b/tests/test_utils.go index e63eb75846..0cb99a1f2c 100644 --- a/tests/test_utils.go +++ b/tests/test_utils.go @@ -6,7 +6,6 @@ package tests import ( "database/sql" "fmt" - "os" "path/filepath" "testing" @@ -18,7 +17,6 @@ import ( "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/storage" - "code.gitea.io/gitea/modules/test" "code.gitea.io/gitea/modules/testlogger" "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/routers" @@ -29,40 +27,7 @@ import ( func InitTest(requireGitea bool) { testlogger.Init() - giteaRoot := test.SetupGiteaRoot() - - // TODO: Speedup tests that rely on the event source ticker, confirm whether there is any bug or failure. - // setting.UI.Notification.EventSourceUpdateTime = time.Second - - setting.AppWorkPath = giteaRoot - setting.CustomPath = filepath.Join(setting.AppWorkPath, "custom") - if requireGitea { - giteaBinary := "gitea" - if setting.IsWindows { - giteaBinary += ".exe" - } - setting.AppPath = filepath.Join(giteaRoot, giteaBinary) - if _, err := os.Stat(setting.AppPath); err != nil { - testlogger.Fatalf("Could not find gitea binary at %s\n", setting.AppPath) - } - } - giteaConf := os.Getenv("GITEA_CONF") - if giteaConf == "" { - // By default, use sqlite.ini for testing, then IDE like GoLand can start the test process with debugger. - // It's easier for developers to debug bugs step by step with a debugger. - // Notice: when doing "ssh push", Gitea executes sub processes, debugger won't work for the sub processes. - giteaConf = "tests/sqlite.ini" - _ = os.Setenv("GITEA_CONF", giteaConf) - _, _ = fmt.Fprintf(os.Stderr, "Environment variable $GITEA_CONF not set - defaulting to %s\n", giteaConf) - if !setting.EnableSQLite3 { - testlogger.Fatalf(`sqlite3 requires: -tags sqlite,sqlite_unlock_notify` + "\n") - } - } - if !filepath.IsAbs(giteaConf) { - setting.CustomConf = filepath.Join(giteaRoot, giteaConf) - } else { - setting.CustomConf = giteaConf - } + setting.SetupGiteaTestEnv() unittest.InitSettingsForTesting() setting.Repository.DefaultBranch = "master" // many test code still assume that default branch is called "master" From de829c7821b4f7cba3fb60de47eef347b2751492 Mon Sep 17 00:00:00 2001 From: silverwind Date: Fri, 30 Jan 2026 11:25:30 +0100 Subject: [PATCH 2/8] Update some go dependencies (#36489) I verified the `.env.local` syntax added in https://github.com/alecthomas/chroma/pull/1197 works as expected. --- go.mod | 6 +++--- go.sum | 12 ++++++------ 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/go.mod b/go.mod index 16154ad6cf..26a2b818ef 100644 --- a/go.mod +++ b/go.mod @@ -28,7 +28,7 @@ require ( github.com/ProtonMail/go-crypto v1.3.0 github.com/PuerkitoBio/goquery v1.10.3 github.com/SaveTheRbtz/zstd-seekable-format-go/pkg v0.8.0 - github.com/alecthomas/chroma/v2 v2.23.0 + github.com/alecthomas/chroma/v2 v2.23.1 github.com/aws/aws-sdk-go-v2/credentials v1.18.10 github.com/aws/aws-sdk-go-v2/service/codecommit v1.32.2 github.com/blakesmith/ar v0.0.0-20190502131153-809d4375e1fb @@ -50,10 +50,10 @@ require ( github.com/gliderlabs/ssh v0.3.8 github.com/go-ap/activitypub v0.0.0-20250810115208-cb73b20a1742 github.com/go-ap/jsonld v0.0.0-20221030091449-f2a191312c73 - github.com/go-chi/chi/v5 v5.2.3 + github.com/go-chi/chi/v5 v5.2.4 github.com/go-chi/cors v1.2.2 github.com/go-co-op/gocron v1.37.0 - github.com/go-enry/go-enry/v2 v2.9.2 + github.com/go-enry/go-enry/v2 v2.9.4 github.com/go-git/go-billy/v5 v5.6.2 github.com/go-git/go-git/v5 v5.16.3 github.com/go-ldap/ldap/v3 v3.4.11 diff --git a/go.sum b/go.sum index acd5d8912e..7490b3923c 100644 --- a/go.sum +++ b/go.sum @@ -98,8 +98,8 @@ github.com/SaveTheRbtz/zstd-seekable-format-go/pkg v0.8.0/go.mod h1:1HmmMEVsr+0R github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0= github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k= github.com/alecthomas/chroma/v2 v2.2.0/go.mod h1:vf4zrexSH54oEjJ7EdB65tGNHmH3pGZmVkgTP5RHvAs= -github.com/alecthomas/chroma/v2 v2.23.0 h1:u/Orux1J0eLuZDeQ44froV8smumheieI0EofhbyKhhk= -github.com/alecthomas/chroma/v2 v2.23.0/go.mod h1:NqVhfBR0lte5Ouh3DcthuUCTUpDC9cxBOfyMbMQPs3o= +github.com/alecthomas/chroma/v2 v2.23.1 h1:nv2AVZdTyClGbVQkIzlDm/rnhk1E9bU9nXwmZ/Vk/iY= +github.com/alecthomas/chroma/v2 v2.23.1/go.mod h1:NqVhfBR0lte5Ouh3DcthuUCTUpDC9cxBOfyMbMQPs3o= github.com/alecthomas/repr v0.0.0-20220113201626-b1b626ac65ae/go.mod h1:2kn6fqh/zIyPLmm3ugklbEi5hg5wS435eygvNfaDQL8= github.com/alecthomas/repr v0.5.2 h1:SU73FTI9D1P5UNtvseffFSGmdNci/O6RsqzeXJtP0Qs= github.com/alecthomas/repr v0.5.2/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= @@ -316,14 +316,14 @@ github.com/go-ap/jsonld v0.0.0-20221030091449-f2a191312c73/go.mod h1:jyveZeGw5La github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 h1:BP4M0CvQ4S3TGls2FvczZtj5Re/2ZzkV9VwqPHH/3Bo= github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0= github.com/go-chi/chi/v5 v5.0.1/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= -github.com/go-chi/chi/v5 v5.2.3 h1:WQIt9uxdsAbgIYgid+BpYc+liqQZGMHRaUwp0JUcvdE= -github.com/go-chi/chi/v5 v5.2.3/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops= +github.com/go-chi/chi/v5 v5.2.4 h1:WtFKPHwlywe8Srng8j2BhOD9312j9cGUxG1SP4V2cR4= +github.com/go-chi/chi/v5 v5.2.4/go.mod h1:X7Gx4mteadT3eDOMTsXzmI4/rwUpOwBHLpAfupzFJP0= github.com/go-chi/cors v1.2.2 h1:Jmey33TE+b+rB7fT8MUy1u0I4L+NARQlK6LhzKPSyQE= github.com/go-chi/cors v1.2.2/go.mod h1:sSbTewc+6wYHBBCW7ytsFSn836hqM7JxpglAy2Vzc58= github.com/go-co-op/gocron v1.37.0 h1:ZYDJGtQ4OMhTLKOKMIch+/CY70Brbb1dGdooLEhh7b0= github.com/go-co-op/gocron v1.37.0/go.mod h1:3L/n6BkO7ABj+TrfSVXLRzsP26zmikL4ISkLQ0O8iNY= -github.com/go-enry/go-enry/v2 v2.9.2 h1:giOQAtCgBX08kosrX818DCQJTCNtKwoPBGu0qb6nKTY= -github.com/go-enry/go-enry/v2 v2.9.2/go.mod h1:9yrj4ES1YrbNb1Wb7/PWYr2bpaCXUGRt0uafN0ISyG8= +github.com/go-enry/go-enry/v2 v2.9.4 h1:DS4l06/NgMzYjsJ2J52wORo6UsfFDjDCwfAn7w3gG44= +github.com/go-enry/go-enry/v2 v2.9.4/go.mod h1:9yrj4ES1YrbNb1Wb7/PWYr2bpaCXUGRt0uafN0ISyG8= github.com/go-enry/go-oniguruma v1.2.1 h1:k8aAMuJfMrqm/56SG2lV9Cfti6tC4x8673aHCcBk+eo= github.com/go-enry/go-oniguruma v1.2.1/go.mod h1:bWDhYP+S6xZQgiRL7wlTScFYBe023B6ilRZbCAD5Hf4= github.com/go-fed/httpsig v1.1.1-0.20201223112313-55836744818e h1:oRq/fiirun5HqlEWMLIcDmLpIELlG4iGbd0s8iqgPi8= From 208cbd5a6f9be60bc79105c6223f759d42902f52 Mon Sep 17 00:00:00 2001 From: Lunny Xiao Date: Fri, 30 Jan 2026 10:46:34 -0800 Subject: [PATCH 3/8] Fix bug when list pull request commits (#36485) Fix #36483 In git log/rev-list, the "..." syntax represents the symmetric difference between two references, which is different from the meaning of "..." in git diff (where it implies diffing from the merge base). For listing PR commits, we must use `merge-base..head` to include only the commits introduced by the head branch. Otherwise, commits newly pushed to the base branch would also be included, which is incorrect. --- services/git/compare.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/services/git/compare.go b/services/git/compare.go index 6c49fff26a..251a035058 100644 --- a/services/git/compare.go +++ b/services/git/compare.go @@ -84,7 +84,11 @@ func GetCompareInfo(ctx context.Context, baseRepo, headRepo *repo_model.Reposito // We have a common base - therefore we know that ... should work if !fileOnly { - compareInfo.Commits, err = headGitRepo.ShowPrettyFormatLogToList(ctx, compareInfo.BaseCommitID+compareInfo.CompareSeparator+compareInfo.HeadCommitID) + // In git log/rev-list, the "..." syntax represents the symmetric difference between two references, + // which is different from the meaning of "..." in git diff (where it implies diffing from the merge base). + // For listing PR commits, we must use merge-base..head to include only the commits introduced by the head branch. + // Otherwise, commits newly pushed to the base branch would also be included, which is incorrect. + compareInfo.Commits, err = headGitRepo.ShowPrettyFormatLogToList(ctx, compareInfo.MergeBase+".."+compareInfo.HeadCommitID) if err != nil { return nil, fmt.Errorf("ShowPrettyFormatLogToList: %w", err) } From 2d1306291b63a95725c281118340ce2dabd71c8c Mon Sep 17 00:00:00 2001 From: silverwind Date: Fri, 30 Jan 2026 20:42:32 +0100 Subject: [PATCH 4/8] Use reserved .test TLD for unit tests (#36498) `smtp.mydomain.test` is a real domain that resolves to something and which is being connected to while running tests. Instead, use [.test](https://en.wikipedia.org/wiki/.test) which is guaranteed to never be registered on the internet, so all connections to it will fail with NXDOMAIN dns error. --- modules/setting/mailer_test.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/modules/setting/mailer_test.go b/modules/setting/mailer_test.go index ceef35b051..f281715973 100644 --- a/modules/setting/mailer_test.go +++ b/modules/setting/mailer_test.go @@ -11,12 +11,12 @@ import ( func Test_loadMailerFrom(t *testing.T) { kases := map[string]*Mailer{ - "smtp.mydomain.com": { - SMTPAddr: "smtp.mydomain.com", + "smtp.mydomain.test": { + SMTPAddr: "smtp.mydomain.test", SMTPPort: "465", }, - "smtp.mydomain.com:123": { - SMTPAddr: "smtp.mydomain.com", + "smtp.mydomain.test:123": { + SMTPAddr: "smtp.mydomain.test", SMTPPort: "123", }, ":123": { From a16ca3c57cc1fc05a1839bb604a82abcc62052c5 Mon Sep 17 00:00:00 2001 From: silverwind Date: Fri, 30 Jan 2026 21:12:24 +0100 Subject: [PATCH 5/8] Don't create self-references in merged PRs (#36490) Fixes: https://github.com/go-gitea/gitea/issues/36488 Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: Giteabot --- services/issue/commit.go | 23 ++++++++++++++ services/issue/commit_test.go | 56 +++++++++++++++++++++++++++++++++++ 2 files changed, 79 insertions(+) diff --git a/services/issue/commit.go b/services/issue/commit.go index 963d0359fd..66ad93a97d 100644 --- a/services/issue/commit.go +++ b/services/issue/commit.go @@ -89,6 +89,24 @@ func issueAddTime(ctx context.Context, issue *issues_model.Issue, doer *user_mod return err } +// isSelfReference checks if a commit is the merge commit of the PR it references. +// This prevents creating self-referencing timeline entries when a PR merge commit +// contains a reference to its own PR number in the commit message. +func isSelfReference(ctx context.Context, issue *issues_model.Issue, commitSHA string) bool { + if !issue.IsPull { + return false + } + + if err := issue.LoadPullRequest(ctx); err != nil { + if !issues_model.IsErrPullRequestNotExist(err) { + log.Error("LoadPullRequest: %v", err) + } + return false + } + + return issue.PullRequest.MergedCommitID == commitSHA +} + // getIssueFromRef returns the issue referenced by a ref. Returns a nil *Issue // if the provided ref references a non-existent issue. func getIssueFromRef(ctx context.Context, repo *repo_model.Repository, index int64) (*issues_model.Issue, error) { @@ -158,6 +176,11 @@ func UpdateIssuesCommit(ctx context.Context, doer *user_model.User, repo *repo_m continue } + // Skip self-references: if this commit is the merge commit of the PR it references + if isSelfReference(ctx, refIssue, c.Sha1) { + continue + } + message := fmt.Sprintf(`%s`, html.EscapeString(repo.Link()), html.EscapeString(url.PathEscape(c.Sha1)), html.EscapeString(strings.SplitN(c.Message, "\n", 2)[0])) if err = CreateRefComment(ctx, doer, refRepo, refIssue, message, c.Sha1); err != nil { if errors.Is(err, user_model.ErrBlockedUser) { diff --git a/services/issue/commit_test.go b/services/issue/commit_test.go index d19702269a..5950762c44 100644 --- a/services/issue/commit_test.go +++ b/services/issue/commit_test.go @@ -298,3 +298,59 @@ func TestUpdateIssuesCommit_AnotherRepoNoPermission(t *testing.T) { unittest.AssertNotExistsBean(t, issueBean, "is_closed=1") unittest.CheckConsistencyFor(t, &activities_model.Action{}) } + +func TestUpdateIssuesCommit_SelfReference(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) + + // Test that a PR merge commit that references its own PR does not create a self-reference comment + // PR #2 (issue_id=2) has merged_commit_id: 1a8823cd1a9549fde083f992f6b9b87a7ab74fb3 + pushCommits := []*repository.PushCommit{ + { + Sha1: "1a8823cd1a9549fde083f992f6b9b87a7ab74fb3", + CommitterEmail: "user2@example.com", + CommitterName: "User Two", + AuthorEmail: "user2@example.com", + AuthorName: "User Two", + Message: "Merge pull request 'issue2' (#2) from branch1 into master", + }, + } + + repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) + + selfRefCommentBean := &issues_model.Comment{ + Type: issues_model.CommentTypeCommitRef, + CommitSHA: "1a8823cd1a9549fde083f992f6b9b87a7ab74fb3", + PosterID: user.ID, + IssueID: 2, + } + + unittest.AssertNotExistsBean(t, selfRefCommentBean) + assert.NoError(t, UpdateIssuesCommit(t.Context(), user, repo, pushCommits, repo.DefaultBranch)) + unittest.AssertNotExistsBean(t, selfRefCommentBean) + unittest.CheckConsistencyFor(t, &activities_model.Action{}) + + // Test that normal commit references are still created + pushCommits2 := []*repository.PushCommit{ + { + Sha1: "abcdef9876543210", + CommitterEmail: "user2@example.com", + CommitterName: "User Two", + AuthorEmail: "user2@example.com", + AuthorName: "User Two", + Message: "Fix bug, refs #1", + }, + } + + otherRefCommentBean := &issues_model.Comment{ + Type: issues_model.CommentTypeCommitRef, + CommitSHA: "abcdef9876543210", + PosterID: user.ID, + IssueID: 1, + } + + unittest.AssertNotExistsBean(t, otherRefCommentBean) + assert.NoError(t, UpdateIssuesCommit(t.Context(), user, repo, pushCommits2, repo.DefaultBranch)) + unittest.AssertExistsAndLoadBean(t, otherRefCommentBean) + unittest.CheckConsistencyFor(t, &activities_model.Action{}) +} From 8feabe4160ff9bd93e015f94e41dde26ab9955cc Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Fri, 30 Jan 2026 20:48:56 +0000 Subject: [PATCH 6/8] Add FOLDER_ICON_THEME configuration option (#36496) Fixes: https://github.com/go-gitea/gitea/issues/35182 Signed-off-by: silverwind Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: silverwind <115237+silverwind@users.noreply.github.com> Co-authored-by: silverwind --- AGENTS.md | 7 +++ custom/conf/app.example.ini | 5 +- modules/fileicon/render.go | 8 ++- modules/fileicon/render_test.go | 75 ++++++++++++++++++++++++++ modules/setting/ui.go | 2 + services/repository/files/tree_test.go | 19 ++++--- 6 files changed, 106 insertions(+), 10 deletions(-) create mode 100644 AGENTS.md create mode 100644 modules/fileicon/render_test.go diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000000..eda1abeeeb --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,7 @@ +# Instructions for agents + +- Use `make help` to find available development targets +- Before committing go code changes, run `make fmt` +- Before committing `go.mod` changes, run `make tidy` +- Before committing new `.go` files, add the current year into the copyright header +- Before committing files, removed any trailing whitespace diff --git a/custom/conf/app.example.ini b/custom/conf/app.example.ini index 3eaffde970..084c66aab0 100644 --- a/custom/conf/app.example.ini +++ b/custom/conf/app.example.ini @@ -1329,9 +1329,12 @@ LEVEL = Info ;; Leave it empty to allow users to select any theme from "{CustomPath}/public/assets/css/theme-*.css" ;THEMES = ;; -;; The icons for file list (basic/material), this is a temporary option which will be replaced by a user setting in the future. +;; The icon theme for files (basic/material) ;FILE_ICON_THEME = material ;; +;; The icon theme for folders (basic/material) +;FOLDER_ICON_THEME = basic +;; ;; All available reactions users can choose on issues/prs and comments. ;; Values can be emoji alias (:smile:) or a unicode emoji. ;; For custom reactions, add a tightly cropped square image to public/assets/img/emoji/reaction_name.png diff --git a/modules/fileicon/render.go b/modules/fileicon/render.go index 6b2fcfa81e..5bf2a3a02e 100644 --- a/modules/fileicon/render.go +++ b/modules/fileicon/render.go @@ -34,7 +34,13 @@ func (p *RenderedIconPool) RenderToHTML() template.HTML { } func RenderEntryIconHTML(renderedIconPool *RenderedIconPool, entry *EntryInfo) template.HTML { - if setting.UI.FileIconTheme == "material" { + // Use folder theme for directories and symlinks to directories + theme := setting.UI.FileIconTheme + if entry.EntryMode.IsDir() || (entry.EntryMode.IsLink() && entry.SymlinkToMode.IsDir()) { + theme = setting.UI.FolderIconTheme + } + + if theme == "material" { return DefaultMaterialIconProvider().EntryIconHTML(renderedIconPool, entry) } return BasicEntryIconHTML(entry) diff --git a/modules/fileicon/render_test.go b/modules/fileicon/render_test.go new file mode 100644 index 0000000000..d9998f3f4c --- /dev/null +++ b/modules/fileicon/render_test.go @@ -0,0 +1,75 @@ +// Copyright 2026 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package fileicon_test + +import ( + "testing" + + "code.gitea.io/gitea/modules/fileicon" + "code.gitea.io/gitea/modules/git" + "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/test" + + "github.com/stretchr/testify/assert" +) + +func TestRenderEntryIconHTML_WithDifferentThemes(t *testing.T) { + // Test that folder icons use the folder theme + t.Run("FolderUsesBasicTheme", func(t *testing.T) { + defer test.MockVariableValue(&setting.UI.FileIconTheme, "material")() + defer test.MockVariableValue(&setting.UI.FolderIconTheme, "basic")() + + folderEntry := &fileicon.EntryInfo{ + BaseName: "testfolder", + EntryMode: git.EntryModeTree, + } + + html := fileicon.RenderEntryIconHTML(nil, folderEntry) + // Basic theme renders octicon classes + assert.Contains(t, string(html), "octicon-file-directory-fill") + }) + + t.Run("FileUsesMaterialTheme", func(t *testing.T) { + defer test.MockVariableValue(&setting.UI.FileIconTheme, "material")() + defer test.MockVariableValue(&setting.UI.FolderIconTheme, "basic")() + + fileEntry := &fileicon.EntryInfo{ + BaseName: "test.js", + EntryMode: git.EntryModeBlob, + } + + html := fileicon.RenderEntryIconHTML(nil, fileEntry) + // Material theme for files renders material icons + assert.Contains(t, string(html), "svg-mfi-") + }) + + t.Run("SymlinkToFolderUsesBasicTheme", func(t *testing.T) { + defer test.MockVariableValue(&setting.UI.FileIconTheme, "material")() + defer test.MockVariableValue(&setting.UI.FolderIconTheme, "basic")() + + symlinkEntry := &fileicon.EntryInfo{ + BaseName: "link", + EntryMode: git.EntryModeSymlink, + SymlinkToMode: git.EntryModeTree, + } + + html := fileicon.RenderEntryIconHTML(nil, symlinkEntry) + // Symlinks to folders should use folder theme + assert.Contains(t, string(html), "octicon-file-directory-symlink") + }) + + t.Run("BothMaterialTheme", func(t *testing.T) { + defer test.MockVariableValue(&setting.UI.FileIconTheme, "material")() + defer test.MockVariableValue(&setting.UI.FolderIconTheme, "material")() + + folderEntry := &fileicon.EntryInfo{ + BaseName: "testfolder", + EntryMode: git.EntryModeTree, + } + + html := fileicon.RenderEntryIconHTML(nil, folderEntry) + // Material theme for folders renders material folder icons + assert.Contains(t, string(html), "svg-mfi-") + }) +} diff --git a/modules/setting/ui.go b/modules/setting/ui.go index 13cb0f5c66..77a5b45d0a 100644 --- a/modules/setting/ui.go +++ b/modules/setting/ui.go @@ -29,6 +29,7 @@ var UI = struct { DefaultTheme string Themes []string FileIconTheme string + FolderIconTheme string Reactions []string ReactionsLookup container.Set[string] `ini:"-"` CustomEmojis []string @@ -88,6 +89,7 @@ var UI = struct { MaxDisplayFileSize: 8388608, DefaultTheme: `gitea-auto`, FileIconTheme: `material`, + FolderIconTheme: `basic`, Reactions: []string{`+1`, `-1`, `laugh`, `hooray`, `confused`, `heart`, `rocket`, `eyes`}, CustomEmojis: []string{`git`, `gitea`, `codeberg`, `gitlab`, `github`, `gogs`}, CustomEmojisMap: map[string]string{"git": ":git:", "gitea": ":gitea:", "codeberg": ":codeberg:", "gitlab": ":gitlab:", "github": ":github:", "gogs": ":gogs:"}, diff --git a/services/repository/files/tree_test.go b/services/repository/files/tree_test.go index e7511b3eed..b85f65f431 100644 --- a/services/repository/files/tree_test.go +++ b/services/repository/files/tree_test.go @@ -56,6 +56,7 @@ func TestGetTreeBySHA(t *testing.T) { func TestGetTreeViewNodes(t *testing.T) { unittest.PrepareTestEnv(t) + ctx, _ := contexttest.MockContext(t, "user2/repo1") ctx.Repo.RefFullName = git.RefNameFromBranch("sub-home-md-img-check") contexttest.LoadRepo(t, ctx, 1) @@ -69,11 +70,13 @@ func TestGetTreeViewNodes(t *testing.T) { mockIconForFile := func(id string) template.HTML { return template.HTML(``) } - mockIconForFolder := func(id string) template.HTML { - return template.HTML(``) + mockIconForFolder := func() template.HTML { + // With basic theme (default for folders), we get octicon icons without IDs + return template.HTML(`octicon-file-directory-fill(16/)`) } - mockOpenIconForFolder := func(id string) template.HTML { - return template.HTML(``) + mockOpenIconForFolder := func() template.HTML { + // With basic theme (default for folders), we get octicon icons without IDs + return template.HTML(`octicon-file-directory-open-fill(16/)`) } treeNodes, err := GetTreeViewNodes(ctx, curRepoLink, renderedIconPool, ctx.Repo.Commit, "", "") assert.NoError(t, err) @@ -82,8 +85,8 @@ func TestGetTreeViewNodes(t *testing.T) { EntryName: "docs", EntryMode: "tree", FullPath: "docs", - EntryIcon: mockIconForFolder(`svg-mfi-folder-docs`), - EntryIconOpen: mockOpenIconForFolder(`svg-mfi-folder-docs`), + EntryIcon: mockIconForFolder(), + EntryIconOpen: mockOpenIconForFolder(), }, }, treeNodes) @@ -94,8 +97,8 @@ func TestGetTreeViewNodes(t *testing.T) { EntryName: "docs", EntryMode: "tree", FullPath: "docs", - EntryIcon: mockIconForFolder(`svg-mfi-folder-docs`), - EntryIconOpen: mockOpenIconForFolder(`svg-mfi-folder-docs`), + EntryIcon: mockIconForFolder(), + EntryIconOpen: mockOpenIconForFolder(), Children: []*TreeViewNode{ { EntryName: "README.md", From 0acaad191924fffed6c38ed2ec65df7a663e64ac Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Fri, 30 Jan 2026 21:41:43 +0000 Subject: [PATCH 7/8] Fix editorconfig not respected in PR Conversation view (#36492) Fixes: https://github.com/go-gitea/gitea/issues/24991 Signed-off-by: silverwind Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: silverwind <115237+silverwind@users.noreply.github.com> Co-authored-by: silverwind --- routers/web/web.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/routers/web/web.go b/routers/web/web.go index c37add30d5..22b78793ef 100644 --- a/routers/web/web.go +++ b/routers/web/web.go @@ -1530,7 +1530,7 @@ func registerWebRoutes(m *web.Router) { m.Group("/{username}/{reponame}", func() { m.Get("/{type:pulls}", repo.Issues) m.Group("/{type:pulls}/{index}", func() { - m.Get("", repo.SetWhitespaceBehavior, repo.GetPullDiffStats, repo.ViewIssue) + m.Get("", repo.SetEditorconfigIfExists, repo.SetWhitespaceBehavior, repo.GetPullDiffStats, repo.ViewIssue) m.Get(".diff", repo.DownloadPullDiff) m.Get(".patch", repo.DownloadPullPatch) m.Get("/merge_box", repo.ViewPullMergeBox) From 8c9247e7171a71ff2f45a215d4e0b2307658d4bf Mon Sep 17 00:00:00 2001 From: GiteaBot Date: Sat, 31 Jan 2026 00:45:54 +0000 Subject: [PATCH 8/8] [skip ci] Updated translations via Crowdin --- options/locale/locale_zh-CN.json | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/options/locale/locale_zh-CN.json b/options/locale/locale_zh-CN.json index 661096d165..a5721012cc 100644 --- a/options/locale/locale_zh-CN.json +++ b/options/locale/locale_zh-CN.json @@ -271,7 +271,7 @@ "install.smtp_port": "SMTP 端口", "install.smtp_from": "邮件发件人", "install.smtp_from_invalid": "「邮件发件人」地址无效", - "install.smtp_from_helper": "请输入一个用于 Gitea 的邮箱地址,或者使用完整格式:「名称」。", + "install.smtp_from_helper": "请输入一个用于 Gitea 的邮箱地址,或者使用完整格式:\"名称\" 。", "install.mailer_user": "SMTP 用户名", "install.mailer_password": "SMTP 密码", "install.register_confirm": "需要邮件确认注册", @@ -1796,6 +1796,7 @@ "repo.pulls.remove_prefix": "删除 %s 前缀", "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": "此分支上的更改已经在目标分支上。这将是一个空提交。", @@ -2932,7 +2933,7 @@ "admin.dashboard.delete_old_actions.started": "已开始从数据库中删除所有旧工作流记录。", "admin.dashboard.update_checker": "更新检查器", "admin.dashboard.delete_old_system_notices": "从数据库中删除所有旧系统通知", - "admin.dashboard.gc_lfs": "垃圾回收 LFS 元数据", + "admin.dashboard.gc_lfs": "对 LFS 元数据进行垃圾回收", "admin.dashboard.stop_zombie_tasks": "停止僵尸工作流任务", "admin.dashboard.stop_endless_tasks": "停止无限循环的工作流任务", "admin.dashboard.cancel_abandoned_jobs": "取消已放弃的工作流任务", @@ -3324,7 +3325,7 @@ "admin.monitor.queue.numberinqueue": "队列中的数量", "admin.monitor.queue.review_add": "查看 / 添加工作者", "admin.monitor.queue.settings.title": "池设置", - "admin.monitor.queue.settings.desc": "因为工作者队列阻塞,池正在动态扩展。", + "admin.monitor.queue.settings.desc": "工作池会根据其工作队列阻塞情况动态增长。", "admin.monitor.queue.settings.maxnumberworkers": "最大工作者数量", "admin.monitor.queue.settings.maxnumberworkers.placeholder": "当前 %[1]d", "admin.monitor.queue.settings.maxnumberworkers.error": "最大工作者数必须是数字",