mirror of
https://codeberg.org/forgejo/forgejo
synced 2025-10-19 00:40:51 +02:00
198 lines
6.7 KiB
Go
198 lines
6.7 KiB
Go
// Copyright 2025 The Forgejo Authors. All rights reserved.
|
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
|
|
package forgejo_migrations
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"regexp"
|
|
"runtime"
|
|
"slices"
|
|
"strings"
|
|
|
|
"forgejo.org/modules/container"
|
|
"forgejo.org/modules/log"
|
|
"forgejo.org/modules/setting"
|
|
"forgejo.org/modules/timeutil"
|
|
|
|
"xorm.io/xorm"
|
|
"xorm.io/xorm/names"
|
|
)
|
|
|
|
// ForgejoMigration table contains a record of migrations applied to the database. (Note that there are older
|
|
// migrations in the forgejo_version table from before this table was introduced, and the `version` table from Gitea
|
|
// migrations). Each record in this table represents one successfully completed migration which was completed at the
|
|
// `CreatedUnix` time.
|
|
type ForgejoMigration struct {
|
|
ID string `xorm:"pk"`
|
|
CreatedUnix timeutil.TimeStamp `xorm:"created"`
|
|
}
|
|
|
|
type Migration struct {
|
|
Description string // short plaintext explanation of the migration
|
|
Upgrade func(*xorm.Engine) error // perform the migration
|
|
|
|
id string // unique migration identifier
|
|
}
|
|
|
|
var (
|
|
rawMigrations []*Migration
|
|
migrationFilenameRegex = regexp.MustCompile(`/(?P<migration_group>v[0-9]+[a-z])_(?P<migration_id>[^/]+)\.go$`)
|
|
)
|
|
|
|
var getMigrationFilename = func() string {
|
|
_, migrationFilename, _, _ := runtime.Caller(2)
|
|
return migrationFilename
|
|
}
|
|
|
|
func registerMigration(migration *Migration) {
|
|
migrationFilename := getMigrationFilename()
|
|
|
|
if migrationResolutionComplete {
|
|
panic(fmt.Sprintf("attempted to register migration from %s after migration resolution is already complete", migrationFilename))
|
|
}
|
|
|
|
matches := migrationFilenameRegex.FindStringSubmatch(migrationFilename)
|
|
if len(matches) == 0 {
|
|
panic(fmt.Sprintf("registerMigration must be invoked from a file matching migrationFilenameRegex, but was invoked from %q", migrationFilename))
|
|
}
|
|
migration.id = fmt.Sprintf("%s_%s", matches[1], matches[2]) // this just rebuilds the filename, but guarantees that the regex applied for consistent naming
|
|
|
|
rawMigrations = append(rawMigrations, migration)
|
|
}
|
|
|
|
// For testing only
|
|
func resetMigrations() {
|
|
rawMigrations = nil
|
|
orderedMigrations = nil
|
|
migrationResolutionComplete = false
|
|
inMemoryMigrationIDs = nil
|
|
}
|
|
|
|
var (
|
|
migrationResolutionComplete = false
|
|
inMemoryMigrationIDs container.Set[string]
|
|
orderedMigrations []*Migration
|
|
)
|
|
|
|
func resolveMigrations() {
|
|
if migrationResolutionComplete {
|
|
return
|
|
}
|
|
|
|
inMemoryMigrationIDs = make(container.Set[string])
|
|
for _, m := range rawMigrations {
|
|
if inMemoryMigrationIDs.Contains(m.id) {
|
|
// With the filename-based migration ID this shouldn't be possible, but a bit of a sanity check..
|
|
panic(fmt.Sprintf("migration id is duplicated: %q", m.id))
|
|
}
|
|
inMemoryMigrationIDs.Add(m.id)
|
|
}
|
|
|
|
orderedMigrations = slices.Clone(rawMigrations)
|
|
slices.SortFunc(orderedMigrations, func(m1, m2 *Migration) int {
|
|
return strings.Compare(m1.id, m2.id)
|
|
})
|
|
|
|
migrationResolutionComplete = true
|
|
}
|
|
|
|
func inDBMigrationIDs(x *xorm.Engine) (container.Set[string], error) {
|
|
var inDBMigrations []ForgejoMigration
|
|
err := x.Find(&inDBMigrations)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
inDBMigrationIDs := make(container.Set[string], len(inDBMigrations))
|
|
for _, inDB := range inDBMigrations {
|
|
inDBMigrationIDs.Add(inDB.ID)
|
|
}
|
|
|
|
return inDBMigrationIDs, nil
|
|
}
|
|
|
|
// EnsureUpToDate will check if the Forgejo database is at the correct version.
|
|
func EnsureUpToDate(x *xorm.Engine) error {
|
|
resolveMigrations()
|
|
|
|
inDBMigrationIDs, err := inDBMigrationIDs(x)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// invalidMigrations are those that are in the database, but aren't registered.
|
|
invalidMigrations := inDBMigrationIDs.Difference(inMemoryMigrationIDs)
|
|
if len(invalidMigrations) > 0 {
|
|
return fmt.Errorf("current Forgejo database has migration(s) %s applied, which are not registered migrations", strings.Join(invalidMigrations.Slice(), ", "))
|
|
}
|
|
|
|
// unappliedMigrations are those that haven't yet been applied, but seem valid
|
|
unappliedMigrations := inMemoryMigrationIDs.Difference(inDBMigrationIDs)
|
|
if len(unappliedMigrations) > 0 {
|
|
return fmt.Errorf(`current Forgejo database is missing migration(s) %s. Please run "forgejo [--config /path/to/app.ini] migrate" to update the database version`, strings.Join(unappliedMigrations.Slice(), ", "))
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Migrate Forgejo database to current version.
|
|
func Migrate(x *xorm.Engine) error {
|
|
resolveMigrations()
|
|
|
|
// Set a new clean the default mapper to GonicMapper as that is the default for .
|
|
x.SetMapper(names.GonicMapper{})
|
|
if err := x.Sync(new(ForgejoMigration)); err != nil {
|
|
return fmt.Errorf("sync: %w", err)
|
|
}
|
|
|
|
inDBMigrationIDs, err := inDBMigrationIDs(x)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// invalidMigrations are those that are in the database, but aren't registered.
|
|
invalidMigrations := inDBMigrationIDs.Difference(inMemoryMigrationIDs)
|
|
if len(invalidMigrations) > 0 {
|
|
// Downgrading Forgejo's database version not supported
|
|
msg := fmt.Sprintf("Your Forgejo database has %d migration(s) (%s) for a newer version of Forgejo, you cannot use the newer database for this old Forgejo release.", len(invalidMigrations), strings.Join(invalidMigrations.Slice(), ", "))
|
|
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 += "\nIf you are in development and know what you're doing, you can remove the migration records from the forgejo_migration table. The affect of those migrations will still be present."
|
|
quoted := slices.Clone(invalidMigrations.Slice())
|
|
for i, s := range quoted {
|
|
quoted[i] = "'" + s + "'"
|
|
}
|
|
msg += fmt.Sprintf("\n DELETE FROM forgejo_migration WHERE id IN (%s)", strings.Join(quoted, ", "))
|
|
}
|
|
_, _ = fmt.Fprintln(os.Stderr, msg)
|
|
log.Fatal(msg)
|
|
return nil
|
|
}
|
|
|
|
// unappliedMigrations are those that are registered but haven't been applied.
|
|
unappliedMigrations := inMemoryMigrationIDs.Difference(inDBMigrationIDs)
|
|
for _, migration := range orderedMigrations {
|
|
if !unappliedMigrations.Contains(migration.id) {
|
|
continue
|
|
}
|
|
|
|
log.Info("Migration[%s]: %s", migration.id, migration.Description)
|
|
|
|
// Reset the mapper between each migration - migrations are not supposed to depend on each other
|
|
x.SetMapper(names.GonicMapper{})
|
|
if err = migration.Upgrade(x); err != nil {
|
|
return fmt.Errorf("migration[%s]: %s failed: %w", migration.id, migration.Description, err)
|
|
}
|
|
|
|
affected, err := x.Insert(&ForgejoMigration{ID: migration.id})
|
|
if err != nil {
|
|
return err
|
|
} else if affected != 1 {
|
|
return fmt.Errorf("migration[%s]: failed to insert into DB, %d records affected", migration.id, affected)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|