refactor: prepare to decouple the "model migration" package and "models" package (#38533)

Migrations should never use model structs directly, because the model
structs can be different in different releases. e.g. if one migration uses
"User" model, it works in the early releases, then one day, when the
User model changes, the migration breaks because it will use the
new (incorrect) User model, it should only use the old User model.

The same to "modules/structs".

---------

Signed-off-by: wxiaoguang <wxiaoguang@gmail.com>
Co-authored-by: delvh <dev.lh@web.de>
This commit is contained in:
wxiaoguang
2026-07-20 03:42:02 +00:00
committed by GitHub
co-authored by delvh
parent 775e3bdb34
commit fff32e9469
378 changed files with 708 additions and 721 deletions
+14
View File
@@ -0,0 +1,14 @@
// Copyright 2021 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package v1_14
import (
"testing"
"gitea.dev/modelmigration/migrationtest"
)
func TestMain(m *testing.M) {
migrationtest.MainTest(m)
}
+21
View File
@@ -0,0 +1,21 @@
// Copyright 2020 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package v1_14
import (
"fmt"
"gitea.dev/modelmigration/base"
)
func AddChangedProtectedFilesPullRequestColumn(x base.EngineMigration) error {
type PullRequest struct {
ChangedProtectedFiles []string `xorm:"TEXT JSON"`
}
if err := x.Sync(new(PullRequest)); err != nil {
return fmt.Errorf("Sync: %w", err)
}
return nil
}
+167
View File
@@ -0,0 +1,167 @@
// Copyright 2020 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package v1_14
import (
"context"
"fmt"
"strings"
"gitea.dev/modelmigration/base"
repo_model "gitea.dev/models/repo"
"gitea.dev/modules/git"
"gitea.dev/modules/log"
)
func FixPublisherIDforTagReleases(ctx context.Context, x base.EngineMigration) error {
type Release struct {
ID int64
RepoID int64
Sha1 string
TagName string
PublisherID int64
}
type Repository struct {
ID int64
OwnerID int64
OwnerName string
Name string
}
type User struct {
ID int64
Name string
Email string
}
const batchSize = 100
sess := x.NewSession()
defer sess.Close()
var (
repo *Repository
gitRepo *git.Repository
user *User
)
defer func() {
if gitRepo != nil {
gitRepo.Close()
}
}()
for start := 0; ; start += batchSize {
releases := make([]*Release, 0, batchSize)
if err := sess.Begin(); err != nil {
return err
}
if err := sess.Limit(batchSize, start).
Where("publisher_id = 0 OR publisher_id is null").
Asc("repo_id", "id").Where("is_tag=?", true).
Find(&releases); err != nil {
return err
}
if len(releases) == 0 {
break
}
for _, release := range releases {
if repo == nil || repo.ID != release.RepoID {
if gitRepo != nil {
gitRepo.Close()
gitRepo = nil
}
repo = new(Repository)
has, err := sess.ID(release.RepoID).Get(repo)
if err != nil {
log.Error("Error whilst loading repository[%d] for release[%d] with tag name %s. Error: %v", release.RepoID, release.ID, release.TagName, err)
return err
} else if !has {
log.Warn("Release[%d] is orphaned and refers to non-existing repository %d", release.ID, release.RepoID)
log.Warn("This release should be deleted")
continue
}
if repo.OwnerName == "" {
// v120.go migration may not have been run correctly - we'll just replicate it here
// because this appears to be a common-ish problem.
if _, err := sess.Exec("UPDATE repository SET owner_name = (SELECT name FROM `user` WHERE `user`.id = repository.owner_id)"); err != nil {
log.Error("Error whilst updating repository[%d] owner name", repo.ID)
return err
}
if _, err := sess.ID(release.RepoID).Get(repo); err != nil {
log.Error("Error whilst loading repository[%d] for release[%d] with tag name %s. Error: %v", release.RepoID, release.ID, release.TagName, err)
return err
}
}
gitRepo, err = git.OpenRepository(repo_model.CodeRepoByName(repo.OwnerName, repo.Name))
if err != nil {
log.Error("Error whilst opening git repo for [%d]%s/%s. Error: %v", repo.ID, repo.OwnerName, repo.Name, err)
return err
}
}
commit, err := gitRepo.GetTagCommit(ctx, release.TagName)
if err != nil {
if git.IsErrNotExist(err) {
log.Warn("Unable to find commit %s for Tag: %s in [%d]%s/%s. Cannot update publisher ID.", err.(git.ErrNotExist).ID, release.TagName, repo.ID, repo.OwnerName, repo.Name)
continue
}
log.Error("Error whilst getting commit for Tag: %s in [%d]%s/%s. Error: %v", release.TagName, repo.ID, repo.OwnerName, repo.Name, err)
return fmt.Errorf("GetTagCommit: %w", err)
}
if commit.Author.Email == "" {
log.Warn("Tag: %s in Repo[%d]%s/%s does not have a tagger.", release.TagName, repo.ID, repo.OwnerName, repo.Name)
commit, err = gitRepo.GetCommit(ctx, commit.ID.String())
if err != nil {
if git.IsErrNotExist(err) {
log.Warn("Unable to find commit %s for Tag: %s in [%d]%s/%s. Cannot update publisher ID.", err.(git.ErrNotExist).ID, release.TagName, repo.ID, repo.OwnerName, repo.Name)
continue
}
log.Error("Error whilst getting commit for Tag: %s in [%d]%s/%s. Error: %v", release.TagName, repo.ID, repo.OwnerName, repo.Name, err)
return fmt.Errorf("GetCommit: %w", err)
}
}
if commit.Author.Email == "" {
log.Warn("Tag: %s in Repo[%d]%s/%s does not have a Tagger and its underlying commit does not have an Author either!", release.TagName, repo.ID, repo.OwnerName, repo.Name)
continue
}
if user == nil || !strings.EqualFold(user.Email, commit.Author.Email) {
user = new(User)
_, err = sess.Where("email=?", commit.Author.Email).Get(user)
if err != nil {
log.Error("Error whilst getting commit author by email: %s for Tag: %s in [%d]%s/%s. Error: %v", commit.Author.Email, release.TagName, repo.ID, repo.OwnerName, repo.Name, err)
return err
}
user.Email = commit.Author.Email
}
if user.ID <= 0 {
continue
}
release.PublisherID = user.ID
if _, err := sess.ID(release.ID).Cols("publisher_id").Update(release); err != nil {
log.Error("Error whilst updating publisher[%d] for release[%d] with tag name %s. Error: %v", release.PublisherID, release.ID, release.TagName, err)
return err
}
}
if gitRepo != nil {
gitRepo.Close()
}
if err := sess.Commit(); err != nil {
return err
}
}
return nil
}
+53
View File
@@ -0,0 +1,53 @@
// Copyright 2020 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package v1_14
import "gitea.dev/modelmigration/base"
func FixRepoTopics(x base.EngineMigration) error {
type Repository struct {
ID int64 `xorm:"pk autoincr"`
Topics []string `xorm:"TEXT JSON"`
}
const batchSize = 100
sess := x.NewSession()
defer sess.Close()
repos := make([]*Repository, 0, batchSize)
topics := make([]string, 0, batchSize)
for start := 0; ; start += batchSize {
repos = repos[:0]
if err := sess.Begin(); err != nil {
return err
}
if err := sess.Limit(batchSize, start).Find(&repos); err != nil {
return err
}
if len(repos) == 0 {
break
}
for _, repo := range repos {
topics = topics[:0]
if err := sess.Select("name").Table("topic").
Join("INNER", "repo_topic", "repo_topic.topic_id = topic.id").
Where("repo_topic.repo_id = ?", repo.ID).Desc("topic.repo_count").Find(&topics); err != nil {
return err
}
repo.Topics = topics
if _, err := sess.ID(repo.ID).Cols("topics").Update(repo); err != nil {
return err
}
}
if err := sess.Commit(); err != nil {
return err
}
}
return nil
}
+110
View File
@@ -0,0 +1,110 @@
// Copyright 2020 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package v1_14
import (
"errors"
"strconv"
"gitea.dev/modelmigration/base"
"gitea.dev/modules/log"
"gitea.dev/modules/setting"
)
func UpdateCodeCommentReplies(x base.EngineMigration) error {
type Comment struct {
ID int64 `xorm:"pk autoincr"`
CommitSHA string `xorm:"VARCHAR(40)"`
Patch string `xorm:"TEXT patch"`
Invalidated bool
// Not extracted but used in the below query
Type int `xorm:"INDEX"`
Line int64 // - previous line / + proposed line
TreePath string
ReviewID int64 `xorm:"index"`
}
if err := x.Sync(new(Comment)); err != nil {
return err
}
sqlSelect := `SELECT comment.id as id, first.commit_sha as commit_sha, first.patch as patch, first.invalidated as invalidated`
sqlTail := ` FROM comment INNER JOIN (
SELECT C.id, C.review_id, C.line, C.tree_path, C.patch, C.commit_sha, C.invalidated
FROM comment AS C
WHERE C.type = 21
AND C.created_unix =
(SELECT MIN(comment.created_unix)
FROM comment
WHERE comment.review_id = C.review_id
AND comment.type = 21
AND comment.line = C.line
AND comment.tree_path = C.tree_path)
) AS first
ON comment.review_id = first.review_id
AND comment.tree_path = first.tree_path AND comment.line = first.line
WHERE comment.type = 21
AND comment.id != first.id
AND comment.commit_sha != first.commit_sha`
var (
sqlCmd string
start = 0
batchSize = 100
sess = x.NewSession()
)
defer sess.Close()
for {
if err := sess.Begin(); err != nil {
return err
}
if setting.Database.Type.IsMSSQL() {
if _, err := sess.Exec(sqlSelect + " INTO #temp_comments" + sqlTail); err != nil {
log.Error("unable to create temporary table")
return err
}
}
comments := make([]*Comment, 0, batchSize)
switch {
case setting.Database.Type.IsMySQL():
sqlCmd = sqlSelect + sqlTail + " LIMIT " + strconv.Itoa(batchSize) + ", " + strconv.Itoa(start)
case setting.Database.Type.IsPostgreSQL():
fallthrough
case setting.Database.Type.IsSQLite3():
sqlCmd = sqlSelect + sqlTail + " LIMIT " + strconv.Itoa(batchSize) + " OFFSET " + strconv.Itoa(start)
case setting.Database.Type.IsMSSQL():
sqlCmd = "SELECT TOP " + strconv.Itoa(batchSize) + " * FROM #temp_comments WHERE " +
"(id NOT IN ( SELECT TOP " + strconv.Itoa(start) + " id FROM #temp_comments ORDER BY id )) ORDER BY id"
default:
return errors.New("Unsupported database type")
}
if err := sess.SQL(sqlCmd).Find(&comments); err != nil {
log.Error("failed to select: %v", err)
return err
}
for _, comment := range comments {
if _, err := sess.Table("comment").ID(comment.ID).Cols("commit_sha", "patch", "invalidated").Update(comment); err != nil {
log.Error("failed to update comment[%d]: %v %v", comment.ID, comment, err)
return err
}
}
start += len(comments)
if err := sess.Commit(); err != nil {
return err
}
if len(comments) < batchSize {
break
}
}
return nil
}
+36
View File
@@ -0,0 +1,36 @@
// Copyright 2020 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package v1_14
import (
"gitea.dev/modelmigration/base"
"gitea.dev/modules/timeutil"
)
func UpdateReactionConstraint(x base.EngineMigration) error {
// Reaction represents a reactions on issues and comments.
type Reaction struct {
ID int64 `xorm:"pk autoincr"`
Type string `xorm:"INDEX UNIQUE(s) NOT NULL"`
IssueID int64 `xorm:"INDEX UNIQUE(s) NOT NULL"`
CommentID int64 `xorm:"INDEX UNIQUE(s)"`
UserID int64 `xorm:"INDEX UNIQUE(s) NOT NULL"`
OriginalAuthorID int64 `xorm:"INDEX UNIQUE(s) NOT NULL DEFAULT(0)"`
OriginalAuthor string `xorm:"INDEX UNIQUE(s)"`
CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"`
}
sess := x.NewSession()
defer sess.Close()
if err := sess.Begin(); err != nil {
return err
}
if err := base.RecreateTable(sess, &Reaction{}); err != nil {
return err
}
return sess.Commit()
}
+14
View File
@@ -0,0 +1,14 @@
// Copyright 2020 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package v1_14
import "gitea.dev/modelmigration/base"
func AddBlockOnOfficialReviewRequests(x base.EngineMigration) error {
type ProtectedBranch struct {
BlockOnOfficialReviewRequests bool `xorm:"NOT NULL DEFAULT false"`
}
return x.Sync(new(ProtectedBranch))
}
+71
View File
@@ -0,0 +1,71 @@
// Copyright 2020 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package v1_14
import (
"context"
"gitea.dev/modelmigration/base"
)
func ConvertTaskTypeToString(x base.EngineMigration) error {
const (
GOGS int = iota + 1
SLACK
GITEA
DISCORD
DINGTALK
TELEGRAM
MSTEAMS
FEISHU
MATRIX
WECHATWORK
)
hookTaskTypes := map[int]string{
GITEA: "gitea",
GOGS: "gogs",
SLACK: "slack",
DISCORD: "discord",
DINGTALK: "dingtalk",
TELEGRAM: "telegram",
MSTEAMS: "msteams",
FEISHU: "feishu",
MATRIX: "matrix",
WECHATWORK: "wechatwork",
}
type HookTask struct {
Typ string `xorm:"VARCHAR(16) index"`
}
if err := x.Sync(new(HookTask)); err != nil {
return err
}
// to keep the migration could be rerun
exist, err := x.Dialect().IsColumnExist(x.DB(), context.Background(), "hook_task", "type")
if err != nil {
return err
}
if !exist {
return nil
}
for i, s := range hookTaskTypes {
if _, err := x.Exec("UPDATE hook_task set typ = ? where `type`=?", s, i); err != nil {
return err
}
}
sess := x.NewSession()
defer sess.Close()
if err := sess.Begin(); err != nil {
return err
}
if err := base.DropTableColumns(sess, "hook_task", "type"); err != nil {
return err
}
return sess.Commit()
}
+60
View File
@@ -0,0 +1,60 @@
// Copyright 2020 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package v1_14
import (
"gitea.dev/modelmigration/base"
)
func ConvertWebhookTaskTypeToString(x base.EngineMigration) error {
const (
GOGS int = iota + 1
SLACK
GITEA
DISCORD
DINGTALK
TELEGRAM
MSTEAMS
FEISHU
MATRIX
WECHATWORK
)
hookTaskTypes := map[int]string{
GITEA: "gitea",
GOGS: "gogs",
SLACK: "slack",
DISCORD: "discord",
DINGTALK: "dingtalk",
TELEGRAM: "telegram",
MSTEAMS: "msteams",
FEISHU: "feishu",
MATRIX: "matrix",
WECHATWORK: "wechatwork",
}
type Webhook struct {
Type string `xorm:"char(16) index"`
}
if err := x.Sync(new(Webhook)); err != nil {
return err
}
for i, s := range hookTaskTypes {
if _, err := x.Exec("UPDATE webhook set type = ? where hook_task_type=?", s, i); err != nil {
return err
}
}
sess := x.NewSession()
defer sess.Close()
if err := sess.Begin(); err != nil {
return err
}
if err := base.DropTableColumns(sess, "webhook", "hook_task_type"); err != nil {
return err
}
return sess.Commit()
}
+33
View File
@@ -0,0 +1,33 @@
// Copyright 2020 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package v1_14
import (
"gitea.dev/modelmigration/base"
)
func ConvertTopicNameFrom25To50(x base.EngineMigration) error {
type Topic struct {
ID int64 `xorm:"pk autoincr"`
Name string `xorm:"UNIQUE VARCHAR(50)"`
RepoCount int
CreatedUnix int64 `xorm:"INDEX created"`
UpdatedUnix int64 `xorm:"INDEX updated"`
}
if err := x.Sync(new(Topic)); err != nil {
return err
}
sess := x.NewSession()
defer sess.Close()
if err := sess.Begin(); err != nil {
return err
}
if err := base.RecreateTable(sess, new(Topic)); err != nil {
return err
}
return sess.Commit()
}
+37
View File
@@ -0,0 +1,37 @@
// Copyright 2020 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package v1_14
import (
"fmt"
"gitea.dev/modelmigration/base"
)
// OAuth2Grant here is a snapshot of models.OAuth2Grant for this version
// of the database, as it does not appear to have been added as a part
// of a previous migration.
type OAuth2Grant struct {
ID int64 `xorm:"pk autoincr"`
UserID int64 `xorm:"INDEX unique(user_application)"`
ApplicationID int64 `xorm:"INDEX unique(user_application)"`
Counter int64 `xorm:"NOT NULL DEFAULT 1"`
Scope string `xorm:"TEXT"`
Nonce string `xorm:"TEXT"`
CreatedUnix int64 `xorm:"created"`
UpdatedUnix int64 `xorm:"updated"`
}
// TableName sets the database table name to be the correct one, as the
// autogenerated table name for this struct is "o_auth2_grant".
func (grant *OAuth2Grant) TableName() string {
return "oauth2_grant"
}
func AddScopeAndNonceColumnsToOAuth2Grant(x base.EngineMigration) error {
if err := x.Sync(new(OAuth2Grant)); err != nil {
return fmt.Errorf("Sync: %w", err)
}
return nil
}
+62
View File
@@ -0,0 +1,62 @@
// Copyright 2020 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package v1_14
import (
"gitea.dev/modelmigration/base"
"xorm.io/xorm/schemas"
)
func ConvertHookTaskTypeToVarcharAndTrim(x base.EngineMigration) error {
dbType := x.Dialect().URI().DBType
if dbType == schemas.SQLITE { // For SQLITE, varchar or char will always be represented as TEXT
return nil
}
// HookTask: Typ string `xorm:"VARCHAR(16) index"`
if err := base.ModifyColumn(x, "hook_task", &schemas.Column{
Name: "typ",
SQLType: schemas.SQLType{
Name: "VARCHAR",
},
Length: 16,
Nullable: true, // To keep compatible as nullable
DefaultIsEmpty: true,
}); err != nil {
return err
}
var hookTaskTrimSQL string
if dbType == schemas.MSSQL {
hookTaskTrimSQL = "UPDATE hook_task SET typ = RTRIM(LTRIM(typ))"
} else {
hookTaskTrimSQL = "UPDATE hook_task SET typ = TRIM(typ)"
}
if _, err := x.Exec(hookTaskTrimSQL); err != nil {
return err
}
// Webhook: Type string `xorm:"VARCHAR(16) index"`
if err := base.ModifyColumn(x, "webhook", &schemas.Column{
Name: "type",
SQLType: schemas.SQLType{
Name: "VARCHAR",
},
Length: 16,
Nullable: true, // To keep compatible as nullable
DefaultIsEmpty: true,
}); err != nil {
return err
}
var webhookTrimSQL string
if dbType == schemas.MSSQL {
webhookTrimSQL = "UPDATE webhook SET type = RTRIM(LTRIM(type))"
} else {
webhookTrimSQL = "UPDATE webhook SET type = TRIM(type)"
}
_, err := x.Exec(webhookTrimSQL)
return err
}
+113
View File
@@ -0,0 +1,113 @@
// Copyright 2021 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package v1_14
import (
"crypto/sha256"
"encoding/hex"
"gitea.dev/modelmigration/base"
"golang.org/x/crypto/argon2"
"golang.org/x/crypto/bcrypt"
"golang.org/x/crypto/pbkdf2"
"golang.org/x/crypto/scrypt"
"xorm.io/builder"
)
func RecalculateUserEmptyPWD(x base.EngineMigration) (err error) {
const (
algoBcrypt = "bcrypt"
algoScrypt = "scrypt"
algoArgon2 = "argon2"
algoPbkdf2 = "pbkdf2"
)
type User struct {
ID int64 `xorm:"pk autoincr"`
Passwd string `xorm:"NOT NULL"`
PasswdHashAlgo string `xorm:"NOT NULL DEFAULT 'argon2'"`
MustChangePassword bool `xorm:"NOT NULL DEFAULT false"`
LoginType int
LoginName string
Type int
Salt string `xorm:"VARCHAR(10)"`
}
// hashPassword hash password based on algo and salt
// state 461406070c
hashPassword := func(passwd, salt, algo string) string {
var tempPasswd []byte
switch algo {
case algoBcrypt:
tempPasswd, _ = bcrypt.GenerateFromPassword([]byte(passwd), bcrypt.DefaultCost)
return string(tempPasswd)
case algoScrypt:
tempPasswd, _ = scrypt.Key([]byte(passwd), []byte(salt), 65536, 16, 2, 50)
case algoArgon2:
tempPasswd = argon2.IDKey([]byte(passwd), []byte(salt), 2, 65536, 8, 50)
case algoPbkdf2:
fallthrough
default:
tempPasswd = pbkdf2.Key([]byte(passwd), []byte(salt), 10000, 50, sha256.New)
}
return hex.EncodeToString(tempPasswd)
}
// ValidatePassword checks if given password matches the one belongs to the user.
// state 461406070c, changed since it's not necessary to be time constant
ValidatePassword := func(u *User, passwd string) bool {
tempHash := hashPassword(passwd, u.Salt, u.PasswdHashAlgo)
if u.PasswdHashAlgo != algoBcrypt && u.Passwd == tempHash {
return true
}
if u.PasswdHashAlgo == algoBcrypt && bcrypt.CompareHashAndPassword([]byte(u.Passwd), []byte(passwd)) == nil {
return true
}
return false
}
sess := x.NewSession()
defer sess.Close()
const batchSize = 100
for start := 0; ; start += batchSize {
users := make([]*User, 0, batchSize)
if err = sess.Limit(batchSize, start).Where(builder.Neq{"passwd": ""}, 0).Find(&users); err != nil {
return err
}
if len(users) == 0 {
break
}
if err = sess.Begin(); err != nil {
return err
}
for _, user := range users {
if ValidatePassword(user, "") {
user.Passwd = ""
user.Salt = ""
user.PasswdHashAlgo = ""
if _, err = sess.ID(user.ID).Cols("passwd", "salt", "passwd_hash_algo").Update(user); err != nil {
return err
}
}
}
if err = sess.Commit(); err != nil {
return err
}
}
// delete salt and algo where password is empty
_, err = sess.Where(builder.Eq{"passwd": ""}.And(builder.Neq{"salt": ""}.Or(builder.Neq{"passwd_hash_algo": ""}))).
Cols("salt", "passwd_hash_algo").Update(&User{})
return err
}
+23
View File
@@ -0,0 +1,23 @@
// Copyright 2021 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package v1_14
import (
"fmt"
"gitea.dev/modelmigration/base"
)
func AddUserRedirect(x base.EngineMigration) (err error) {
type UserRedirect struct {
ID int64 `xorm:"pk autoincr"`
LowerName string `xorm:"UNIQUE(s) INDEX NOT NULL"`
RedirectUserID int64
}
if err := x.Sync(new(UserRedirect)); err != nil {
return fmt.Errorf("Sync: %w", err)
}
return nil
}
+10
View File
@@ -0,0 +1,10 @@
// Copyright 2021 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package v1_14
import "gitea.dev/modelmigration/base"
func RecreateUserTableToFixDefaultValues(_ base.EngineMigration) error {
return nil
}
+11
View File
@@ -0,0 +1,11 @@
// Copyright 2021 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package v1_14
import "gitea.dev/modelmigration/base"
func CommentTypeDeleteBranchUseOldRef(x base.EngineMigration) error {
_, err := x.Exec("UPDATE comment SET old_ref = commit_sha, commit_sha = '' WHERE type = 11")
return err
}
+21
View File
@@ -0,0 +1,21 @@
// Copyright 2021 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package v1_14
import (
"fmt"
"gitea.dev/modelmigration/base"
)
func AddDismissedReviewColumn(x base.EngineMigration) error {
type Review struct {
Dismissed bool `xorm:"NOT NULL DEFAULT false"`
}
if err := x.Sync(new(Review)); err != nil {
return fmt.Errorf("Sync: %w", err)
}
return nil
}
+21
View File
@@ -0,0 +1,21 @@
// Copyright 2021 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package v1_14
import (
"fmt"
"gitea.dev/modelmigration/base"
)
func AddSortingColToProjectBoard(x base.EngineMigration) error {
type ProjectBoard struct {
Sorting int8 `xorm:"NOT NULL DEFAULT 0"`
}
if err := x.Sync(new(ProjectBoard)); err != nil {
return fmt.Errorf("Sync: %w", err)
}
return nil
}
+18
View File
@@ -0,0 +1,18 @@
// Copyright 2020 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package v1_14
import (
"gitea.dev/modelmigration/base"
"gitea.dev/modules/timeutil"
)
func AddSessionTable(x base.EngineMigration) error {
type Session struct {
Key string `xorm:"pk CHAR(16)"`
Data []byte `xorm:"BLOB"`
Expiry timeutil.TimeStamp
}
return x.Sync(new(Session))
}
+21
View File
@@ -0,0 +1,21 @@
// Copyright 2021 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package v1_14
import (
"fmt"
"gitea.dev/modelmigration/base"
)
func AddTimeIDCommentColumn(x base.EngineMigration) error {
type Comment struct {
TimeID int64
}
if err := x.Sync(new(Comment)); err != nil {
return fmt.Errorf("Sync: %w", err)
}
return nil
}
+34
View File
@@ -0,0 +1,34 @@
// Copyright 2021 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package v1_14
import (
"fmt"
"gitea.dev/modelmigration/base"
)
func AddRepoTransfer(x base.EngineMigration) error {
type RepoTransfer struct {
ID int64 `xorm:"pk autoincr"`
DoerID int64
RecipientID int64
RepoID int64
TeamIDs []int64
CreatedUnix int64 `xorm:"INDEX NOT NULL created"`
UpdatedUnix int64 `xorm:"INDEX NOT NULL updated"`
}
sess := x.NewSession()
defer sess.Close()
if err := sess.Begin(); err != nil {
return err
}
if err := sess.Sync(new(RepoTransfer)); err != nil {
return fmt.Errorf("Sync: %w", err)
}
return sess.Commit()
}
+52
View File
@@ -0,0 +1,52 @@
// Copyright 2021 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package v1_14
import (
"fmt"
"regexp"
"gitea.dev/modelmigration/base"
"gitea.dev/modules/log"
"gitea.dev/modules/setting"
)
func FixPostgresIDSequences(x base.EngineMigration) error {
if !setting.Database.Type.IsPostgreSQL() {
return nil
}
sess := x.NewSession()
defer sess.Close()
if err := sess.Begin(); err != nil {
return err
}
var sequences []string
schema := sess.Engine().Dialect().URI().Schema
sess.Engine().SetSchema("")
if err := sess.Table("information_schema.sequences").Cols("sequence_name").Where("sequence_name LIKE 'tmp_recreate__%_id_seq%' AND sequence_catalog = ?", setting.Database.Name).Find(&sequences); err != nil {
log.Error("Unable to find sequences: %v", err)
return err
}
sess.Engine().SetSchema(schema)
sequenceRegexp := regexp.MustCompile(`tmp_recreate__(\w+)_id_seq.*`)
for _, sequence := range sequences {
tableName := sequenceRegexp.FindStringSubmatch(sequence)[1]
newSequenceName := tableName + "_id_seq"
if _, err := sess.Exec(fmt.Sprintf("ALTER SEQUENCE `%s` RENAME TO `%s`", sequence, newSequenceName)); err != nil {
log.Error("Unable to rename %s to %s. Error: %v", sequence, newSequenceName, err)
return err
}
if _, err := sess.Exec(fmt.Sprintf("SELECT setval('%s', COALESCE((SELECT MAX(id)+1 FROM `%s`), 1), false)", newSequenceName, tableName)); err != nil {
log.Error("Unable to reset sequence %s for %s. Error: %v", newSequenceName, tableName, err)
return err
}
}
return sess.Commit()
}
+74
View File
@@ -0,0 +1,74 @@
// Copyright 2021 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package v1_14
import "gitea.dev/modelmigration/base"
// RemoveInvalidLabels looks through the database to look for comments and issue_labels
// that refer to labels do not belong to the repository or organization that repository
// that the issue is in
func RemoveInvalidLabels(x base.EngineMigration) error {
type Comment struct {
ID int64 `xorm:"pk autoincr"`
Type int `xorm:"INDEX"`
IssueID int64 `xorm:"INDEX"`
LabelID int64
}
type Issue struct {
ID int64 `xorm:"pk autoincr"`
RepoID int64 `xorm:"INDEX UNIQUE(repo_index)"`
Index int64 `xorm:"UNIQUE(repo_index)"` // Index in one repository.
}
type Repository struct {
ID int64 `xorm:"pk autoincr"`
OwnerID int64 `xorm:"UNIQUE(s) index"`
LowerName string `xorm:"UNIQUE(s) INDEX NOT NULL"`
}
type Label struct {
ID int64 `xorm:"pk autoincr"`
RepoID int64 `xorm:"INDEX"`
OrgID int64 `xorm:"INDEX"`
}
type IssueLabel struct {
ID int64 `xorm:"pk autoincr"`
IssueID int64 `xorm:"UNIQUE(s)"`
LabelID int64 `xorm:"UNIQUE(s)"`
}
if err := x.Sync(new(Comment), new(Issue), new(Repository), new(Label), new(IssueLabel)); err != nil {
return err
}
if _, err := x.Exec(`DELETE FROM issue_label WHERE issue_label.id IN (
SELECT il_too.id FROM (
SELECT il_too_too.id
FROM issue_label AS il_too_too
INNER JOIN label ON il_too_too.label_id = label.id
INNER JOIN issue on issue.id = il_too_too.issue_id
INNER JOIN repository on repository.id = issue.repo_id
WHERE
(label.org_id = 0 AND issue.repo_id != label.repo_id) OR (label.repo_id = 0 AND label.org_id != repository.owner_id)
) AS il_too )`); err != nil {
return err
}
if _, err := x.Exec(`DELETE FROM comment WHERE comment.id IN (
SELECT il_too.id FROM (
SELECT com.id
FROM comment AS com
INNER JOIN label ON com.label_id = label.id
INNER JOIN issue on issue.id = com.issue_id
INNER JOIN repository on repository.id = issue.repo_id
WHERE
com.type = ? AND ((label.org_id = 0 AND issue.repo_id != label.repo_id) OR (label.repo_id = 0 AND label.org_id != repository.owner_id))
) AS il_too)`, 7); err != nil {
return err
}
return nil
}
+128
View File
@@ -0,0 +1,128 @@
// Copyright 2021 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package v1_14
import (
"testing"
"gitea.dev/modelmigration/migrationtest"
"github.com/stretchr/testify/assert"
)
func Test_RemoveInvalidLabels(t *testing.T) {
// Models used by the migration
type Comment struct {
ID int64 `xorm:"pk autoincr"`
Type int `xorm:"INDEX"`
IssueID int64 `xorm:"INDEX"`
LabelID int64
ShouldRemain bool // <- Flag for testing the migration
}
type Issue struct {
ID int64 `xorm:"pk autoincr"`
RepoID int64 `xorm:"INDEX UNIQUE(repo_index)"`
Index int64 `xorm:"UNIQUE(repo_index)"` // Index in one repository.
}
type Repository struct {
ID int64 `xorm:"pk autoincr"`
OwnerID int64 `xorm:"UNIQUE(s) index"`
LowerName string `xorm:"UNIQUE(s) INDEX NOT NULL"`
}
type Label struct {
ID int64 `xorm:"pk autoincr"`
RepoID int64 `xorm:"INDEX"`
OrgID int64 `xorm:"INDEX"`
}
type IssueLabel struct {
ID int64 `xorm:"pk autoincr"`
IssueID int64 `xorm:"UNIQUE(s)"`
LabelID int64 `xorm:"UNIQUE(s)"`
ShouldRemain bool // <- Flag for testing the migration
}
// load and prepare the test database
x, deferable := migrationtest.PrepareTestEnv(t, 0, new(Comment), new(Issue), new(Repository), new(IssueLabel), new(Label))
if x == nil || t.Failed() {
defer deferable()
return
}
defer deferable()
var issueLabels []*IssueLabel
ilPreMigration := map[int64]*IssueLabel{}
ilPostMigration := map[int64]*IssueLabel{}
var comments []*Comment
comPreMigration := map[int64]*Comment{}
comPostMigration := map[int64]*Comment{}
// Get pre migration values
if err := x.Find(&issueLabels); err != nil {
t.Errorf("Unable to find issueLabels: %v", err)
return
}
for _, issueLabel := range issueLabels {
ilPreMigration[issueLabel.ID] = issueLabel
}
if err := x.Find(&comments); err != nil {
t.Errorf("Unable to find comments: %v", err)
return
}
for _, comment := range comments {
comPreMigration[comment.ID] = comment
}
// Run the migration
if err := RemoveInvalidLabels(x); err != nil {
t.Errorf("unable to RemoveInvalidLabels: %v", err)
}
// Get the post migration values
issueLabels = issueLabels[:0]
if err := x.Find(&issueLabels); err != nil {
t.Errorf("Unable to find issueLabels: %v", err)
return
}
for _, issueLabel := range issueLabels {
ilPostMigration[issueLabel.ID] = issueLabel
}
comments = comments[:0]
if err := x.Find(&comments); err != nil {
t.Errorf("Unable to find comments: %v", err)
return
}
for _, comment := range comments {
comPostMigration[comment.ID] = comment
}
// Finally test results of the migration
for id, comment := range comPreMigration {
post, ok := comPostMigration[id]
if ok {
if !comment.ShouldRemain {
t.Errorf("Comment[%d] remained but should have been deleted", id)
}
assert.Equal(t, comment, post)
} else if comment.ShouldRemain {
t.Errorf("Comment[%d] was deleted but should have remained", id)
}
}
for id, il := range ilPreMigration {
post, ok := ilPostMigration[id]
if ok {
if !il.ShouldRemain {
t.Errorf("IssueLabel[%d] remained but should have been deleted", id)
}
assert.Equal(t, il, post)
} else if il.ShouldRemain {
t.Errorf("IssueLabel[%d] was deleted but should have remained", id)
}
}
}
+42
View File
@@ -0,0 +1,42 @@
// Copyright 2021 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package v1_14
import (
"fmt"
"gitea.dev/modelmigration/base"
)
// DeleteOrphanedIssueLabels looks through the database for issue_labels where the label no longer exists and deletes them.
func DeleteOrphanedIssueLabels(x base.EngineMigration) error {
type IssueLabel struct {
ID int64 `xorm:"pk autoincr"`
IssueID int64 `xorm:"UNIQUE(s)"`
LabelID int64 `xorm:"UNIQUE(s)"`
}
sess := x.NewSession()
defer sess.Close()
if err := sess.Begin(); err != nil {
return err
}
if err := sess.Sync(new(IssueLabel)); err != nil {
return fmt.Errorf("Sync: %w", err)
}
if _, err := sess.Exec(`DELETE FROM issue_label WHERE issue_label.id IN (
SELECT ill.id FROM (
SELECT il.id
FROM issue_label AS il
LEFT JOIN label ON il.label_id = label.id
WHERE
label.id IS NULL
) AS ill)`); err != nil {
return err
}
return sess.Commit()
}
+88
View File
@@ -0,0 +1,88 @@
// Copyright 2021 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package v1_14
import (
"testing"
"gitea.dev/modelmigration/migrationtest"
"gitea.dev/modules/timeutil"
"github.com/stretchr/testify/assert"
)
func Test_DeleteOrphanedIssueLabels(t *testing.T) {
// Create the models used in the migration
type IssueLabel struct {
ID int64 `xorm:"pk autoincr"`
IssueID int64 `xorm:"UNIQUE(s)"`
LabelID int64 `xorm:"UNIQUE(s)"`
}
type Label struct {
ID int64 `xorm:"pk autoincr"`
RepoID int64 `xorm:"INDEX"`
OrgID int64 `xorm:"INDEX"`
Name string
Description string
Color string `xorm:"VARCHAR(7)"`
NumIssues int
NumClosedIssues int
CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"`
UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated"`
}
// Prepare and load the testing database
x, deferable := migrationtest.PrepareTestEnv(t, 0, new(IssueLabel), new(Label))
if x == nil || t.Failed() {
defer deferable()
return
}
defer deferable()
var issueLabels []*IssueLabel
preMigration := map[int64]*IssueLabel{}
postMigration := map[int64]*IssueLabel{}
// Load issue labels that exist in the database pre-migration
if err := x.Find(&issueLabels); err != nil {
assert.NoError(t, err)
return
}
for _, issueLabel := range issueLabels {
preMigration[issueLabel.ID] = issueLabel
}
// Run the migration
if err := DeleteOrphanedIssueLabels(x); err != nil {
assert.NoError(t, err)
return
}
// Load the remaining issue-labels
issueLabels = issueLabels[:0]
if err := x.Find(&issueLabels); err != nil {
assert.NoError(t, err)
return
}
for _, issueLabel := range issueLabels {
postMigration[issueLabel.ID] = issueLabel
}
// Now test what is left
if _, ok := postMigration[2]; ok {
t.Errorf("Orphaned Label[2] survived the migration")
return
}
if _, ok := postMigration[5]; ok {
t.Errorf("Orphaned Label[5] survived the migration")
return
}
for id, post := range postMigration {
pre := preMigration[id]
assert.Equal(t, pre, post, "migration changed issueLabel %d", id)
}
}