diff --git a/models/git/branch.go b/models/git/branch.go index ffd1d7ed164a0..05af42a0d810b 100644 --- a/models/git/branch.go +++ b/models/git/branch.go @@ -13,10 +13,12 @@ import ( user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/timeutil" "code.gitea.io/gitea/modules/util" "xorm.io/builder" + "xorm.io/xorm/schemas" ) // ErrBranchNotExist represents an error that branch with such name does not exist. @@ -103,7 +105,7 @@ func (err ErrBranchesEqual) Unwrap() error { type Branch struct { ID int64 RepoID int64 `xorm:"UNIQUE(s)"` - Name string `xorm:"UNIQUE(s) NOT NULL"` // git's ref-name is case-sensitive internally, however, in some databases (mssql, mysql, by default), it's case-insensitive at the moment + Name string `xorm:"UNIQUE(s) NOT NULL"` CommitID string CommitMessage string `xorm:"TEXT"` // it only stores the message summary (the first line) PusherID int64 @@ -117,6 +119,20 @@ type Branch struct { UpdatedUnix timeutil.TimeStamp `xorm:"updated"` } +// TableCollations is to make the "name" column case-sensitve for MySQL/MSSQL +func (b *Branch) TableCollations() []*schemas.Collation { + if setting.Database.Type.IsMySQL() { + return []*schemas.Collation{ + {Column: "name", Name: "utf8mb4_bin"}, + } + } else if setting.Database.Type.IsMSSQL() { + return []*schemas.Collation{ + {Column: "name", Name: "Latin1_General_CS_AS"}, + } + } + return nil +} + func (b *Branch) LoadDeletedBy(ctx context.Context) (err error) { if b.DeletedBy == nil { b.DeletedBy, err = user_model.GetUserByID(ctx, b.DeletedByID) diff --git a/models/migrations/migrations.go b/models/migrations/migrations.go index 578cbca035d85..68510ed851aa8 100644 --- a/models/migrations/migrations.go +++ b/models/migrations/migrations.go @@ -552,6 +552,8 @@ var migrations = []Migration{ NewMigration("Add Index to pull_auto_merge.doer_id", v1_22.AddIndexToPullAutoMergeDoerID), // v283 -> v284 NewMigration("Add combined Index to issue_user.uid and issue_id", v1_22.AddCombinedIndexToIssueUser), + // v284 -> v285 + NewMigration("Alter branch name collation to be case-sensitvie", v1_22.AlterBranchNameCollation), } // GetCurrentDBVersion returns the current db version diff --git a/models/migrations/v1_22/v283_test.go b/models/migrations/v1_22/v283_test.go index 864f47f840c01..bb09cc2e06056 100644 --- a/models/migrations/v1_22/v283_test.go +++ b/models/migrations/v1_22/v283_test.go @@ -7,22 +7,22 @@ import ( "testing" "code.gitea.io/gitea/models/migrations/base" + + "github.com/stretchr/testify/assert" ) func Test_AddCombinedIndexToIssueUser(t *testing.T) { type IssueUser struct { - UID int64 `xorm:"INDEX unique(uid_to_issue)"` // User ID. - IssueID int64 `xorm:"INDEX unique(uid_to_issue)"` + ID int64 `xorm:"pk autoincr"` + UID int64 `xorm:"INDEX"` // User ID. + IssueID int64 `xorm:"INDEX"` + IsRead bool + IsMentioned bool } // Prepare and load the testing database x, deferable := base.PrepareTestEnv(t, 0, new(IssueUser)) defer deferable() - if x == nil || t.Failed() { - return - } - if err := AddCombinedIndexToIssueUser(x); err != nil { - t.Fatal(err) - } + assert.NoError(t, AddCombinedIndexToIssueUser(x)) } diff --git a/models/migrations/v1_22/v284.go b/models/migrations/v1_22/v284.go new file mode 100644 index 0000000000000..5a8cba4cc63fe --- /dev/null +++ b/models/migrations/v1_22/v284.go @@ -0,0 +1,29 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package v1_22 //nolint + +import ( + "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/setting" + + "xorm.io/xorm" +) + +func AlterBranchNameCollation(x *xorm.Engine) error { + if setting.Database.Type.IsMySQL() { + _, err := x.Exec("ALTER TABLE branch MODIFY COLUMN `name` VARCHAR(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL") + return err + } else if setting.Database.Type.IsMSSQL() { + if _, err := x.Exec("DROP INDEX UQE_branch_s ON branch"); err != nil { + log.Error("Failed to drop index UQE_branch_s on branch: %v", err) // ignore this error, in case the index has been dropped in previous migration + } + if _, err := x.Exec("ALTER TABLE branch ALTER COLUMN [name] nvarchar(255) COLLATE Latin1_General_CS_AS NOT NULL"); err != nil { + return err + } + if _, err := x.Exec("CREATE UNIQUE NONCLUSTERED INDEX UQE_branch_s ON branch (repo_id ASC, [name] ASC)"); err != nil { + return err + } + } + return nil +} diff --git a/models/migrations/v1_22/v284_test.go b/models/migrations/v1_22/v284_test.go new file mode 100644 index 0000000000000..12980dce08ab1 --- /dev/null +++ b/models/migrations/v1_22/v284_test.go @@ -0,0 +1,25 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package v1_22 //nolint + +import ( + "testing" + + "code.gitea.io/gitea/models/migrations/base" + + "github.com/stretchr/testify/assert" +) + +func TestAlterBranchNameCollation(t *testing.T) { + type Branch struct { + ID int64 + RepoID int64 `xorm:"UNIQUE(s)"` + Name string `xorm:"UNIQUE(s) NOT NULL"` + } + + x, deferable := base.PrepareTestEnv(t, 0, new(Branch)) + defer deferable() + + assert.NoError(t, AlterBranchNameCollation(x)) +} diff --git a/tests/integration/api_branch_test.go b/tests/integration/api_branch_test.go index 28e690b356890..6572e5263841b 100644 --- a/tests/integration/api_branch_test.go +++ b/tests/integration/api_branch_test.go @@ -142,6 +142,18 @@ func testAPICreateBranches(t *testing.T, giteaURL *url.URL) { NewBranch: "branch_2", ExpectedHTTPStatus: http.StatusCreated, }, + // Trying to create a case-sensive branch name + { + OldBranch: "new_branch_from_master_1", + NewBranch: "Branch_2", + ExpectedHTTPStatus: http.StatusCreated, + }, + // Trying to create a branch with UTF8 + { + OldBranch: "master", + NewBranch: "test-👀", + ExpectedHTTPStatus: http.StatusCreated, + }, // Trying to create from a branch which does not exist { OldBranch: "does_not_exist",