feat: vendor gitea 1.16.2

This commit is contained in:
2026-06-02 08:36:01 +00:00
parent 247cd60f69
commit 54798b4615
2145 changed files with 162965 additions and 126447 deletions
@@ -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"
@@ -409,7 +402,7 @@ func DropTableColumns(sess *xorm.Session, tableName string, columnNames ...strin
cols += "DROP COLUMN `" + col + "` CASCADE"
}
if _, err := sess.Exec(fmt.Sprintf("ALTER TABLE `%s` %s", tableName, cols)); err != nil {
return fmt.Errorf("Drop table `%s` columns %v: %v", tableName, columnNames, err)
return fmt.Errorf("drop table `%s` columns %v: %w", tableName, columnNames, err)
}
case setting.Database.Type.IsMySQL():
// Drop indexes on columns first
@@ -437,7 +430,7 @@ func DropTableColumns(sess *xorm.Session, tableName string, columnNames ...strin
cols += "DROP COLUMN `" + col + "`"
}
if _, err := sess.Exec(fmt.Sprintf("ALTER TABLE `%s` %s", tableName, cols)); err != nil {
return fmt.Errorf("Drop table `%s` columns %v: %v", tableName, columnNames, err)
return fmt.Errorf("drop table `%s` columns %v: %w", tableName, columnNames, err)
}
case setting.Database.Type.IsMSSQL():
cols := ""
@@ -451,27 +444,27 @@ func DropTableColumns(sess *xorm.Session, tableName string, columnNames ...strin
tableName, strings.ReplaceAll(cols, "`", "'"))
constraints := make([]string, 0)
if err := sess.SQL(sql).Find(&constraints); err != nil {
return fmt.Errorf("Find constraints: %v", err)
return fmt.Errorf("find constraints: %w", err)
}
for _, constraint := range constraints {
if _, err := sess.Exec(fmt.Sprintf("ALTER TABLE `%s` DROP CONSTRAINT `%s`", tableName, constraint)); err != nil {
return fmt.Errorf("Drop table `%s` default constraint `%s`: %v", tableName, constraint, err)
return fmt.Errorf("drop table `%s` default constraint `%s`: %w", tableName, constraint, err)
}
}
sql = fmt.Sprintf("SELECT DISTINCT Name FROM sys.indexes INNER JOIN sys.index_columns ON indexes.index_id = index_columns.index_id AND indexes.object_id = index_columns.object_id WHERE indexes.object_id = OBJECT_ID('%[1]s') AND index_columns.column_id IN (SELECT column_id FROM sys.columns WHERE LOWER(name) IN (%[2]s) AND object_id = OBJECT_ID('%[1]s'))",
tableName, strings.ReplaceAll(cols, "`", "'"))
constraints = make([]string, 0)
if err := sess.SQL(sql).Find(&constraints); err != nil {
return fmt.Errorf("Find constraints: %v", err)
return fmt.Errorf("find constraints: %w", err)
}
for _, constraint := range constraints {
if _, err := sess.Exec(fmt.Sprintf("DROP INDEX `%[2]s` ON `%[1]s`", tableName, constraint)); err != nil {
return fmt.Errorf("Drop index `%[2]s` on `%[1]s`: %v", tableName, constraint, err)
return fmt.Errorf("drop index `%[2]s` on `%[1]s`: %[3]w", tableName, constraint, err)
}
}
if _, err := sess.Exec(fmt.Sprintf("ALTER TABLE `%s` DROP COLUMN %s", tableName, cols)); err != nil {
return fmt.Errorf("Drop table `%s` columns %v: %v", tableName, columnNames, err)
return fmt.Errorf("drop table `%s` columns %v: %w", tableName, columnNames, err)
}
default:
log.Fatal("Unrecognized DB")
@@ -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
}
@@ -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() {
@@ -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)
}
@@ -4,25 +4,127 @@
package base
import (
"database/sql"
"fmt"
"os"
"path"
"path/filepath"
"runtime"
"testing"
"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"
"xorm.io/xorm/schemas"
)
// FIXME: this file shouldn't be in a normal package, it should only be compiled for tests
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.
//
@@ -39,7 +141,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
@@ -88,51 +190,36 @@ func PrepareTestEnv(t *testing.T, skip int, syncModels ...any) (*xorm.Engine, fu
return x, deferFn
}
func MainTest(m *testing.M) {
func LoadTableSchemasMap(t *testing.T, x *xorm.Engine) map[string]*schemas.Table {
tables, err := x.DBMetas()
require.NoError(t, err)
tableMap := make(map[string]*schemas.Table)
for _, table := range tables {
tableMap[table.Name] = table
}
return tableMap
}
func mainTest(m *testing.M) int {
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
}
tmpDataPath, cleanup, err := tempdir.OsTempDir("gitea-test").MkdirTempRandom("data")
if err != nil {
testlogger.Fatalf("Unable to create temporary data path %v\n", err)
testlogger.Panicf("Unable to create temporary data path %v\n", err)
}
defer cleanup()
setting.CustomPath = filepath.Join(setting.AppWorkPath, "custom")
setting.AppDataPath = tmpDataPath
unittest.InitSettingsForTesting()
if err = git.InitFull(); err != nil {
testlogger.Fatalf("Unable to InitFull: %v\n", err)
testlogger.Panicf("Unable to InitFull: %v\n", err)
}
setting.LoadDBSetting()
setting.InitLoggersForTest()
exitStatus := m.Run()
if err := removeAllWithRetry(setting.RepoRootPath); err != nil {
_, _ = fmt.Fprintf(os.Stderr, "os.RemoveAll: %v\n", err)
}
os.Exit(exitStatus)
return m.Run()
}
func MainTest(m *testing.M) {
os.Exit(mainTest(m))
}
@@ -0,0 +1,29 @@
# type ActionRun struct {
# ID int64 `xorm:"pk autoincr"`
# RepoID int64 `xorm:"index"`
# Index int64
# CommitSHA string `xorm:"commit_sha"`
# Event string
# TriggerEvent string
# EventPayload string `xorm:"LONGTEXT"`
# }
-
id: 990
repo_id: 100
index: 7
commit_sha: merge-sha
event: pull_request
event_payload: '{"pull_request":{"head":{"sha":"sha-shared"}}}'
-
id: 991
repo_id: 100
index: 8
commit_sha: sha-shared
event: push
event_payload: '{"head_commit":{"id":"sha-shared"}}'
-
id: 1991
repo_id: 100
index: 9
commit_sha: sha-other
event: release
@@ -0,0 +1,16 @@
# type ActionRunJob struct {
# ID int64 `xorm:"pk autoincr"`
# RunID int64 `xorm:"index"`
# }
-
id: 997
run_id: 990
-
id: 998
run_id: 990
-
id: 1997
run_id: 991
-
id: 1998
run_id: 1991
@@ -0,0 +1,51 @@
# type CommitStatus struct {
# ID int64 `xorm:"pk autoincr"`
# RepoID int64 `xorm:"index"`
# SHA string
# TargetURL string
# }
-
id: 10010
repo_id: 100
sha: sha-shared
target_url: /testuser/repo1/actions/runs/7/jobs/0
-
id: 10011
repo_id: 100
sha: sha-shared
target_url: /testuser/repo1/actions/runs/7/jobs/1
-
id: 10012
repo_id: 100
sha: sha-shared
target_url: /testuser/repo1/actions/runs/8/jobs/0
-
id: 10013
repo_id: 100
sha: sha-other
target_url: /testuser/repo1/actions/runs/9/jobs/0
-
id: 10014
repo_id: 100
sha: sha-shared
target_url: /otheruser/badrepo/actions/runs/7/jobs/0
-
id: 10015
repo_id: 100
sha: sha-shared
target_url: /testuser/repo1/actions/runs/10/jobs/0
-
id: 10016
repo_id: 100
sha: sha-shared
target_url: /testuser/repo1/actions/runs/7/jobs/3
-
id: 10017
repo_id: 100
sha: sha-shared
target_url: https://ci.example.com/build/123
-
id: 10018
repo_id: 100
sha: sha-shared
target_url: /testuser/repo1/actions/runs/990/jobs/997
@@ -0,0 +1,19 @@
# type CommitStatusSummary struct {
# ID int64 `xorm:"pk autoincr"`
# RepoID int64 `xorm:"index"`
# SHA string `xorm:"VARCHAR(64) NOT NULL"`
# State string `xorm:"VARCHAR(7) NOT NULL"`
# TargetURL string
# }
-
id: 10020
repo_id: 100
sha: sha-shared
state: pending
target_url: /testuser/repo1/actions/runs/7/jobs/0
-
id: 10021
repo_id: 100
sha: sha-other
state: pending
target_url: /testuser/repo1/actions/runs/9/jobs/0
@@ -0,0 +1,9 @@
# type Repository struct {
# ID int64 `xorm:"pk autoincr"`
# OwnerName string
# Name string
# }
-
id: 100
owner_name: testuser
name: repo1
@@ -25,6 +25,7 @@ import (
"code.gitea.io/gitea/models/migrations/v1_23"
"code.gitea.io/gitea/models/migrations/v1_24"
"code.gitea.io/gitea/models/migrations/v1_25"
"code.gitea.io/gitea/models/migrations/v1_26"
"code.gitea.io/gitea/models/migrations/v1_6"
"code.gitea.io/gitea/models/migrations/v1_7"
"code.gitea.io/gitea/models/migrations/v1_8"
@@ -379,8 +380,8 @@ func prepareMigrationTasks() []*migration {
newMigration(309, "Improve Notification table indices", v1_23.ImproveNotificationTableIndices),
newMigration(310, "Add Priority to ProtectedBranch", v1_23.AddPriorityToProtectedBranch),
newMigration(311, "Add TimeEstimate to Issue table", v1_23.AddTimeEstimateColumnToIssueTable),
// Gitea 1.23.0-rc0 ends at migration ID number 311 (database version 312)
newMigration(312, "Add DeleteBranchAfterMerge to AutoMerge", v1_24.AddDeleteBranchAfterMergeForAutoMerge),
newMigration(313, "Move PinOrder from issue table to a new table issue_pin", v1_24.MovePinOrderToTableIssuePin),
newMigration(314, "Update OwnerID as zero for repository level action tables", v1_24.UpdateOwnerIDOfRepoLevelActionsTables),
@@ -390,10 +391,20 @@ func prepareMigrationTasks() []*migration {
newMigration(318, "Add anonymous_access_mode for repo_unit", v1_24.AddRepoUnitAnonymousAccessMode),
newMigration(319, "Add ExclusiveOrder to Label table", v1_24.AddExclusiveOrderColumnToLabelTable),
newMigration(320, "Migrate two_factor_policy to login_source table", v1_24.MigrateSkipTwoFactor),
// Gitea 1.24.0 ends at migration ID number 320 (database version 321)
// Gitea 1.24.0 ends at database version 321
newMigration(321, "Use LONGTEXT for some columns and fix review_state.updated_files column", v1_25.UseLongTextInSomeColumnsAndFixBugs),
newMigration(322, "Extend comment tree_path length limit", v1_25.ExtendCommentTreePathLength),
// Gitea 1.25.0 ends at migration ID number 322 (database version 323)
newMigration(323, "Add support for actions concurrency", v1_26.AddActionsConcurrency),
newMigration(324, "Fix closed milestone completeness for milestones with no issues", v1_26.FixClosedMilestoneCompleteness),
newMigration(325, "Fix missed repo_id when migrate attachments", v1_26.FixMissedRepoIDWhenMigrateAttachments),
newMigration(326, "Partially migrate commit status target URL to use run ID and job ID", v1_26.FixCommitStatusTargetURLToUseRunAndJobID),
newMigration(327, "Add disabled state to action runners", v1_26.AddDisabledToActionRunner),
newMigration(328, "Add TokenPermissions column to ActionRunJob", v1_26.AddTokenPermissionsToActionRunJob),
newMigration(329, "Add unique constraint for user badge", v1_26.AddUniqueIndexForUserBadge),
newMigration(330, "Add name column to webhook", v1_26.AddNameToWebhook),
}
return preparedMigrations
}
@@ -84,17 +84,17 @@ func FixMergeBase(ctx context.Context, x *xorm.Engine) error {
if !pr.HasMerged {
var err error
pr.MergeBase, _, err = gitcmd.NewCommand("merge-base").AddDashesAndList(pr.BaseBranch, gitRefName).RunStdString(ctx, &gitcmd.RunOpts{Dir: repoPath})
pr.MergeBase, _, err = gitcmd.NewCommand("merge-base").AddDashesAndList(pr.BaseBranch, gitRefName).WithDir(repoPath).RunStdString(ctx)
if err != nil {
var err2 error
pr.MergeBase, _, err2 = gitcmd.NewCommand("rev-parse").AddDynamicArguments(git.BranchPrefix+pr.BaseBranch).RunStdString(ctx, &gitcmd.RunOpts{Dir: repoPath})
pr.MergeBase, _, err2 = gitcmd.NewCommand("rev-parse").AddDynamicArguments(git.BranchPrefix + pr.BaseBranch).WithDir(repoPath).RunStdString(ctx)
if err2 != nil {
log.Error("Unable to get merge base for PR ID %d, Index %d in %s/%s. Error: %v & %v", pr.ID, pr.Index, baseRepo.OwnerName, baseRepo.Name, err, err2)
continue
}
}
} else {
parentsString, _, err := gitcmd.NewCommand("rev-list", "--parents", "-n", "1").AddDynamicArguments(pr.MergedCommitID).RunStdString(ctx, &gitcmd.RunOpts{Dir: repoPath})
parentsString, _, err := gitcmd.NewCommand("rev-list", "--parents", "-n", "1").AddDynamicArguments(pr.MergedCommitID).WithDir(repoPath).RunStdString(ctx)
if err != nil {
log.Error("Unable to get parents for merged PR ID %d, Index %d in %s/%s. Error: %v", pr.ID, pr.Index, baseRepo.OwnerName, baseRepo.Name, err)
continue
@@ -108,7 +108,7 @@ func FixMergeBase(ctx context.Context, x *xorm.Engine) error {
refs = append(refs, gitRefName)
cmd := gitcmd.NewCommand("merge-base").AddDashesAndList(refs...)
pr.MergeBase, _, err = cmd.RunStdString(ctx, &gitcmd.RunOpts{Dir: repoPath})
pr.MergeBase, _, err = cmd.WithDir(repoPath).RunStdString(ctx)
if err != nil {
log.Error("Unable to get merge base for merged PR ID %d, Index %d in %s/%s. Error: %v", pr.ID, pr.Index, baseRepo.OwnerName, baseRepo.Name, err)
continue
@@ -80,7 +80,7 @@ func RefixMergeBase(ctx context.Context, x *xorm.Engine) error {
gitRefName := fmt.Sprintf("refs/pull/%d/head", pr.Index)
parentsString, _, err := gitcmd.NewCommand("rev-list", "--parents", "-n", "1").AddDynamicArguments(pr.MergedCommitID).RunStdString(ctx, &gitcmd.RunOpts{Dir: repoPath})
parentsString, _, err := gitcmd.NewCommand("rev-list", "--parents", "-n", "1").AddDynamicArguments(pr.MergedCommitID).WithDir(repoPath).RunStdString(ctx)
if err != nil {
log.Error("Unable to get parents for merged PR ID %d, Index %d in %s/%s. Error: %v", pr.ID, pr.Index, baseRepo.OwnerName, baseRepo.Name, err)
continue
@@ -95,7 +95,7 @@ func RefixMergeBase(ctx context.Context, x *xorm.Engine) error {
refs = append(refs, gitRefName)
cmd := gitcmd.NewCommand("merge-base").AddDashesAndList(refs...)
pr.MergeBase, _, err = cmd.RunStdString(ctx, &gitcmd.RunOpts{Dir: repoPath})
pr.MergeBase, _, err = cmd.WithDir(repoPath).RunStdString(ctx)
if err != nil {
log.Error("Unable to get merge base for merged PR ID %d, Index %d in %s/%s. Error: %v", pr.ID, pr.Index, baseRepo.OwnerName, baseRepo.Name, err)
continue
@@ -6,11 +6,10 @@ package v1_12
import (
"fmt"
"math"
"path/filepath"
"strings"
"time"
"code.gitea.io/gitea/modules/git"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/modules/gitrepo"
"code.gitea.io/gitea/modules/graceful"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
@@ -85,12 +84,9 @@ func AddCommitDivergenceToPulls(x *xorm.Engine) error {
log.Error("Missing base repo with id %d for PR ID %d", pr.BaseRepoID, pr.ID)
continue
}
userPath := filepath.Join(setting.RepoRootPath, strings.ToLower(baseRepo.OwnerName))
repoPath := filepath.Join(userPath, strings.ToLower(baseRepo.Name)+".git")
repoStore := repo_model.StorageRepo(repo_model.RelativePath(baseRepo.OwnerName, baseRepo.Name))
gitRefName := fmt.Sprintf("refs/pull/%d/head", pr.Index)
divergence, err := git.GetDivergingCommits(graceful.GetManager().HammerContext(), repoPath, pr.BaseBranch, gitRefName)
divergence, err := gitrepo.GetDivergingCommits(graceful.GetManager().HammerContext(), repoStore, pr.BaseBranch, gitRefName)
if err != nil {
log.Warn("Could not recalculate Divergence for pull: %d", pr.ID)
pr.CommitsAhead = 0
@@ -5,14 +5,10 @@ package v1_21
import (
"context"
"fmt"
"path/filepath"
"strings"
"code.gitea.io/gitea/modules/git"
giturl "code.gitea.io/gitea/modules/git/url"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/modules/gitrepo"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/util"
"xorm.io/xorm"
)
@@ -163,16 +159,13 @@ func migratePushMirrors(x *xorm.Engine) error {
}
func getRemoteAddress(ownerName, repoName, remoteName string) (string, error) {
repoPath := filepath.Join(setting.RepoRootPath, strings.ToLower(ownerName), strings.ToLower(repoName)+".git")
if exist, _ := util.IsExist(repoPath); !exist {
ctx := context.Background()
relativePath := repo_model.RelativePath(ownerName, repoName)
if exist, _ := gitrepo.IsRepositoryExist(ctx, repo_model.StorageRepo(relativePath)); !exist {
return "", nil
}
remoteURL, err := git.GetRemoteAddress(context.Background(), repoPath, remoteName)
if err != nil {
return "", fmt.Errorf("get remote %s's address of %s/%s failed: %v", remoteName, ownerName, repoName, err)
}
u, err := giturl.ParseGitURL(remoteURL)
u, err := gitrepo.GitRemoteGetURL(ctx, repo_model.StorageRepo(relativePath), remoteName)
if err != nil {
return "", err
}
@@ -11,6 +11,7 @@ import (
"code.gitea.io/gitea/modules/timeutil"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func Test_UseLongTextInSomeColumnsAndFixBugs(t *testing.T) {
@@ -38,33 +39,26 @@ func Test_UseLongTextInSomeColumnsAndFixBugs(t *testing.T) {
type Notice struct {
ID int64 `xorm:"pk autoincr"`
Type int
Description string `xorm:"LONGTEXT"`
Description string `xorm:"TEXT"`
CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"`
}
// Prepare and load the testing database
x, deferable := base.PrepareTestEnv(t, 0, new(ReviewState), new(PackageProperty), new(Notice))
defer deferable()
x, deferrable := base.PrepareTestEnv(t, 0, new(ReviewState), new(PackageProperty), new(Notice))
defer deferrable()
assert.NoError(t, UseLongTextInSomeColumnsAndFixBugs(x))
require.NoError(t, UseLongTextInSomeColumnsAndFixBugs(x))
tables, err := x.DBMetas()
assert.NoError(t, err)
tables := base.LoadTableSchemasMap(t, x)
table := tables["review_state"]
column := table.GetColumn("updated_files")
assert.Equal(t, "LONGTEXT", column.SQLType.Name)
for _, table := range tables {
switch table.Name {
case "review_state":
column := table.GetColumn("updated_files")
assert.NotNil(t, column)
assert.Equal(t, "LONGTEXT", column.SQLType.Name)
case "package_property":
column := table.GetColumn("value")
assert.NotNil(t, column)
assert.Equal(t, "LONGTEXT", column.SQLType.Name)
case "notice":
column := table.GetColumn("description")
assert.NotNil(t, column)
assert.Equal(t, "LONGTEXT", column.SQLType.Name)
}
}
table = tables["package_property"]
column = table.GetColumn("value")
assert.Equal(t, "LONGTEXT", column.SQLType.Name)
table = tables["notice"]
column = table.GetColumn("description")
assert.Equal(t, "LONGTEXT", column.SQLType.Name)
}
@@ -0,0 +1,34 @@
// Copyright 2025 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package v1_25
import (
"testing"
"code.gitea.io/gitea/models/migrations/base"
"code.gitea.io/gitea/modules/setting"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func Test_ExtendCommentTreePathLength(t *testing.T) {
if setting.Database.Type.IsSQLite3() {
t.Skip("For SQLITE, varchar or char will always be represented as TEXT")
}
type Comment struct {
ID int64 `xorm:"pk autoincr"`
TreePath string `xorm:"VARCHAR(255)"`
}
x, deferrable := base.PrepareTestEnv(t, 0, new(Comment))
defer deferrable()
require.NoError(t, ExtendCommentTreePathLength(x))
table := base.LoadTableSchemasMap(t, x)["comment"]
column := table.GetColumn("tree_path")
assert.Contains(t, []string{"NVARCHAR", "VARCHAR"}, column.SQLType.Name)
assert.EqualValues(t, 4000, column.Length)
}
@@ -0,0 +1,14 @@
// Copyright 2025 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package v1_26
import (
"testing"
"code.gitea.io/gitea/models/migrations/base"
)
func TestMain(m *testing.M) {
base.MainTest(m)
}
@@ -0,0 +1,43 @@
// Copyright 2025 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package v1_26
import (
"xorm.io/xorm"
)
func AddActionsConcurrency(x *xorm.Engine) error {
type ActionRun struct {
RepoID int64 `xorm:"index(repo_concurrency)"`
RawConcurrency string
ConcurrencyGroup string `xorm:"index(repo_concurrency) NOT NULL DEFAULT ''"`
ConcurrencyCancel bool `xorm:"NOT NULL DEFAULT FALSE"`
}
if _, err := x.SyncWithOptions(xorm.SyncOptions{
IgnoreDropIndices: true,
}, new(ActionRun)); err != nil {
return err
}
if err := x.Sync(new(ActionRun)); err != nil {
return err
}
type ActionRunJob struct {
RepoID int64 `xorm:"index(repo_concurrency)"`
RawConcurrency string
IsConcurrencyEvaluated bool
ConcurrencyGroup string `xorm:"index(repo_concurrency) NOT NULL DEFAULT ''"`
ConcurrencyCancel bool `xorm:"NOT NULL DEFAULT FALSE"`
}
if _, err := x.SyncWithOptions(xorm.SyncOptions{
IgnoreDropIndices: true,
}, new(ActionRunJob)); err != nil {
return err
}
return nil
}
@@ -0,0 +1,24 @@
// Copyright 2025 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package v1_26
import (
"fmt"
"xorm.io/xorm"
)
func FixClosedMilestoneCompleteness(x *xorm.Engine) error {
// Update all milestones to recalculate completeness with the new logic:
// - Closed milestones with 0 issues should show 100%
// - All other milestones should calculate based on closed/total ratio
_, err := x.Exec("UPDATE `milestone` SET completeness=(CASE WHEN is_closed = ? AND num_issues = 0 THEN 100 ELSE 100*num_closed_issues/(CASE WHEN num_issues > 0 THEN num_issues ELSE 1 END) END)",
true,
)
if err != nil {
return fmt.Errorf("error updating milestone completeness: %w", err)
}
return nil
}
@@ -0,0 +1,18 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package v1_26
import (
"xorm.io/xorm"
)
func FixMissedRepoIDWhenMigrateAttachments(x *xorm.Engine) error {
_, err := x.Exec("UPDATE `attachment` SET `repo_id` = (SELECT `repo_id` FROM `issue` WHERE `issue`.`id` = `attachment`.`issue_id`) WHERE `issue_id` > 0 AND (`repo_id` IS NULL OR `repo_id` = 0);")
if err != nil {
return err
}
_, err = x.Exec("UPDATE `attachment` SET `repo_id` = (SELECT `repo_id` FROM `release` WHERE `release`.`id` = `attachment`.`release_id`) WHERE `release_id` > 0 AND (`repo_id` IS NULL OR `repo_id` = 0);")
return err
}
@@ -0,0 +1,45 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package v1_26
import (
"testing"
"code.gitea.io/gitea/models/migrations/base"
"code.gitea.io/gitea/modules/timeutil"
"github.com/stretchr/testify/require"
)
func Test_FixMissedRepoIDWhenMigrateAttachments(t *testing.T) {
type Attachment struct {
ID int64 `xorm:"pk autoincr"`
UUID string `xorm:"uuid UNIQUE"`
RepoID int64 `xorm:"INDEX"` // this should not be zero
IssueID int64 `xorm:"INDEX"` // maybe zero when creating
ReleaseID int64 `xorm:"INDEX"` // maybe zero when creating
UploaderID int64 `xorm:"INDEX DEFAULT 0"` // Notice: will be zero before this column added
CommentID int64 `xorm:"INDEX"`
Name string
DownloadCount int64 `xorm:"DEFAULT 0"`
Size int64 `xorm:"DEFAULT 0"`
CreatedUnix timeutil.TimeStamp `xorm:"created"`
}
type Issue struct {
ID int64 `xorm:"pk autoincr"`
RepoID int64 `xorm:"INDEX"`
}
type Release struct {
ID int64 `xorm:"pk autoincr"`
RepoID int64 `xorm:"INDEX"`
}
// Prepare and load the testing database
x, deferrable := base.PrepareTestEnv(t, 0, new(Attachment), new(Issue), new(Release))
defer deferrable()
require.NoError(t, FixMissedRepoIDWhenMigrateAttachments(x))
}
@@ -0,0 +1,315 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package v1_26
import (
"errors"
"fmt"
"net/url"
"strconv"
"strings"
"code.gitea.io/gitea/modules/json"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
api "code.gitea.io/gitea/modules/structs"
webhook_module "code.gitea.io/gitea/modules/webhook"
"xorm.io/xorm"
)
const (
actionsRunPath = "/actions/runs/"
// Only commit status target URLs whose resolved run ID is smaller than this threshold are rewritten by this partial migration.
// The fixed value 1000 is a conservative cutoff chosen to cover the smaller legacy run indexes that are most likely to be confused with ID-based URLs at runtime.
// Larger legacy {run} or {job} numbers are usually easier to disambiguate. For example:
// * /actions/runs/1200/jobs/1420 is most likely an ID-based URL, because a run should not contain more than 256 jobs.
// * /actions/runs/1500/jobs/3 is most likely an index-based URL, because a job ID cannot be smaller than its run ID.
// But URLs with small numbers, such as /actions/runs/5/jobs/6, are much harder to distinguish reliably.
// This migration therefore prioritizes rewriting target URLs for runs in that lower range.
legacyURLIDThreshold int64 = 1000
)
type migrationRepository struct {
ID int64
OwnerName string
Name string
}
type migrationActionRun struct {
ID int64
RepoID int64
Index int64
CommitSHA string `xorm:"commit_sha"`
Event webhook_module.HookEventType
TriggerEvent string
EventPayload string
}
type migrationActionRunJob struct {
ID int64
RunID int64
}
type migrationCommitStatus struct {
ID int64
RepoID int64
TargetURL string
}
type commitSHAAndRuns struct {
commitSHA string
runs map[int64]*migrationActionRun
}
// FixCommitStatusTargetURLToUseRunAndJobID partially migrates legacy Actions
// commit status target URLs to the new run/job ID-based form.
//
// Only rows whose resolved run ID is below legacyURLIDThreshold are rewritten.
// This is because smaller legacy run indexes are more likely to collide with run ID URLs during runtime resolution,
// so this migration prioritizes that lower range and leaves the remaining legacy target URLs to the web compatibility logic.
func FixCommitStatusTargetURLToUseRunAndJobID(x *xorm.Engine) error {
jobsByRunIDCache := make(map[int64][]int64)
repoLinkCache := make(map[int64]string)
groups, err := loadLegacyMigrationRunGroups(x)
if err != nil {
return err
}
for repoID, groupsBySHA := range groups {
for _, group := range groupsBySHA {
if err := migrateCommitStatusTargetURLForGroup(x, "commit_status", repoID, group.commitSHA, group.runs, jobsByRunIDCache, repoLinkCache); err != nil {
return err
}
if err := migrateCommitStatusTargetURLForGroup(x, "commit_status_summary", repoID, group.commitSHA, group.runs, jobsByRunIDCache, repoLinkCache); err != nil {
return err
}
}
}
return nil
}
func loadLegacyMigrationRunGroups(x *xorm.Engine) (map[int64]map[string]*commitSHAAndRuns, error) {
var runs []migrationActionRun
if err := x.Table("action_run").
Where("id < ?", legacyURLIDThreshold).
Cols("id", "repo_id", "`index`", "commit_sha", "event", "trigger_event", "event_payload").
Find(&runs); err != nil {
return nil, fmt.Errorf("query action_run: %w", err)
}
groups := make(map[int64]map[string]*commitSHAAndRuns)
for i := range runs {
run := runs[i]
commitID, err := getCommitStatusCommitID(&run)
if err != nil {
log.Warn("skip action_run id=%d when resolving commit status commit SHA: %v", run.ID, err)
continue
}
if commitID == "" {
// empty commitID means the run didn't create any commit status records, just skip
continue
}
if groups[run.RepoID] == nil {
groups[run.RepoID] = make(map[string]*commitSHAAndRuns)
}
if groups[run.RepoID][commitID] == nil {
groups[run.RepoID][commitID] = &commitSHAAndRuns{
commitSHA: commitID,
runs: make(map[int64]*migrationActionRun),
}
}
groups[run.RepoID][commitID].runs[run.Index] = &run
}
return groups, nil
}
func migrateCommitStatusTargetURLForGroup(
x *xorm.Engine,
table string,
repoID int64,
sha string,
runs map[int64]*migrationActionRun,
jobsByRunIDCache map[int64][]int64,
repoLinkCache map[int64]string,
) error {
var rows []migrationCommitStatus
if err := x.Table(table).
Where("repo_id = ?", repoID).
And("sha = ?", sha).
Cols("id", "repo_id", "target_url").
Find(&rows); err != nil {
return fmt.Errorf("query %s for repo_id=%d sha=%s: %w", table, repoID, sha, err)
}
for _, row := range rows {
repoLink, err := getRepoLinkCached(x, repoLinkCache, row.RepoID)
if err != nil || repoLink == "" {
if err != nil {
log.Warn("convert %s id=%d getRepoLinkCached: %v", table, row.ID, err)
} else {
log.Warn("convert %s id=%d: repo=%d not found", table, row.ID, row.RepoID)
}
continue
}
runNum, jobNum, ok := parseTargetURL(row.TargetURL, repoLink)
if !ok {
continue
}
run, ok := runs[runNum]
if !ok {
continue
}
jobID, ok, err := getJobIDByIndexCached(x, jobsByRunIDCache, run.ID, jobNum)
if err != nil || !ok {
if err != nil {
log.Warn("convert %s id=%d getJobIDByIndexCached: %v", table, row.ID, err)
} else {
log.Warn("convert %s id=%d: job not found for run_id=%d job_index=%d", table, row.ID, run.ID, jobNum)
}
continue
}
oldURL := row.TargetURL
newURL := fmt.Sprintf("%s%s%d/jobs/%d", repoLink, actionsRunPath, run.ID, jobID)
if oldURL == newURL {
continue
}
if _, err := x.Table(table).ID(row.ID).Cols("target_url").Update(&migrationCommitStatus{TargetURL: newURL}); err != nil {
return fmt.Errorf("update %s id=%d target_url from %s to %s: %w", table, row.ID, oldURL, newURL, err)
}
}
return nil
}
func getRepoLinkCached(x *xorm.Engine, cache map[int64]string, repoID int64) (string, error) {
if link, ok := cache[repoID]; ok {
return link, nil
}
repo := &migrationRepository{}
has, err := x.Table("repository").Where("id=?", repoID).Get(repo)
if err != nil {
return "", err
}
if !has {
cache[repoID] = ""
return "", nil
}
link := setting.AppSubURL + "/" + url.PathEscape(repo.OwnerName) + "/" + url.PathEscape(repo.Name)
cache[repoID] = link
return link, nil
}
func getJobIDByIndexCached(x *xorm.Engine, cache map[int64][]int64, runID, jobIndex int64) (int64, bool, error) {
jobIDs, ok := cache[runID]
if !ok {
var jobs []migrationActionRunJob
if err := x.Table("action_run_job").Where("run_id=?", runID).Asc("id").Cols("id").Find(&jobs); err != nil {
return 0, false, err
}
jobIDs = make([]int64, 0, len(jobs))
for _, job := range jobs {
jobIDs = append(jobIDs, job.ID)
}
cache[runID] = jobIDs
}
if jobIndex < 0 || jobIndex >= int64(len(jobIDs)) {
return 0, false, nil
}
return jobIDs[jobIndex], true, nil
}
func parseTargetURL(targetURL, repoLink string) (runNum, jobNum int64, ok bool) {
prefix := repoLink + actionsRunPath
if !strings.HasPrefix(targetURL, prefix) {
return 0, 0, false
}
rest := targetURL[len(prefix):]
parts := strings.Split(rest, "/")
if len(parts) == 3 && parts[1] == "jobs" {
runNum, err1 := strconv.ParseInt(parts[0], 10, 64)
jobNum, err2 := strconv.ParseInt(parts[2], 10, 64)
if err1 != nil || err2 != nil {
return 0, 0, false
}
return runNum, jobNum, true
}
return 0, 0, false
}
func getCommitStatusCommitID(run *migrationActionRun) (string, error) {
switch run.Event {
case webhook_module.HookEventPush:
payload, err := getPushEventPayload(run)
if err != nil {
return "", fmt.Errorf("getPushEventPayload: %w", err)
}
if payload.HeadCommit == nil {
return "", errors.New("head commit is missing in event payload")
}
return payload.HeadCommit.ID, nil
case webhook_module.HookEventPullRequest,
webhook_module.HookEventPullRequestSync,
webhook_module.HookEventPullRequestAssign,
webhook_module.HookEventPullRequestLabel,
webhook_module.HookEventPullRequestReviewRequest,
webhook_module.HookEventPullRequestMilestone:
payload, err := getPullRequestEventPayload(run)
if err != nil {
return "", fmt.Errorf("getPullRequestEventPayload: %w", err)
}
if payload.PullRequest == nil {
return "", errors.New("pull request is missing in event payload")
} else if payload.PullRequest.Head == nil {
return "", errors.New("head of pull request is missing in event payload")
}
return payload.PullRequest.Head.Sha, nil
case webhook_module.HookEventPullRequestReviewApproved,
webhook_module.HookEventPullRequestReviewRejected,
webhook_module.HookEventPullRequestReviewComment:
payload, err := getPullRequestEventPayload(run)
if err != nil {
return "", fmt.Errorf("getPullRequestEventPayload: %w", err)
}
if payload.PullRequest == nil {
return "", errors.New("pull request is missing in event payload")
} else if payload.PullRequest.Head == nil {
return "", errors.New("head of pull request is missing in event payload")
}
return payload.PullRequest.Head.Sha, nil
case webhook_module.HookEventRelease:
return run.CommitSHA, nil
default:
return "", nil
}
}
func getPushEventPayload(run *migrationActionRun) (*api.PushPayload, error) {
if run.Event != webhook_module.HookEventPush {
return nil, fmt.Errorf("event %s is not a push event", run.Event)
}
var payload api.PushPayload
if err := json.Unmarshal([]byte(run.EventPayload), &payload); err != nil {
return nil, err
}
return &payload, nil
}
func getPullRequestEventPayload(run *migrationActionRun) (*api.PullRequestPayload, error) {
if !run.Event.IsPullRequest() && !run.Event.IsPullRequestReview() {
return nil, fmt.Errorf("event %s is not a pull request event", run.Event)
}
var payload api.PullRequestPayload
if err := json.Unmarshal([]byte(run.EventPayload), &payload); err != nil {
return nil, err
}
return &payload, nil
}
@@ -0,0 +1,113 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package v1_26
import (
"testing"
"code.gitea.io/gitea/models/migrations/base"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/test"
_ "code.gitea.io/gitea/models/actions"
_ "code.gitea.io/gitea/models/git"
_ "code.gitea.io/gitea/models/repo"
"github.com/stretchr/testify/require"
"xorm.io/xorm"
)
func Test_FixCommitStatusTargetURLToUseRunAndJobID(t *testing.T) {
defer test.MockVariableValue(&setting.AppSubURL, "")()
type Repository struct {
ID int64 `xorm:"pk autoincr"`
OwnerName string
Name string
}
type ActionRun struct {
ID int64 `xorm:"pk autoincr"`
RepoID int64 `xorm:"index"`
Index int64
CommitSHA string `xorm:"commit_sha"`
Event string
TriggerEvent string
EventPayload string `xorm:"LONGTEXT"`
}
type ActionRunJob struct {
ID int64 `xorm:"pk autoincr"`
RunID int64 `xorm:"index"`
}
type CommitStatus struct {
ID int64 `xorm:"pk autoincr"`
RepoID int64 `xorm:"index"`
SHA string
TargetURL string
}
type CommitStatusSummary struct {
ID int64 `xorm:"pk autoincr"`
RepoID int64 `xorm:"index"`
SHA string `xorm:"VARCHAR(64) NOT NULL"`
State string `xorm:"VARCHAR(7) NOT NULL"`
TargetURL string
}
x, deferable := base.PrepareTestEnv(t, 0,
new(Repository),
new(ActionRun),
new(ActionRunJob),
new(CommitStatus),
new(CommitStatusSummary),
)
defer deferable()
require.NoError(t, FixCommitStatusTargetURLToUseRunAndJobID(x))
cases := []struct {
table string
id int64
want string
}{
// Legacy URLs for runs whose resolved run IDs are below the threshold should be rewritten.
{table: "commit_status", id: 10010, want: "/testuser/repo1/actions/runs/990/jobs/997"},
{table: "commit_status", id: 10011, want: "/testuser/repo1/actions/runs/990/jobs/998"},
{table: "commit_status", id: 10012, want: "/testuser/repo1/actions/runs/991/jobs/1997"},
// Runs whose resolved IDs are above the threshold are intentionally left unchanged.
{table: "commit_status", id: 10013, want: "/testuser/repo1/actions/runs/9/jobs/0"},
// URLs that do not resolve cleanly as legacy Actions URLs should remain untouched.
{table: "commit_status", id: 10014, want: "/otheruser/badrepo/actions/runs/7/jobs/0"},
{table: "commit_status", id: 10015, want: "/testuser/repo1/actions/runs/10/jobs/0"},
{table: "commit_status", id: 10016, want: "/testuser/repo1/actions/runs/7/jobs/3"},
{table: "commit_status", id: 10017, want: "https://ci.example.com/build/123"},
// Already ID-based URLs are valid inputs and should not be rewritten again.
{table: "commit_status", id: 10018, want: "/testuser/repo1/actions/runs/990/jobs/997"},
// The same rewrite rules apply to commit_status_summary rows.
{table: "commit_status_summary", id: 10020, want: "/testuser/repo1/actions/runs/990/jobs/997"},
{table: "commit_status_summary", id: 10021, want: "/testuser/repo1/actions/runs/9/jobs/0"},
}
for _, tc := range cases {
assertTargetURL(t, x, tc.table, tc.id, tc.want)
}
}
func assertTargetURL(t *testing.T, x *xorm.Engine, table string, id int64, want string) {
t.Helper()
var row struct {
TargetURL string
}
has, err := x.Table(table).Where("id=?", id).Cols("target_url").Get(&row)
require.NoError(t, err)
require.Truef(t, has, "row not found: table=%s id=%d", table, id)
require.Equal(t, want, row.TargetURL)
}
@@ -0,0 +1,17 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package v1_26
import "xorm.io/xorm"
func AddDisabledToActionRunner(x *xorm.Engine) error {
type ActionRunner struct {
IsDisabled bool `xorm:"is_disabled NOT NULL DEFAULT false"`
}
_, err := x.SyncWithOptions(xorm.SyncOptions{
IgnoreDropIndices: true,
}, new(ActionRunner))
return err
}
@@ -0,0 +1,33 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package v1_26
import (
"testing"
"code.gitea.io/gitea/models/migrations/base"
"github.com/stretchr/testify/require"
)
func Test_AddDisabledToActionRunner(t *testing.T) {
type ActionRunner struct {
ID int64 `xorm:"pk autoincr"`
Name string
}
x, deferable := base.PrepareTestEnv(t, 0, new(ActionRunner))
defer deferable()
_, err := x.Insert(&ActionRunner{Name: "runner"})
require.NoError(t, err)
require.NoError(t, AddDisabledToActionRunner(x))
var isDisabled bool
has, err := x.SQL("SELECT is_disabled FROM action_runner WHERE id = ?", 1).Get(&isDisabled)
require.NoError(t, err)
require.True(t, has)
require.False(t, isDisabled)
}
@@ -0,0 +1,16 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package v1_26
import (
"xorm.io/xorm"
)
func AddTokenPermissionsToActionRunJob(x *xorm.Engine) error {
type ActionRunJob struct {
TokenPermissions string `xorm:"JSON TEXT"`
}
_, err := x.SyncWithOptions(xorm.SyncOptions{IgnoreDropIndices: true}, new(ActionRunJob))
return err
}
@@ -0,0 +1,62 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package v1_26
import (
"fmt"
"xorm.io/xorm"
"xorm.io/xorm/schemas"
)
type UserBadge struct { //revive:disable-line:exported
ID int64 `xorm:"pk autoincr"`
BadgeID int64
UserID int64
}
// TableIndices implements xorm's TableIndices interface
func (n *UserBadge) TableIndices() []*schemas.Index {
indices := make([]*schemas.Index, 0, 1)
ubUnique := schemas.NewIndex("unique_user_badge", schemas.UniqueType)
ubUnique.AddColumn("user_id", "badge_id")
indices = append(indices, ubUnique)
return indices
}
// AddUniqueIndexForUserBadge adds a compound unique indexes for user badge table
// and it replaces an old index on user_id
func AddUniqueIndexForUserBadge(x *xorm.Engine) error {
// remove possible duplicated records in table user_badge
type result struct {
UserID int64
BadgeID int64
Cnt int
}
var results []result
if err := x.Select("user_id, badge_id, count(*) as cnt").
Table("user_badge").
GroupBy("user_id, badge_id").
Having("count(*) > 1").
Find(&results); err != nil {
return err
}
for _, r := range results {
if x.Dialect().URI().DBType == schemas.MSSQL {
if _, err := x.Exec(fmt.Sprintf("delete from user_badge where id in (SELECT top %d id FROM user_badge WHERE user_id = ? and badge_id = ?)", r.Cnt-1), r.UserID, r.BadgeID); err != nil {
return err
}
} else {
var ids []int64
if err := x.SQL("SELECT id FROM user_badge WHERE user_id = ? and badge_id = ? limit ?", r.UserID, r.BadgeID, r.Cnt-1).Find(&ids); err != nil {
return err
}
if _, err := x.Table("user_badge").In("id", ids).Delete(); err != nil {
return err
}
}
}
return x.Sync(new(UserBadge))
}
@@ -0,0 +1,85 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package v1_26
import (
"testing"
"code.gitea.io/gitea/models/migrations/base"
"github.com/stretchr/testify/assert"
)
type UserBadgeBefore struct {
ID int64 `xorm:"pk autoincr"`
BadgeID int64
UserID int64 `xorm:"INDEX"`
}
func (UserBadgeBefore) TableName() string {
return "user_badge"
}
func Test_AddUniqueIndexForUserBadge(t *testing.T) {
x, deferable := base.PrepareTestEnv(t, 0, new(UserBadgeBefore))
defer deferable()
if x == nil || t.Failed() {
return
}
testData := []*UserBadgeBefore{
{UserID: 1, BadgeID: 1},
{UserID: 1, BadgeID: 1}, // duplicate
{UserID: 2, BadgeID: 1},
{UserID: 1, BadgeID: 2},
{UserID: 3, BadgeID: 3},
{UserID: 3, BadgeID: 3}, // duplicate
}
for _, data := range testData {
_, err := x.Insert(data)
assert.NoError(t, err)
}
// check that we have duplicates
count, err := x.Where("user_id = ? AND badge_id = ?", 1, 1).Count(&UserBadgeBefore{})
assert.NoError(t, err)
assert.Equal(t, int64(2), count)
count, err = x.Where("user_id = ? AND badge_id = ?", 3, 3).Count(&UserBadgeBefore{})
assert.NoError(t, err)
assert.Equal(t, int64(2), count)
totalCount, err := x.Count(&UserBadgeBefore{})
assert.NoError(t, err)
assert.Equal(t, int64(6), totalCount)
// run the migration
if err := AddUniqueIndexForUserBadge(x); err != nil {
assert.NoError(t, err)
return
}
// verify the duplicates were removed
count, err = x.Where("user_id = ? AND badge_id = ?", 1, 1).Count(&UserBadgeBefore{})
assert.NoError(t, err)
assert.Equal(t, int64(1), count)
count, err = x.Where("user_id = ? AND badge_id = ?", 3, 3).Count(&UserBadgeBefore{})
assert.NoError(t, err)
assert.Equal(t, int64(1), count)
// check total count
totalCount, err = x.Count(&UserBadgeBefore{})
assert.NoError(t, err)
assert.Equal(t, int64(4), totalCount)
// fail to insert a duplicate
_, err = x.Insert(&UserBadge{UserID: 1, BadgeID: 1})
assert.Error(t, err)
// succeed adding a non-duplicate
_, err = x.Insert(&UserBadge{UserID: 4, BadgeID: 1})
assert.NoError(t, err)
}
@@ -0,0 +1,16 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package v1_26
import (
"xorm.io/xorm"
)
func AddNameToWebhook(x *xorm.Engine) error {
type Webhook struct {
Name string `xorm:"VARCHAR(255) NOT NULL DEFAULT ''"`
}
_, err := x.SyncWithOptions(xorm.SyncOptions{IgnoreDropIndices: true}, new(Webhook))
return err
}
@@ -6,11 +6,10 @@ package v1_9
import (
"context"
"fmt"
"path/filepath"
"strings"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/gitrepo"
"xorm.io/xorm"
)
@@ -34,16 +33,6 @@ func FixReleaseSha1OnReleaseTable(ctx context.Context, x *xorm.Engine) error {
Name string
}
// UserPath returns the path absolute path of user repositories.
UserPath := func(userName string) string {
return filepath.Join(setting.RepoRootPath, strings.ToLower(userName))
}
// RepoPath returns repository path by given user and repository name.
RepoPath := func(userName, repoName string) string {
return filepath.Join(UserPath(userName), strings.ToLower(repoName)+".git")
}
// Update release sha1
const batchSize = 100
sess := x.NewSession()
@@ -99,7 +88,7 @@ func FixReleaseSha1OnReleaseTable(ctx context.Context, x *xorm.Engine) error {
userCache[repo.OwnerID] = user
}
gitRepo, err = git.OpenRepository(ctx, RepoPath(user.Name, repo.Name))
gitRepo, err = gitrepo.OpenRepository(ctx, repo_model.StorageRepo(repo_model.RelativePath(user.Name, repo.Name)))
if err != nil {
return err
}