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
+20
View File
@@ -0,0 +1,20 @@
// Copyright 2019 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package v1_11
import (
"gitea.dev/modelmigration/base"
)
func DropColumnHeadUserNameOnPullRequest(x base.EngineMigration) error {
sess := x.NewSession()
defer sess.Close()
if err := sess.Begin(); err != nil {
return err
}
if err := base.DropTableColumns(sess, "pull_request", "head_user_name"); err != nil {
return err
}
return sess.Commit()
}
+15
View File
@@ -0,0 +1,15 @@
// Copyright 2019 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package v1_11
import "gitea.dev/modelmigration/base"
func AddWhitelistDeployKeysToBranches(x base.EngineMigration) error {
type ProtectedBranch struct {
ID int64
WhitelistDeployKeys bool `xorm:"NOT NULL DEFAULT false"`
}
return x.Sync(new(ProtectedBranch))
}
+32
View File
@@ -0,0 +1,32 @@
// Copyright 2019 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package v1_11
import (
"gitea.dev/modelmigration/base"
)
func RemoveLabelUneededCols(x base.EngineMigration) error {
// Make sure the columns exist before dropping them
type Label struct {
QueryString string
IsSelected bool
}
if err := x.Sync(new(Label)); err != nil {
return err
}
sess := x.NewSession()
defer sess.Close()
if err := sess.Begin(); err != nil {
return err
}
if err := base.DropTableColumns(sess, "label", "query_string"); err != nil {
return err
}
if err := base.DropTableColumns(sess, "label", "is_selected"); err != nil {
return err
}
return sess.Commit()
}
+21
View File
@@ -0,0 +1,21 @@
// Copyright 2019 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package v1_11
import "gitea.dev/modelmigration/base"
func AddTeamIncludesAllRepositories(x base.EngineMigration) error {
type Team struct {
ID int64 `xorm:"pk autoincr"`
IncludesAllRepositories bool `xorm:"NOT NULL DEFAULT false"`
}
if err := x.Sync(new(Team)); err != nil {
return err
}
_, err := x.Exec("UPDATE `team` SET `includes_all_repositories` = ? WHERE `name`=?",
true, "Owners")
return err
}
+23
View File
@@ -0,0 +1,23 @@
// Copyright 2019 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package v1_11
import "gitea.dev/modelmigration/base"
// RepoWatchMode specifies what kind of watch the user has on a repository
type RepoWatchMode int8
// Watch is connection request for receiving repository notification.
type Watch struct {
ID int64 `xorm:"pk autoincr"`
Mode RepoWatchMode `xorm:"SMALLINT NOT NULL DEFAULT 1"`
}
func AddModeColumnToWatch(x base.EngineMigration) error {
if err := x.Sync(new(Watch)); err != nil {
return err
}
_, err := x.Exec("UPDATE `watch` SET `mode` = 1")
return err
}
+15
View File
@@ -0,0 +1,15 @@
// Copyright 2019 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package v1_11
import "gitea.dev/modelmigration/base"
func AddTemplateToRepo(x base.EngineMigration) error {
type Repository struct {
IsTemplate bool `xorm:"INDEX NOT NULL DEFAULT false"`
TemplateID int64 `xorm:"INDEX"`
}
return x.Sync(new(Repository))
}
+15
View File
@@ -0,0 +1,15 @@
// Copyright 2019 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package v1_11
import "gitea.dev/modelmigration/base"
func AddCommentIDOnNotification(x base.EngineMigration) error {
type Notification struct {
ID int64 `xorm:"pk autoincr"`
CommentID int64
}
return x.Sync(new(Notification))
}
+14
View File
@@ -0,0 +1,14 @@
// Copyright 2019 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package v1_11
import "gitea.dev/modelmigration/base"
func AddCanCreateOrgRepoColumnForTeam(x base.EngineMigration) error {
type Team struct {
CanCreateOrgRepo bool `xorm:"NOT NULL DEFAULT false"`
}
return x.Sync(new(Team))
}
+30
View File
@@ -0,0 +1,30 @@
// Copyright 2019 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package v1_11
import (
"gitea.dev/modelmigration/base"
"xorm.io/xorm/schemas"
)
func ChangeReviewContentToText(x base.EngineMigration) error {
switch x.Dialect().URI().DBType {
case schemas.MYSQL:
_, err := x.Exec("ALTER TABLE review MODIFY COLUMN content TEXT")
return err
case schemas.ORACLE:
_, err := x.Exec("ALTER TABLE review MODIFY content TEXT")
return err
case schemas.MSSQL:
_, err := x.Exec("ALTER TABLE review ALTER COLUMN content TEXT")
return err
case schemas.POSTGRES:
_, err := x.Exec("ALTER TABLE review ALTER COLUMN content TYPE TEXT")
return err
default:
// SQLite doesn't support ALTER COLUMN, and it seem to already make String to _TEXT_ default so no migration needed
return nil
}
}
+435
View File
@@ -0,0 +1,435 @@
// Copyright 2019 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package v1_11
import (
"fmt"
"slices"
"gitea.dev/modelmigration/base"
)
func AddBranchProtectionCanPushAndEnableWhitelist(x base.EngineMigration) error {
type ProtectedBranch struct {
CanPush bool `xorm:"NOT NULL DEFAULT false"`
EnableApprovalsWhitelist bool `xorm:"NOT NULL DEFAULT false"`
ApprovalsWhitelistUserIDs []int64 `xorm:"JSON TEXT"`
ApprovalsWhitelistTeamIDs []int64 `xorm:"JSON TEXT"`
RequiredApprovals int64 `xorm:"NOT NULL DEFAULT 0"`
}
type User struct {
ID int64 `xorm:"pk autoincr"`
Type int
// Permissions
IsAdmin bool
IsRestricted bool `xorm:"NOT NULL DEFAULT false"`
Visibility int `xorm:"NOT NULL DEFAULT 0"`
}
type Review struct {
ID int64 `xorm:"pk autoincr"`
Official bool `xorm:"NOT NULL DEFAULT false"`
ReviewerID int64 `xorm:"index"`
IssueID int64 `xorm:"index"`
}
if err := x.Sync(new(ProtectedBranch)); err != nil {
return err
}
if err := x.Sync(new(Review)); err != nil {
return err
}
const (
// ReviewTypeApprove approves changes
ReviewTypeApprove int = 1
// ReviewTypeReject gives feedback blocking merge
ReviewTypeReject int = 3
// VisibleTypePublic Visible for everyone
VisibleTypePublic int = 0
// VisibleTypePrivate Visible only for organization's members
VisibleTypePrivate int = 2
// unit.UnitTypeCode is unit type code
UnitTypeCode int = 1
// AccessModeNone no access
AccessModeNone int = 0
// AccessModeRead read access
AccessModeRead int = 1
// AccessModeWrite write access
AccessModeWrite int = 2
// AccessModeOwner owner access
AccessModeOwner int = 4
)
// Repository represents a git repository.
type Repository struct {
ID int64 `xorm:"pk autoincr"`
OwnerID int64 `xorm:"UNIQUE(s) index"`
IsPrivate bool `xorm:"INDEX"`
}
type PullRequest struct {
ID int64 `xorm:"pk autoincr"`
BaseRepoID int64 `xorm:"INDEX"`
BaseBranch string
}
// RepoUnit describes all units of a repository
type RepoUnit struct {
ID int64
RepoID int64 `xorm:"INDEX(s)"`
Type int `xorm:"INDEX(s)"`
}
type Permission struct {
AccessMode int
Units []*RepoUnit
UnitsMode map[int]int
}
type TeamUser struct {
ID int64 `xorm:"pk autoincr"`
TeamID int64 `xorm:"UNIQUE(s)"`
UID int64 `xorm:"UNIQUE(s)"`
}
type Collaboration struct {
ID int64 `xorm:"pk autoincr"`
RepoID int64 `xorm:"UNIQUE(s) INDEX NOT NULL"`
UserID int64 `xorm:"UNIQUE(s) INDEX NOT NULL"`
Mode int `xorm:"DEFAULT 2 NOT NULL"`
}
type Access struct {
ID int64 `xorm:"pk autoincr"`
UserID int64 `xorm:"UNIQUE(s)"`
RepoID int64 `xorm:"UNIQUE(s)"`
Mode int
}
type TeamUnit struct {
ID int64 `xorm:"pk autoincr"`
OrgID int64 `xorm:"INDEX"`
TeamID int64 `xorm:"UNIQUE(s)"`
Type int `xorm:"UNIQUE(s)"`
}
// Team represents a organization team.
type Team struct {
ID int64 `xorm:"pk autoincr"`
OrgID int64 `xorm:"INDEX"`
Authorize int
}
// getUserRepoPermission static function based on issues_model.IsOfficialReviewer at 5d78792385
getUserRepoPermission := func(sess base.Session, repo *Repository, user *User) (Permission, error) {
var perm Permission
repoOwner := new(User)
has, err := sess.ID(repo.OwnerID).Get(repoOwner)
if err != nil || !has {
return perm, err
}
// Prevent strangers from checking out public repo of private organization
// Allow user if they are collaborator of a repo within a private organization but not a member of the organization itself
hasOrgVisible := true
// Not SignedUser
if user == nil {
hasOrgVisible = repoOwner.Visibility == VisibleTypePublic
} else if !user.IsAdmin {
hasMemberWithUserID, err := sess.
Where("uid=?", user.ID).
And("org_id=?", repoOwner.ID).
Table("org_user").
Exist()
if err != nil {
hasOrgVisible = false
}
if (repoOwner.Visibility == VisibleTypePrivate || user.IsRestricted) && !hasMemberWithUserID {
hasOrgVisible = false
}
}
isCollaborator, err := sess.Get(&Collaboration{RepoID: repo.ID, UserID: user.ID})
if err != nil {
return perm, err
}
if repoOwner.Type == 1 && !hasOrgVisible && !isCollaborator {
perm.AccessMode = AccessModeNone
return perm, err
}
var units []*RepoUnit
if err := sess.Where("repo_id = ?", repo.ID).Find(&units); err != nil {
return perm, err
}
perm.Units = units
// anonymous visit public repo
if user == nil {
perm.AccessMode = AccessModeRead
return perm, err
}
// Admin or the owner has super access to the repository
if user.IsAdmin || user.ID == repo.OwnerID {
perm.AccessMode = AccessModeOwner
return perm, err
}
accessLevel := func(user *User, repo *Repository) (int, error) {
mode := AccessModeNone
var userID int64
restricted := false
if user != nil {
userID = user.ID
restricted = user.IsRestricted
}
if !restricted && !repo.IsPrivate {
mode = AccessModeRead
}
if userID == 0 {
return mode, nil
}
if userID == repo.OwnerID {
return AccessModeOwner, nil
}
a := &Access{UserID: userID, RepoID: repo.ID}
if has, err := sess.Get(a); !has || err != nil {
return mode, err
}
return a.Mode, nil
}
// plain user
perm.AccessMode, err = accessLevel(user, repo)
if err != nil {
return perm, err
}
// If Owner is no Org
if repoOwner.Type != 1 {
return perm, err
}
perm.UnitsMode = make(map[int]int)
// Collaborators on organization
if isCollaborator {
for _, u := range units {
perm.UnitsMode[u.Type] = perm.AccessMode
}
}
// get units mode from teams
var teams []*Team
err = sess.
Join("INNER", "team_user", "team_user.team_id = team.id").
Join("INNER", "team_repo", "team_repo.team_id = team.id").
Where("team.org_id = ?", repo.OwnerID).
And("team_user.uid=?", user.ID).
And("team_repo.repo_id=?", repo.ID).
Find(&teams)
if err != nil {
return perm, err
}
// if user in an owner team
for _, team := range teams {
if team.Authorize >= AccessModeOwner {
perm.AccessMode = AccessModeOwner
perm.UnitsMode = nil
return perm, err
}
}
for _, u := range units {
var found bool
for _, team := range teams {
var teamU []*TeamUnit
var unitEnabled bool
err = sess.Where("team_id = ?", team.ID).Find(&teamU)
for _, tu := range teamU {
if tu.Type == u.Type {
unitEnabled = true
break
}
}
if unitEnabled {
m := perm.UnitsMode[u.Type]
if m < team.Authorize {
perm.UnitsMode[u.Type] = team.Authorize
}
found = true
}
}
// for a public repo on an organization, a non-restricted user has read permission on non-team defined units.
if !found && !repo.IsPrivate && !user.IsRestricted {
if _, ok := perm.UnitsMode[u.Type]; !ok {
perm.UnitsMode[u.Type] = AccessModeRead
}
}
}
// remove no permission units
perm.Units = make([]*RepoUnit, 0, len(units))
for t := range perm.UnitsMode {
for _, u := range units {
if u.Type == t {
perm.Units = append(perm.Units, u)
}
}
}
return perm, err
}
// isOfficialReviewer static function based on 5d78792385
isOfficialReviewer := func(sess base.Session, issueID int64, reviewer *User) (bool, error) {
pr := new(PullRequest)
has, err := sess.ID(issueID).Get(pr)
if err != nil {
return false, err
} else if !has {
return false, fmt.Errorf("PullRequest for issueID %d not exist", issueID)
}
baseRepo := new(Repository)
has, err = sess.ID(pr.BaseRepoID).Get(baseRepo)
if err != nil {
return false, err
} else if !has {
return false, fmt.Errorf("baseRepo with id %d not exist", pr.BaseRepoID)
}
protectedBranch := new(ProtectedBranch)
has, err = sess.Where("repo_id=? AND branch_name=?", baseRepo.ID, pr.BaseBranch).Get(protectedBranch)
if err != nil {
return false, err
}
if !has {
return false, nil
}
if !protectedBranch.EnableApprovalsWhitelist {
perm, err := getUserRepoPermission(sess, baseRepo, reviewer)
if err != nil {
return false, err
}
if len(perm.UnitsMode) == 0 {
for _, u := range perm.Units {
if u.Type == UnitTypeCode {
return AccessModeWrite <= perm.AccessMode, nil
}
}
return false, nil
}
return AccessModeWrite <= perm.UnitsMode[UnitTypeCode], nil
}
if slices.Contains(protectedBranch.ApprovalsWhitelistUserIDs, reviewer.ID) {
return true, nil
}
// isUserInTeams
return sess.Where("uid=?", reviewer.ID).In("team_id", protectedBranch.ApprovalsWhitelistTeamIDs).Exist(new(TeamUser))
}
if _, err := x.Exec("UPDATE `protected_branch` SET `enable_whitelist` = ? WHERE enable_whitelist IS NULL", false); err != nil {
return err
}
if _, err := x.Exec("UPDATE `protected_branch` SET `can_push` = `enable_whitelist`"); err != nil {
return err
}
if _, err := x.Exec("UPDATE `protected_branch` SET `enable_approvals_whitelist` = ? WHERE `required_approvals` > ?", true, 0); err != nil {
return err
}
var pageSize int64 = 20
qresult, err := x.QueryInterface("SELECT max(id) as max_id FROM issue")
if err != nil {
return err
}
var totalIssues int64
totalIssues, ok := qresult[0]["max_id"].(int64)
if !ok {
// If there are no issues at all we ignore it
return nil
}
totalPages := totalIssues / pageSize
executeBody := func(page, pageSize int64) error {
// Find latest review of each user in each pull request, and set official field if appropriate
reviews := []*Review{}
if err := x.SQL("SELECT * FROM review WHERE id IN (SELECT max(id) as id FROM review WHERE issue_id > ? AND issue_id <= ? AND type in (?, ?) GROUP BY issue_id, reviewer_id)",
page*pageSize, (page+1)*pageSize, ReviewTypeApprove, ReviewTypeReject).
Find(&reviews); err != nil {
return err
}
if len(reviews) == 0 {
return nil
}
sess := x.NewSession()
defer sess.Close()
if err := sess.Begin(); err != nil {
return err
}
var updated int
for _, review := range reviews {
reviewer := new(User)
has, err := sess.ID(review.ReviewerID).Get(reviewer)
if err != nil || !has {
// Error might occur if user doesn't exist, ignore it.
continue
}
official, err := isOfficialReviewer(sess, review.IssueID, reviewer)
if err != nil {
// Branch might not be protected or other error, ignore it.
continue
}
review.Official = official
updated++
if _, err := sess.ID(review.ID).Cols("official").Update(review); err != nil {
return err
}
}
if updated > 0 {
return sess.Commit()
}
return nil
}
var page int64
for page = 0; page <= totalPages; page++ {
if err := executeBody(page, pageSize); err != nil {
return err
}
}
return nil
}
+47
View File
@@ -0,0 +1,47 @@
// Copyright 2019 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package v1_11
import (
"path/filepath"
"gitea.dev/modelmigration/base"
"gitea.dev/modules/log"
"gitea.dev/modules/setting"
"gitea.dev/modules/util"
"xorm.io/builder"
)
func RemoveAttachmentMissedRepo(x base.EngineMigration) error {
type Attachment struct {
UUID string `xorm:"uuid"`
}
var start int
attachments := make([]*Attachment, 0, 50)
for {
err := x.Select("uuid").Where(builder.NotIn("release_id", builder.Select("id").From("`release`"))).
And("release_id > 0").
OrderBy("id").Limit(50, start).Find(&attachments)
if err != nil {
return err
}
for i := 0; i < len(attachments); i++ {
uuid := attachments[i].UUID
if err = util.RemoveAll(filepath.Join(setting.Attachment.Storage.Path, uuid[0:1], uuid[1:2], uuid)); err != nil {
log.Warn("Unable to remove attachment file by UUID %s: %v", uuid, err)
}
}
if len(attachments) < 50 {
break
}
start += 50
attachments = attachments[:0]
}
_, err := x.Exec("DELETE FROM attachment WHERE release_id > 0 AND release_id NOT IN (SELECT id FROM `release`)")
return err
}
+22
View File
@@ -0,0 +1,22 @@
// Copyright 2019 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package v1_11
import (
"fmt"
"gitea.dev/modelmigration/base"
)
func FeatureChangeTargetBranch(x base.EngineMigration) error {
type Comment struct {
OldRef string
NewRef string
}
if err := x.Sync(new(Comment)); err != nil {
return fmt.Errorf("Sync: %w", err)
}
return nil
}
+50
View File
@@ -0,0 +1,50 @@
// Copyright 2019 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package v1_11
import (
"net/url"
"gitea.dev/modelmigration/base"
)
func SanitizeOriginalURL(x base.EngineMigration) error {
type Repository struct {
ID int64
OriginalURL string `xorm:"VARCHAR(2048)"`
}
var last int
const batchSize = 50
for {
results := make([]Repository, 0, batchSize)
err := x.Where("original_url <> '' AND original_url IS NOT NULL").
And("original_service_type = 0 OR original_service_type IS NULL").
OrderBy("id").
Limit(batchSize, last).
Find(&results)
if err != nil {
return err
}
if len(results) == 0 {
break
}
last += len(results)
for _, res := range results {
u, err := url.Parse(res.OriginalURL)
if err != nil {
// it is ok to continue here, we only care about fixing URLs that we can read
continue
}
u.User = nil
originalURL := u.String()
_, err = x.Exec("UPDATE repository SET original_url = ? WHERE id = ?", originalURL, res.ID)
if err != nil {
return err
}
}
}
return nil
}
+158
View File
@@ -0,0 +1,158 @@
// Copyright 2019 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package v1_11
import (
"crypto/md5"
"fmt"
"io"
"math"
"os"
"path/filepath"
"time"
"gitea.dev/modelmigration/base"
"gitea.dev/modules/container"
"gitea.dev/modules/log"
"gitea.dev/modules/setting"
"gitea.dev/modules/util"
)
func RenameExistingUserAvatarName(x base.EngineMigration) error {
sess := x.NewSession()
defer sess.Close()
type User struct {
ID int64 `xorm:"pk autoincr"`
LowerName string `xorm:"UNIQUE NOT NULL"`
Avatar string
}
ticker := time.NewTicker(5 * time.Second)
defer ticker.Stop()
count, err := x.Count(new(User))
if err != nil {
return err
}
log.Info("%d User Avatar(s) to migrate ...", count)
deleteList := make(container.Set[string])
start := 0
migrated := 0
for {
if err := sess.Begin(); err != nil {
return fmt.Errorf("session.Begin: %w", err)
}
users := make([]*User, 0, 50)
if err := sess.Table("user").Asc("id").Limit(50, start).Find(&users); err != nil {
return fmt.Errorf("select users from id [%d]: %w", start, err)
}
if len(users) == 0 {
_ = sess.Rollback()
break
}
log.Info("select users [%d - %d]", start, start+len(users))
start += 50
for _, user := range users {
oldAvatar := user.Avatar
if stat, err := os.Stat(filepath.Join(setting.Avatar.Storage.Path, oldAvatar)); err != nil || !stat.Mode().IsRegular() {
if err == nil {
err = fmt.Errorf("Error: \"%s\" is not a regular file", oldAvatar)
}
log.Warn("[user: %s] os.Stat: %v", user.LowerName, err)
// avatar doesn't exist in the storage
// no need to move avatar and update database
// we can just skip this
continue
}
newAvatar, err := copyOldAvatarToNewLocation(user.ID, oldAvatar)
if err != nil {
_ = sess.Rollback()
return fmt.Errorf("[user: %s] %w", user.LowerName, err)
} else if newAvatar == oldAvatar {
continue
}
user.Avatar = newAvatar
if _, err := sess.ID(user.ID).Cols("avatar").Update(user); err != nil {
_ = sess.Rollback()
return fmt.Errorf("[user: %s] user table update: %w", user.LowerName, err)
}
deleteList.Add(filepath.Join(setting.Avatar.Storage.Path, oldAvatar))
migrated++
select {
case <-ticker.C:
log.Info(
"%d/%d (%2.0f%%) User Avatar(s) migrated (%d old avatars to be deleted) in %d batches. %d Remaining ...",
migrated,
count,
float64(migrated)/float64(count)*100,
len(deleteList),
int(math.Ceil(float64(migrated)/float64(50))),
count-int64(migrated))
default:
}
}
if err := sess.Commit(); err != nil {
_ = sess.Rollback()
return fmt.Errorf("commit session: %w", err)
}
}
deleteCount := len(deleteList)
log.Info("Deleting %d old avatars ...", deleteCount)
i := 0
for file := range deleteList {
if err := util.Remove(file); err != nil {
log.Warn("util.Remove: %v", err)
}
i++
select {
case <-ticker.C:
log.Info(
"%d/%d (%2.0f%%) Old User Avatar(s) deleted. %d Remaining ...",
i,
deleteCount,
float64(i)/float64(deleteCount)*100,
deleteCount-i)
default:
}
}
log.Info("Completed migrating %d User Avatar(s) and deleting %d Old Avatars", count, deleteCount)
return nil
}
// copyOldAvatarToNewLocation copies oldAvatar to newAvatarLocation
// and returns newAvatar location
func copyOldAvatarToNewLocation(userID int64, oldAvatar string) (string, error) {
fr, err := os.Open(filepath.Join(setting.Avatar.Storage.Path, oldAvatar))
if err != nil {
return "", fmt.Errorf("os.Open: %w", err)
}
defer fr.Close()
data, err := io.ReadAll(fr)
if err != nil {
return "", fmt.Errorf("io.ReadAll: %w", err)
}
newAvatar := fmt.Sprintf("%x", md5.Sum(fmt.Appendf(nil, "%d-%x", userID, md5.Sum(data))))
if newAvatar == oldAvatar {
return newAvatar, nil
}
if err := os.WriteFile(filepath.Join(setting.Avatar.Storage.Path, newAvatar), data, 0o666); err != nil {
return "", fmt.Errorf("os.WriteFile: %w", err)
}
return newAvatar, nil
}
+30
View File
@@ -0,0 +1,30 @@
// Copyright 2019 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package v1_11
import "gitea.dev/modelmigration/base"
func ExtendTrackedTimes(x base.EngineMigration) error {
type TrackedTime struct {
Time int64 `xorm:"NOT NULL"`
Deleted bool `xorm:"NOT NULL DEFAULT false"`
}
sess := x.NewSession()
defer sess.Close()
if err := sess.Begin(); err != nil {
return err
}
if _, err := sess.Exec("DELETE FROM tracked_time WHERE time IS NULL"); err != nil {
return err
}
if err := sess.Sync(new(TrackedTime)); err != nil {
return err
}
return sess.Commit()
}