diff --git a/Makefile b/Makefile index 59ec6bb49f..f5375261e1 100644 --- a/Makefile +++ b/Makefile @@ -100,7 +100,7 @@ LDFLAGS := $(LDFLAGS) -X "main.MakeVersion=$(MAKE_VERSION)" -X "main.Version=$(G LINUX_ARCHS ?= linux/amd64,linux/386,linux/arm-5,linux/arm-6,linux/arm64 GO_PACKAGES ?= $(filter-out code.gitea.io/gitea/tests/integration/migration-test code.gitea.io/gitea/tests code.gitea.io/gitea/tests/integration code.gitea.io/gitea/tests/e2e,$(shell $(GO) list ./... | grep -v /vendor/)) -GO_TEST_PACKAGES ?= $(filter-out $(shell $(GO) list code.gitea.io/gitea/models/migrations/...) code.gitea.io/gitea/tests/integration/migration-test code.gitea.io/gitea/tests code.gitea.io/gitea/tests/integration code.gitea.io/gitea/tests/e2e,$(shell $(GO) list ./... | grep -v /vendor/)) +GO_TEST_PACKAGES ?= $(filter-out $(shell $(GO) list code.gitea.io/gitea/models/migrations/...) $(shell $(GO) list code.gitea.io/gitea/models/forgejo_migrations/...) code.gitea.io/gitea/tests/integration/migration-test code.gitea.io/gitea/tests code.gitea.io/gitea/tests/integration code.gitea.io/gitea/tests/e2e,$(shell $(GO) list ./... | grep -v /vendor/)) FOMANTIC_WORK_DIR := web_src/fomantic @@ -712,7 +712,7 @@ migrations.sqlite.test: $(GO_SOURCES) generate-ini-sqlite .PHONY: migrations.individual.mysql.test migrations.individual.mysql.test: $(GO_SOURCES) - for pkg in $(shell $(GO) list code.gitea.io/gitea/models/migrations/...); do \ + for pkg in $(shell $(GO) list code.gitea.io/gitea/models/migrations/... code.gitea.io/gitea/models/forgejo_migrations/...); do \ GITEA_ROOT="$(CURDIR)" GITEA_CONF=tests/mysql.ini $(GO) test $(GOTESTFLAGS) -tags '$(TEST_TAGS)' $$pkg; \ done @@ -722,7 +722,7 @@ migrations.individual.sqlite.test\#%: $(GO_SOURCES) generate-ini-sqlite .PHONY: migrations.individual.pgsql.test migrations.individual.pgsql.test: $(GO_SOURCES) - for pkg in $(shell $(GO) list code.gitea.io/gitea/models/migrations/...); do \ + for pkg in $(shell $(GO) list code.gitea.io/gitea/models/migrations/... code.gitea.io/gitea/models/forgejo_migrations/...); do \ GITEA_ROOT="$(CURDIR)" GITEA_CONF=tests/pgsql.ini $(GO) test $(GOTESTFLAGS) -tags '$(TEST_TAGS)' $$pkg; \ done @@ -733,7 +733,7 @@ migrations.individual.pgsql.test\#%: $(GO_SOURCES) generate-ini-pgsql .PHONY: migrations.individual.mssql.test migrations.individual.mssql.test: $(GO_SOURCES) generate-ini-mssql - for pkg in $(shell $(GO) list code.gitea.io/gitea/models/migrations/...); do \ + for pkg in $(shell $(GO) list code.gitea.io/gitea/models/migrations/... code.gitea.io/gitea/models/forgejo_migrations/...); do \ GITEA_ROOT="$(CURDIR)" GITEA_CONF=tests/mssql.ini $(GO) test $(GOTESTFLAGS) -tags '$(TEST_TAGS)' $$pkg -test.failfast; \ done @@ -743,7 +743,7 @@ migrations.individual.mssql.test\#%: $(GO_SOURCES) generate-ini-mssql .PHONY: migrations.individual.sqlite.test migrations.individual.sqlite.test: $(GO_SOURCES) generate-ini-sqlite - for pkg in $(shell $(GO) list code.gitea.io/gitea/models/migrations/...); do \ + for pkg in $(shell $(GO) list code.gitea.io/gitea/models/migrations/... code.gitea.io/gitea/models/forgejo_migrations/...); do \ GITEA_ROOT="$(CURDIR)" GITEA_CONF=tests/sqlite.ini $(GO) test $(GOTESTFLAGS) -tags '$(TEST_TAGS)' $$pkg; \ done diff --git a/models/forgejo_migrations/main_test.go b/models/forgejo_migrations/main_test.go new file mode 100644 index 0000000000..42579f8194 --- /dev/null +++ b/models/forgejo_migrations/main_test.go @@ -0,0 +1,14 @@ +// Copyright 2023 The Forgejo Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package forgejo_migrations //nolint:revive + +import ( + "testing" + + "code.gitea.io/gitea/models/migrations/base" +) + +func TestMain(m *testing.M) { + base.MainTest(m) +} diff --git a/models/forgejo_migrations/migrate.go b/models/forgejo_migrations/migrate.go new file mode 100644 index 0000000000..88bbef70c7 --- /dev/null +++ b/models/forgejo_migrations/migrate.go @@ -0,0 +1,139 @@ +// Copyright 2023 The Forgejo Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package forgejo_migrations //nolint:revive + +import ( + "context" + "fmt" + "os" + + "code.gitea.io/gitea/modules/git" + "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/setting" + + "xorm.io/xorm" + "xorm.io/xorm/names" +) + +// ForgejoVersion describes the Forgejo version table. Should have only one row with id = 1. +type ForgejoVersion struct { + ID int64 `xorm:"pk autoincr"` + Version int64 +} + +type Migration struct { + description string + migrate func(*xorm.Engine) error +} + +// NewMigration creates a new migration. +func NewMigration(desc string, fn func(*xorm.Engine) error) *Migration { + return &Migration{desc, fn} +} + +// This is a sequence of additional Forgejo migrations. +// Add new migrations to the bottom of the list. +var migrations = []*Migration{} + +// GetCurrentDBVersion returns the current Forgejo database version. +func GetCurrentDBVersion(x *xorm.Engine) (int64, error) { + if err := x.Sync(new(ForgejoVersion)); err != nil { + return -1, fmt.Errorf("sync: %w", err) + } + + currentVersion := &ForgejoVersion{ID: 1} + has, err := x.Get(currentVersion) + if err != nil { + return -1, fmt.Errorf("get: %w", err) + } + if !has { + return -1, nil + } + return currentVersion.Version, nil +} + +// ExpectedVersion returns the expected Forgejo database version. +func ExpectedVersion() int64 { + return int64(len(migrations)) +} + +// EnsureUpToDate will check if the Forgejo database is at the correct version. +func EnsureUpToDate(x *xorm.Engine) error { + currentDB, err := GetCurrentDBVersion(x) + if err != nil { + return err + } + + if currentDB < 0 { + return fmt.Errorf("database has not been initialized") + } + + expected := ExpectedVersion() + + if currentDB != expected { + return fmt.Errorf(`current Forgejo database version %d is not equal to the expected version %d. Please run "forgejo [--config /path/to/app.ini] migrate" to update the database version`, currentDB, expected) + } + + return nil +} + +// Migrate Forgejo database to current version. +func Migrate(x *xorm.Engine) error { + // Set a new clean the default mapper to GonicMapper as that is the default for . + x.SetMapper(names.GonicMapper{}) + if err := x.Sync(new(ForgejoVersion)); err != nil { + return fmt.Errorf("sync: %w", err) + } + + currentVersion := &ForgejoVersion{ID: 1} + has, err := x.Get(currentVersion) + if err != nil { + return fmt.Errorf("get: %w", err) + } else if !has { + // If the version record does not exist we think + // it is a fresh installation and we can skip all migrations. + currentVersion.ID = 0 + currentVersion.Version = ExpectedVersion() + + if _, err = x.InsertOne(currentVersion); err != nil { + return fmt.Errorf("insert: %w", err) + } + } + + v := currentVersion.Version + + // Downgrading Forgejo's database version not supported + if v > ExpectedVersion() { + msg := fmt.Sprintf("Your Forgejo database (migration version: %d) is for a newer version of Forgejo, you cannot use the newer database for this old Forgejo release (%d).", v, ExpectedVersion()) + msg += "\nForgejo will exit to keep your database safe and unchanged. Please use the correct Forgejo release, do not change the migration version manually (incorrect manual operation may cause data loss)." + if !setting.IsProd { + msg += fmt.Sprintf("\nIf you are in development and really know what you're doing, you can force changing the migration version by executing: UPDATE forgejo_version SET version=%d WHERE id=1;", ExpectedVersion()) + } + _, _ = fmt.Fprintln(os.Stderr, msg) + log.Fatal(msg) + return nil + } + + // Some migration tasks depend on the git command + if git.DefaultContext == nil { + if err = git.InitSimple(context.Background()); err != nil { + return err + } + } + + // Migrate + for i, m := range migrations[v:] { + log.Info("Migration[%d]: %s", v+int64(i), m.description) + // Reset the mapper between each migration - migrations are not supposed to depend on each other + x.SetMapper(names.GonicMapper{}) + if err = m.migrate(x); err != nil { + return fmt.Errorf("migration[%d]: %s failed: %w", v+int64(i), m.description, err) + } + currentVersion.Version = v + int64(i) + 1 + if _, err = x.ID(1).Update(currentVersion); err != nil { + return err + } + } + return nil +} diff --git a/models/forgejo_migrations/migrate_test.go b/models/forgejo_migrations/migrate_test.go new file mode 100644 index 0000000000..2ae3c39fce --- /dev/null +++ b/models/forgejo_migrations/migrate_test.go @@ -0,0 +1,39 @@ +// Copyright 2023 The Forgejo Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package forgejo_migrations //nolint:revive + +import ( + "testing" + + "code.gitea.io/gitea/models/migrations/base" + + "github.com/stretchr/testify/assert" +) + +// TestEnsureUpToDate tests the behavior of EnsureUpToDate. +func TestEnsureUpToDate(t *testing.T) { + x, deferable := base.PrepareTestEnv(t, 0, new(ForgejoVersion)) + defer deferable() + if x == nil || t.Failed() { + return + } + + // Ensure error if there's no row in Forgejo Version. + err := EnsureUpToDate(x) + assert.Error(t, err) + + // Insert 'good' Forgejo Version row. + _, err = x.InsertOne(&ForgejoVersion{ID: 1, Version: ExpectedVersion()}) + assert.NoError(t, err) + + err = EnsureUpToDate(x) + assert.NoError(t, err) + + // Modify forgejo version to have a lower version. + _, err = x.Exec("UPDATE `forgejo_version` SET version = ? WHERE id = 1", ExpectedVersion()-1) + assert.NoError(t, err) + + err = EnsureUpToDate(x) + assert.Error(t, err) +} diff --git a/models/migrations/migrations.go b/models/migrations/migrations.go index 578cbca035..39598000be 100644 --- a/models/migrations/migrations.go +++ b/models/migrations/migrations.go @@ -8,6 +8,7 @@ import ( "context" "fmt" + "code.gitea.io/gitea/models/forgejo_migrations" "code.gitea.io/gitea/models/migrations/v1_10" "code.gitea.io/gitea/models/migrations/v1_11" "code.gitea.io/gitea/models/migrations/v1_12" @@ -597,7 +598,7 @@ func EnsureUpToDate(x *xorm.Engine) error { return fmt.Errorf(`Current database version %d is not equal to the expected version %d. Please run "gitea [--config /path/to/app.ini] migrate" to update the database version`, currentDB, expected) } - return nil + return forgejo_migrations.EnsureUpToDate(x) } // Migrate database to current version @@ -661,5 +662,7 @@ Please try upgrading to a lower version first (suggested v1.6.4), then upgrade t return err } } - return nil + + // Execute Forgejo specific migrations. + return forgejo_migrations.Migrate(x) }